Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 6 additions & 0 deletions doc/api/globals.md
Original file line number Diff line number Diff line change
Expand Up @@ -1373,6 +1373,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:
Expand All @@ -1397,6 +1401,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`

Expand Down Expand Up @@ -1540,5 +1545,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
24 changes: 21 additions & 3 deletions lib/internal/webworker.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const {
globalThis,
} = primordials;

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

const {
ERR_ILLEGAL_CONSTRUCTOR,
ERR_INVALID_STATE,
Expand All @@ -37,6 +39,8 @@ const {
initEventTarget,
} = require('internal/event_target');

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

const {
assignFunctionName,
defineOperation,
Expand Down Expand Up @@ -89,6 +93,7 @@ const {
const {
URL,
URLParse,
fileURLToPath,
} = require('internal/url');

const {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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')) {
Comment thread
jasnell marked this conversation as resolved.
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);
},
);
}

Expand Down
1 change: 1 addition & 0 deletions test/fixtures/web-worker/typescript/dependency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value: number = 42;
2 changes: 2 additions & 0 deletions test/fixtures/web-worker/typescript/entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const value: number = 1;
postMessage(value);
4 changes: 4 additions & 0 deletions test/fixtures/web-worker/typescript/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { value } from './dependency.ts';

const result: number = value;
postMessage(result);
13 changes: 13 additions & 0 deletions test/parallel/test-webworker-typescript-disabled.js
Original file line number Diff line number Diff line change
@@ -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');
});
64 changes: 64 additions & 0 deletions test/parallel/test-webworker-typescript.js
Original file line number Diff line number Diff line change
@@ -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');
Loading