import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; import { lstat, readdir, readFile, realpath } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { verifyModulePackage } from "@ekklesia/core/modules"; import { afterEach, describe, expect, it } from "vitest"; import { packModule } from "../src/index.js"; import { cleanupFixtures, SOURCE_REVISION, scratchDirectory } from "./support/fixture-module.js"; import { fileEntries } from "./support/tar.js"; /** * The canonical template module, packaged end to end (ADR 0049): the real built template, the real * packager, and Core's real verifier. Turbo builds the template and the verifier before this suite * runs (both are development dependencies of this package), and runs the template's own tests, which * rebuild its UI in place, before this one (turbo.json). */ const execFileAsync = promisify(execFile); const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const repositoryRoot = path.resolve(packageRoot, "..", ".."); afterEach(cleanupFixtures); async function templateDirectory(): Promise { return realpath(path.join(packageRoot, "node_modules", "@ekklesia", "module-template")); } async function readJson(file: string): Promise> { return JSON.parse(await readFile(file, "utf8")) as Record; } async function platform() { const core = await readJson(path.join(repositoryRoot, "core", "package.json")); return { databaseRuntime: { "drizzle-orm": (core.dependencies as Record)["drizzle-orm"] ?? "", }, moduleSdkVersion: String( (await readJson(path.join(repositoryRoot, "packages", "module-sdk", "package.json"))).version, ), platformVersion: String((await readJson(path.join(repositoryRoot, "package.json"))).version), }; } async function fingerprint(file: string): Promise { try { return createHash("sha256") .update(await readFile(file)) .digest("hex"); } catch { return "absent"; } } async function treeListing(directory: string): Promise { const entries = await readdir(directory, { recursive: true, withFileTypes: true }); return entries .filter((entry) => !path.join(entry.parentPath, entry.name).includes(`${path.sep}node_modules`)) .map((entry) => path.relative(directory, path.join(entry.parentPath, entry.name))) .sort(); } async function packTemplate() { const outDirectory = await scratchDirectory("ekklesia-template-package-"); const result = await packModule({ moduleDirectory: await templateDirectory(), outDirectory, sourceRevision: SOURCE_REVISION, target: "any", }); return { archive: await readFile(result.archivePath), result }; } describe("the module template as a Module Package v1", () => { it("packs deterministically, without touching the workspace it was built in", async () => { const template = await templateDirectory(); const watched = [ path.join(repositoryRoot, "node_modules", ".pnpm-workspace-state-v1.json"), path.join(repositoryRoot, "node_modules", ".modules.yaml"), path.join(repositoryRoot, "pnpm-lock.yaml"), ]; const before = await Promise.all(watched.map(fingerprint)); const templateBefore = await treeListing(template); const first = await packTemplate(); const second = await packTemplate(); expect(first.result.sha256).toBe(second.result.sha256); expect(first.archive.equals(second.archive)).toBe(true); expect(path.basename(first.result.archivePath)).toMatch( /^example-\d+\.\d+\.\d+-any\.tar\.gz$/u, ); expect(await Promise.all(watched.map(fingerprint))).toEqual(before); expect(await treeListing(template)).toEqual(templateBefore); }); it("verifies against this platform and extracts to an unrelated directory that holds no link", async () => { const { result } = await packTemplate(); const target = path.join(await scratchDirectory("ekklesia-installed-elsewhere-"), "example"); const verified = await verifyModulePackage({ archivePath: result.archivePath, expectedSha256: result.sha256, extractTo: target, platform: await platform(), }); expect(verified).toMatchObject({ moduleId: "example", target: "any" }); for (const entry of await readdir(target, { recursive: true, withFileTypes: true })) { const stats = await lstat(path.join(entry.parentPath, entry.name)); expect(stats.isSymbolicLink(), entry.name).toBe(false); expect(stats.isFile() ? stats.nlink : 1, entry.name).toBe(1); } const listing = await treeListing(target); expect(listing).not.toContain("node_modules"); expect(listing.some((file) => /(^|\/)(src|tests)(\/|$)|\.d\.ts$|\.map$/u.test(file))).toBe( false, ); }); it("loads from its new location without resolving anything outside itself", async () => { const { result } = await packTemplate(); const target = path.join(await scratchDirectory("ekklesia-portable-"), "example"); await verifyModulePackage({ archivePath: result.archivePath, extractTo: target, platform: await platform(), }); // Every specifier the backend resolves while loading, recorded by Node itself. const script = ` import { registerHooks } from "node:module"; import { pathToFileURL } from "node:url"; const resolved = []; registerHooks({ resolve(specifier, context, next) { const result = next(specifier, context); resolved.push(result.url); return result; } }); for (const file of ["dist/server/index.js", "dist/schema/index.js"]) { await import(pathToFileURL(${JSON.stringify(target)} + "/" + file).href); } process.stdout.write(JSON.stringify(resolved)); `; const { stdout } = await execFileAsync( process.execPath, ["--input-type=module", "-e", script], { cwd: path.dirname(target), env: {}, }, ); const resolved = JSON.parse(stdout) as string[]; expect(resolved.length).toBeGreaterThan(0); for (const url of resolved) { const inside = url.startsWith(`file://${target}/`); expect(inside || url.startsWith("node:"), url).toBe(true); } }); it("credits every third-party package its bundles say they contain", async () => { const { archive } = await packTemplate(); const files = fileEntries(archive); const notices = files.get("example/THIRD_PARTY_NOTICES")?.toString("utf8") ?? ""; const bundled = new Set(); for (const [file, data] of files) { if (file.endsWith(".js")) { // esbuild names every bundled source file in a path comment. for (const match of data .toString("utf8") .matchAll(/node_modules\/((?:@[^/]+\/)?[^/.][^/]*)\//gu)) { if (match[1] && !match[1].startsWith("@ekklesia/")) { bundled.add(match[1]); } } } } expect([...bundled]).toEqual( expect.arrayContaining(["drizzle-orm", "react", "react-dom", "zod"]), ); for (const name of bundled) { expect(notices, name).toMatch( new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&")} \\S+$`, "mu"), ); } const licence = await readFile(path.join(packageRoot, "licenses", "Apache-2.0.txt")); expect(createHash("sha256").update(licence).digest("hex")).toBe( "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30", ); }); it("refers to no absolute path of the checkout it was built in", async () => { const { archive } = await packTemplate(); for (const [file, data] of fileEntries(archive)) { expect(data.toString("utf8"), file).not.toContain(repositoryRoot); } }); });