feat: support new Worker() instantiation in universal targets#21195
Conversation
In a universal (node + web) target, a global `new Worker(new URL(...))` crashed in node because there is no global `Worker`. Resolve the constructor through a runtime helper that uses the global `Worker` on the web and `worker_threads.Worker` (via `process.getBuiltinModule`) in node, only when the target is universal.
…orker Both the universal node-commonjs external loader and the universal worker constructor obtained a node builtin via `process.getBuiltinModule`, guarded for the browser and old node. Extract that into `RuntimeTemplate.getBuiltinModule` and use it in both, so the universal node-builtin access lives in one place.
🦋 Changeset detectedLatest commit: 0720271 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
This PR is packaged and the instant preview is available (80f5ed8). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@80f5ed8
yarn add -D webpack@https://pkg.pr.new/webpack@80f5ed8
pnpm add -D webpack@https://pkg.pr.new/webpack@80f5ed8 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21195 +/- ##
=======================================
Coverage 92.70% 92.70%
=======================================
Files 588 589 +1
Lines 64144 64180 +36
Branches 17791 17802 +11
=======================================
+ Hits 59464 59500 +36
Misses 4680 4680
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The universal worker resolves `worker_threads.Worker` via `process.getBuiltinModule`, available only in Node >= 22.3. On older Node the bundle still loads (the helper is falsy-guarded) but the worker cannot be instantiated, so skip the case there instead of failing.
Merging this PR will degrade performance by 51.39%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Memory | benchmark "lodash", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
126.3 KB | 859.1 KB | -85.3% |
| ❌ | Memory | benchmark "side-effects-reexport", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
768.9 KB | 1,198.8 KB | -35.86% |
| ⚡ | Memory | benchmark "css-modules", scenario '{"name":"mode-production","mode":"production"}' |
9 MB | 7.4 MB | +21.78% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing feat/universal-worker-instantiation (0720271) with main (7552543)
Footnotes
-
18 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
…ility Derive `output.environment.nodeBuiltinModuleGetter` from the target node version (`process.getBuiltinModule()` is Node >= 22.3). The shared `RuntimeTemplate.getBuiltinModule` now calls the API directly when it is known-supported, and otherwise tries it and falls back to `createRequire` inside `try/catch` so a non-node or old-node load never throws. Applies to both the universal node-commonjs external and the universal worker.
Consume a node builtin through the generated getter in the `.mjs` (ESM) output and assert a working runtime value, proving the createRequire/ getBuiltinModule getter runs in a real ECMAScript module. Also refreshes the CLI args and browserslist environment snapshots for the new `nodeBuiltinModuleGetter` capability.
…r fallback Replace the IIFE + double try/catch with a single `typeof`-guarded ternary: prefer `process.getBuiltinModule()`, else `createRequire` when `require` exists. `typeof` never throws on undeclared names, so the expression stays safe in the browser and in ESM (where `require` is absent) while shrinking the generated code.
`require` doesn't exist in ESM and universal output is always ESM, so the createRequire-via-require fallback could never run there. The fallback now just probes `process.getBuiltinModule` with `typeof` and calls it; the createRequire access for node-commonjs externals is built on that getter, so no `require` is referenced.
…catch Probe process.getBuiltinModule at runtime and use it when present; otherwise load via require()/createRequire wrapped in try/catch so the ReferenceError it throws in ESM (and the browser) is swallowed. Supports old and new Node in ECMAScript module output.
…e guard Universal output is ESM, where require doesn't exist, so the require/createRequire fallback could never run. Use the same process.getBuiltinModule typeof guard the universal node-commonjs externals loader uses; node <22.3 in ESM stays falsy (no synchronous builtin access is possible there).
Replace the inline externalsPresets.node && web destructure in the worker and node-commonjs externals with a named RuntimeTemplate helper. platform flags are null for a universal target, so externals presets remain the source of truth; the helper makes that intent explicit and shared.
Match the existing isUniversalTarget determination in WebpackOptionsApply (output.module + null node/web platform flags) instead of externals presets, and use a target-neutral name that extends to future target combinations. Shared by the worker rewrite and the node-commonjs externals loader.
Summary
In a universal (
["node", "web"]+output.module) target a globalnew Worker(new URL(...))crashed in node because there is no globalWorker. This resolves the constructor through a runtime helper that uses the globalWorkeron the web andworker_threads.Worker(viaprocess.getBuiltinModule) in node, only for universal targets. The defensive node-builtin getter introduced for universalnode-commonjsexternals (#21187) is unified intoRuntimeTemplate.getBuiltinModuleand reused here so the logic lives in one place.What kind of change does this PR introduce?
feat (plus a small refactor unifying the node-builtin getter).
Did you add tests for your changes?
Yes —
test/configCases/worker/universalnow actually instantiates the worker under node (it was a no-op there before); the existing externals cases cover the shared helper.Does this PR introduce a breaking change?
No — pure web and pure node builds are unchanged; the rewrite only applies to universal targets.
If relevant, what needs to be documented once your changes are merged or what have you already documented?
n/a — the workers documentation could note that
new Worker(new URL(...))now works under a universal target (constructor only; the messaging API still differs betweenworker_threadsand web Workers).Use of AI
Yes — the implementation, tests and this description were produced with Claude Code (Anthropic) and reviewed before submission.
Generated by Claude Code