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
9 changes: 9 additions & 0 deletions doc/api/globals.md
Original file line number Diff line number Diff line change
Expand Up @@ -1373,6 +1373,15 @@ 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.

### Differences from the HTML Standard

Besides script loading, mentioned above:
Expand Down
5 changes: 5 additions & 0 deletions doc/api/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ 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: 22 additions & 3 deletions lib/internal/webworker.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const {
ERR_ILLEGAL_CONSTRUCTOR,
ERR_INVALID_STATE,
ERR_NO_CRYPTO,
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
} = require('internal/errors').codes;

const {
Expand All @@ -43,6 +44,7 @@ const {
exposeInterface,
getCWDURL,
getLazy,
isUnderNodeModules,
kEmptyObject,
kEnumerableProperty,
lazyDOMException,
Expand Down Expand Up @@ -89,6 +91,7 @@ const {
const {
URL,
URLParse,
fileURLToPath,
} = require('internal/url');

const {
Expand Down Expand Up @@ -253,10 +256,26 @@ 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) => {
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);
}
}
}
// 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);
10 changes: 10 additions & 0 deletions test/fixtures/web-worker/typescript/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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,
});
28 changes: 28 additions & 0 deletions test/parallel/test-webworker-typescript-disabled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Flags: --experimental-web-worker --no-strip-types
'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();

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();
});
85 changes: 85 additions & 0 deletions test/parallel/test-webworker-typescript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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.deepStrictEqual(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);
});
}

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]);
}

// The stripped entry can still import TypeScript and keeps its original URL.
{
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,
});
}

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

// 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');
expectError(createEntry('entry.js', 'const value: number = 1;'), 'SyntaxError');
Loading