import { describe, expect, test } from "bun:test"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { buildServerBundle, readExtraRuntimeExternals } from "../build-server-bundle"; // Baut ein Mini-App-Fixture (bin/main.ts + bin/kumiko.ts teilen ein Modul) und // prüft das Variante-B-Verhalten: ein Bun.build-Call → server.js + kumiko.js als // Entries + geteilter chunk statt zwei vollen Bundles. function makeFixture(): string { const dir = mkdtempSync(join(tmpdir(), "server-bundle-")); mkdirSync(join(dir, "bin")); mkdirSync(join(dir, "src")); writeFileSync(join(dir, "package.json"), `${JSON.stringify({ name: "fixture-app" })}\n`); writeFileSync(join(dir, "src/shared.ts"), "export function shared(): number { return 42; }\n"); // main.ts → wird zu server.js umbenannt; kumiko.ts macht findRepoRoot zum // repoRoot dieses Fixtures, also wird kumiko.js gebaut. writeFileSync( join(dir, "bin/main.ts"), `import { shared } from "../src/shared";\nconsole.log("server", shared());\n`, ); writeFileSync( join(dir, "bin/kumiko.ts"), `import { shared } from "../src/shared";\nconsole.log("migrate", shared());\n`, ); return dir; } describe("buildServerBundle (multi-entry + splitting)", () => { test("produces server.js + kumiko.js as entries + a shared chunk", async () => { const dir = makeFixture(); try { const result = await buildServerBundle({ cwd: dir, outDir: join(dir, "dist-server") }); const outDir = result.outDir; // main.ts wurde zu server.js umbenannt. expect(existsSync(join(outDir, "server.js"))).toBe(true); expect(existsSync(join(outDir, "main.js"))).toBe(false); expect(existsSync(join(outDir, "kumiko.js"))).toBe(true); const entryFiles = result.entries.map((e) => e.file).sort(); expect(entryFiles).toEqual(["kumiko.js", "server.js"]); // Das geteilte Modul liegt in einem chunk, nicht in beiden Entries inlined. expect(result.chunks.length).toBeGreaterThanOrEqual(1); const serverSrc = readFileSync(join(outDir, "server.js"), "utf8"); expect(serverSrc).toContain("chunk-"); // Kein Legacy-Drizzle-Output mehr. expect(existsSync(join(outDir, "migration-hooks.js"))).toBe(false); expect(existsSync(join(outDir, "drizzle.config.ts"))).toBe(false); // Runtime-package.json mit start-Script, ohne drizzle-deps. const pkg = JSON.parse(readFileSync(join(outDir, "package.json"), "utf8")); expect(pkg.scripts.start).toBe("bun run server.js"); expect(Object.keys(pkg.dependencies)).not.toContain("drizzle-kit"); expect(Object.keys(pkg.dependencies)).not.toContain("drizzle-orm"); // Positive counterpart to the negative checks above — a runtime // external accidentally dropped from RUNTIME_EXTERNALS (into // BUILD_ONLY_EXTERNALS instead) would slip through silently. This // is the exact regression class behind the money-horse prod crash // ("Cannot find package 'meilisearch'"). expect(Object.keys(pkg.dependencies)).toContain("meilisearch"); expect(result.totalBytes).toBeGreaterThan(0); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("pins runtime-deps from node_modules/@cosmicdrift/* for consumer apps without a packages/ dir", async () => { // Reproduziert #1217: findRepoRoot liefert für eine Consumer-App die // App-Wurzel selbst (kein packages/-Ordner) — die Version muss aus den // installierten node_modules/@cosmicdrift/*-Packages kommen, nicht "*". const dir = makeFixture(); try { const frameworkPkgDir = join(dir, "node_modules/@cosmicdrift/kumiko-framework"); mkdirSync(frameworkPkgDir, { recursive: true }); writeFileSync( join(frameworkPkgDir, "package.json"), `${JSON.stringify({ name: "@cosmicdrift/kumiko-framework", dependencies: { meilisearch: "^0.58.0" } })}\n`, ); const bundledFeaturesPkgDir = join(dir, "node_modules/@cosmicdrift/kumiko-bundled-features"); mkdirSync(bundledFeaturesPkgDir, { recursive: true }); writeFileSync( join(bundledFeaturesPkgDir, "package.json"), `${JSON.stringify({ name: "@cosmicdrift/kumiko-bundled-features", dependencies: { ioredis: "^5.4.1" } })}\n`, ); const result = await buildServerBundle({ cwd: dir, outDir: join(dir, "dist-server") }); expect(result.runtimeDeps["meilisearch"]).toBe("^0.58.0"); expect(result.runtimeDeps["ioredis"]).toBe("^5.4.1"); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("prefers node_modules/@cosmicdrift/* over packages/* when both declare a version", async () => { // Framework-monorepo checkout: both the installed node_modules package // (symlinked in real life, diverging here) and packages/framework exist. // node_modules must win — it reflects what actually gets shipped/installed // for the consumer, packages/framework is only a repo-root fallback. const dir = makeFixture(); try { const frameworkPkgDir = join(dir, "node_modules/@cosmicdrift/kumiko-framework"); mkdirSync(frameworkPkgDir, { recursive: true }); writeFileSync( join(frameworkPkgDir, "package.json"), `${JSON.stringify({ name: "@cosmicdrift/kumiko-framework", dependencies: { meilisearch: "^0.58.0" } })}\n`, ); mkdirSync(join(dir, "packages/framework"), { recursive: true }); writeFileSync( join(dir, "packages/framework/package.json"), `${JSON.stringify({ name: "@cosmicdrift/kumiko-framework", dependencies: { meilisearch: "^0.30.0" } })}\n`, ); const result = await buildServerBundle({ cwd: dir, outDir: join(dir, "dist-server") }); expect(result.runtimeDeps["meilisearch"]).toBe("^0.58.0"); } finally { rmSync(dir, { recursive: true, force: true }); } }); }); describe("readExtraRuntimeExternals (package.json kumiko field, #1484)", () => { test("reads string list from kumiko.extraRuntimeExternals", () => { const dir = mkdtempSync(join(tmpdir(), "extra-ext-")); try { writeFileSync( join(dir, "package.json"), `${JSON.stringify({ name: "app", kumiko: { extraRuntimeExternals: ["@napi-rs/canvas", "pdf-parse"] }, })}\n`, ); expect(readExtraRuntimeExternals(dir)).toEqual(["@napi-rs/canvas", "pdf-parse"]); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("missing / invalid shapes yield empty list", () => { const dir = mkdtempSync(join(tmpdir(), "extra-ext-bad-")); try { writeFileSync(join(dir, "package.json"), `${JSON.stringify({ name: "app" })}\n`); expect(readExtraRuntimeExternals(dir)).toEqual([]); writeFileSync( join(dir, "package.json"), `${JSON.stringify({ name: "app", kumiko: { extraRuntimeExternals: "nope" } })}\n`, ); expect(readExtraRuntimeExternals(dir)).toEqual([]); writeFileSync( join(dir, "package.json"), `${JSON.stringify({ name: "app", kumiko: { extraRuntimeExternals: ["ok", 1, "", " "] }, })}\n`, ); expect(readExtraRuntimeExternals(dir)).toEqual(["ok"]); } finally { rmSync(dir, { recursive: true, force: true }); } }); }); describe("buildServerBundle + package.json extras (#1484)", () => { test("extraRuntimeExternals from options land in runtimeDeps with app pin", async () => { const dir = makeFixture(); try { writeFileSync( join(dir, "package.json"), `${JSON.stringify({ name: "fixture-app", dependencies: { "@napi-rs/canvas": "0.1.65" }, kumiko: { extraRuntimeExternals: ["@napi-rs/canvas"] }, })}\n`, ); const extras = readExtraRuntimeExternals(dir); const result = await buildServerBundle({ cwd: dir, outDir: join(dir, "dist-server"), extraRuntimeExternals: extras, }); expect(result.runtimeDeps["@napi-rs/canvas"]).toBe("0.1.65"); const distPkg = JSON.parse(readFileSync(join(dir, "dist-server/package.json"), "utf-8")) as { dependencies: Record; }; expect(distPkg.dependencies["@napi-rs/canvas"]).toBe("0.1.65"); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("app package.json does not override framework runtime pins", async () => { const dir = makeFixture(); try { const frameworkPkgDir = join(dir, "node_modules/@cosmicdrift/kumiko-framework"); mkdirSync(frameworkPkgDir, { recursive: true }); writeFileSync( join(frameworkPkgDir, "package.json"), `${JSON.stringify({ name: "@cosmicdrift/kumiko-framework", dependencies: { meilisearch: "^0.58.0" }, })}\n`, ); writeFileSync( join(dir, "package.json"), `${JSON.stringify({ name: "fixture-app", dependencies: { meilisearch: "^0.1.0", "@napi-rs/canvas": "0.1.65" }, kumiko: { extraRuntimeExternals: ["@napi-rs/canvas"] }, })}\n`, ); const result = await buildServerBundle({ cwd: dir, outDir: join(dir, "dist-server"), extraRuntimeExternals: readExtraRuntimeExternals(dir), }); expect(result.runtimeDeps["meilisearch"]).toBe("^0.58.0"); expect(result.runtimeDeps["@napi-rs/canvas"]).toBe("0.1.65"); } finally { rmSync(dir, { recursive: true, force: true }); } }); });