Skip to content

ssr.target: 'webworker' invokes esmExternalRequirePlugin() with an empty external, so bundled CJS require("<node builtin>") is left as a throwing __require stub #22618

Description

@basuke

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    feat: ssrp2-edge-caseBug, but has workaround or limited in scope (priority)

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions