1import * as Effect from "effect/Effect";
2import * as NodeFs from "node:fs";
3import * as NodeOs from "node:os";
4import * as NodePath from "node:path";
5import { afterAll, beforeAll, describe, expect, it } from "vitest";
6import {
7 bundleWorker,
8 CREATE_REQUIRE_BANNER,
9 WORKER_ENTRY_NAME,
10} from "../Bundle.ts";
11
12/**
13 * A synthetic `.open-next` output exercising the four final-bundle rules:
14 * relative imports that must be inlined (`middleware/handler.mjs`,
15 * `.build/durable-objects/*.js`), the dynamic server-handler import that must
16 * stay a lazy chunk, an absolute `?module`-suffixed `.wasm` import, a
17 * `cloudflare:*` external, and a CJS-style `require` of a node builtin.
18 */
19const makeOpenNextFixture = (root: string) => {
20 const openNext = NodePath.join(root, ".open-next");
21 NodeFs.mkdirSync(NodePath.join(openNext, "middleware"), { recursive: true });
22 NodeFs.mkdirSync(NodePath.join(openNext, ".build", "durable-objects"), {
23 recursive: true,
24 });
25 NodeFs.mkdirSync(NodePath.join(openNext, "server-functions", "default"), {
26 recursive: true,
27 });
28
29 const wasmPath = NodePath.join(
30 openNext,
31 "server-functions",
32 "default",
33 "example.wasm",
34 );
35 // Minimal valid wasm header: "\0asm" + version 1.
36 NodeFs.writeFileSync(
37 wasmPath,
38 Buffer.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]),
39 );
40
41 NodeFs.writeFileSync(
42 NodePath.join(openNext, WORKER_ENTRY_NAME),
43 [
44 `import { env } from "cloudflare:workers";`,
45 `import { handleMiddleware } from "./middleware/handler.mjs";`,
46 `export { DOQueueHandler } from "./.build/durable-objects/queue.js";`,
47 `export default {`,
48 ` async fetch(request) {`,
49 ` handleMiddleware(env);`,
50 ` const { default: handler } = await import("./server-functions/default/handler.mjs");`,
51 ` return handler(request);`,
52 ` },`,
53 `};`,
54 ].join("\n"),
55 );
56 NodeFs.writeFileSync(
57 NodePath.join(openNext, "middleware", "handler.mjs"),
58 `export const handleMiddleware = () => "MIDDLEWARE_MARKER";`,
59 );
60 NodeFs.writeFileSync(
61 NodePath.join(openNext, ".build", "durable-objects", "queue.js"),
62 `export class DOQueueHandler {}`,
63 );
64 NodeFs.writeFileSync(
65 NodePath.join(openNext, "server-functions", "default", "handler.mjs"),
66 [
67 // The absolute `?module` import OpenNext's setWranglerExternal leaves
68 // behind "for wrangler to bundle".
69 `import wasm from ${JSON.stringify(`${wasmPath}?module`)};`,
70 // CJS-converted Next server code requires node builtins dynamically.
71 `const pickRequire = () => require("fs");`,
72 `export default () => ["HANDLER_MARKER", wasm, pickRequire];`,
73 ].join("\n"),
74 );
75 return openNext;
76};
77
78describe("bundleWorker", () => {
79 const root = NodeFs.mkdtempSync(
80 NodePath.join(NodeOs.tmpdir(), "distilled-nextjs-bundle-"),
81 );
82 const outDirectory = NodePath.join(root, "dist", "worker");
83 let files: Array<string> = [];
84
85 beforeAll(async () => {
86 const openNextDirectory = makeOpenNextFixture(root);
87 await Effect.runPromise(bundleWorker({ openNextDirectory, outDirectory }));
88 // Windows readdir yields backslash-separated relative paths; the
89 // assertions below match POSIX module-name shapes.
90 files = NodeFs.readdirSync(outDirectory, { recursive: true }).map((file) =>
91 String(file).replaceAll("\\", "/"),
92 );
93 });
94
95 afterAll(() => {
96 NodeFs.rmSync(root, { recursive: true, force: true });
97 });
98
99 const read = (name: string) =>
100 NodeFs.readFileSync(NodePath.join(outDirectory, name), "utf8");
101
102 it("emits the worker entry with the createRequire banner", () => {
103 expect(files).toContain(WORKER_ENTRY_NAME);
104 const entry = read(WORKER_ENTRY_NAME);
105 for (const line of CREATE_REQUIRE_BANNER.split("\n")) {
106 expect(entry).toContain(line);
107 }
108 // The `import.meta.url ?? "file:///"` fallback is load-bearing: workerd
109 // modules have an undefined import.meta.url.
110 expect(entry).toContain(`import.meta.url ?? "file:///"`);
111 });
112
113 it("inlines the middleware and durable-object imports into the entry", () => {
114 const entry = read(WORKER_ENTRY_NAME);
115 expect(entry).toContain("MIDDLEWARE_MARKER");
116 expect(entry).toContain("DOQueueHandler");
117 });
118
119 it("keeps the server handler a lazy chunk", () => {
120 const entry = read(WORKER_ENTRY_NAME);
121 expect(entry).not.toContain("HANDLER_MARKER");
122 const chunks = files.filter(
123 (file) => file.startsWith("chunks/") && file.endsWith(".js"),
124 );
125 expect(chunks.length).toBeGreaterThan(0);
126 expect(chunks.some((chunk) => read(chunk).includes("HANDLER_MARKER"))).toBe(
127 true,
128 );
129 });
130
131 it("keeps cloudflare:* and node:* imports external", () => {
132 const entry = read(WORKER_ENTRY_NAME);
133 expect(entry).toContain(`"cloudflare:workers"`);
134 expect(entry).toContain(`"node:module"`);
135 });
136
137 it("copies ?module wasm imports out as .wasm files with relative imports", () => {
138 const wasmFiles = files.filter((file) => file.endsWith(".wasm"));
139 expect(wasmFiles.length).toBe(1);
140 const chunks = files.filter(
141 (file) => file.startsWith("chunks/") && file.endsWith(".js"),
142 );
143 const importer = chunks.find((chunk) =>
144 read(chunk).includes("HANDLER_MARKER"),
145 );
146 expect(importer).toBeDefined();
147 const content = read(importer!);
148 expect(content).not.toContain("?module");
149 expect(content).toContain(NodePath.basename(wasmFiles[0]!));
150 });
151});