mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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:
1 parent
1d5f8e6cba
commit
8ae0ad0683
23 files changed
+229
-1
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default 'custom-worker';
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default 'custom';
|
||||
+1
@@ -0,0 +1 @@
|
||||
module.exports = 'default';
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default 'default';
|
||||
+23
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
module.exports = 'worker';
|
||||
+1
@@ -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 });
|
||||
};
|
||||
@@ -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());
|
||||
Reference in new issue
Block a user