CI / verify (pull_request) Successful in 1m2s
CI / infrastructure-smoke (pull_request) Successful in 37s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / standalone-module (pull_request) Successful in 26s
CI / image-build (pull_request) Successful in 28s
CI / deploy-smoke (same-origin) (pull_request) Successful in 43s
CI / deploy-smoke (split) (pull_request) Successful in 1m30s
@ekklesia/contracts still emitted a type-only import of drizzle-orm/node-postgres for the legacy ModuleDatabase facade, so a standalone module type-checked only because skipLibCheck hid it. - ModuleDatabase, LegacyModuleRuntimeServices and AnyModuleRuntimeServices move to core/src/modules/database.ts, beside the facade Core builds; the facade itself is unchanged. - contracts' runtime, route, and services generics no longer name an ORM: the database type is a plain parameter defaulting to the ABI v1 executor, and the legacy definition's database is opaque until Core specializes it. Core and apps/api name the legacy types from Core. - contracts drops its drizzle-orm optional peer and devDependency; Drizzle stays public only through @ekklesia/module-database/drizzle. - The facade's type-level test moves from contracts to Core. - Regression proof: the standalone fixture type-checks with skipLibCheck: false against the packed tarballs, with no drizzle-orm installed (the smoke asserts both). Against the previous contracts tarball the same check fails with TS2307 on drizzle-orm/node-postgres. Co-Authored-By: Claude Opus 5.5 <[email protected]>
141 lines
6.9 KiB
Bash
Executable File
141 lines
6.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# The standalone module-development proof (ADR 0053): a module outside this repository, installed
|
|
# from locally packed @ekklesia/* tarballs, with no workspace link and no Ekklesia registry, must
|
|
# install, typecheck, build its backend and UI, package with the canonical packager, and verify with
|
|
# the platform's own verifier.
|
|
#
|
|
# bash scripts/test-standalone-module.sh
|
|
#
|
|
# Third-party packages (React, TypeScript, esbuild, ...) come from the public npm registry as usual.
|
|
# Nothing is published, and nothing outside a temporary directory is written except the packed
|
|
# public packages' rebuilt dist/ output.
|
|
set -euo pipefail
|
|
|
|
repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
work="$(mktemp -d "${TMPDIR:-/tmp}/ekklesia-standalone-XXXXXX")"
|
|
trap 'rm -rf "$work"' EXIT
|
|
|
|
fail() {
|
|
echo "standalone module proof failed: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
version="$(node -p 'require(process.argv[1]).version' "$repository_root/package.json")"
|
|
|
|
echo "==> Packing and inspecting the public package set ($version)"
|
|
node "$repository_root/scripts/pack-public-packages.ts" --out "$work/tarballs"
|
|
|
|
module="$work/standalone-proof"
|
|
cp -R "$repository_root/tests/fixtures/standalone-module" "$module"
|
|
cp "$repository_root/LICENSE" "$module/LICENSE"
|
|
|
|
# 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, the module's own and the ones
|
|
# they depend on, resolves to its packed tarball, exactly as it would to a published version.
|
|
{
|
|
echo "allowBuilds:"
|
|
echo " esbuild: true"
|
|
echo "overrides:"
|
|
for tarball in "$work"/tarballs/*.tgz; do
|
|
name="$(tar -xzOf "$tarball" package/package.json | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).name')"
|
|
echo " \"$name\": \"file:$tarball\""
|
|
done
|
|
} >"$module/pnpm-workspace.yaml"
|
|
|
|
echo "==> Installing the module from the tarballs"
|
|
(cd "$module" && pnpm install --reporter=append-only >"$work/install.log" 2>&1) ||
|
|
{ tail -40 "$work/install.log" >&2; fail "install"; }
|
|
|
|
echo "==> Checking what the install resolved"
|
|
(cd "$module" && node --input-type=module - "$module" <<'EOF')
|
|
import { readdirSync, realpathSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import path from "node:path";
|
|
|
|
const [moduleDirectory] = process.argv.slice(2);
|
|
const store = readdirSync(path.join(moduleDirectory, "node_modules", ".pnpm"));
|
|
const failures = [];
|
|
// The Module SDK and its dependencies never pull Drizzle into a module that does not use 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(moduleDirectory, "node_modules", "@ekklesia", "ui", "package.json"));
|
|
const fromUi = createRequire(ui);
|
|
if (realpathSync(fromModule.resolve("react")) !== realpathSync(fromUi.resolve("react"))) {
|
|
failures.push("@ekklesia/ui resolves a different react");
|
|
}
|
|
// Every Ekklesia package came from a tarball into the module's own store, never from a link.
|
|
for (const name of readdirSync(path.join(moduleDirectory, "node_modules", "@ekklesia"))) {
|
|
const real = realpathSync(path.join(moduleDirectory, "node_modules", "@ekklesia", name));
|
|
if (!real.startsWith(path.join(moduleDirectory, "node_modules", ".pnpm") + path.sep)) {
|
|
failures.push(`@ekklesia/${name} is linked from outside the module`);
|
|
}
|
|
}
|
|
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 canonical packager"
|
|
(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" <<'EOF'
|
|
import { readdirSync, readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const [verified, version, repositoryRoot, work] = 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]}`);
|
|
}
|
|
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");
|
|
if (text.includes(repositoryRoot) || text.includes(work)) failures.push(`${entry.name} names a local path`);
|
|
if (/drizzle-orm/u.test(text) && entry.name.endsWith(".js")) failures.push(`${entry.name} bundles Drizzle`);
|
|
}
|
|
if (failures.length > 0) {
|
|
console.error(failures.join("\n"));
|
|
process.exit(1);
|
|
}
|
|
console.log(`incorporates ${JSON.stringify(incorporates)}`);
|
|
EOF
|
|
|
|
echo "Standalone module proof passed."
|