#!/bin/sh # The catalogue's TypeScript part of its own check (novox/hq issue 302's `mesh-check-also`), declared in # merge-check.sh's header and run by the build seat in the TypeScript toolchain, after the Go part. The media # catalogue's twin is issue 311. Before this, a TypeScript module's change ran nothing of its own before it # merged: the Go part said "NOT TESTED HERE", and a type error in the forge module would first have shown in # its walk. # # For every touched module that has a package.json, in a copy of it outside the clone: # # 1. its dependencies are the toolchain's own (/app/node_modules: the mesh's SDK, Node's types and what the # runtime carries), copied beside it, never fetched: the check holds no credential for the package # registry. A module that depends on a package the toolchain does not hold **fails the check**, by name: # it could not be type-checked or tested, and what could not run is a failure, never a note (novox/hq # issue 385); # 2. every source that is not a test is type-checked with the toolchain's compiler and the flags a build # compiles with — the same check the build makes, before the merge rather than in the walk; # 3. its tests (test/*.test.ts) run under Node's own runner with types stripped, as its own `npm test` does # after its build: the sources are compiled to dist/ and beside themselves first, so a test that imports # the compiled module, or a source that imports its sibling by its .js name, finds it. # # Said, never passed silently: a module with no tests is said NOT TESTED on every run that touches it — # nothing of it could fail. A module whose tests exist and could not run fails (novox/hq issue 385). # # By hand: MESH_CHECK_MODULES=gitea sh merge-check-typescript.sh, in the toolchain image (it reads # /app/node_modules), or with TOOLCHAIN_MODULES pointing at a node_modules that holds the same. # # **In an agent's session, outside the toolchain image** (novox/hq issue 742), the same node_modules is # assembled first, in a throwaway directory under the check's own workspace, the way the image's `deps` stage # assembles it: the toolchain's own package.json and lock (mesh-tools' node-tools/, read from the forge this # clone came from, so the list is never copied here) installed with `npm ci`, then the SDK the mesh last # published (`@novox/mesh-sdk@latest`), the @novox scope from the forge's package registry, read without a # credential. Node and npm are the dev-tools module's; nothing is installed on the machine, no npm # configuration or cache of the account's is read or written. Only when neither TOOLCHAIN_MODULES nor # /app/node_modules is there: the forge's check, in the image, fetches nothing, as before. The forge is # MESH_FORGE (default: the origin remote's), the owner MESH_FORGE_OWNER (default: the origin's owner). set -eu work=$(mktemp -d) trap 'rm -rf "$work"' EXIT deps="${TOOLCHAIN_MODULES:-/app/node_modules}" if [ -z "${TOOLCHAIN_MODULES:-}" ] && [ ! -f "$deps/typescript/bin/tsc" ]; then origin=$(git remote get-url origin 2>/dev/null || true) forge="${MESH_FORGE:-$(printf '%s\n' "$origin" | sed -n 's#^\(https\{0,1\}://[^/]*\)/.*#\1#p')}" owner="${MESH_FORGE_OWNER:-$(printf '%s\n' "$origin" | sed -n 's#^https\{0,1\}://[^/]*/\([^/]*\)/.*#\1#p')}" if [ -z "$forge" ] || [ -z "$owner" ]; then echo "NOT CHECKED: no TypeScript toolchain at $deps, and no forge to assemble one from (origin is '$origin'; set MESH_FORGE and MESH_FORGE_OWNER, or TOOLCHAIN_MODULES)" exit 1 fi echo "no TypeScript toolchain at $deps: assembling one from $forge/$owner/mesh-tools node-tools/ and $owner's package registry (novox/hq issue 742)" tc="$work/toolchain" mkdir -p "$tc" printf '@%s:registry=%s/api/packages/%s/npm/\n' "$owner" "$forge" "$owner" >"$work/npmrc" # Read with Node's own fetch (dev-tools declares Node, not curl); a status that is not 2xx fails. if ! { node -e ' const [base, dir] = process.argv.slice(1); Promise.all(["package.json", "package-lock.json"].map(async (f) => { const r = await fetch(base + f); if (!r.ok) throw new Error(base + f + ": " + r.status); require("fs").writeFileSync(dir + "/" + f, await r.text()); })).catch((e) => { console.error(e.message); process.exit(1); });' \ "$forge/$owner/mesh-tools/raw/branch/main/node-tools/" "$tc" && (cd "$tc" && export npm_config_userconfig="$work/npmrc" npm_config_cache="$work/npm-cache" && npm ci --ignore-scripts --no-audit --no-fund --loglevel=error && npm install --ignore-scripts --no-audit --no-fund --no-save --loglevel=error "@$owner/mesh-sdk@latest"); }; then echo "NOT CHECKED: the TypeScript toolchain could not be assembled from $forge (novox/hq issue 742)" exit 1 fi deps="$tc/node_modules" fi tsc="$deps/typescript/bin/tsc" if [ ! -f "$tsc" ]; then echo "NOT CHECKED: no TypeScript compiler at $tsc — this part runs in the TypeScript toolchain" exit 1 fi # The modules the change reaches: the planner's answer (hq ADR 0238), and every module a changed file is in — # a change to a module's tests alone moves no build (hq ADR 0267), and its tests must still run. touched=$({ printf '%s\n' "${MESH_CHECK_MODULES:-}" | tr ',' '\n' | sed -e 's#^modules/##' -e '/\//d' printf '%s\n' "${MESH_CHECK_CHANGED:-}" | tr ',' '\n' | sed -n 's#^modules/\([^/]*\)/.*#\1#p' } | sed '/^$/d' | sort -u) failed="" for m in $touched; do d="modules/$m" [ -f "$d/package.json" ] || continue copy="$work/$m" mkdir -p "$copy" (cd "$d" && tar --exclude=./node_modules --exclude=./dist -cf - .) | (cd "$copy" && tar -xf -) # What it depends on, against what the toolchain holds. missing=$(node -e ' const p = require(process.argv[1]); const have = require("fs").readdirSync(process.argv[2]); const scoped = (n) => n.startsWith("@") ? n.split("/")[0] : n; const names = Object.keys({ ...(p.dependencies || {}), ...(p.devDependencies || {}) }); const held = (n) => have.includes(n) || (n.startsWith("@") && have.includes(scoped(n)) && require("fs").existsSync(process.argv[2] + "/" + n)); console.log(names.filter((n) => n !== "typescript" && !held(n)).join(" "));' "$copy/package.json" "$deps") if [ -n "$missing" ]; then echo "FAILED: $d could not be checked — it depends on $missing, which the toolchain does not hold and the check cannot fetch (novox/hq issue 385)" failed="$failed $m" continue fi mkdir -p "$copy/node_modules" for p in "$deps"/*; do case "$(basename "$p")" in typescript | esbuild | @esbuild) continue ;; esac cp -r "$p" "$copy/node_modules/" done sources=$(cd "$copy" && find . -name '*.ts' ! -name '*.d.ts' ! -path './test/*' ! -path './node_modules/*' ! -path './dist/*' | sort) if [ -n "$sources" ]; then echo "type-checking $d (TypeScript)" # shellcheck disable=SC2086 # the module's sources, none with a space in its name if ! (cd "$copy" && node "$tsc" --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 \ --types node $sources); then failed="$failed $m" continue fi else echo "$d has no TypeScript sources of its own to type-check: its tests read its files" fi if ! ls "$copy"/test/*.test.ts >/dev/null 2>&1; then echo "NOT TESTED: $d is TypeScript and has no tests of its own" continue fi if [ -n "$sources" ]; then # shellcheck disable=SC2086 (cd "$copy" && node "$tsc" --module NodeNext --moduleResolution NodeNext --target ES2022 --types node \ --rootDir . --outDir dist $sources && node "$tsc" --module NodeNext --moduleResolution NodeNext \ --target ES2022 --types node --rootDir . --outDir . $sources) fi echo "testing $d (TypeScript)" if ! (cd "$copy" && node --test --test-reporter=spec --experimental-strip-types \ --no-warnings=ExperimentalWarning test/*.test.ts); then failed="$failed $m" fi done if [ -n "$failed" ]; then echo "TypeScript modules that failed:$failed" exit 1 fi