diff --git a/doc/api/globals.md b/doc/api/globals.md index a5998d14885..3016f27c267 100644 --- a/doc/api/globals.md +++ b/doc/api/globals.md @@ -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: @@ -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` @@ -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 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');