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

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions