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());