Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/api/globals.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions doc/api/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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._

Expand Down Expand Up @@ -1356,6 +1361,7 @@ This field defines [subpath imports][] for the current package.
[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
Expand All @@ -1371,6 +1377,7 @@ This field defines [subpath imports][] for the current package.
[`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
Expand Down
1 change: 1 addition & 0 deletions lib/internal/main/worker_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/modules/esm/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -531,6 +532,7 @@ class AsyncLoaderHookWorker {
trackUnmanagedFds: false,
workerData: {
lock,
workerCondition: hasWorkerCondition(),
},
});
this.#worker.unref(); // ! Allows the process to eventually exit.
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/modules/esm/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const {
} = require('internal/util');
const assert = require('internal/assert');
const {
hasWorkerCondition,
normalizeReferrerURL,
loadBuiltinModuleForEmbedder,
} = require('internal/modules/helpers');
Expand Down Expand Up @@ -84,6 +85,7 @@ function initializeDefaultConditions() {
'import',
...moduleConditions,
...addonConditions,
...(hasWorkerCondition() ? ['worker'] : []),
...userConditions,
]);
defaultConditionsSet = new SafeSet(defaultConditions);
Expand Down
7 changes: 6 additions & 1 deletion lib/internal/modules/esm/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions lib/internal/modules/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,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[]} */
Expand All @@ -212,6 +223,7 @@ function initializeCjsConditions() {
'require',
'node',
...addonConditions,
...(workerCondition ? ['worker'] : []),
...userConditions,
];
if (getOptionValue('--require-module')) {
Expand Down Expand Up @@ -691,11 +703,13 @@ module.exports = {
purgeRealpathCacheForPrefix,
constants,
enableCompileCache,
enableWorkerCondition,
flushCompileCache,
getBuiltinModule,
getCjsConditions,
getCjsConditionsArray,
getCompileCacheDir,
hasWorkerCondition,
initializeCjsConditions,
legacyMainResolveExtensions,
legacyMainResolveExtensionsIndexes,
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/es-modules/worker-condition/classic-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';
const required = require('pkg');
import('pkg').then(({ default: imported }) => {
postMessage({ require: required, import: imported, resolve: require.resolve('pkg') });
});
6 changes: 6 additions & 0 deletions test/fixtures/es-modules/worker-condition/hooks-worker.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { register } from 'node:module';

register('./hooks.mjs', import.meta.url);

const { default: value } = await import('pkg');
postMessage(value);
6 changes: 6 additions & 0 deletions test/fixtures/es-modules/worker-condition/hooks.mjs
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions test/fixtures/es-modules/worker-condition/main.mjs
Original file line number Diff line number Diff line change
@@ -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),
};
31 changes: 31 additions & 0 deletions test/fixtures/es-modules/worker-condition/module-worker.mjs
Original file line number Diff line number Diff line change
@@ -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,
});
2 changes: 2 additions & 0 deletions test/fixtures/es-modules/worker-condition/nested-worker.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import value from 'pkg';
postMessage(value);

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions test/fixtures/es-modules/worker-condition/thread.mjs
Original file line number Diff line number Diff line change
@@ -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'),
});
77 changes: 77 additions & 0 deletions test/parallel/test-webworker-conditions.js
Original file line number Diff line number Diff line change
@@ -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());
Loading