| 1 | import { createMiniflareFromRolldown } from "../../../../cloudflare-test-tools/src/miniflare/miniflare.ts"; |
| 2 | import { assert, describe, expect, it } from "vitest"; |
| 3 | import { fileURLToPath } from "node:url"; |
| 4 | import { toPosixPath } from "../utils.ts"; |
| 5 | import cloudflare from "../plugin.ts"; |
| 6 | import { buildFixture } from "./utils/build-fixture.ts"; |
| 7 | |
| 8 | describe("nodejs_compat", () => { |
| 9 | it("rewrites external CommonJS requires when compatibility enables Node by date", () => { |
| 10 | const plugins = cloudflare({ compatibilityDate: "2026-08-31" }); |
| 11 | |
| 12 | expect(plugins[0]?.name).toBe("builtin:esm-external-require"); |
| 13 | }); |
| 14 | |
| 15 | it("can omit the external require rewrite for single-module internal workers", () => { |
| 16 | const plugins = cloudflare({ |
| 17 | compatibilityDate: "2026-08-31", |
| 18 | externalRequire: false, |
| 19 | }); |
| 20 | |
| 21 | expect(plugins[0]?.name).not.toBe("builtin:esm-external-require"); |
| 22 | }); |
| 23 | |
| 24 | it("runs node builtin imports with nodejs_compat enabled", async () => { |
| 25 | const built = await buildFixture({ |
| 26 | fixture: "node-compat/index.ts", |
| 27 | pluginOptions: { |
| 28 | compatibilityDate: "2025-07-01", |
| 29 | compatibilityFlags: ["nodejs_compat"], |
| 30 | }, |
| 31 | }); |
| 32 | |
| 33 | await using miniflare = await createMiniflareFromRolldown(built.output, { |
| 34 | compatibilityDate: "2025-07-01", |
| 35 | compatibilityFlags: ["nodejs_compat"], |
| 36 | }); |
| 37 | |
| 38 | expect(await miniflare.fetchText("/")).toBe("OK!"); |
| 39 | }); |
| 40 | |
| 41 | it("creates and injects virtual globals for nodejs_compat entries", async () => { |
| 42 | const plugins = cloudflare({ |
| 43 | compatibilityDate: "2025-07-01", |
| 44 | compatibilityFlags: ["nodejs_compat"], |
| 45 | }) as Array<{ |
| 46 | name: string; |
| 47 | resolveId?: { handler?: (...args: Array<unknown>) => unknown }; |
| 48 | load?: { handler?: (...args: Array<unknown>) => unknown }; |
| 49 | transform?: (code: string, id: string) => string | undefined; |
| 50 | buildStart?: (options: { plugins: Array<unknown> }) => void; |
| 51 | }>; |
| 52 | |
| 53 | const virtualModulesPlugin = plugins.find( |
| 54 | (plugin) => plugin?.name === "distilled-cloudflare:virtual-modules", |
| 55 | ); |
| 56 | |
| 57 | assert(virtualModulesPlugin, "virtualModulesPlugin is not defined"); |
| 58 | |
| 59 | virtualModulesPlugin.buildStart?.({ plugins: plugins.filter(Boolean) }); |
| 60 | |
| 61 | const resolved = virtualModulesPlugin.resolveId?.handler?.( |
| 62 | "\0distilled:inject:process", |
| 63 | ); |
| 64 | expect(resolved).toEqual({ id: "\0distilled:inject:process" }); |
| 65 | |
| 66 | const loaded = virtualModulesPlugin.load?.handler?.( |
| 67 | "\0distilled:inject:process", |
| 68 | ); |
| 69 | expect(loaded).toContain("globalThis.process = process;"); |
| 70 | |
| 71 | const transformed = virtualModulesPlugin?.load?.handler?.( |
| 72 | "\0distilled:worker-entry:test/fixtures/node-compat/index.ts", |
| 73 | ); |
| 74 | |
| 75 | expect(transformed).toContain( |
| 76 | `import "${toPosixPath(fileURLToPath(import.meta.resolve("@cloudflare/unenv-preset/polyfill/performance")))}";`, |
| 77 | ); |
| 78 | expect(transformed).toContain('import "\0distilled:inject:process";'); |
| 79 | }); |
| 80 | |
| 81 | it("warns when unresolved Node builtins are imported without nodejs_compat", async () => { |
| 82 | const plugins = cloudflare({ |
| 83 | compatibilityDate: "2025-07-01", |
| 84 | }); |
| 85 | |
| 86 | const nodejsImportWarningPlugin = plugins.find( |
| 87 | (plugin) => plugin?.name === "distilled-cloudflare:nodejs-import-warning", |
| 88 | ) as unknown as { |
| 89 | buildStart: () => void; |
| 90 | resolveId: { handler: (...args: Array<any>) => unknown }; |
| 91 | buildEnd: () => void; |
| 92 | }; |
| 93 | |
| 94 | const warnings: Array<string> = []; |
| 95 | nodejsImportWarningPlugin?.buildStart?.call({}); |
| 96 | |
| 97 | await nodejsImportWarningPlugin?.resolveId?.handler?.call( |
| 98 | {}, |
| 99 | "node:fs", |
| 100 | "test/fixtures/example-a.ts", |
| 101 | {}, |
| 102 | ); |
| 103 | await nodejsImportWarningPlugin?.resolveId?.handler?.call( |
| 104 | {}, |
| 105 | "node:fs", |
| 106 | "test/fixtures/example-b.ts", |
| 107 | {}, |
| 108 | ); |
| 109 | |
| 110 | nodejsImportWarningPlugin?.buildEnd?.call({ |
| 111 | warn(message: string) { |
| 112 | warnings.push(message); |
| 113 | }, |
| 114 | getModuleInfo() { |
| 115 | return {}; |
| 116 | }, |
| 117 | }); |
| 118 | |
| 119 | expect(warnings).toHaveLength(1); |
| 120 | expect(warnings[0]).toContain("Unexpected Node.js imports."); |
| 121 | expect(warnings[0]).toContain( |
| 122 | 'Do you need to enable the "nodejs_compat" compatibility flag?', |
| 123 | ); |
| 124 | expect(warnings[0]).toContain( |
| 125 | "https://developers.cloudflare.com/workers/runtime-apis/nodejs/", |
| 126 | ); |
| 127 | expect(warnings[0]).toContain( |
| 128 | '- "node:fs" imported from "test/fixtures/example-a.ts"', |
| 129 | ); |
| 130 | expect(warnings[0]).toContain( |
| 131 | '- "node:fs" imported from "test/fixtures/example-b.ts"', |
| 132 | ); |
| 133 | }); |
| 134 | }); |