From c76caa7b097393980156b5a4589da83286adf5f1 Mon Sep 17 00:00:00 2001 From: Matin Gathani Date: Mon, 8 Jun 2026 13:34:26 +0530 Subject: [PATCH 1/3] [vite-plugin] fix: allow Node.js built-ins in resolve.external for Worker environments Fixes #14215. Vitest 4 automatically adds all Node.js built-in module specifiers to `resolve.external` for non-standard Vite environments via its internal `runnerTransform` plugin. The Cloudflare Vite plugin previously threw on any non-empty `resolve.external` array, causing a startup error when Vitest 4 is used alongside the plugin. Filter out Node.js built-in entries (bare and node:-prefixed forms) before validation. Only throw when resolve.external is `true` (externalize everything) or contains non-built-in package names. --- .../vite-plugin-vitest4-resolve-external.md | 15 +++++++++++ ...alidate-worker-environment-options.spec.ts | 23 ++++++++++++++++ .../vite-plugin-cloudflare/src/vite-config.ts | 27 ++++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 .changeset/vite-plugin-vitest4-resolve-external.md diff --git a/.changeset/vite-plugin-vitest4-resolve-external.md b/.changeset/vite-plugin-vitest4-resolve-external.md new file mode 100644 index 0000000000..aebfc7efc6 --- /dev/null +++ b/.changeset/vite-plugin-vitest4-resolve-external.md @@ -0,0 +1,15 @@ +--- +"@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. + +Built-in module entries (both bare `fs` and `node:fs` forms) are now filtered out +before validation. 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..0e7c245b13 100644 --- a/packages/vite-plugin-cloudflare/src/vite-config.ts +++ b/packages/vite-plugin-cloudflare/src/vite-config.ts @@ -1,10 +1,32 @@ 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 we filter them out before validation. +const NODE_BUILTIN_SET = new Set([ + ...builtinModules, + ...builtinModules.map((m) => `node:${m}`), +]); + +function isOnlyNodeBuiltins( + external: vite.ResolveOptions["external"] +): boolean { + if (external === true || !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 +52,10 @@ export function validateWorkerEnvironmentOptions( const { resolve } = environmentOptions; const disallowedEnvironmentOptions: DisallowedEnvironmentOptions = {}; - if (resolve.external === true || resolve.external.length) { + if ( + (resolve.external === true || resolve.external.length) && + !isOnlyNodeBuiltins(resolve.external) + ) { disallowedEnvironmentOptions.resolveExternal = resolve.external; } From b27c90a81422cf49cf4a66f8830b3d2aed4e4a1a Mon Sep 17 00:00:00 2001 From: Matin Gathani Date: Tue, 9 Jun 2026 11:42:23 +0530 Subject: [PATCH 2/3] address review: guard Array.isArray, fix node:node: double-prefix, clarify changeset --- .changeset/vite-plugin-vitest4-resolve-external.md | 8 ++++---- packages/vite-plugin-cloudflare/src/vite-config.ts | 14 ++++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.changeset/vite-plugin-vitest4-resolve-external.md b/.changeset/vite-plugin-vitest4-resolve-external.md index aebfc7efc6..368cf2d498 100644 --- a/.changeset/vite-plugin-vitest4-resolve-external.md +++ b/.changeset/vite-plugin-vitest4-resolve-external.md @@ -9,7 +9,7 @@ modules for non-standard Vite environments via its internal `runnerTransform` pl Previously, the Cloudflare Vite plugin rejected any non-empty `resolve.external` array, throwing an incompatibility error on startup when used alongside Vitest 4. -Built-in module entries (both bare `fs` and `node:fs` forms) are now filtered out -before validation. 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. +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/vite-config.ts b/packages/vite-plugin-cloudflare/src/vite-config.ts index 0e7c245b13..2a5ccc2171 100644 --- a/packages/vite-plugin-cloudflare/src/vite-config.ts +++ b/packages/vite-plugin-cloudflare/src/vite-config.ts @@ -10,16 +10,21 @@ import type * as vite from "vite"; // 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 we filter them out before validation. +// or don't use them — so validation is skipped when the array contains only +// these entries. const NODE_BUILTIN_SET = new Set([ ...builtinModules, - ...builtinModules.map((m) => `node:${m}`), + // 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 (external === true || !Array.isArray(external)) { + if (!Array.isArray(external)) { return false; } return external.every( @@ -53,7 +58,8 @@ export function validateWorkerEnvironmentOptions( const disallowedEnvironmentOptions: DisallowedEnvironmentOptions = {}; if ( - (resolve.external === true || resolve.external.length) && + (resolve.external === true || + (Array.isArray(resolve.external) && resolve.external.length > 0)) && !isOnlyNodeBuiltins(resolve.external) ) { disallowedEnvironmentOptions.resolveExternal = resolve.external; From acde302840cb5f6c053aca62a13b3803fa9a4c7c Mon Sep 17 00:00:00 2001 From: Matin Gathani Date: Wed, 17 Jun 2026 13:49:01 +0530 Subject: [PATCH 3/3] chore: fix changeset formatting (oxfmt) --- .changeset/vite-plugin-vitest4-resolve-external.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.changeset/vite-plugin-vitest4-resolve-external.md b/.changeset/vite-plugin-vitest4-resolve-external.md index 368cf2d498..b09c19bdad 100644 --- a/.changeset/vite-plugin-vitest4-resolve-external.md +++ b/.changeset/vite-plugin-vitest4-resolve-external.md @@ -4,12 +4,6 @@ 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. +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. +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.