Describe the bug
Follow-up to #21969 / #21963 (merged, in 8.0.15). I intend to submit a PR for this if maintainers agree on the approach below.
PR #21963 made ssr.target: 'webworker' SSR builds use platform: 'browser' and added esmExternalRequirePlugin() so external CJS require() calls become ESM imports — fixing the createRequire(import.meta.url) startup crash on Cloudflare Workers (workerd).
However, a CommonJS dependency that internally calls require("<node builtin>") (e.g. node-forge doing require("crypto")) is still not handled, because the plugin is invoked without an external option:
// resolved webworker SSR build — esmExternalRequirePlugin is added with no `external`
if (isSsrTargetWebworkerEnvironment) {
plugins.push(esmExternalRequirePlugin()) // empty `external` ⇒ matches nothing
}
With an empty external, the plugin converts nothing, so the bundled module's nested require("crypto") falls through to Rolldown's __require runtime stub:
var crypto = __require("crypto");
// __require throws: Calling `require` for "crypto" in an environment that doesn't
// expose the `require` function. (https://rolldown.rs/in-depth/bundling-cjs#require-external-modules)
This crashes on workerd (no global require). In a framework that evaluates the SSR output during build analysis (e.g. SvelteKit adapter-cloudflare), the build itself fails at this stub.
By contrast, Vite 7 / Rollup statically converts the same nested require("crypto") into import nodeCrypto from "crypto", which works on workerd. So this is a regression for the webworker target.
Expected: for webworker SSR, Vite should pass the node builtins / resolved externals to the plugin, i.e. esmExternalRequirePlugin({ external: [/^node:/, ...nodeBuiltins] }), so nested builtin require() in bundled CJS is converted to ESM imports (which workerd's nodejs_compat then resolves).
Demonstrated fix (see the repro's vite.config.fix.js): building with esmExternalRequirePlugin({ external: [/^node:/, 'crypto'] }) turns the stub into a real ESM import:
import * as m from "crypto";
var crypto = require_builtin_esm_external_require_crypto(); // -> m.default
Reproduction
https://github.com/basuke/vite-webworker-cjs-require-repro
Steps to reproduce
npm install
npm run build # vite build, uses vite.config.js (ssr.target: 'webworker')
cat dist/entry.js # -> contains `var crypto = __require("crypto")` (throwing stub)
Fix demonstration:
npx vite build --config vite.config.fix.js # adds esmExternalRequirePlugin({ external: [/^node:/, 'crypto'] })
cat dist-fixed/entry.js # -> contains `import * as m from "crypto"`
System Info
System:
OS: macOS 26.5
CPU: (20) arm64 Apple M1 Ultra
Binaries:
Node: 22.21.1
npm: 11.14.1
Browsers:
Chrome: 148.0.7778.217
Safari: 26.5
npmPackages:
vite: 8.0.15 => 8.0.15
Used Package Manager
npm
Logs
In a framework build that evaluates the SSR output (SvelteKit adapter-cloudflare), the stub throws during build:
Error: Calling require for "crypto" in an environment that doesn't expose the require function.
See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.
Validations
Describe the bug
Follow-up to #21969 / #21963 (merged, in 8.0.15). I intend to submit a PR for this if maintainers agree on the approach below.
PR #21963 made
ssr.target: 'webworker'SSR builds useplatform: 'browser'and addedesmExternalRequirePlugin()so external CJSrequire()calls become ESM imports — fixing thecreateRequire(import.meta.url)startup crash on Cloudflare Workers (workerd).However, a CommonJS dependency that internally calls
require("<node builtin>")(e.g.node-forgedoingrequire("crypto")) is still not handled, because the plugin is invoked without anexternaloption:With an empty
external, the plugin converts nothing, so the bundled module's nestedrequire("crypto")falls through to Rolldown's__requireruntime stub:This crashes on workerd (no global
require). In a framework that evaluates the SSR output during build analysis (e.g. SvelteKitadapter-cloudflare), the build itself fails at this stub.By contrast, Vite 7 / Rollup statically converts the same nested
require("crypto")intoimport nodeCrypto from "crypto", which works on workerd. So this is a regression for the webworker target.Expected: for webworker SSR, Vite should pass the node builtins / resolved externals to the plugin, i.e.
esmExternalRequirePlugin({ external: [/^node:/, ...nodeBuiltins] }), so nested builtinrequire()in bundled CJS is converted to ESM imports (which workerd'snodejs_compatthen resolves).Demonstrated fix (see the repro's
vite.config.fix.js): building withesmExternalRequirePlugin({ external: [/^node:/, 'crypto'] })turns the stub into a real ESM import:Reproduction
https://github.com/basuke/vite-webworker-cjs-require-repro
Steps to reproduce
Fix demonstration:
System Info
System: OS: macOS 26.5 CPU: (20) arm64 Apple M1 Ultra Binaries: Node: 22.21.1 npm: 11.14.1 Browsers: Chrome: 148.0.7778.217 Safari: 26.5 npmPackages: vite: 8.0.15 => 8.0.15Used Package Manager
npm
Logs
In a framework build that evaluates the SSR output (SvelteKit adapter-cloudflare), the stub throws during build:
Error: Calling
requirefor "crypto" in an environment that doesn't expose therequirefunction.See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.
Validations