Commit 8ae0ad06835 for nodejs
commit 8ae0ad0683555abca523c0bb7798bd58fe50173e
Author: Guy Bedford <guybedford@gmail.com>
Date: Mon Oct 19 10:09:42 2026 -0700
module: add "worker" condition for Web Worker threads
Adds a "worker" package condition that is applied within Web Worker
threads created via the `Worker` global, for the worker entry point,
its dependencies, `import()`, `require()` and `import.meta.resolve()`.
Threads created via `worker_threads`, including from within a Web
Worker, do not apply the condition. Other conditions are unaffected.
Assisted-by: OpenCode
Signed-off-by: Guy Bedford <guybedford@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66635
Reviewed-By: James M Snell <jasnell@gmail.com>
diff --git a/doc/api/globals.md b/doc/api/globals.md
index e717b9a05ac..00383dcfb7d 100644
--- a/doc/api/globals.md
+++ b/doc/api/globals.md
@@ -1378,6 +1378,12 @@ accepted and how failures are reported:
`file:` URLs. The `type` option, not the file extension, decides how an entry is
run, so a `.cts` entry is still evaluated as an ES module.
+Package resolution within a Web Worker, including the entry point, its
+dependencies, `import()`, `require()`, and `import.meta.resolve()`, applies
+the `"worker"` [package condition][] in addition to the usual conditions.
+Threads created through [`node:worker_threads`][] from within a Web Worker do
+not apply it.
+
### Differences from the HTML Standard
Besides script loading, mentioned above:
@@ -1545,6 +1551,7 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
[browser `LockManager`]: https://developer.mozilla.org/en-US/docs/Web/API/LockManager
[buffer section]: buffer.md
[built-in objects]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
+[package condition]: packages.md#conditional-exports
[timers]: timers.md
[type stripping]: typescript.md#type-stripping
[webassembly-mdn]: https://developer.mozilla.org/en-US/docs/WebAssembly
diff --git a/doc/api/packages.md b/doc/api/packages.md
index f448cd68ba4..7d598a533ea 100644
--- a/doc/api/packages.md
+++ b/doc/api/packages.md
@@ -710,6 +710,11 @@ specific to least specific as conditions should be defined:
`import()` or `require()`. The format is expected to be ES modules that does
not contain top-level await in its module graph - if it does,
`ERR_REQUIRE_ASYNC_MODULE` will be thrown when the module is `require()`-ed.
+* `"worker"` - matches when the package is loaded within a [Web Worker][]
+ thread, including the worker entry point and all of its dependencies,
+ `import()`, `require()`, and `import.meta.resolve()`. Does not apply to
+ threads created with [`node:worker_threads`][], including those created
+ from within a Web Worker.
* `"default"` - the generic fallback that always matches. Can be a CommonJS
or ES module file. _This condition should always come last._
@@ -1386,6 +1391,7 @@ Node.js allows type stripping for:
[Runtime Keys]: https://runtime-keys.proposal.wintercg.org/
[Syntax detection]: #syntax-detection
[TypeScript]: typescript.md
+[Web Worker]: globals.md#class-worker
[WebAssembly modules]: esm.md#wasm-modules
[WinterCG]: https://wintercg.org/
[`"exports"`]: #exports
@@ -1403,6 +1409,7 @@ Node.js allows type stripping for:
[`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported
[`ERR_UNKNOWN_FILE_EXTENSION`]: errors.md#err_unknown_file_extension
[`URL`]: url.md#the-whatwg-url-api
+[`node:worker_threads`]: worker_threads.md
[`package.json`]: #nodejs-packagejson-field-definitions
[customization hooks]: module.md#customization-hooks
[entry points]: #package-entry-points
diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js
index 5a534d71603..1f5fa8f8212 100644
--- a/lib/internal/main/worker_thread.js
+++ b/lib/internal/main/worker_thread.js
@@ -119,6 +119,7 @@ port.on('message', (message) => {
setupMainThreadPort(mainThreadPort);
if (webWorkerData !== undefined) {
+ require('internal/modules/helpers').enableWorkerCondition();
require('internal/webworker')
.installDedicatedWorkerGlobalScope(webWorkerData.url, webWorkerData);
}
diff --git a/lib/internal/modules/esm/hooks.js b/lib/internal/modules/esm/hooks.js
index 4ced5521aca..220dcd33754 100644
--- a/lib/internal/modules/esm/hooks.js
+++ b/lib/internal/modules/esm/hooks.js
@@ -50,6 +50,7 @@ const {
const {
getDefaultConditions,
} = require('internal/modules/esm/utils');
+const { hasWorkerCondition } = require('internal/modules/helpers');
const { deserializeError } = require('internal/error_serdes');
const {
SHARED_MEMORY_BYTE_LENGTH,
@@ -531,6 +532,7 @@ class AsyncLoaderHookWorker {
trackUnmanagedFds: false,
workerData: {
lock,
+ workerCondition: hasWorkerCondition(),
},
});
this.#worker.unref(); // ! Allows the process to eventually exit.
diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js
index 9e27a2db1eb..c6b101fb907 100644
--- a/lib/internal/modules/esm/utils.js
+++ b/lib/internal/modules/esm/utils.js
@@ -44,6 +44,7 @@ const {
} = require('internal/util');
const assert = require('internal/assert');
const {
+ hasWorkerCondition,
normalizeReferrerURL,
loadBuiltinModuleForEmbedder,
} = require('internal/modules/helpers');
@@ -84,6 +85,7 @@ function initializeDefaultConditions() {
'import',
...moduleConditions,
...addonConditions,
+ ...(hasWorkerCondition() ? ['worker'] : []),
...userConditions,
]);
defaultConditionsSet = new SafeSet(defaultConditions);
diff --git a/lib/internal/modules/esm/worker.js b/lib/internal/modules/esm/worker.js
index e2553b253db..7d023059a4b 100644
--- a/lib/internal/modules/esm/worker.js
+++ b/lib/internal/modules/esm/worker.js
@@ -35,6 +35,7 @@ const { isMarkedAsUntransferable } = require('internal/buffer');
const { getCWDURL } = require('internal/util');
const { isCascadedLoaderInitialized, getOrInitializeCascadedLoader } = require('internal/modules/esm/loader');
const { AsyncLoaderHooksOnLoaderHookWorker } = require('internal/modules/esm/hooks');
+const { enableWorkerCondition } = require('internal/modules/helpers');
/**
* Register asynchronous module loader customization hooks. This should only be run in the loader
@@ -285,12 +286,16 @@ async function customizedModuleWorker(lock, syncCommPort, errorHandler) {
/**
* Initializes a worker thread for a module with customized hooks.
* ! Run everything possible within this function so errors get reported.
- * @param {{lock: SharedArrayBuffer}} workerData - The lock used to synchronize with the main thread.
+ * @param {{lock: SharedArrayBuffer, workerCondition: boolean}} workerData - The lock used to
+ * synchronize with the main thread, and whether the spawning thread is a Web Worker.
* @param {MessagePort} syncCommPort - The communication port used to communicate with the main thread.
* @returns {object}
*/
module.exports = function setupModuleWorker(workerData, syncCommPort) {
const lock = new Int32Array(workerData.lock);
+ if (workerData.workerCondition) {
+ enableWorkerCondition();
+ }
/**
* Handles errors that occur in the worker thread.
diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js
index 851547d2216..e30b892ad5e 100644
--- a/lib/internal/modules/helpers.js
+++ b/lib/internal/modules/helpers.js
@@ -196,6 +196,17 @@ function purgeRealpathCacheForPrefix(mountPoint) {
cleanForVfsPrefix(realpathCache, mountPoint);
}
+/** Whether the `worker` condition applies, i.e. this is a Web Worker thread. */
+let workerCondition = false;
+
+function enableWorkerCondition() {
+ workerCondition = true;
+}
+
+function hasWorkerCondition() {
+ return workerCondition;
+}
+
/** @type {Set<string>} */
let cjsConditions;
/** @type {string[]} */
@@ -214,6 +225,7 @@ function initializeCjsConditions() {
'require',
'node',
...addonConditions,
+ ...(workerCondition ? ['worker'] : []),
...userConditions,
];
if (getOptionValue('--require-module')) {
@@ -693,11 +705,13 @@ module.exports = {
purgeRealpathCacheForPrefix,
constants,
enableCompileCache,
+ enableWorkerCondition,
flushCompileCache,
getBuiltinModule,
getCjsConditions,
getCjsConditionsArray,
getCompileCacheDir,
+ hasWorkerCondition,
initializeCjsConditions,
legacyMainResolveExtensions,
legacyMainResolveExtensionsIndexes,
diff --git a/test/fixtures/es-modules/worker-condition/classic-worker.js b/test/fixtures/es-modules/worker-condition/classic-worker.js
new file mode 100644
index 00000000000..d1f5f4e8d41
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/classic-worker.js
@@ -0,0 +1,5 @@
+'use strict';
+const required = require('pkg');
+import('pkg').then(({ default: imported }) => {
+ postMessage({ require: required, import: imported, resolve: require.resolve('pkg') });
+});
diff --git a/test/fixtures/es-modules/worker-condition/hooks-worker.mjs b/test/fixtures/es-modules/worker-condition/hooks-worker.mjs
new file mode 100644
index 00000000000..ee9cd45bd6c
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/hooks-worker.mjs
@@ -0,0 +1,6 @@
+import { register } from 'node:module';
+
+register('./hooks.mjs', import.meta.url);
+
+const { default: value } = await import('pkg');
+postMessage(value);
diff --git a/test/fixtures/es-modules/worker-condition/hooks.mjs b/test/fixtures/es-modules/worker-condition/hooks.mjs
new file mode 100644
index 00000000000..8505bafc98d
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/hooks.mjs
@@ -0,0 +1,6 @@
+export async function resolve(specifier, context, nextResolve) {
+ if (specifier === 'pkg' && !context.conditions.includes('worker')) {
+ throw new Error(`missing worker condition: ${context.conditions}`);
+ }
+ return nextResolve(specifier, context);
+}
diff --git a/test/fixtures/es-modules/worker-condition/main.mjs b/test/fixtures/es-modules/worker-condition/main.mjs
new file mode 100644
index 00000000000..2565f04c420
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/main.mjs
@@ -0,0 +1,13 @@
+import { createRequire } from 'node:module';
+import value from 'pkg';
+import custom from 'pkg/custom';
+
+const require = createRequire(import.meta.url);
+
+export default {
+ import: value,
+ custom,
+ require: require('pkg'),
+ resolve: import.meta.resolve('pkg'),
+ workerOnly: await import('pkg/worker-only').then(() => 'resolved', (err) => err.code),
+};
diff --git a/test/fixtures/es-modules/worker-condition/module-worker.mjs b/test/fixtures/es-modules/worker-condition/module-worker.mjs
new file mode 100644
index 00000000000..fb069c69213
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/module-worker.mjs
@@ -0,0 +1,31 @@
+import { once } from 'node:events';
+import { createRequire } from 'node:module';
+import { Worker as ThreadWorker } from 'node:worker_threads';
+import staticImport from 'pkg';
+import custom from 'pkg/custom';
+import workerOnly from 'pkg/worker-only';
+
+const require = createRequire(import.meta.url);
+
+const { default: dynamicImport } = await import('pkg');
+
+const thread = new ThreadWorker(new URL('./thread.mjs', import.meta.url));
+const [threadResult] = await once(thread, 'message');
+
+const nested = new Worker(new URL('./nested-worker.mjs', import.meta.url), { type: 'module' });
+const nestedResult = await new Promise((resolve, reject) => {
+ nested.onmessage = ({ data }) => resolve(data);
+ nested.onerror = reject;
+});
+nested.terminate();
+
+postMessage({
+ staticImport,
+ dynamicImport,
+ custom,
+ workerOnly,
+ resolve: import.meta.resolve('pkg'),
+ require: require('pkg'),
+ thread: threadResult,
+ nested: nestedResult,
+});
diff --git a/test/fixtures/es-modules/worker-condition/nested-worker.mjs b/test/fixtures/es-modules/worker-condition/nested-worker.mjs
new file mode 100644
index 00000000000..48a03e4fe41
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/nested-worker.mjs
@@ -0,0 +1,2 @@
+import value from 'pkg';
+postMessage(value);
diff --git a/test/fixtures/es-modules/worker-condition/node_modules/pkg/custom-worker.mjs b/test/fixtures/es-modules/worker-condition/node_modules/pkg/custom-worker.mjs
new file mode 100644
index 00000000000..e80a870ddab
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/node_modules/pkg/custom-worker.mjs
@@ -0,0 +1 @@
+export default 'custom-worker';
diff --git a/test/fixtures/es-modules/worker-condition/node_modules/pkg/custom.mjs b/test/fixtures/es-modules/worker-condition/node_modules/pkg/custom.mjs
new file mode 100644
index 00000000000..bcd83d82be0
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/node_modules/pkg/custom.mjs
@@ -0,0 +1 @@
+export default 'custom';
diff --git a/test/fixtures/es-modules/worker-condition/node_modules/pkg/default.cjs b/test/fixtures/es-modules/worker-condition/node_modules/pkg/default.cjs
new file mode 100644
index 00000000000..024b12f7391
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/node_modules/pkg/default.cjs
@@ -0,0 +1 @@
+module.exports = 'default';
diff --git a/test/fixtures/es-modules/worker-condition/node_modules/pkg/default.mjs b/test/fixtures/es-modules/worker-condition/node_modules/pkg/default.mjs
new file mode 100644
index 00000000000..413f601a377
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/node_modules/pkg/default.mjs
@@ -0,0 +1 @@
+export default 'default';
diff --git a/test/fixtures/es-modules/worker-condition/node_modules/pkg/package.json b/test/fixtures/es-modules/worker-condition/node_modules/pkg/package.json
new file mode 100644
index 00000000000..b50fb3299d0
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/node_modules/pkg/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "pkg",
+ "exports": {
+ ".": {
+ "worker": {
+ "import": "./worker.mjs",
+ "require": "./worker.cjs"
+ },
+ "import": "./default.mjs",
+ "require": "./default.cjs"
+ },
+ "./custom": {
+ "custom-condition": {
+ "worker": "./custom-worker.mjs",
+ "default": "./custom.mjs"
+ },
+ "default": "./default.mjs"
+ },
+ "./worker-only": {
+ "worker": "./worker.mjs"
+ }
+ }
+}
diff --git a/test/fixtures/es-modules/worker-condition/node_modules/pkg/worker.cjs b/test/fixtures/es-modules/worker-condition/node_modules/pkg/worker.cjs
new file mode 100644
index 00000000000..a8baa369c92
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/node_modules/pkg/worker.cjs
@@ -0,0 +1 @@
+module.exports = 'worker';
diff --git a/test/fixtures/es-modules/worker-condition/node_modules/pkg/worker.mjs b/test/fixtures/es-modules/worker-condition/node_modules/pkg/worker.mjs
new file mode 100644
index 00000000000..314cb87d069
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/node_modules/pkg/worker.mjs
@@ -0,0 +1 @@
+export default 'worker';
diff --git a/test/fixtures/es-modules/worker-condition/thread-spawns-webworker.mjs b/test/fixtures/es-modules/worker-condition/thread-spawns-webworker.mjs
new file mode 100644
index 00000000000..c76a741d6cc
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/thread-spawns-webworker.mjs
@@ -0,0 +1,8 @@
+import { parentPort } from 'node:worker_threads';
+import value from 'pkg';
+
+const worker = new Worker(new URL('./nested-worker.mjs', import.meta.url), { type: 'module' });
+worker.onmessage = ({ data }) => {
+ worker.terminate();
+ parentPort.postMessage({ thread: value, webWorker: data });
+};
diff --git a/test/fixtures/es-modules/worker-condition/thread.mjs b/test/fixtures/es-modules/worker-condition/thread.mjs
new file mode 100644
index 00000000000..d5e0b615cc0
--- /dev/null
+++ b/test/fixtures/es-modules/worker-condition/thread.mjs
@@ -0,0 +1,13 @@
+import { parentPort } from 'node:worker_threads';
+import { createRequire } from 'node:module';
+import value from 'pkg';
+import custom from 'pkg/custom';
+
+const require = createRequire(import.meta.url);
+
+parentPort.postMessage({
+ import: value,
+ custom,
+ require: require('pkg'),
+ resolve: import.meta.resolve('pkg'),
+});
diff --git a/test/parallel/test-webworker-conditions.js b/test/parallel/test-webworker-conditions.js
new file mode 100644
index 00000000000..313e3f3d643
--- /dev/null
+++ b/test/parallel/test-webworker-conditions.js
@@ -0,0 +1,77 @@
+// Flags: --experimental-web-worker --conditions=custom-condition
+'use strict';
+
+// The `worker` package condition applies only within Web Worker threads,
+// not on the main thread or in `worker_threads` threads.
+
+const common = require('../common');
+const fixtures = require('../common/fixtures');
+const assert = require('node:assert');
+const { once } = require('node:events');
+const { Worker: ThreadWorker } = require('node:worker_threads');
+
+const base = fixtures.fileURL('es-modules/worker-condition/');
+const pkg = new URL('node_modules/pkg/', base);
+
+function runWebWorker(url, options) {
+ const worker = new Worker(url, options);
+ worker.onerror = common.mustNotCall('worker failed');
+ return new Promise((resolve) => {
+ worker.onmessage = common.mustCall(({ data }) => {
+ worker.terminate();
+ resolve(data);
+ });
+ });
+}
+
+(async () => {
+ // Main thread: no worker condition.
+ assert.deepStrictEqual((await import(new URL('main.mjs', base))).default, {
+ import: 'default',
+ custom: 'custom',
+ require: 'default',
+ resolve: new URL('default.mjs', pkg).href,
+ workerOnly: 'ERR_PACKAGE_PATH_NOT_EXPORTED',
+ });
+
+ // Module Web Worker: entry point dependencies, dynamic import,
+ // import.meta.resolve, require, nested threads.
+ assert.deepStrictEqual(
+ await runWebWorker(new URL('module-worker.mjs', base), { type: 'module' }),
+ {
+ staticImport: 'worker',
+ dynamicImport: 'worker',
+ custom: 'custom-worker',
+ workerOnly: 'worker',
+ resolve: new URL('worker.mjs', pkg).href,
+ require: 'worker',
+ // worker_threads created from a Web Worker revert to no worker condition.
+ thread: {
+ import: 'default',
+ custom: 'custom',
+ require: 'default',
+ resolve: new URL('default.mjs', pkg).href,
+ },
+ // Nested Web Workers retain it.
+ nested: 'worker',
+ });
+
+ // Classic Web Worker.
+ assert.deepStrictEqual(
+ await runWebWorker(new URL('classic-worker.js', base)),
+ {
+ require: 'worker',
+ import: 'worker',
+ resolve: fixtures.path('es-modules/worker-condition/node_modules/pkg/worker.cjs'),
+ });
+
+ // Web Worker created from a worker_threads thread.
+ const thread = new ThreadWorker(new URL('thread-spawns-webworker.mjs', base));
+ const [threadResult] = await once(thread, 'message');
+ assert.deepStrictEqual(threadResult, { thread: 'default', webWorker: 'worker' });
+
+ // Async module customization hooks registered from a Web Worker.
+ assert.strictEqual(
+ await runWebWorker(new URL('hooks-worker.mjs', base), { type: 'module' }),
+ 'worker');
+})().then(common.mustCall());