diff --git a/.changeset/vite-plugin-vitest4-resolve-external.md b/.changeset/vite-plugin-vitest4-resolve-external.md new file mode 100644 index 0000000000..b09c19bdad --- /dev/null +++ b/.changeset/vite-plugin-vitest4-resolve-external.md @@ -0,0 +1,9 @@ +--- +"@cloudflare/vite-plugin": patch +--- + +Allow `resolve.external` containing only Node.js built-ins in Worker environments + +Vitest 4 automatically sets `resolve.external` to the full list of Node.js built-in modules for non-standard Vite environments via its internal `runnerTransform` plugin. Previously, the Cloudflare Vite plugin rejected any non-empty `resolve.external` array, throwing an incompatibility error on startup when used alongside Vitest 4. + +The validation check now allows `resolve.external` arrays that contain only Node.js built-in module names (both bare `fs` and `node:fs` forms). The error is only thrown when `resolve.external` is `true` or contains non-built-in package names that would prevent user code from being bundled into the Worker. diff --git a/packages/vite-plugin-cloudflare/src/__tests__/validate-worker-environment-options.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/validate-worker-environment-options.spec.ts index 74db70c2fc..b45eccf7c1 100644 --- a/packages/vite-plugin-cloudflare/src/__tests__/validate-worker-environment-options.spec.ts +++ b/packages/vite-plugin-cloudflare/src/__tests__/validate-worker-environment-options.spec.ts @@ -42,6 +42,29 @@ describe("validateWorkerEnvironmentOptions", () => { ); }); + test("doesn't throw when resolve.external contains only Node.js built-ins (set by Vitest 4)", ({ + expect, + }) => { + const resolvedPluginConfig = { + environmentNameToWorkerMap: new Map([["worker", { config: {} }]]), + } as unknown as AssetsOnlyResolvedConfig | WorkersResolvedConfig; + const resolvedViteConfig = { + environments: { + worker: { + resolve: { + // Vitest 4 sets resolve.external to all Node.js built-ins for + // non-standard environments via its runnerTransform plugin + external: ["assert", "buffer", "fs", "node:path", "node:fs"], + }, + }, + }, + } as unknown as ResolvedConfig; + + expect(() => + validateWorkerEnvironmentOptions(resolvedPluginConfig, resolvedViteConfig) + ).not.toThrow(); + }); + test("throws with an appropriate error message if multiple worker environments contain config violations", ({ expect, }) => { diff --git a/packages/vite-plugin-cloudflare/src/vite-config.ts b/packages/vite-plugin-cloudflare/src/vite-config.ts index 32a04a5bb4..2a5ccc2171 100644 --- a/packages/vite-plugin-cloudflare/src/vite-config.ts +++ b/packages/vite-plugin-cloudflare/src/vite-config.ts @@ -1,10 +1,37 @@ import assert from "node:assert"; +import { builtinModules } from "node:module"; import type { AssetsOnlyResolvedConfig, WorkersResolvedConfig, } from "./plugin-config"; import type * as vite from "vite"; +// Node.js built-in module names in both bare and `node:` prefixed forms. +// Vitest 4 automatically adds these to `resolve.external` for non-standard +// environments (via its `runnerTransform` plugin). They are harmless for +// Worker environments — Workers either handle them via the node-compat layer +// or don't use them — so validation is skipped when the array contains only +// these entries. +const NODE_BUILTIN_SET = new Set([ + ...builtinModules, + // Only add the `node:` prefix for modules that don't already have it, + // avoiding hypothetical `node:node:*` entries on future Node versions. + ...builtinModules + .filter((m) => !m.startsWith("node:")) + .map((m) => `node:${m}`), +]); + +function isOnlyNodeBuiltins( + external: vite.ResolveOptions["external"] +): boolean { + if (!Array.isArray(external)) { + return false; + } + return external.every( + (entry) => typeof entry === "string" && NODE_BUILTIN_SET.has(entry) + ); +} + interface DisallowedEnvironmentOptions { resolveExternal?: vite.ResolveOptions["external"]; } @@ -30,7 +57,11 @@ export function validateWorkerEnvironmentOptions( const { resolve } = environmentOptions; const disallowedEnvironmentOptions: DisallowedEnvironmentOptions = {}; - if (resolve.external === true || resolve.external.length) { + if ( + (resolve.external === true || + (Array.isArray(resolve.external) && resolve.external.length > 0)) && + !isOnlyNodeBuiltins(resolve.external) + ) { disallowedEnvironmentOptions.resolveExternal = resolve.external; }