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