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]>
613 lines
24 KiB
TypeScript
613 lines
24 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { readFile, symlink, writeFile } 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<string, unknown> = {}) {
|
|
const moduleDirectory = await fixtureModule(options);
|
|
const outDirectory = await scratchDirectory("ekklesia-pack-out-");
|
|
const result = await packModule({
|
|
licenseFile: path.join(moduleDirectory, "LICENSE"),
|
|
moduleDirectory,
|
|
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<string, unknown> = {},
|
|
): Promise<ModulePackError> {
|
|
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/LICENSE",
|
|
"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("copies the explicit license bytes unchanged and records their source SPDX expression", async () => {
|
|
const licenseText = Buffer.from([0x4d, 0x49, 0x54, 0x0d, 0x0a, 0x00, 0xff]);
|
|
const { archive } = await pack({ license: "MIT OR Apache-2.0", licenseText });
|
|
const files = fileEntries(archive);
|
|
const packaged = files.get("example/LICENSE");
|
|
const metadata = JSON.parse(
|
|
files.get("example/ekklesia.package.json")?.toString("utf8") ?? "{}",
|
|
) as {
|
|
license?: string;
|
|
files?: { path: string; size: number; sha256: string }[];
|
|
};
|
|
|
|
expect(metadata.license).toBe("MIT OR Apache-2.0");
|
|
expect(packaged?.equals(licenseText)).toBe(true);
|
|
expect(metadata.files?.filter((file) => file.path === "LICENSE")).toEqual([
|
|
{
|
|
path: "LICENSE",
|
|
size: licenseText.length,
|
|
sha256: createHash("sha256").update(licenseText).digest("hex"),
|
|
},
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
["a missing declaration", undefined],
|
|
["an empty declaration", ""],
|
|
["UNLICENSED", "UNLICENSED"],
|
|
["legacy object metadata", { type: "MIT", url: "https://example.invalid/license" }],
|
|
["a malformed SPDX expression", "MIT OR"],
|
|
])("refuses %s", async (_label, license) => {
|
|
await packRejection({ license }, "MODULE_PACK_MODULE_LICENSE_INVALID");
|
|
});
|
|
|
|
it("requires a readable regular license-text input", async () => {
|
|
await packRejection({}, "MODULE_PACK_MODULE_LICENSE_FILE_INVALID", {
|
|
licenseFile: undefined,
|
|
});
|
|
await packRejection({}, "MODULE_PACK_MODULE_LICENSE_FILE_INVALID", {
|
|
licenseFile: path.join(await scratchDirectory("ekklesia-missing-license-"), "missing"),
|
|
});
|
|
await packRejection({}, "MODULE_PACK_MODULE_LICENSE_FILE_INVALID", {
|
|
licenseFile: await scratchDirectory("ekklesia-license-directory-"),
|
|
});
|
|
const emptyDirectory = await scratchDirectory("ekklesia-empty-license-");
|
|
await writeFile(path.join(emptyDirectory, "LICENSE"), "");
|
|
await packRejection({}, "MODULE_PACK_MODULE_LICENSE_FILE_INVALID", {
|
|
licenseFile: path.join(emptyDirectory, "LICENSE"),
|
|
});
|
|
const linkDirectory = await scratchDirectory("ekklesia-linked-license-");
|
|
await writeFile(path.join(linkDirectory, "real"), "MIT License\n");
|
|
await symlink(path.join(linkDirectory, "real"), path.join(linkDirectory, "LICENSE"));
|
|
await packRejection({}, "MODULE_PACK_MODULE_LICENSE_FILE_INVALID", {
|
|
licenseFile: path.join(linkDirectory, "LICENSE"),
|
|
});
|
|
});
|
|
|
|
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 targets it does not produce, and a native target a pure-JavaScript module does not need", async () => {
|
|
await packRejection({}, "MODULE_PACK_TARGET_UNSUPPORTED", { target: "linux-arm64-glibc" });
|
|
await packRejection({}, "MODULE_PACK_NATIVE_TARGET_UNNEEDED", { 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("takes the supported Node.js range from the module's own engines.node", async () => {
|
|
const { archive } = await pack({ engines: { node: ">=24.12.0 <25" } });
|
|
const metadata = JSON.parse(
|
|
fileEntries(archive).get("example/ekklesia.package.json")?.toString("utf8") ?? "",
|
|
);
|
|
expect(metadata.runtime).toEqual({ node: ">=24.12.0 <25" });
|
|
});
|
|
|
|
it("refuses a module that does not declare engines.node, without looking for a workspace", async () => {
|
|
for (const engines of [null, {}, { node: "" }, { node: 24 }]) {
|
|
const error = await packRejection({ engines }, "MODULE_PACK_RUNTIME_UNKNOWN");
|
|
expect(error.message).toBe(
|
|
"The module's package.json must declare the Node.js versions it supports in engines.node.",
|
|
);
|
|
}
|
|
});
|
|
|
|
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: recognising Ekklesia's packages by identity (ADR 0053)", () => {
|
|
async function metadataAndNotices(options: FixtureModuleOptions) {
|
|
const { archive } = await pack(options);
|
|
const files = fileEntries(archive);
|
|
return {
|
|
metadata: JSON.parse(files.get("example/ekklesia.package.json")?.toString("utf8") ?? ""),
|
|
notices: files.get("example/THIRD_PARTY_NOTICES")?.toString("utf8") ?? "",
|
|
};
|
|
}
|
|
|
|
it("incorporates registry-installed Ekklesia packages exactly as workspace-linked ones", async () => {
|
|
const options: FixtureModuleOptions = { databaseAbi: 1, withSchema: true };
|
|
const linked = await metadataAndNotices(options);
|
|
const installed = await metadataAndNotices({ ...options, ekklesiaInstall: "registry" });
|
|
expect(Object.keys(installed.metadata.incorporates).sort()).toEqual([
|
|
"@ekklesia/contracts",
|
|
"@ekklesia/module-database",
|
|
"@ekklesia/module-sdk",
|
|
]);
|
|
expect(installed.metadata.incorporates).toEqual(linked.metadata.incorporates);
|
|
// Never credited as third-party code, however they were installed.
|
|
expect(installed.notices).not.toContain("@ekklesia/");
|
|
expect(linked.notices).not.toContain("@ekklesia/");
|
|
});
|
|
|
|
it("still checks a registry-installed SDK against the manifest's moduleSdk range", async () => {
|
|
await packRejection(
|
|
{
|
|
ekklesiaInstall: "registry",
|
|
manifest: (manifest) => ({
|
|
...manifest,
|
|
compatibility: { platform: "^0.1.0", moduleSdk: "^9.0.0" },
|
|
}),
|
|
},
|
|
"MODULE_PACK_SDK_INCOMPATIBLE",
|
|
);
|
|
});
|
|
|
|
it("treats a package that only borrows the @ekklesia scope, or only the marker, as third-party", async () => {
|
|
const { metadata, notices } = await metadataAndNotices({
|
|
fakePackages: [
|
|
{
|
|
files: { "index.js": "export default 1;\n", LICENSE: MIT },
|
|
name: "@ekklesia/squatter",
|
|
packageJson: { ekklesia: { layer: "package" } },
|
|
},
|
|
{
|
|
files: { "index.js": "export default 2;\n", LICENSE: MIT },
|
|
name: "not-ekklesia",
|
|
packageJson: { ekklesia: { distribution: "module-development" } },
|
|
},
|
|
],
|
|
serverSource:
|
|
'import a from "@ekklesia/squatter";\nimport b from "not-ekklesia";\nexport default { id: "example", version: "0.1.0", register() { return a + b; } };\n',
|
|
});
|
|
expect(metadata.incorporates).toEqual({});
|
|
expect(notices).toContain("\n@ekklesia/squatter 1.0.0\nLicense: MIT\n");
|
|
expect(notices).toContain("\nnot-ekklesia 1.0.0\nLicense: MIT\n");
|
|
});
|
|
});
|
|
|
|
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 runs = [];
|
|
for (let run = 0; run < 2; run += 1) {
|
|
const moduleDirectory = await fixtureModule({ withSchema: true });
|
|
const outDirectory = await scratchDirectory("ekklesia-pack-out-");
|
|
const result = await packModule({
|
|
licenseFile: path.join(moduleDirectory, "LICENSE"),
|
|
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);
|
|
}
|
|
});
|
|
});
|