import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import { isBuiltin } from "node:module"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { ModulePackError, type ModulePackErrorCode, packModule } from "../src/index.js"; import { cleanupFixtures, type FixtureModuleOptions, fixtureModule, SOURCE_REVISION, scratchDirectory, } from "./support/fixture-module.js"; import { fileEntries, readTarGz } from "./support/tar.js"; afterEach(cleanupFixtures); const MIT = "MIT License\n\nCopyright (c) 2026 Example Author\n\nPermission is hereby granted.\n"; async function pack(options: FixtureModuleOptions = {}, overrides: Record = {}) { const moduleDirectory = await fixtureModule(options); const outDirectory = await scratchDirectory("ekklesia-pack-out-"); const result = await packModule({ moduleDirectory, nodeRange: ">=24.11.0 <25", outDirectory, sourceRevision: SOURCE_REVISION, target: "any", ...overrides, }); return { archive: await readFile(result.archivePath), moduleDirectory, outDirectory, result }; } async function packRejection( options: FixtureModuleOptions, code: ModulePackErrorCode, overrides: Record = {}, ): Promise { try { await pack(options, overrides); } catch (error) { expect(error).toBeInstanceOf(ModulePackError); expect((error as ModulePackError).code).toBe(code); return error as ModulePackError; } throw new Error(`Expected ${code}.`); } describe("packModule: what a package contains (ADR 0049)", () => { it("produces one archive and its sha256sum line, under the recommended name", async () => { const { archive, outDirectory, result } = await pack(); const digest = createHash("sha256").update(archive).digest("hex"); expect(path.basename(result.archivePath)).toBe("example-0.1.0-any.tar.gz"); expect(result.sha256).toBe(digest); expect(await readFile(path.join(outDirectory, "example-0.1.0-any.tar.gz.sha256"), "utf8")).toBe( `${digest} example-0.1.0-any.tar.gz\n`, ); }); it("holds exactly the runtime payload, under one top-level directory named with the module id", async () => { const { archive } = await pack({ withSchema: true }); const files = [...fileEntries(archive).keys()].sort(); expect(files).toEqual([ "example/THIRD_PARTY_NOTICES", expect.stringMatching(/^example\/dist\/chunks\/chunk-[A-Z0-9]+\.js$/u), "example/dist/schema/index.js", "example/dist/server/index.js", "example/drizzle/0000_initial.sql", "example/drizzle/meta/0000_snapshot.json", "example/drizzle/meta/_ekklesia.json", "example/drizzle/meta/_journal.json", "example/ekklesia.module.json", "example/ekklesia.package.json", "example/package.json", ]); for (const file of files) { expect(file).not.toMatch(/\/src\/|\/tests\/|\.d\.ts$|\.map$|README|node_modules/u); } expect(new Set(readTarGz(archive).map((entry) => entry.path.split("/")[0]))).toEqual( new Set(["example"]), ); }); it("generates the runtime package.json instead of copying the development one", async () => { const { archive } = await pack(); const runtimePackage = fileEntries(archive).get("example/package.json")?.toString("utf8"); expect(JSON.parse(runtimePackage ?? "")).toEqual({ name: "@test/module-example", version: "0.1.0", private: true, type: "module", ekklesia: { layer: "module" }, }); for (const [file, data] of fileEntries(archive)) { if (file.endsWith(".json")) { expect(data.toString("utf8"), file).not.toContain("workspace:"); } } }); it("copies the manifest byte for byte", async () => { const { archive, moduleDirectory } = await pack(); expect(fileEntries(archive).get("example/ekklesia.module.json")).toEqual( await readFile(path.join(moduleDirectory, "ekklesia.module.json")), ); }); it("describes the package completely in ekklesia.package.json, never hashing itself", async () => { const { archive } = await pack({ withSchema: true }); const files = fileEntries(archive); const metadata = JSON.parse(files.get("example/ekklesia.package.json")?.toString("utf8") ?? ""); expect(metadata).toMatchObject({ packageFormat: 1, module: { id: "example", version: "0.1.0" }, target: "any", runtime: { node: ">=24.11.0 <25" }, provenance: { revision: SOURCE_REVISION, packager: { name: "@ekklesia/module-pack", version: expect.any(String) }, }, incorporates: { "@ekklesia/contracts": expect.any(String), "@ekklesia/module-sdk": expect.any(String), }, transitional: { databaseRuntime: { "drizzle-orm": "0.45.2" } }, }); const listed = metadata.files.map((file: { path: string }) => `example/${file.path}`); expect(listed).not.toContain("example/ekklesia.package.json"); expect(new Set(listed)).toEqual( new Set([...files.keys()].filter((file) => file !== "example/ekklesia.package.json")), ); for (const file of metadata.files as { path: string; size: number; sha256: string }[]) { const data = files.get(`example/${file.path}`); expect(data?.length).toBe(file.size); expect( createHash("sha256") .update(data ?? "") .digest("hex"), ).toBe(file.sha256); } }); it("records only the provenance it was given, and a repository only when asked", async () => { const { archive } = await pack({}, { repository: "https://example.org/acme/example.git" }); const metadata = JSON.parse( fileEntries(archive).get("example/ekklesia.package.json")?.toString("utf8") ?? "", ); expect(metadata.provenance.repository).toBe("https://example.org/acme/example.git"); }); it("omits transitional database facts from a module that bundles no Drizzle", async () => { const { archive } = await pack({ serverSource: 'export default { id: "example", version: "0.1.0", register() {} };\n', }); const metadata = JSON.parse( fileEntries(archive).get("example/ekklesia.package.json")?.toString("utf8") ?? "", ); expect(metadata.transitional).toBeUndefined(); expect(metadata.incorporates).toEqual({}); }); }); describe("packModule: a self-contained backend", () => { it("bundles the SDK, contracts, and Drizzle, leaving only Node built-ins to import", async () => { const { archive } = await pack({ withSchema: true }); for (const [file, data] of fileEntries(archive)) { if (!file.endsWith(".js")) { continue; } const code = data.toString("utf8"); const specifiers = [ ...code.matchAll(/(?:^|\n)\s*(?:import|export)\s[^;]*?from\s*"([^"]+)"/gu), ...code.matchAll(/(?:^|\n)\s*import\s*"([^"]+)"/gu), ...code.matchAll(/\bimport\(\s*"([^"]+)"\s*\)/gu), ].map((match) => match[1] ?? ""); for (const specifier of specifiers) { expect(specifier.startsWith(".") || isBuiltin(specifier), `${file}: ${specifier}`).toBe( true, ); } } }); it("bundles ABI-v1 Drizzle as a module detail without transitional runtime metadata", async () => { const { archive } = await pack({ databaseAbi: 1, serverSource: `import { moduleDrizzle } from "@ekklesia/module-database/drizzle"; export const database = (executor) => moduleDrizzle(executor); export default { database: { abi: 1 }, id: "example", version: "0.1.0", register() {} }; `, withModuleDatabase: true, }); const files = fileEntries(archive); const metadata = JSON.parse(files.get("example/ekklesia.package.json")?.toString("utf8") ?? ""); expect(metadata.incorporates).toMatchObject({ "@ekklesia/module-database": expect.any(String), }); expect(metadata.transitional).toBeUndefined(); for (const [file, data] of files) { if (!file.endsWith(".js")) { continue; } const code = data.toString("utf8"); const specifiers = [ ...code.matchAll(/(?:^|\n)\s*(?:import|export)\s[^;]*?from\s*"([^"]+)"/gu), ...code.matchAll(/(?:^|\n)\s*import\s*"([^"]+)"/gu), ...code.matchAll(/\bimport\(\s*"([^"]+)"\s*\)/gu), ].map((match) => match[1] ?? ""); for (const specifier of specifiers) { expect(specifier.startsWith(".") || isBuiltin(specifier), `${file}: ${specifier}`).toBe( true, ); } } }); it("keeps legacy-v8 Drizzle runtime metadata unchanged", async () => { const { archive } = await pack({ withSchema: true }); const metadata = JSON.parse( fileEntries(archive).get("example/ekklesia.package.json")?.toString("utf8") ?? "", ); expect(metadata.transitional).toEqual({ databaseRuntime: { "drizzle-orm": "0.45.2" } }); }); it("refuses a dependency the bundle cannot resolve rather than leaving it external", async () => { await packRejection( { serverSource: 'import pad from "ekklesia-not-installed";\nexport default { id: "example", version: "0.1.0", register() { pad(); } };\n', }, "MODULE_PACK_BUNDLE_FAILED", ); }); it.each([ [ "a package that declares an operating system", { name: "fake-native", packageJson: { os: ["linux"], cpu: ["x64"] }, files: { "index.js": "export default 1;\n" }, }, ], [ "a package built with node-gyp", { name: "fake-gyp", packageJson: { gypfile: true }, files: { "index.js": "export default 1;\n", "binding.gyp": "{}" }, }, ], [ "a package that ships a compiled addon", { name: "fake-addon", files: { "index.js": "export default 1;\n", "build/Release/addon.node": new Uint8Array([0x7f, 0x45, 0x4c, 0x46]), }, }, ], ])("refuses %s in an any-target package", async (_label, fake) => { await packRejection( { fakePackages: [fake], serverSource: `import value from "${fake.name}";\nexport default { id: "example", version: "0.1.0", register() { return value; } };\n`, }, "MODULE_PACK_NATIVE_DEPENDENCY", ); }); it("refuses a package whose installed dependency is platform-specific", async () => { await packRejection( { fakePackages: [ { name: "fake-wrapper", packageJson: { optionalDependencies: { "fake-wrapper-linux-x64": "1.0.0" } }, files: { "index.js": "export default 1;\n", LICENSE: MIT }, }, { name: "fake-wrapper-linux-x64", packageJson: { os: ["linux"], cpu: ["x64"] }, files: { "index.js": "export default 2;\n" }, }, ], serverSource: 'import value from "fake-wrapper";\nexport default { id: "example", version: "0.1.0", register() { return value; } };\n', }, "MODULE_PACK_NATIVE_DEPENDENCY", ); }); it("refuses any target but any in this release", async () => { await packRejection({}, "MODULE_PACK_TARGET_UNSUPPORTED", { target: "linux-x64-glibc" }); }); }); describe("packModule: identity and inputs", () => { it("refuses a development package.json whose version differs from the manifest", async () => { await packRejection( { manifest: (manifest) => ({ ...manifest, version: "0.2.0" }) }, "MODULE_PACK_IDENTITY_MISMATCH", ); }); it("refuses a module whose backend was never built", async () => { await packRejection( { manifest: (manifest) => ({ ...manifest, entrypoints: { server: "./dist/missing/index.js" }, }), }, "MODULE_PACK_ENTRYPOINT_MISSING", ); }); it("refuses an abbreviated or missing source revision", async () => { await packRejection({}, "MODULE_PACK_REVISION_INVALID", { sourceRevision: "86ae907" }); }); it("refuses an incorporated SDK outside the manifest's own range", async () => { await packRejection( { manifest: (manifest) => ({ ...manifest, compatibility: { platform: "^0.1.0", moduleSdk: "^9.0.0" }, }), }, "MODULE_PACK_SDK_INCOMPATIBLE", ); }); }); describe("packModule: THIRD_PARTY_NOTICES", () => { const usesFake = (name: string) => `import value from "${name}";\nexport default { id: "example", version: "0.1.0", register() { return value; } };\n`; it("credits every bundled third-party package once, with its licence and notice text", async () => { const { archive } = await pack({ fakePackages: [ { name: "fake-mit", version: "2.3.4", files: { "index.js": 'import other from "fake-apache-notice";\nexport default other;\n', LICENSE: MIT, }, }, { name: "fake-apache-notice", version: "1.0.0", packageJson: { license: "Apache-2.0" }, files: { "index.js": "export default 1;\n", "LICENSE.txt": "Apache License text shipped by the package.\n", NOTICE: "Fake Apache Notice\nCopyright 2026 The Fake Foundation\n", }, }, ], serverSource: usesFake("fake-mit"), }); const notices = fileEntries(archive).get("example/THIRD_PARTY_NOTICES")?.toString("utf8") ?? ""; expect(notices).toContain("fake-mit 2.3.4"); expect(notices).toContain("Copyright (c) 2026 Example Author"); expect(notices).toContain("fake-apache-notice 1.0.0"); expect(notices).toContain("Copyright 2026 The Fake Foundation"); expect(notices.match(/^fake-mit 2\.3\.4$/gmu)).toHaveLength(1); // Only what the bundle actually contains is credited: this backend uses no Drizzle code. expect(notices).not.toContain("drizzle-orm"); // The Ekklesia platform's own packages are credited in ekklesia.package.json, not here. expect(notices).not.toMatch(/^@ekklesia\//mu); }); it("supplies the standard Apache-2.0 text only for a package that ships none, and says so", async () => { const { archive } = await pack({ withSchema: true }); const notices = fileEntries(archive).get("example/THIRD_PARTY_NOTICES")?.toString("utf8") ?? ""; const drizzle = notices.slice(notices.indexOf("drizzle-orm 0.45.2")); expect(drizzle).toContain("License: Apache-2.0"); expect(drizzle).toContain("ships no licence file"); expect(drizzle).toContain("TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION"); }); it("is byte-identical across packages of the same inputs", async () => { const options = { withSchema: true }; const first = await pack(options); const second = await pack(options); expect(fileEntries(first.archive).get("example/THIRD_PARTY_NOTICES")).toEqual( fileEntries(second.archive).get("example/THIRD_PARTY_NOTICES"), ); }); it("refuses a bundled package that declares no licence", async () => { await packRejection( { fakePackages: [ { name: "fake-unlicensed", packageJson: { license: undefined }, files: { "index.js": "export default 1;\n" }, }, ], serverSource: usesFake("fake-unlicensed"), }, "MODULE_PACK_LICENCE_UNKNOWN", ); }); it("refuses an MIT package that ships no licence text, because only it holds the copyright line", async () => { await packRejection( { fakePackages: [{ name: "fake-textless", files: { "index.js": "export default 1;\n" } }], serverSource: usesFake("fake-textless"), }, "MODULE_PACK_LICENCE_TEXT_MISSING", ); }); }); describe("packModule: deterministic archives", () => { it("produces byte-identical archives and digests for identical inputs", async () => { const moduleDirectory = await fixtureModule({ withSchema: true }); const runs = []; for (let run = 0; run < 2; run += 1) { const outDirectory = await scratchDirectory("ekklesia-pack-out-"); const result = await packModule({ moduleDirectory, nodeRange: ">=24.11.0 <25", outDirectory, sourceRevision: SOURCE_REVISION, target: "any", }); runs.push({ bytes: await readFile(result.archivePath), sha256: result.sha256 }); } expect(runs[0]?.sha256).toBe(runs[1]?.sha256); expect(runs[0]?.bytes.equals(runs[1]?.bytes ?? Buffer.alloc(0))).toBe(true); }); it("normalizes every entry and the gzip header, and contains no links", async () => { const { archive } = await pack({ withSchema: true }); // gzip: no file name flag, no modification time. expect(archive[3]).toBe(0); expect(archive.readUInt32LE(4)).toBe(0); const entries = readTarGz(archive); const paths = entries.map((entry) => entry.path); expect(paths).toEqual( [...paths].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0)), ); for (const entry of entries) { expect(["0", "5"], entry.path).toContain(entry.typeflag); expect(entry.mode, entry.path).toBe(entry.typeflag === "5" ? 0o755 : 0o644); expect([entry.uid, entry.gid, entry.mtime]).toEqual([0, 0, 0]); expect([entry.uname, entry.gname]).toEqual(["", ""]); expect(entry.magic).toBe("ustar/00"); } // Every directory precedes what it contains. for (const entry of entries) { const parent = entry.path.split("/").slice(0, -1).join("/"); if (parent !== "") { expect(paths.indexOf(parent), entry.path).toBeLessThan(paths.indexOf(entry.path)); } } }); it("refers to no absolute path of the build machine", async () => { const { archive, moduleDirectory } = await pack({ withSchema: true }); const root = path.dirname(path.dirname(moduleDirectory)); for (const [file, data] of fileEntries(archive)) { expect(data.toString("utf8"), file).not.toContain(moduleDirectory); expect(data.toString("utf8"), file).not.toContain(root); } }); });