Skip to content

Commit db1e36b

Browse files
authored
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: #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>
1 parent 1e0167d commit db1e36b

7 files changed

Lines changed: 111 additions & 3 deletions

File tree

‎doc/api/globals.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,6 +1374,10 @@ accepted and how failures are reported:
13741374
* For `blob:` URLs, the script must be held in memory, so blobs backed by a file,
13751375
such as those returned by [`fs.openAsBlob()`][], cannot be used.
13761376

1377+
[Type stripping][type stripping] only applies to module workers loaded from
1378+
`file:` URLs. The `type` option, not the file extension, decides how an entry is
1379+
run, so a `.cts` entry is still evaluated as an ES module.
1380+
13771381
### Differences from the HTML Standard
13781382

13791383
Besides script loading, mentioned above:
@@ -1398,6 +1402,7 @@ Besides script loading, mentioned above:
13981402
`unhandledrejection`, since Node.js exposes the equivalent does not
13991403
implement the `PromiseRejectionEvent` interface or the per-rejection
14001404
`preventDefault()` behavior required by the HTML Standard.
1405+
* Module workers loaded from `file:` URLs support [type stripping][].
14011406

14021407
### Web Workers and `node:worker_threads`
14031408

@@ -1541,5 +1546,6 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
15411546
[buffer section]: buffer.md
15421547
[built-in objects]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
15431548
[timers]: timers.md
1549+
[type stripping]: typescript.md#type-stripping
15441550
[webassembly-mdn]: https://developer.mozilla.org/en-US/docs/WebAssembly
15451551
[webassembly-org]: https://webassembly.org

‎lib/internal/webworker.js‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const {
2525
globalThis,
2626
} = primordials;
2727

28+
const { extname } = require('path');
29+
2830
const {
2931
ERR_ILLEGAL_CONSTRUCTOR,
3032
ERR_INVALID_STATE,
@@ -37,6 +39,8 @@ const {
3739
initEventTarget,
3840
} = require('internal/event_target');
3941

42+
const { getOptionValue } = require('internal/options');
43+
4044
const {
4145
assignFunctionName,
4246
defineOperation,
@@ -89,6 +93,7 @@ const {
8993
const {
9094
URL,
9195
URLParse,
96+
fileURLToPath,
9297
} = require('internal/url');
9398

9499
const {
@@ -117,6 +122,9 @@ let scopeBaseURL = null;
117122
const lazyErrorEvent =
118123
getLazy(() => require('internal/deps/undici/undici').ErrorEvent);
119124

125+
const lazyStripTypeScriptModuleTypes = getLazy(
126+
() => require('internal/modules/typescript').stripTypeScriptModuleTypes);
127+
120128
function createErrorEvent(init) {
121129
const ErrorEvent = lazyErrorEvent();
122130
return new ErrorEvent('error', init);
@@ -253,10 +261,20 @@ function runClassicScriptSource(source, url) {
253261
* @returns {Promise}
254262
*/
255263
function runModuleScriptSource(source, url) {
256-
// Necessary to reset RegExp statics before user code runs.
257-
RegExpPrototypeExec(/^/, '');
258264
return require('internal/modules/run_main').runEntryPointWithESMLoader(
259-
(loader) => loader.eval(source, url, true),
265+
(loader) => {
266+
const parsedURL = new URL(url);
267+
if (parsedURL.protocol === 'file:' && getOptionValue('--strip-types')) {
268+
const filename = fileURLToPath(parsedURL);
269+
const extension = extname(filename);
270+
if (extension === '.ts' || extension === '.mts' || extension === '.cts') {
271+
source = lazyStripTypeScriptModuleTypes()(source, filename, url);
272+
}
273+
}
274+
// Necessary to reset RegExp statics before user code runs.
275+
RegExpPrototypeExec(/^/, '');
276+
return loader.eval(source, url, true);
277+
},
260278
);
261279
}
262280

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const value: number = 42;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const value: number = 1;
2+
postMessage(value);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { value } from './dependency.ts';
2+
3+
const result: number = value;
4+
postMessage(result);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Flags: --experimental-web-worker --no-strip-types
2+
'use strict';
3+
4+
const common = require('../common');
5+
const assert = require('node:assert');
6+
const fixtures = require('../common/fixtures');
7+
8+
// Without type stripping, annotated `.ts` entries fail to parse.
9+
const worker = new Worker(fixtures.fileURL('web-worker', 'typescript', 'entry.ts'), { type: 'module' });
10+
worker.onmessage = common.mustNotCall('types must not be stripped');
11+
worker.onerror = common.mustCall(({ error }) => {
12+
assert.strictEqual(error.name, 'SyntaxError');
13+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Flags: --experimental-web-worker
2+
'use strict';
3+
4+
const common = require('../common');
5+
if (!process.config.variables.node_use_amaro) {
6+
common.skip('Requires Amaro');
7+
}
8+
9+
// Strip file entry types without changing the worker's module semantics.
10+
const assert = require('node:assert');
11+
const { mkdirSync, writeFileSync } = require('node:fs');
12+
const { join } = require('node:path');
13+
const { pathToFileURL } = require('node:url');
14+
const fixtures = require('../common/fixtures');
15+
const tmpdir = require('../common/tmpdir');
16+
17+
tmpdir.refresh();
18+
19+
function createEntry(name, source) {
20+
const path = join(tmpdir.path, name);
21+
writeFileSync(path, source);
22+
return pathToFileURL(path);
23+
}
24+
25+
function expectMessage(url, expected) {
26+
const worker = new Worker(url, { type: 'module' });
27+
worker.onerror = common.mustNotCall('worker failed');
28+
worker.onmessage = common.mustCall(({ data }) => {
29+
assert.strictEqual(data, expected);
30+
worker.terminate();
31+
});
32+
}
33+
34+
function expectError(url, code, type = 'module') {
35+
const worker = new Worker(url, { type });
36+
worker.onmessage = common.mustNotCall('worker unexpectedly succeeded');
37+
worker.onerror = common.mustCall(({ error }) => {
38+
assert.strictEqual(error.code ?? error.name, code);
39+
});
40+
}
41+
42+
// Worker type takes precedence over both the extension and package type.
43+
writeFileSync(join(tmpdir.path, 'package.json'), '{ "type": "commonjs" }');
44+
for (const extension of ['ts', 'mts', 'cts']) {
45+
const url = createEntry(`entry.${extension}`, 'const type: string = typeof require; postMessage(type);');
46+
expectMessage(url, 'undefined');
47+
}
48+
49+
// The entry can import TypeScript, and a query or hash does not affect detection.
50+
{
51+
const url = fixtures.fileURL('web-worker', 'typescript', 'module.ts');
52+
url.search = '?version=1';
53+
url.hash = '#entry';
54+
expectMessage(url, 42);
55+
}
56+
57+
// Stripping errors reach the parent's error handler.
58+
mkdirSync(join(tmpdir.path, 'node_modules'));
59+
expectError(createEntry('node_modules/entry.ts', 'postMessage(1);'),
60+
'ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING');
61+
62+
// Classic workers and `.js` entries are not stripped.
63+
expectError(fixtures.fileURL('web-worker', 'typescript', 'entry.ts'), 'SyntaxError', 'classic');
64+
expectError(createEntry('entry.js', 'const value: number = 1;'), 'SyntaxError');

0 commit comments

Comments
 (0)