Commit db1e36b8dd7 for nodejs
commit db1e36b8dd7e6ab9bd2ddbfd8d7a6bbc7bdce5f4
Author: Hubert Walczak <hubertwalczak8@gmail.com>
Date: Sat Sep 26 20:28:17 2026 +0200
worker: strip types in Web Worker module entries
Strip TypeScript from file-backed module worker entries before
evaluating the fetched source.
Add regression coverage and document the supported entry types.
Assisted-by: Codex
Signed-off-by: Hubert Walczak <hubertwalczak8@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66085
Reviewed-By: Aviv Keller <me@aviv.sh>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matthew Aitken <maitken033380023@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
diff --git a/doc/api/globals.md b/doc/api/globals.md
index 5b95fe7465a..e717b9a05ac 100644
--- a/doc/api/globals.md
+++ b/doc/api/globals.md
@@ -1374,6 +1374,10 @@ accepted and how failures are reported:
* For `blob:` URLs, the script must be held in memory, so blobs backed by a file,
such as those returned by [`fs.openAsBlob()`][], cannot be used.
+[Type stripping][type stripping] only applies to module workers loaded from
+`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.
+
### Differences from the HTML Standard
Besides script loading, mentioned above:
@@ -1398,6 +1402,7 @@ Besides script loading, mentioned above:
`unhandledrejection`, since Node.js exposes the equivalent does not
implement the `PromiseRejectionEvent` interface or the per-rejection
`preventDefault()` behavior required by the HTML Standard.
+* Module workers loaded from `file:` URLs support [type stripping][].
### Web Workers and `node:worker_threads`
@@ -1541,5 +1546,6 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
[buffer section]: buffer.md
[built-in objects]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
[timers]: timers.md
+[type stripping]: typescript.md#type-stripping
[webassembly-mdn]: https://developer.mozilla.org/en-US/docs/WebAssembly
[webassembly-org]: https://webassembly.org
diff --git a/lib/internal/webworker.js b/lib/internal/webworker.js
index e2a2086f903..d7e768372de 100644
--- a/lib/internal/webworker.js
+++ b/lib/internal/webworker.js
@@ -25,6 +25,8 @@ const {
globalThis,
} = primordials;
+const { extname } = require('path');
+
const {
ERR_ILLEGAL_CONSTRUCTOR,
ERR_INVALID_STATE,
@@ -37,6 +39,8 @@ const {
initEventTarget,
} = require('internal/event_target');
+const { getOptionValue } = require('internal/options');
+
const {
assignFunctionName,
defineOperation,
@@ -89,6 +93,7 @@ const {
const {
URL,
URLParse,
+ fileURLToPath,
} = require('internal/url');
const {
@@ -117,6 +122,9 @@ let scopeBaseURL = null;
const lazyErrorEvent =
getLazy(() => require('internal/deps/undici/undici').ErrorEvent);
+const lazyStripTypeScriptModuleTypes = getLazy(
+ () => require('internal/modules/typescript').stripTypeScriptModuleTypes);
+
function createErrorEvent(init) {
const ErrorEvent = lazyErrorEvent();
return new ErrorEvent('error', init);
@@ -253,10 +261,20 @@ function runClassicScriptSource(source, url) {
* @returns {Promise}
*/
function runModuleScriptSource(source, url) {
- // Necessary to reset RegExp statics before user code runs.
- RegExpPrototypeExec(/^/, '');
return require('internal/modules/run_main').runEntryPointWithESMLoader(
- (loader) => loader.eval(source, url, true),
+ (loader) => {
+ const parsedURL = new URL(url);
+ if (parsedURL.protocol === 'file:' && getOptionValue('--strip-types')) {
+ const filename = fileURLToPath(parsedURL);
+ const extension = extname(filename);
+ if (extension === '.ts' || extension === '.mts' || extension === '.cts') {
+ source = lazyStripTypeScriptModuleTypes()(source, filename, url);
+ }
+ }
+ // Necessary to reset RegExp statics before user code runs.
+ RegExpPrototypeExec(/^/, '');
+ return loader.eval(source, url, true);
+ },
);
}
diff --git a/test/fixtures/web-worker/typescript/dependency.ts b/test/fixtures/web-worker/typescript/dependency.ts
new file mode 100644
index 00000000000..1b55ac618d9
--- /dev/null
+++ b/test/fixtures/web-worker/typescript/dependency.ts
@@ -0,0 +1 @@
+export const value: number = 42;
diff --git a/test/fixtures/web-worker/typescript/entry.ts b/test/fixtures/web-worker/typescript/entry.ts
new file mode 100644
index 00000000000..99961c3cd8e
--- /dev/null
+++ b/test/fixtures/web-worker/typescript/entry.ts
@@ -0,0 +1,2 @@
+const value: number = 1;
+postMessage(value);
diff --git a/test/fixtures/web-worker/typescript/module.ts b/test/fixtures/web-worker/typescript/module.ts
new file mode 100644
index 00000000000..a2a7b8f4af3
--- /dev/null
+++ b/test/fixtures/web-worker/typescript/module.ts
@@ -0,0 +1,4 @@
+import { value } from './dependency.ts';
+
+const result: number = value;
+postMessage(result);
diff --git a/test/parallel/test-webworker-typescript-disabled.js b/test/parallel/test-webworker-typescript-disabled.js
new file mode 100644
index 00000000000..a19e5b122ab
--- /dev/null
+++ b/test/parallel/test-webworker-typescript-disabled.js
@@ -0,0 +1,13 @@
+// Flags: --experimental-web-worker --no-strip-types
+'use strict';
+
+const common = require('../common');
+const assert = require('node:assert');
+const fixtures = require('../common/fixtures');
+
+// Without type stripping, annotated `.ts` entries fail to parse.
+const worker = new Worker(fixtures.fileURL('web-worker', 'typescript', 'entry.ts'), { type: 'module' });
+worker.onmessage = common.mustNotCall('types must not be stripped');
+worker.onerror = common.mustCall(({ error }) => {
+ assert.strictEqual(error.name, 'SyntaxError');
+});
diff --git a/test/parallel/test-webworker-typescript.js b/test/parallel/test-webworker-typescript.js
new file mode 100644
index 00000000000..058c1728f34
--- /dev/null
+++ b/test/parallel/test-webworker-typescript.js
@@ -0,0 +1,64 @@
+// Flags: --experimental-web-worker
+'use strict';
+
+const common = require('../common');
+if (!process.config.variables.node_use_amaro) {
+ common.skip('Requires Amaro');
+}
+
+// Strip file entry types without changing the worker's module semantics.
+const assert = require('node:assert');
+const { mkdirSync, writeFileSync } = require('node:fs');
+const { join } = require('node:path');
+const { pathToFileURL } = require('node:url');
+const fixtures = require('../common/fixtures');
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+function createEntry(name, source) {
+ const path = join(tmpdir.path, name);
+ writeFileSync(path, source);
+ return pathToFileURL(path);
+}
+
+function expectMessage(url, expected) {
+ const worker = new Worker(url, { type: 'module' });
+ worker.onerror = common.mustNotCall('worker failed');
+ worker.onmessage = common.mustCall(({ data }) => {
+ assert.strictEqual(data, expected);
+ worker.terminate();
+ });
+}
+
+function expectError(url, code, type = 'module') {
+ const worker = new Worker(url, { type });
+ worker.onmessage = common.mustNotCall('worker unexpectedly succeeded');
+ worker.onerror = common.mustCall(({ error }) => {
+ assert.strictEqual(error.code ?? error.name, code);
+ });
+}
+
+// Worker type takes precedence over both the extension and package type.
+writeFileSync(join(tmpdir.path, 'package.json'), '{ "type": "commonjs" }');
+for (const extension of ['ts', 'mts', 'cts']) {
+ const url = createEntry(`entry.${extension}`, 'const type: string = typeof require; postMessage(type);');
+ expectMessage(url, 'undefined');
+}
+
+// The entry can import TypeScript, and a query or hash does not affect detection.
+{
+ const url = fixtures.fileURL('web-worker', 'typescript', 'module.ts');
+ url.search = '?version=1';
+ url.hash = '#entry';
+ expectMessage(url, 42);
+}
+
+// Stripping errors reach the parent's error handler.
+mkdirSync(join(tmpdir.path, 'node_modules'));
+expectError(createEntry('node_modules/entry.ts', 'postMessage(1);'),
+ 'ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING');
+
+// Classic workers and `.js` entries are not stripped.
+expectError(fixtures.fileURL('web-worker', 'typescript', 'entry.ts'), 'SyntaxError', 'classic');
+expectError(createEntry('entry.js', 'const value: number = 1;'), 'SyntaxError');