import { readFile, symlink } from "node:fs/promises"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { describeElf, ElfFormatError } from "../src/elf.js"; import { type ModulePackErrorCode, packModule } from "../src/index.js"; import { cleanupFixtures, type FakePackage, type FixtureModuleOptions, fixtureModule, installedPackageFile, SOURCE_REVISION, scratchDirectory, } from "./support/fixture-module.js"; import { fileEntries, readTarGz } from "./support/tar.js"; /** * The native closure rules (ADR 0049), with fake packages built around the real sharp addon's bytes, * so each rule can be broken on its own: which packages are copied, which are left out, and what * fails closed. */ afterEach(cleanupFixtures); const MIT = "MIT License\n\nCopyright (c) 2026 Example Author\n\nPermission is hereby granted.\n"; async function sharpAddon(): Promise { const sharp = await installedPackageFile("sharp"); return readFile( path.join(path.dirname(sharp), "@img", "sharp-linux-x64", "lib", "sharp-linux-x64-0.35.5.node"), ); } function wrapper(extra: Record = {}): FakePackage { return { files: { "index.js": "export default 1;\n", LICENSE: MIT, "index.d.ts": "export {};\n" }, name: "fake-native", packageJson: { dependencies: { "fake-helper": "1.0.0" }, devDependencies: { "fake-devtool": "1.0.0" }, optionalDependencies: { "fake-native-darwin-arm64": "1.0.0", "fake-native-linux-x64-gnu": "1.0.0", "fake-native-linux-x64-musl": "1.0.0", }, ...extra, }, }; } async function platformPackages(addon: Uint8Array): Promise { return [ { files: { "lib/fake.node": addon, LICENSE: MIT, "index.js": "module.exports = 1;\n" }, name: "fake-native-linux-x64-gnu", packageJson: { cpu: ["x64"], libc: ["glibc"], os: ["linux"], type: "commonjs" }, }, { files: { "lib/fake.node": addon, LICENSE: MIT }, name: "fake-native-darwin-arm64", packageJson: { cpu: ["arm64"], os: ["darwin"] }, }, { files: { "lib/fake.node": addon, LICENSE: MIT }, name: "fake-native-linux-x64-musl", packageJson: { cpu: ["x64"], libc: ["musl"], os: ["linux"] }, }, { files: { "index.js": "export default 2;\n", LICENSE: MIT }, name: "fake-helper" }, { files: { "index.js": "export default 3;\n", LICENSE: MIT }, name: "fake-devtool" }, ]; } const importsFakeNative = 'import value from "fake-native";\nexport default { id: "example", version: "0.1.0", register() { return value; } };\n'; async function packNative(options: FixtureModuleOptions, target = "linux-x64-glibc") { const moduleDirectory = await fixtureModule(options); const outDirectory = await scratchDirectory("ekklesia-native-closure-out-"); const result = await packModule({ licenseFile: path.join(moduleDirectory, "LICENSE"), moduleDirectory, nodeRange: ">=24.11.0 <25", outDirectory, sourceRevision: SOURCE_REVISION, target, }); return { archive: await readFile(result.archivePath), result }; } async function refusal( options: FixtureModuleOptions, code: ModulePackErrorCode, target = "linux-x64-glibc", ): Promise { await expect(packNative(options, target)).rejects.toMatchObject({ code }); } describe("the native closure", () => { it("copies the native package, its production dependencies, and only the matching platform package", async () => { const { archive, result } = await packNative({ fakePackages: [wrapper(), ...(await platformPackages(await sharpAddon()))], serverSource: importsFakeNative, }); const files = [...fileEntries(archive).keys()].map((name) => name.replace(/^example\//u, "")); const copied = new Set( files.filter((name) => name.startsWith("node_modules/")).map((name) => name.split("/")[1]), ); expect([...copied].sort()).toEqual(["fake-helper", "fake-native", "fake-native-linux-x64-gnu"]); // Every copied file is a real file; declarations never ship. expect( readTarGz(archive).every((entry) => entry.typeflag === "0" || entry.typeflag === "5"), ).toBe(true); expect(files).not.toContain("node_modules/fake-native/index.d.ts"); expect(files).toContain("node_modules/fake-native-linux-x64-gnu/lib/fake.node"); const metadata = JSON.parse( fileEntries(archive).get("example/ekklesia.package.json")?.toString("utf8") ?? "{}", ); // The sharp addon alone: Node-API 9, and GLIBC_2.17 is the newest symbol version it needs. expect(metadata.runtime).toEqual({ libcVersion: ">=2.17.0", node: ">=24.11.0 <25", nodeApi: 9, }); expect(result.target).toBe("linux-x64-glibc"); const notices = fileEntries(archive).get("example/THIRD_PARTY_NOTICES")?.toString("utf8") ?? ""; expect(notices).toContain( "fake-native-linux-x64-gnu 1.0.0\nLicense: MIT\nUsed in: node_modules", ); expect(notices).not.toContain("fake-devtool"); expect(notices).not.toContain("fake-native-darwin-arm64"); }); it("refuses a required dependency that is not installed", async () => { const packages = (await platformPackages(await sharpAddon())).filter( (fake) => fake.name !== "fake-helper", ); await refusal( { fakePackages: [wrapper(), ...packages], serverSource: importsFakeNative }, "MODULE_PACK_NATIVE_DEPENDENCY_MISSING", ); }); it("refuses a required dependency built for another platform", async () => { await refusal( { fakePackages: [ wrapper({ dependencies: { "fake-helper": "1.0.0", "fake-required-darwin": "1.0.0" } }), ...(await platformPackages(await sharpAddon())), { files: { "index.js": "export default 4;\n", LICENSE: MIT }, name: "fake-required-darwin", packageJson: { os: ["darwin"] }, }, ], serverSource: importsFakeNative, }, "MODULE_PACK_NATIVE_TARGET_MISMATCH", ); }); it("refuses two versions of one package in the flat layout", async () => { const addon = await sharpAddon(); const packages = await platformPackages(addon); const platform = packages.find((fake) => fake.name === "fake-native-linux-x64-gnu"); const conflicting: FakePackage = { ...(platform as FakePackage), files: { ...(platform?.files ?? {}), "node_modules/fake-helper/package.json": JSON.stringify({ license: "MIT", name: "fake-helper", version: "2.0.0", }), "node_modules/fake-helper/LICENSE": MIT, }, packageJson: { ...(platform?.packageJson ?? {}), dependencies: { "fake-helper": "2.0.0" } }, }; await refusal( { fakePackages: [wrapper(), conflicting, ...packages.filter((fake) => fake !== platform)], serverSource: importsFakeNative, }, "MODULE_PACK_NATIVE_LAYOUT_CONFLICT", ); }); it("refuses an addon built for another machine, or one that is not an ELF object", async () => { const foreign = Buffer.from(await sharpAddon()); foreign.writeUInt16LE(183, 18); // EM_AARCH64 await refusal( { fakePackages: [wrapper(), ...(await platformPackages(foreign))], serverSource: importsFakeNative, }, "MODULE_PACK_NATIVE_TARGET_MISMATCH", ); await refusal( { fakePackages: [wrapper(), ...(await platformPackages(Buffer.from("not an addon")))], serverSource: importsFakeNative, }, "MODULE_PACK_NATIVE_TARGET_MISMATCH", ); }); it("refuses a link inside a native package", async () => { const moduleDirectory = await fixtureModule({ fakePackages: [wrapper(), ...(await platformPackages(await sharpAddon()))], serverSource: importsFakeNative, }); await symlink( "/etc/hostname", path.join(moduleDirectory, "node_modules", "fake-native-linux-x64-gnu", "escape"), ); await expect( packModule({ licenseFile: path.join(moduleDirectory, "LICENSE"), moduleDirectory, nodeRange: ">=24.11.0 <25", outDirectory: await scratchDirectory(), sourceRevision: SOURCE_REVISION, target: "linux-x64-glibc", }), ).rejects.toMatchObject({ code: "MODULE_PACK_NATIVE_PACKAGE_INVALID" }); }); it("refuses a native dependency that does not support the package's Node.js range", async () => { await refusal( { fakePackages: [ wrapper({ engines: { node: ">=99" } }), ...(await platformPackages(await sharpAddon())), ], serverSource: importsFakeNative, }, "MODULE_PACK_NATIVE_NODE_INCOMPATIBLE", ); }); it("refuses a native target for a module with no native addon", async () => { await refusal({}, "MODULE_PACK_NATIVE_TARGET_UNNEEDED"); }); it("refuses targets it does not produce", async () => { for (const target of ["linux-arm64-glibc", "linux-x64-musl", "darwin-arm64", "win32-x64"]) { await refusal({}, "MODULE_PACK_TARGET_UNSUPPORTED", target); } }); it("still refuses the same native package in an any-target package", async () => { await refusal( { fakePackages: [wrapper(), ...(await platformPackages(await sharpAddon()))], serverSource: importsFakeNative, }, "MODULE_PACK_NATIVE_DEPENDENCY", "any", ); }); }); describe("reading an ELF object", () => { it("reads the machine, needed libraries, symbol versions, and declared Node-API version", async () => { const description = describeElf(await sharpAddon()); expect(description.machine).toBe(62); expect(description.needed).toContain("libvips-cpp.so.8.18.7"); expect(description.exported.has("napi_register_module_v1")).toBe(true); expect(description.nodeApiModuleVersion).toBe(9); expect(description.requiredVersions).toContain("GLIBC_2.17"); }); it("refuses what it cannot read", () => { expect(() => describeElf(Buffer.from("\u007fELF"))).toThrow(ElfFormatError); const thirtyTwoBit = Buffer.alloc(64); Buffer.from([0x7f, 0x45, 0x4c, 0x46, 1, 1]).copy(thirtyTwoBit); expect(() => describeElf(thirtyTwoBit)).toThrow(ElfFormatError); }); });