/** * Packaging bundles the module's hook-script runtime deps in full (ISS-0046): * scripts/node_modules must land in the .netapp's checksummed payload so a * module's hooks resolve their third-party deps (e.g. tldts) on a target with * no reachable package registry. Other node_modules trees stay * @celilo/capabilities-only. */ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; import { execFileSync } from 'node:child_process'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { buildModule, computeChecksums } from './build'; describe('computeChecksums — scripts/node_modules bundling (ISS-0046)', () => { let dir: string; function write(rel: string, content = 'x'): void { const full = join(dir, rel); mkdirSync(join(full, '..'), { recursive: true }); writeFileSync(full, content); } beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'celilo-build-test-')); write('manifest.yml', 'id: demo\n'); write('scripts/on-install.ts'); // Hook-script runtime closure — must all be bundled. write('scripts/node_modules/@celilo/capabilities/src/dns-internal.ts'); write('scripts/node_modules/tldts/index.js'); write('scripts/node_modules/drizzle-orm/index.js'); // .bin holds symlink shims — excluded. write('scripts/node_modules/.bin/tldts'); // A module-root node_modules: only @celilo/capabilities is kept. write('node_modules/@celilo/capabilities/index.js'); write('node_modules/lodash/index.js'); // e2e/ is never part of the deployed module. write('e2e/foo.test.ts'); }); afterEach(() => { try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ } }); it('bundles the full scripts/node_modules closure, drops .bin', async () => { const { files } = await computeChecksums(dir); const paths = Object.keys(files); expect(paths).toContain('scripts/node_modules/@celilo/capabilities/src/dns-internal.ts'); expect(paths).toContain('scripts/node_modules/tldts/index.js'); expect(paths).toContain('scripts/node_modules/drizzle-orm/index.js'); expect(paths).not.toContain('scripts/node_modules/.bin/tldts'); }); it('keeps only @celilo/capabilities in a non-scripts node_modules', async () => { const { files } = await computeChecksums(dir); const paths = Object.keys(files); expect(paths).toContain('node_modules/@celilo/capabilities/index.js'); expect(paths).not.toContain('node_modules/lodash/index.js'); }); it('excludes the module e2e/ directory and the manifest stays', async () => { const { files } = await computeChecksums(dir); const paths = Object.keys(files); expect(paths).toContain('manifest.yml'); expect(paths).toContain('scripts/on-install.ts'); expect(paths.some((p) => p.startsWith('e2e/'))).toBe(false); }); }); /** * The pack-staging branch must not eat the hook runtime (celilo#1310). * * `buildModule` stages through `bun pm pack` whenever a module has a root * package.json, and npm-pack semantics drop every `node_modules` — a `files` * entry naming the path does not override it. That silently contradicted * `includeNodeModulesPath`, which exists to ship `scripts/node_modules` in full * so hooks resolve their deps on a target with no reachable registry. * * Nothing in `modules/` can reach this branch: every module there has only * `scripts/package.json` and takes the `cpSync` path. So the regression is only * visible against a fixture shaped like a module that is ALSO a bun project. */ describe('buildModule — a root package.json must not strip the hook runtime', () => { let source: string; let out: string; function write(rel: string, content: string): void { const full = join(source, rel); mkdirSync(join(full, '..'), { recursive: true }); writeFileSync(full, content, 'utf-8'); } beforeEach(() => { source = mkdtempSync(join(tmpdir(), 'celilo-pack-root-pkg-')); out = join(mkdtempSync(join(tmpdir(), 'celilo-pack-out-')), 'm.netapp'); write('manifest.yml', 'celilo_contract: "1.0"\nid: root-pkg-module\nname: M\nversion: 0.1.0\n'); // The shape that triggers it: a module that is also a bun project. `files` // deliberately names scripts/ — proving the loss is npm-pack semantics and // not a missing entry. write( 'package.json', JSON.stringify({ name: 'root-pkg-module', version: '0.1.0', private: true, files: ['manifest.yml', 'scripts/'], }), ); write('scripts/on_install.ts', "import '@celilo/capabilities';\n"); write( 'scripts/package.json', JSON.stringify({ name: 'm-hooks', dependencies: { tldts: '^7.0.0' } }), ); write('scripts/node_modules/tldts/index.js', 'module.exports = {};\n'); write( 'scripts/node_modules/tldts/package.json', JSON.stringify({ name: 'tldts', version: '7.4.12' }), ); write('scripts/node_modules/.bin/tldts', '#!/bin/sh\n'); }); afterEach(() => { rmSync(source, { recursive: true, force: true }); rmSync(join(out, '..'), { recursive: true, force: true }); }); it('ships scripts/node_modules even though bun pm pack dropped it', async () => { const result = await buildModule({ sourceDir: source, outputPath: out }); expect(result.success).toBe(true); const listed = execFileSync('tar', ['tzf', out], { encoding: 'utf-8' }).split('\n'); expect(listed).toContain('scripts/node_modules/tldts/index.js'); // The one thing the runtime closure still excludes. expect(listed).not.toContain('scripts/node_modules/.bin/tldts'); }); });