// Validates the built Signage package and its migrations the way the platform reads them, with only // Ekklesia's published module-development packages: no Ekklesia checkout, no database. // // pnpm build && pnpm validate // // What it checks: // - the manifest parses, and its id and version match package.json and the built runtime; // - every declared entrypoint exists in the built package; // - the built runtime declares the same Module Database ABI as the manifest (Core refuses a // mismatch); // - the built schema entrypoint's only export is plain ABI v1 metadata for this module's own, // namespaced tables, and every foreign key targets one of them; // - the installed module SDK satisfies the manifest's `compatibility.moduleSdk`; // - the migration journal and policy parse, name the same migrations in the same order, and every // migration's SQL is present. // // What it cannot check, because only Ekklesia itself can: compatibility with a particular platform // installation, capability and module dependencies between installed modules, and a migration's // checksum against a database's applied history. The module packager and the platform's package // verifier re-check all of the above when Signage is built as a Module Package. import { readFile, stat } from "node:fs/promises"; import { fileURLToPath, pathToFileURL } from "node:url"; import { defineModuleSchemaMetadata, moduleTablePrefix } from "@ekklesia/module-database"; import { parseDrizzleMigrationJournal, parseModuleManifest, parseModuleMigrationPolicy, } from "@ekklesia/module-sdk"; const root = fileURLToPath(new URL("..", import.meta.url)); const failures = []; const passes = []; function check(condition, description) { (condition ? passes : failures).push(description); } async function json(relative) { return JSON.parse(await readFile(new URL(relative, pathToFileURL(root)), "utf8")); } async function exists(relative) { try { return (await stat(new URL(relative, pathToFileURL(root)))).isFile(); } catch { return false; } } /** A caret range, `^X.Y.Z`, as the platform's manifests write them. Anything else is refused here * rather than half understood. */ function satisfiesCaret(version, range) { const wanted = /^\^(\d+)\.(\d+)\.(\d+)$/u.exec(range); const actual = /^(\d+)\.(\d+)\.(\d+)$/u.exec(version); if (!wanted || !actual) { return false; } const [w, a] = [wanted.slice(1).map(Number), actual.slice(1).map(Number)]; if (w[0] !== a[0] || (w[0] === 0 && w[1] !== a[1])) { return false; } for (let index = 0; index < 3; index += 1) { if (a[index] !== w[index]) { return a[index] > w[index]; } } return true; } const packageJson = await json("package.json"); const manifest = parseModuleManifest(await json("ekklesia.module.json")); check(true, "ekklesia.module.json is a valid manifest"); check(packageJson.version === manifest.version, "package.json and the manifest name one version"); check(packageJson.type === "module", "the package is ESM"); check(packageJson.ekklesia?.layer === "module", "the package carries the module layer marker"); const { server, schema, migrations, ui, display } = manifest.entrypoints; for (const entrypoint of [server, schema, ui, display?.artifact].filter(Boolean)) { check(await exists(entrypoint), `the built package has ${entrypoint}`); } const runtime = (await import(new URL(server, pathToFileURL(root)).href)).default; check(runtime?.id === manifest.id, "the built runtime's id is the manifest's"); check(runtime?.version === manifest.version, "the built runtime's version is the manifest's"); check( JSON.stringify(runtime?.database ?? null) === JSON.stringify(manifest.database ?? null), "the built runtime declares the manifest's database ABI", ); if (schema) { const namespace = await import(new URL(schema, pathToFileURL(root)).href); check( Object.keys(namespace).join() === "default", "the schema entrypoint exports nothing but its metadata", ); const metadata = defineModuleSchemaMetadata(namespace.default); check( JSON.stringify(JSON.parse(JSON.stringify(metadata))) === JSON.stringify(metadata), "the schema metadata is plain data", ); check(metadata.moduleId === manifest.id, "the schema metadata is this module's"); const prefix = moduleTablePrefix(manifest.id); const owned = new Set(metadata.tables.map((table) => table.name)); check( metadata.tables.every((table) => table.name.startsWith(prefix)), `every table is in the ${prefix} namespace`, ); check( metadata.tables.every((table) => table.foreignKeys.every((key) => owned.has(key.table))), "every foreign key targets one of this module's own tables", ); } const sdkVersion = (await json("node_modules/@ekklesia/module-sdk/package.json")).version; check( satisfiesCaret(sdkVersion, manifest.compatibility.moduleSdk), `the installed module SDK ${sdkVersion} satisfies compatibility.moduleSdk ${manifest.compatibility.moduleSdk}`, ); if (migrations) { const directory = migrations.endsWith("/") ? migrations : `${migrations}/`; const journal = parseDrizzleMigrationJournal(await json(`${directory}meta/_journal.json`)); const policy = parseModuleMigrationPolicy(await json(`${directory}meta/_ekklesia.json`)); check(policy.moduleId === manifest.id, "the migration policy is this module's"); check( journal.entries.map((entry) => entry.tag).join() === policy.entries.map((entry) => entry.tag).join(), "the migration journal and policy name the same migrations in the same order", ); for (const entry of journal.entries) { check(await exists(`${directory}${entry.tag}.sql`), `${entry.tag}.sql is present`); } } for (const description of passes) { console.log(`ok ${description}`); } for (const description of failures) { console.log(`FAIL ${description}`); } if (failures.length > 0) { console.error(`validate: ${failures.length} check(s) failed.`); process.exit(1); } console.log(`validate: ${manifest.id}@${manifest.version} passed ${passes.length} checks.`);