Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
worker: simplify Web Worker type stripping
Pass the decoded path to stripTypeScriptModuleTypes() so it does the
node_modules check, instead of repeating it in webworker.js.

Shorten the note in globals.md and drop the typescript.md paragraph.
Remove test cases that repeat what other cases already cover.

Signed-off-by: Hubert Walczak <hubertwalczak8@gmail.com>
  • Loading branch information
osztenkurden committed Sep 23, 2026
commit 29c8d520186e74db4046f8c20bbb1e185b5043d6
12 changes: 4 additions & 8 deletions doc/api/globals.md
Original file line number Diff line number Diff line change
Expand Up @@ -1373,14 +1373,9 @@ 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.

Module workers (`{ type: 'module' }`) loaded from `file:` URLs support
[TypeScript type stripping](typescript.md#type-stripping) for `.ts`, `.mts`, and
`.cts` entries, unless `--no-strip-types` is enabled. The worker's `type` option
determines how the entry is executed, regardless of its extension or the
surrounding `package.json`: even a `.cts` module-worker entry is evaluated as an
ES module. Imported modules follow the usual Node.js module-loading rules.
Classic worker entries, `importScripts()`, and `data:` or `blob:` entry sources
do not support 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.
Comment thread
osztenkurden marked this conversation as resolved.
Outdated

### Differences from the HTML Standard

Expand Down Expand Up @@ -1476,6 +1471,7 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
[Navigator API]: https://html.spec.whatwg.org/multipage/system-state.html#the-navigator-object
[RFC 5646]: https://www.rfc-editor.org/rfc/rfc5646.txt
[Refable protocol]: process.md#processrefmayberefable
[Type stripping]: typescript.md#type-stripping
[Web Crypto API]: webcrypto.md
[`--experimental-eventsource`]: cli.md#--experimental-eventsource
[`--experimental-web-worker`]: cli.md#--experimental-web-worker
Expand Down
5 changes: 0 additions & 5 deletions doc/api/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,6 @@ if you want your code to run as CommonJS you must use `require` and
* `.cts` files will always be run as CommonJS modules, similar to `.cjs` files.
* `.tsx` files are unsupported.

For [Web Worker module entries](globals.md#loading-worker-scripts), the worker's
`type` option takes precedence over these module-system rules. A TypeScript
file used as a module-worker entry is evaluated as an ES module, including when
it has a `.cts` extension. Its imports still follow the rules above.

As in JavaScript files, [file extensions are mandatory][] in `import` statements
and `import()` expressions: `import './file.ts'`, not `import './file'`. Because
of backward compatibility, file extensions are also mandatory in `require()`
Expand Down
25 changes: 11 additions & 14 deletions lib/internal/webworker.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ const {
globalThis,
} = primordials;

const { extname } = require('path');

const {
ERR_ILLEGAL_CONSTRUCTOR,
ERR_INVALID_STATE,
ERR_NO_CRYPTO,
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
} = require('internal/errors').codes;

const {
Expand All @@ -38,13 +39,14 @@ const {
initEventTarget,
} = require('internal/event_target');

const { getOptionValue } = require('internal/options');

const {
assignFunctionName,
defineOperation,
exposeInterface,
getCWDURL,
getLazy,
isUnderNodeModules,
kEmptyObject,
kEnumerableProperty,
lazyDOMException,
Expand Down Expand Up @@ -258,18 +260,13 @@ function runClassicScriptSource(source, url) {
function runModuleScriptSource(source, url) {
return require('internal/modules/run_main').runEntryPointWithESMLoader(
(loader) => {
if (require('internal/options').getOptionValue('--strip-types')) {
const parsedURL = new URL(url);
if (parsedURL.protocol === 'file:') {
const filename = fileURLToPath(parsedURL);
const extension = require('path').extname(filename);
if (extension === '.ts' || extension === '.mts' || extension === '.cts') {
if (isUnderNodeModules(filename)) {
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
}
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
source = stripTypeScriptModuleTypes(source, url);
}
const parsedURL = new URL(url);
if (parsedURL.protocol === 'file:' && getOptionValue('--strip-types')) {
Comment thread
jasnell marked this conversation as resolved.
const filename = fileURLToPath(parsedURL);
const extension = extname(filename);
if (extension === '.ts' || extension === '.mts' || extension === '.cts') {
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
Comment thread
osztenkurden marked this conversation as resolved.
Outdated
source = stripTypeScriptModuleTypes(source, filename, url);
}
}
// Necessary to reset RegExp statics before user code runs.
Expand Down
10 changes: 2 additions & 8 deletions test/fixtures/web-worker/typescript/module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import { value } from './dependency.ts';

const result: number = await Promise.resolve(value);
postMessage({
value: result,
url: import.meta.url,
main: import.meta.main,
requireType: typeof require,
thisIsUndefined: this === undefined,
});
const result: number = value;
postMessage(result);
17 changes: 1 addition & 16 deletions test/parallel/test-webworker-typescript-disabled.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,12 @@
'use strict';

const common = require('../common');
// Disabling stripping must reject annotations, not JavaScript-compatible .ts files.
const assert = require('node:assert');
const { 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();

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

const path = join(tmpdir.path, 'entry.ts');
writeFileSync(path, 'postMessage(1);');
const untyped = new Worker(pathToFileURL(path), { type: 'module' });
untyped.onerror = common.mustNotCall('JavaScript-compatible entries must still work');
untyped.onmessage = common.mustCall(({ data }) => {
assert.strictEqual(data, 1);
untyped.terminate();
});
41 changes: 10 additions & 31 deletions test/parallel/test-webworker-typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function expectMessage(url, expected) {
const worker = new Worker(url, { type: 'module' });
worker.onerror = common.mustNotCall('worker failed');
worker.onmessage = common.mustCall(({ data }) => {
assert.deepStrictEqual(data, expected);
assert.strictEqual(data, expected);
worker.terminate();
});
}
Expand All @@ -39,47 +39,26 @@ function expectError(url, code, type = 'module') {
});
}

const entry = fixtures.fileURL('web-worker', 'typescript', 'entry.ts');
expectMessage(entry, 1);
expectError(entry, 'SyntaxError', 'classic');

// 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 value: number = 1;
postMessage([value, typeof require, this === undefined, import.meta.main]);
`);
expectMessage(url, [1, 'undefined', true, true]);
const url = createEntry(`entry.${extension}`, 'const type: string = typeof require; postMessage(type);');
expectMessage(url, 'undefined');
}

// The stripped entry can still import TypeScript and keeps its original URL.
// 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, {
value: 42,
url: url.href,
main: true,
requireType: 'undefined',
thisIsUndefined: true,
});
expectMessage(url, 42);
}

// Extension detection and the node_modules restriction use the decoded path.
{
const url = createEntry('space \u00e9.ts', 'const value: number = 1; postMessage(value);');
expectMessage(url.href.replace('.ts', '.%74s') + '?version=2#entry', 1);
}
// Stripping errors reach the parent's error handler.
mkdirSync(join(tmpdir.path, 'node_modules'));
const dependency = createEntry('node_modules/entry.ts', 'postMessage(1);');
for (const url of [dependency.href, dependency.href.replace('node_modules', '%6eode_modules')]) {
expectError(url, 'ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING');
}
expectError(createEntry('node_modules/entry.ts', 'postMessage(1);'),
'ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING');

// Type-stripping errors must reach the parent's error handler. JavaScript
// entries must not acquire TypeScript support.
expectError(createEntry('invalid.ts', 'const value: = 1;'), 'ERR_INVALID_TYPESCRIPT_SYNTAX');
expectError(createEntry('enum.ts', 'enum Value { A }'), 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX');
// 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');
Loading