#!/usr/bin/env bash # The standalone consumer proof (ADR 0053): a module outside this repository consumes one package # release train, the exact tarballs `scripts/package-release.ts` produced, and nothing else. # # bash scripts/test-standalone-module.sh --release [--allow-modified] # # The release directory is checked against its manifest first; then every @ekklesia/* package, the # module's own and the ones they depend on, installs from those tarballs, with no workspace link, no # Ekklesia source checkout, and no registry: third-party packages come from the local pnpm store the # workspace install already filled (`--offline`). The module then installs again from its lockfile # alone, type-checks every declaration it reaches (`skipLibCheck: false`), builds its backend and # UI, packages with the release's own `@ekklesia/module-pack`, and verifies with Core's verifier. # # `--allow-modified` accepts a release built from a modified source tree, for local development. set -euo pipefail repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" fail() { echo "standalone consumer proof failed: $*" >&2 exit 1 } release="" allow_modified="" while (($#)); do case "$1" in --release) release="${2:-}" shift 2 || fail "--release needs a directory" ;; --allow-modified) allow_modified=1 shift ;; *) fail "usage: test-standalone-module.sh --release [--allow-modified]" ;; esac done [[ -n "$release" && -d "$release" ]] || fail "no release directory given" work="$(mktemp -d "${TMPDIR:-/tmp}/ekklesia-standalone-XXXXXX")" trap 'rm -rf "$work"' EXIT # The module installs from a copy beside it, named by a fixed relative path. pnpm names a file: # tarball's install directory after that path, and the bundler keeps those names in its comments, so # an absolute path would make the module package depend on where the release happened to sit. (From # a registry, the directory is simply `@`.) The copy is what is checked and installed. cp -R "$release" "$work/release" release="$work/release" echo "==> Checking the release against its manifest" # Prints the train version, then one `namefileintegrity` line per package. node --input-type=module - "$repository_root" "$release" "$allow_modified" >"$work/train.tsv" <<'EOF' import { readdirSync, readFileSync } from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; const [repositoryRoot, release, allowModified] = process.argv.slice(2); const { PACKAGE_TRAIN_MANIFEST, releaseDirectoryProblems } = await import( pathToFileURL(path.join(repositoryRoot, "scripts", "package-release.ts")).href ); const manifest = JSON.parse(readFileSync(path.join(release, PACKAGE_TRAIN_MANIFEST), "utf8")); const files = new Map(readdirSync(release).map((file) => [file, readFileSync(path.join(release, file))])); const problems = releaseDirectoryProblems(manifest, files); if (manifest.source?.modified !== false && allowModified !== "1") { problems.push("the release was built from a modified source tree."); } if (problems.length > 0) { console.error(problems.join("\n")); process.exit(1); } console.log(manifest.version); for (const entry of manifest.packages) console.log(`${entry.name}\t${entry.file}\t${entry.integrity}`); EOF version="$(head -1 "$work/train.tsv")" echo " package train $version, $(($(wc -l <"$work/train.tsv") - 1)) packages" module="$work/standalone-proof" cp -R "$repository_root/tests/fixtures/standalone-module" "$module" cp "$repository_root/LICENSE" "$module/LICENSE" # The fixture pins the release's module-pack exactly, as every module repository must. pinned="$(node -p 'require(process.argv[1]).devDependencies["@ekklesia/module-pack"]' "$module/package.json")" [[ "$pinned" == "$version" ]] || fail "the fixture pins @ekklesia/module-pack $pinned, not the release's $version" # A module repository's own pnpm settings: esbuild (used by the UI build and the packager) runs its # install script. And, for this proof only, every @ekklesia/* package resolves to its release # tarball, exactly as it would to the published version. { echo "allowBuilds:" echo " esbuild: true" echo "overrides:" tail -n +2 "$work/train.tsv" | while IFS=$'\t' read -r name file _; do echo " \"$name\": \"file:../release/$file\"" done } >"$module/pnpm-workspace.yaml" echo "==> Installing the module from the release tarballs, offline" (cd "$module" && pnpm install --offline --reporter=append-only >"$work/install.log" 2>&1) || { tail -40 "$work/install.log" >&2 fail "install (third-party packages come from the local store: run pnpm install in the workspace first)" } echo "==> Checking the lockfile records each release artifact's own integrity" node --input-type=module - "$module/pnpm-lock.yaml" "$work/train.tsv" <<'EOF' import { readFileSync } from "node:fs"; const [lockfile, train] = process.argv.slice(2); const lock = readFileSync(lockfile, "utf8"); const failures = []; for (const line of readFileSync(train, "utf8").trim().split("\n").slice(1)) { const [name, file, integrity] = line.split("\t"); // pnpm records a file: tarball under `@file:` with its computed integrity. const at = lock.indexOf(`\n '${name}@file:`); const entry = at === -1 ? "" : lock.slice(at, lock.indexOf("\n\n", at)); if (!entry.includes(file)) failures.push(`${name} is not installed from ${file}`); else if (!entry.includes(`integrity: ${integrity}`)) failures.push(`${name}'s integrity differs from the release manifest`); } if (failures.length > 0) { console.error(failures.join("\n")); process.exit(1); } EOF echo "==> Reinstalling from the lockfile alone (--frozen-lockfile)" rm -rf "$module/node_modules" (cd "$module" && pnpm install --offline --frozen-lockfile --reporter=append-only >"$work/frozen.log" 2>&1) || { tail -40 "$work/frozen.log" >&2; fail "frozen-lockfile install"; } echo "==> Checking what the install resolved" (cd "$module" && node --input-type=module - "$module" "$version" <<'EOF') import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; const [moduleDirectory, version] = process.argv.slice(2); const modules = path.join(moduleDirectory, "node_modules"); const store = readdirSync(path.join(modules, ".pnpm")); const failures = []; // The Module SDK and its dependencies never pull Drizzle into a module that does not ask for it. if (store.some((entry) => entry.startsWith("drizzle-orm@"))) failures.push("drizzle-orm is installed"); // One React, the module's own, shared by @ekklesia/ui. const reacts = store.filter((entry) => /^react@/u.test(entry)); if (reacts.length !== 1) failures.push(`${reacts.length} copies of react are installed`); const fromModule = createRequire(path.join(moduleDirectory, "package.json")); const ui = realpathSync(path.join(modules, "@ekklesia", "ui", "package.json")); if (realpathSync(fromModule.resolve("react")) !== realpathSync(createRequire(ui).resolve("react"))) { failures.push("@ekklesia/ui resolves a different react"); } // @ekklesia/contracts is installed only because the packages above depend on it: never a direct // dependency, and never reachable by name from the module's own code. const own = JSON.parse(readFileSync(path.join(moduleDirectory, "package.json"), "utf8")); for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) { if (own[field]?.["@ekklesia/contracts"] !== undefined) failures.push(`the module lists contracts in ${field}`); } if (existsSync(path.join(modules, "@ekklesia", "contracts"))) failures.push("contracts is a direct install"); if (!store.some((entry) => entry.startsWith("@ekklesia+contracts@"))) failures.push("contracts is not installed"); // Every Ekklesia package came from a tarball into the module's own store, never from a link, and // each one, module-pack included, is exactly the release's version. for (const name of readdirSync(path.join(modules, "@ekklesia"))) { const real = realpathSync(path.join(modules, "@ekklesia", name)); if (!real.startsWith(path.join(modules, ".pnpm") + path.sep)) { failures.push(`@ekklesia/${name} is linked from outside the module`); } const installed = JSON.parse(readFileSync(path.join(real, "package.json"), "utf8")); if (installed.version !== version) failures.push(`@ekklesia/${name} is ${installed.version}`); } if (failures.length > 0) { console.error(failures.join("\n")); process.exit(1); } EOF # The published declarations must stand on their own: no package may name a module a consumer has # not installed (Drizzle above all), and the fixture checks every declaration it reaches. for project in tsconfig.json tsconfig.ui.json; do (cd "$module" && ./node_modules/.bin/tsc --showConfig --project "$project") | node -e 'const c = JSON.parse(require("fs").readFileSync(0, "utf8")); if (c.compilerOptions.skipLibCheck !== false) process.exit(1);' || fail "$project does not typecheck with skipLibCheck: false" done echo "==> Typechecking (skipLibCheck: false) and building" (cd "$module" && pnpm run typecheck && pnpm run build) >"$work/build.log" 2>&1 || { tail -40 "$work/build.log" >&2; fail "typecheck or build"; } # The packager needs no workspace file: the module's Node.js range is its own engines.node. The # binary is run directly, because pnpm would re-resolve the module without the proof's overrides. rm "$module/pnpm-workspace.yaml" echo "==> Packaging with the release's @ekklesia/module-pack" (cd "$module" && ./node_modules/.bin/ekklesia-pack-module --module . --target any --out "$work/out" \ --license-file LICENSE --source-revision 86ae907f59458508e3277666cdd214f5bd7c1078) archive="$work/out/standalone-proof-0.1.0-any.tar.gz" echo "==> Verifying with the platform verifier" (cd "$repository_root" && node core/dist/cli/verify-module-package.js "$archive" \ --sha256-file "$archive.sha256" --extract-to "$work/verified") echo "==> Checking the package's own description" node --input-type=module - "$work/verified" "$version" "$repository_root" "$work" "$release" <<'EOF' import { readdirSync, readFileSync } from "node:fs"; import path from "node:path"; const [verified, version, repositoryRoot, work, release] = process.argv.slice(2); const read = (file) => readFileSync(path.join(verified, file), "utf8"); const metadata = JSON.parse(read("ekklesia.package.json")); const failures = []; const incorporates = metadata.incorporates ?? {}; for (const name of ["@ekklesia/contracts", "@ekklesia/module-database", "@ekklesia/module-sdk"]) { if (incorporates[name] !== version) failures.push(`incorporates ${name}@${incorporates[name]}`); } const packager = metadata.provenance?.packager ?? {}; if (packager.name !== "@ekklesia/module-pack" || packager.version !== version) { failures.push(`packed by ${packager.name}@${packager.version}, not the release's module-pack`); } if (metadata.runtime?.node !== ">=24.11.0 <25") failures.push("runtime.node is not engines.node"); if (metadata.transitional !== undefined) failures.push("records transitional database facts"); const notices = read("THIRD_PARTY_NOTICES"); if (notices.includes("@ekklesia/")) failures.push("credits Ekklesia as third-party"); // The UI bundle holds one React, the module's own. const reacts = notices.split("\n").filter((line) => /^react \d/u.test(line)); if (reacts.length !== 1) failures.push(`credits ${reacts.length} copies of react`); for (const entry of readdirSync(verified, { recursive: true, withFileTypes: true })) { if (!entry.isFile()) continue; const text = readFileSync(path.join(entry.parentPath, entry.name), "latin1"); for (const local of [repositoryRoot, work, release]) { if (text.includes(local)) failures.push(`${entry.name} names a local path`); } if (/drizzle-orm/u.test(text) && entry.name.endsWith(".js")) failures.push(`${entry.name} bundles Drizzle`); // A file: install directory's name keeps the path pnpm installed it from: only the fixed one. for (const match of text.matchAll(/@file\+([^/"'\s]*)/gu)) { if (!match[1]?.startsWith("..+release+")) failures.push(`${entry.name} names an install path`); } } if (failures.length > 0) { console.error(failures.join("\n")); process.exit(1); } console.log(`incorporates ${JSON.stringify(incorporates)}`); EOF echo "Standalone consumer proof passed for package train $version."