feat: standalone module-build readiness for the public @ekklesia packages (ADR 0053)
CI / verify (pull_request) Successful in 1m2s
CI / infrastructure-smoke (pull_request) Successful in 28s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / standalone-module (pull_request) Successful in 26s
CI / image-build (pull_request) Successful in 30s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m42s
CI / verify (pull_request) Successful in 1m2s
CI / infrastructure-smoke (pull_request) Successful in 28s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / standalone-module (pull_request) Successful in 26s
CI / image-build (pull_request) Successful in 30s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m42s
Makes the public module-development package set usable from a module repository outside the monorepo. Nothing is published. - Curated @ekklesia/module-sdk: an explicit author API, no contracts barrel, no identity/authorization internals, no Drizzle and no legacy ModuleDatabase type. defineModuleRuntime returns the SDK's own named interfaces so a module's emitted declarations never reach into @ekklesia/contracts. defineModuleSchema is gone; createModuleTable and defineModuleSchemaMetadata live in @ekklesia/module-database. - Drizzle: no longer a dependency of module-sdk or contracts. Core keeps the legacy Drizzle schema validator (moved to core/src/modules) and its legacy runtime support; drizzle-orm is an optional peer only. - React is a peer (plus devDependency) of @ekklesia/ui; the boundary checker allows exactly that consumer-selected peer. - @ekklesia/typescript-config is publishable; module-ui-host is private. Every public package carries "ekklesia.distribution": "module-development" and no monorepo-root prepack. - module-pack reads the Node.js range from the module's own engines.node and recognizes Ekklesia packages by name plus that marker, so registry-installed packages are incorporated, SDK-compatibility checked, and never credited as third-party. - scripts/pack-public-packages.ts (pnpm packages:pack) rebuilds, packs and inspects the nine public tarballs; scripts/test-standalone-module.sh (pnpm test:standalone-module, new CI job) installs them into a fixture module outside the repository and proves it typechecks, builds, packages, and verifies. - Public-package boundary tests and module-development docs. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9426c83109
commit
89d5d0f2da
@@ -149,13 +149,17 @@ const DEPENDENCY_FIELDS = [
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Runtime peers whose identity is deliberately supplied by the final consumer. The sole entry is
|
||||
* the module-side Drizzle adapter: a module chooses and bundles its own compatible Drizzle runtime,
|
||||
* while Core must never select one for it. Keep this exception package-and-dependency specific;
|
||||
* ordinary workspace packages still need production dependencies for runtime imports.
|
||||
* Runtime peers whose identity is deliberately supplied by the final consumer, and which no
|
||||
* production Node.js process loads from this workspace. The module-side Drizzle adapter: a module
|
||||
* chooses and bundles its own compatible Drizzle runtime, while Core must never select one for it.
|
||||
* And the shared UI elements' React (ADR 0053): `@ekklesia/ui` is only ever bundled into browser
|
||||
* artifacts, by the shell's build and by module UI builds, and the consumer's own React must be the
|
||||
* one in that bundle. Keep this exception package-and-dependency specific; ordinary workspace
|
||||
* packages still need production dependencies for runtime imports.
|
||||
*/
|
||||
const CONSUMER_SELECTED_RUNTIME_PEERS = new Map<string, ReadonlySet<string>>([
|
||||
["@ekklesia/module-database", new Set(["drizzle-orm"])],
|
||||
["@ekklesia/ui", new Set(["react"])],
|
||||
]);
|
||||
const SHARED_PACKAGE_ROLES = new Set([
|
||||
"api-client",
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, mkdtemp, readdir, readFile, realpath, rm, stat } from "node:fs/promises";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
/**
|
||||
* The public module-development package set (ADR 0053), and the release check that packs it.
|
||||
*
|
||||
* node scripts/pack-public-packages.ts --out <directory>
|
||||
*
|
||||
* Rebuilds each public package from clean output, packs it with `pnpm pack`, and inspects every
|
||||
* tarball before anything may consume it: only the package's built files, its declarations, and a
|
||||
* `LICENSE`; a manifest with no `workspace:` range, no install-time lifecycle script, the explicit
|
||||
* distribution marker, and the one coordinated version. It publishes nothing.
|
||||
*/
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
export type PublicPackageRole = "author-api" | "tooling" | "pre-1.0" | "implementation";
|
||||
|
||||
export interface PublicPackage {
|
||||
readonly name: string;
|
||||
/** Relative to the repository root. */
|
||||
readonly directory: string;
|
||||
readonly role: PublicPackageRole;
|
||||
}
|
||||
|
||||
/** ADR 0053's public set. Every other workspace package is private and never published. */
|
||||
export const PUBLIC_PACKAGES: readonly PublicPackage[] = Object.freeze([
|
||||
{ directory: "packages/module-sdk", name: "@ekklesia/module-sdk", role: "author-api" },
|
||||
{ directory: "packages/module-database", name: "@ekklesia/module-database", role: "author-api" },
|
||||
{ directory: "packages/module-ui-sdk", name: "@ekklesia/module-ui-sdk", role: "author-api" },
|
||||
{ directory: "packages/design-tokens", name: "@ekklesia/design-tokens", role: "author-api" },
|
||||
{ directory: "packages/module-ui-build", name: "@ekklesia/module-ui-build", role: "tooling" },
|
||||
{ directory: "packages/module-pack", name: "@ekklesia/module-pack", role: "tooling" },
|
||||
{
|
||||
directory: "packages/typescript-config",
|
||||
name: "@ekklesia/typescript-config",
|
||||
role: "tooling",
|
||||
},
|
||||
{ directory: "packages/ui", name: "@ekklesia/ui", role: "pre-1.0" },
|
||||
// A dependency of the packages above, never a supported direct module-author dependency.
|
||||
{ directory: "packages/contracts", name: "@ekklesia/contracts", role: "implementation" },
|
||||
]);
|
||||
|
||||
/** `ekklesia.distribution` in a public package's manifest: the identity the canonical packager
|
||||
* recognizes an installed Ekklesia package by (`packages/module-pack/src/packages.ts`). */
|
||||
export const PUBLIC_PACKAGE_DISTRIBUTION = "module-development";
|
||||
|
||||
/** Scripts a package manager runs on a consumer's machine when it installs, packs, or publishes. */
|
||||
const LIFECYCLE_SCRIPTS = Object.freeze([
|
||||
"preinstall",
|
||||
"install",
|
||||
"postinstall",
|
||||
"prepare",
|
||||
"prepack",
|
||||
"postpack",
|
||||
"prepublish",
|
||||
"prepublishOnly",
|
||||
"publish",
|
||||
"postpublish",
|
||||
]);
|
||||
|
||||
/** The fields a consumer's install resolves. `devDependencies` are never installed from a published
|
||||
* package, so a tooling package's test-only use of a private package is not a problem there. */
|
||||
const INSTALLED_DEPENDENCY_FIELDS = Object.freeze([
|
||||
"dependencies",
|
||||
"peerDependencies",
|
||||
"optionalDependencies",
|
||||
]);
|
||||
|
||||
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/** What is wrong with a packed `package.json`, or nothing. */
|
||||
export function packedManifestProblems(
|
||||
manifest: Readonly<Record<string, unknown>>,
|
||||
expected: { readonly name: string; readonly version: string },
|
||||
): string[] {
|
||||
const problems: string[] = [];
|
||||
if (manifest.name !== expected.name) problems.push(`is named ${String(manifest.name)}`);
|
||||
if (manifest.version !== expected.version) {
|
||||
problems.push(
|
||||
`has version ${String(manifest.version)}, not the coordinated ${expected.version}`,
|
||||
);
|
||||
}
|
||||
if (manifest.private !== undefined) problems.push("declares private");
|
||||
if (manifest.license !== "AGPL-3.0-or-later") problems.push("does not declare its licence");
|
||||
if (JSON.stringify(manifest).includes("workspace:")) problems.push("keeps a workspace: range");
|
||||
const ekklesia = manifest.ekklesia;
|
||||
if (!isRecord(ekklesia) || ekklesia.distribution !== PUBLIC_PACKAGE_DISTRIBUTION) {
|
||||
problems.push("lacks the module-development distribution marker");
|
||||
}
|
||||
const scripts = isRecord(manifest.scripts) ? manifest.scripts : {};
|
||||
for (const script of LIFECYCLE_SCRIPTS) {
|
||||
if (Object.hasOwn(scripts, script)) problems.push(`runs a ${script} script`);
|
||||
}
|
||||
const publicNames = new Set(PUBLIC_PACKAGES.map((entry) => entry.name));
|
||||
for (const field of INSTALLED_DEPENDENCY_FIELDS) {
|
||||
const dependencies = manifest[field];
|
||||
if (!isRecord(dependencies)) continue;
|
||||
for (const [name, range] of Object.entries(dependencies)) {
|
||||
if (!name.startsWith("@ekklesia/")) continue;
|
||||
if (!publicNames.has(name)) problems.push(`${field} names the private ${name}`);
|
||||
else if (range !== expected.version) {
|
||||
problems.push(`${field} names ${name}@${String(range)}, not ${expected.version}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
|
||||
const BUILT_FILE = /^dist\/(.+?)(?:\.d\.ts|\.js)(?:\.map)?$/u;
|
||||
|
||||
/**
|
||||
* What is wrong with a tarball's file list, or nothing. Every file is `package.json`, `LICENSE`, or
|
||||
* under one of the manifest's `files` entries; nothing is a source, test, or configuration file; and
|
||||
* every built file has the source it was compiled from, so stale output from a deleted source fails.
|
||||
*/
|
||||
export function tarballFileProblems(
|
||||
files: readonly string[],
|
||||
manifest: Readonly<Record<string, unknown>>,
|
||||
hasSource: (relativeWithoutExtension: string) => boolean,
|
||||
): string[] {
|
||||
const problems: string[] = [];
|
||||
const declared = Array.isArray(manifest.files) ? manifest.files.map(String) : [];
|
||||
if (!files.includes("LICENSE")) problems.push("has no LICENSE");
|
||||
if (!files.includes("package.json")) problems.push("has no package.json");
|
||||
for (const file of files) {
|
||||
if (file === "package.json" || file === "LICENSE") continue;
|
||||
if (!declared.some((entry) => file === entry || file.startsWith(`${entry}/`))) {
|
||||
problems.push(`ships ${file}, which its files field does not name`);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
/(?:^|\/)(?:src|tests?|node_modules)\//u.test(file) ||
|
||||
/\.test\.[cm]?[jt]sx?$/u.test(file)
|
||||
) {
|
||||
problems.push(`ships ${file}`);
|
||||
continue;
|
||||
}
|
||||
const built = BUILT_FILE.exec(file);
|
||||
if (file.startsWith("dist/") && (built === null || !hasSource(built[1] ?? ""))) {
|
||||
problems.push(`ships ${file}, which no source produces`);
|
||||
}
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
|
||||
async function readJson(file: string): Promise<Record<string, unknown>> {
|
||||
return JSON.parse(await readFile(file, "utf8")) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async function filesUnder(directory: string): Promise<string[]> {
|
||||
const entries = await readdir(directory, { recursive: true, withFileTypes: true });
|
||||
return entries
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) =>
|
||||
path.relative(directory, path.join(entry.parentPath, entry.name)).split(path.sep).join("/"),
|
||||
)
|
||||
.sort();
|
||||
}
|
||||
|
||||
async function sourceExists(packageDirectory: string, relative: string): Promise<boolean> {
|
||||
for (const extension of [".ts", ".tsx"]) {
|
||||
try {
|
||||
await stat(path.join(packageDirectory, "src", `${relative}${extension}`));
|
||||
return true;
|
||||
} catch {}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface PackedPackage {
|
||||
readonly name: string;
|
||||
readonly tarball: string;
|
||||
readonly files: number;
|
||||
readonly bytes: number;
|
||||
}
|
||||
|
||||
async function pnpm(arguments_: readonly string[], cwd: string): Promise<string> {
|
||||
const { stdout } = await execFileAsync("pnpm", [...arguments_], {
|
||||
cwd,
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
});
|
||||
return stdout;
|
||||
}
|
||||
|
||||
async function packOne(
|
||||
entry: PublicPackage,
|
||||
version: string,
|
||||
outDirectory: string,
|
||||
scratch: string,
|
||||
): Promise<{ packed: PackedPackage; problems: string[] }> {
|
||||
const directory = path.join(repositoryRoot, entry.directory);
|
||||
const report = JSON.parse(
|
||||
await pnpm(["pack", "--json", "--pack-destination", outDirectory], directory),
|
||||
) as { filename: string };
|
||||
const tarball = report.filename;
|
||||
const extracted = path.join(scratch, entry.name.replace("/", "+"));
|
||||
await mkdir(extracted, { recursive: true });
|
||||
await execFileAsync("tar", ["-xzf", tarball, "-C", extracted]);
|
||||
const root = path.join(extracted, "package");
|
||||
const files = await filesUnder(root);
|
||||
const manifest = await readJson(path.join(root, "package.json"));
|
||||
|
||||
const sources = new Map<string, boolean>();
|
||||
for (const file of files) {
|
||||
const built = BUILT_FILE.exec(file)?.[1];
|
||||
if (built !== undefined && !sources.has(built)) {
|
||||
sources.set(built, await sourceExists(directory, built));
|
||||
}
|
||||
}
|
||||
const problems = [
|
||||
...packedManifestProblems(manifest, { name: entry.name, version }),
|
||||
...tarballFileProblems(files, manifest, (relative) => sources.get(relative) === true),
|
||||
];
|
||||
// Nothing the package says may name the machine or checkout it was packed on.
|
||||
for (const file of files) {
|
||||
const text = (await readFile(path.join(root, file))).toString("latin1");
|
||||
for (const local of [repositoryRoot, homedir()]) {
|
||||
if (text.includes(local)) problems.push(`${file} names a local path`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
packed: { bytes: (await stat(tarball)).size, files: files.length, name: entry.name, tarball },
|
||||
problems: [...new Set(problems)],
|
||||
};
|
||||
}
|
||||
|
||||
/** Rebuilds the public set from clean output, packs it into `outDirectory`, and inspects it. */
|
||||
export async function packPublicPackages(outDirectory: string): Promise<{
|
||||
readonly packed: readonly PackedPackage[];
|
||||
readonly problems: readonly string[];
|
||||
}> {
|
||||
const version = String((await readJson(path.join(repositoryRoot, "package.json"))).version);
|
||||
await mkdir(outDirectory, { recursive: true });
|
||||
const out = await realpath(outDirectory);
|
||||
|
||||
// Stale output from a deleted source would otherwise ship: the build only ever adds files, and
|
||||
// Turbo's cache would restore whatever an earlier build left behind.
|
||||
for (const entry of PUBLIC_PACKAGES) {
|
||||
await rm(path.join(repositoryRoot, entry.directory, "dist"), { force: true, recursive: true });
|
||||
}
|
||||
await pnpm(
|
||||
[
|
||||
"exec",
|
||||
"turbo",
|
||||
"run",
|
||||
"build",
|
||||
"--force",
|
||||
"--output-logs=errors-only",
|
||||
...PUBLIC_PACKAGES.map((entry) => `--filter=${entry.name}`),
|
||||
],
|
||||
repositoryRoot,
|
||||
);
|
||||
|
||||
const scratch = await mkdtemp(path.join(tmpdir(), "ekklesia-public-packages-"));
|
||||
try {
|
||||
const packed: PackedPackage[] = [];
|
||||
const problems: string[] = [];
|
||||
for (const entry of PUBLIC_PACKAGES) {
|
||||
const result = await packOne(entry, version, out, scratch);
|
||||
packed.push(result.packed);
|
||||
problems.push(...result.problems.map((problem) => `${entry.name} ${problem}.`));
|
||||
}
|
||||
return { packed, problems };
|
||||
} finally {
|
||||
await rm(scratch, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function main(argv: readonly string[]): Promise<number> {
|
||||
if (argv.length !== 2 || argv[0] !== "--out" || !argv[1]) {
|
||||
process.stderr.write("Usage: node scripts/pack-public-packages.ts --out <directory>\n");
|
||||
return 64;
|
||||
}
|
||||
try {
|
||||
const { packed, problems } = await packPublicPackages(path.resolve(argv[1]));
|
||||
for (const entry of packed) {
|
||||
process.stdout.write(
|
||||
`${path.basename(entry.tarball)}: ${entry.files} files, ${entry.bytes} bytes\n`,
|
||||
);
|
||||
}
|
||||
if (problems.length > 0) {
|
||||
for (const problem of problems) process.stderr.write(`${problem}\n`);
|
||||
return 1;
|
||||
}
|
||||
process.stdout.write(`Packed and inspected ${packed.length} public packages.\n`);
|
||||
return 0;
|
||||
} catch {
|
||||
process.stderr.write("Packing the public packages failed while building or packing.\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
process.exitCode = await main(process.argv.slice(2));
|
||||
}
|
||||
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#!/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
|
||||
|
||||
echo "==> Typechecking 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."
|
||||
Reference in New Issue
Block a user