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>
This commit is contained in:
Guy Bedford authored and GitHub committed 2026-10-19 17:09:42 +00:00
1 parent 1d5f8e6cba
commit 8ae0ad0683
23 files changed
+229 -1

No files matched your search

+7
View File
@@ -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
+7
View File
@@ -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
+1
View File
@@ -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);
}
+2
View File
@@ -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.
+2
View File
@@ -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);
+6 -1
View File
@@ -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.
+14
View File
@@ -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,
@@ -0,0 +1,5 @@
'use strict';
const required = require('pkg');
import('pkg').then(({ default: imported }) => {
postMessage({ require: required, import: imported, resolve: require.resolve('pkg') });
});
@@ -0,0 +1,6 @@
import { register } from 'node:module';
register('./hooks.mjs', import.meta.url);
const { default: value } = await import('pkg');
postMessage(value);
+6
View File
@@ -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);
}
+13
View File
@@ -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),
};
@@ -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,
});
@@ -0,0 +1,2 @@
import value from 'pkg';
postMessage(value);
@@ -0,0 +1 @@
export default 'custom-worker';
@@ -0,0 +1 @@
export default 'custom';
@@ -0,0 +1 @@
module.exports = 'default';
@@ -0,0 +1 @@
export default 'default';
@@ -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"
}
}
}
@@ -0,0 +1 @@
module.exports = 'worker';
@@ -0,0 +1 @@
export default 'worker';
@@ -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 });
};
+13
View File
@@ -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'),
});
@@ -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());