diff --git a/doc/api/cli.md b/doc/api/cli.md index 15da9004b0e7b6..ad916576654072 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1414,8 +1414,23 @@ added: v12.0.0 --> This configures Node.js to interpret `--eval` or `STDIN` input as CommonJS or -as an ES module. Valid values are `"commonjs"` or `"module"`. The default is -`"commonjs"` unless [`--experimental-default-type=module`][] is used. +as an ES module. Valid values are `"commonjs"`, `"module"`, `"module-typescript"` and `"commonjs-typescript"`. +The `"-typescript"` values are available only in combination with the flag `--experimental-strip-types`. +The default is `"commonjs"` unless [`--experimental-default-type=module`][] is used. +If `--experimental-strip-types` is enabled and `--input-type` is not provided, +Node.js will try to detect the syntax with the following steps: + +1. Run the input as CommonJS. +2. If step 1 fails, run the input as an ES module. +3. If step 2 fails with a SyntaxError, strip the types. +4. If step 3 fails with an error code [`ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`][] + or [`ERR_INVALID_TYPESCRIPT_SYNTAX`][], + throw the error from step 2, including the TypeScript error in the message, + else run as CommonJS. +5. If step 4 fails, run the input as an ES module. + +To avoid the delay of multiple syntax detection passes, the `--input-type=type` flag can be used to specify +how the `--eval` input should be interpreted. The REPL does not support this option. Usage of `--input-type=module` with [`--print`][] will throw an error, as `--print` does not support ES module @@ -3712,6 +3727,8 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12 [`Atomics.wait()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait [`Buffer`]: buffer.md#class-buffer [`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man3.0/man3/CRYPTO_secure_malloc_init.html +[`ERR_INVALID_TYPESCRIPT_SYNTAX`]: errors.md#err_invalid_typescript_syntax +[`ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`]: errors.md#err_unsupported_typescript_syntax [`NODE_OPTIONS`]: #node_optionsoptions [`NO_COLOR`]: https://no-color.org [`SlowBuffer`]: buffer.md#class-slowbuffer diff --git a/doc/api/errors.md b/doc/api/errors.md index 3c0158457b3563..af0b0cb57094ec 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -2106,11 +2106,13 @@ does not consist of exactly two elements. -The provided TypeScript syntax is not valid or unsupported. -This could happen when using TypeScript syntax that requires -transformation with [type-stripping][]. +The provided TypeScript syntax is not valid. @@ -3103,6 +3105,18 @@ try { } ``` + + +### `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX` + + + +The provided TypeScript syntax is unsupported. +This could happen when using TypeScript syntax that requires +transformation with [type-stripping][]. + ### `ERR_USE_AFTER_CLOSE` diff --git a/doc/api/typescript.md b/doc/api/typescript.md index d2680670a5f316..4d90f5c285f525 100644 --- a/doc/api/typescript.md +++ b/doc/api/typescript.md @@ -153,10 +153,10 @@ import { fn, FnParams } from './fn.ts'; ### Non-file forms of input -Type stripping can be enabled for `--eval`. The module system +Type stripping can be enabled for `--eval` and STDIN. The module system will be determined by `--input-type`, as it is for JavaScript. -TypeScript syntax is unsupported in the REPL, STDIN input, `--print`, `--check`, and +TypeScript syntax is unsupported in the REPL, `--check`, and `inspect`. ### Source maps diff --git a/lib/internal/errors.js b/lib/internal/errors.js index de880fedd03515..356ed1fd94ec1b 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1841,6 +1841,7 @@ E('ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING', E('ERR_UNSUPPORTED_RESOLVE_REQUEST', 'Failed to resolve module specifier "%s" from "%s": Invalid relative URL or base scheme is not hierarchical.', TypeError); +E('ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX', '%s', SyntaxError); E('ERR_USE_AFTER_CLOSE', '%s was closed', Error); // This should probably be a `TypeError`. diff --git a/lib/internal/main/eval_stdin.js b/lib/internal/main/eval_stdin.js index 3ee4bcdb1d853b..2a0ecf9a4aba5e 100644 --- a/lib/internal/main/eval_stdin.js +++ b/lib/internal/main/eval_stdin.js @@ -11,6 +11,9 @@ const { getOptionValue } = require('internal/options'); const { evalModuleEntryPoint, + evalTypeScript, + parseAndEvalCommonjsTypeScript, + parseAndEvalModuleTypeScript, evalScript, readStdin, } = require('internal/process/execution'); @@ -25,14 +28,32 @@ readStdin((code) => { const print = getOptionValue('--print'); const shouldLoadESM = getOptionValue('--import').length > 0; - if (getOptionValue('--input-type') === 'module' || - (getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs')) { + const inputType = getOptionValue('--input-type'); + const tsEnabled = getOptionValue('--experimental-strip-types'); + if (inputType === 'module' || + (getOptionValue('--experimental-default-type') === 'module' && + inputType !== 'commonjs')) { evalModuleEntryPoint(code, print); + } else if (inputType === 'module-typescript' && tsEnabled) { + parseAndEvalModuleTypeScript(code, print); } else { - evalScript('[stdin]', - code, - getOptionValue('--inspect-brk'), - print, - shouldLoadESM); + + let evalFunction; + if (inputType === 'commonjs') { + evalFunction = evalScript; + } else if (inputType === 'commonjs-typescript' && tsEnabled) { + evalFunction = parseAndEvalCommonjsTypeScript; + } else if (tsEnabled) { + evalFunction = evalTypeScript; + } else { + // Default to commonjs. + evalFunction = evalScript; + } + + evalFunction('[stdin]', + code, + getOptionValue('--inspect-brk'), + print, + shouldLoadESM); } }); diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js index a3be08551425b1..171ab3bae86f16 100644 --- a/lib/internal/main/eval_string.js +++ b/lib/internal/main/eval_string.js @@ -13,9 +13,14 @@ const { prepareMainThreadExecution, markBootstrapComplete, } = require('internal/process/pre_execution'); -const { evalModuleEntryPoint, evalScript } = require('internal/process/execution'); +const { + evalModuleEntryPoint, + evalTypeScript, + parseAndEvalCommonjsTypeScript, + parseAndEvalModuleTypeScript, + evalScript, +} = require('internal/process/execution'); const { addBuiltinLibsToObject } = require('internal/modules/helpers'); -const { stripTypeScriptModuleTypes } = require('internal/modules/typescript'); const { getOptionValue } = require('internal/options'); prepareMainThreadExecution(); @@ -23,21 +28,22 @@ addBuiltinLibsToObject(globalThis, ''); markBootstrapComplete(); const code = getOptionValue('--eval'); -const source = getOptionValue('--experimental-strip-types') ? - stripTypeScriptModuleTypes(code) : - code; const print = getOptionValue('--print'); const shouldLoadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0; -if (getOptionValue('--input-type') === 'module' || - (getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs')) { - evalModuleEntryPoint(source, print); +const inputType = getOptionValue('--input-type'); +const tsEnabled = getOptionValue('--experimental-strip-types'); +if (inputType === 'module' || + (getOptionValue('--experimental-default-type') === 'module' && inputType !== 'commonjs')) { + evalModuleEntryPoint(code, print); +} else if (inputType === 'module-typescript' && tsEnabled) { + parseAndEvalModuleTypeScript(code, print); } else { // For backward compatibility, we want the identifier crypto to be the // `node:crypto` module rather than WebCrypto. const isUsingCryptoIdentifier = - getOptionValue('--experimental-global-webcrypto') && - RegExpPrototypeExec(/\bcrypto\b/, source) !== null; + getOptionValue('--experimental-global-webcrypto') && + RegExpPrototypeExec(/\bcrypto\b/, code) !== null; const shouldDefineCrypto = isUsingCryptoIdentifier && internalBinding('config').hasOpenSSL; if (isUsingCryptoIdentifier && !shouldDefineCrypto) { @@ -52,11 +58,24 @@ if (getOptionValue('--input-type') === 'module' || }; ObjectDefineProperty(object, name, { __proto__: null, set: setReal }); } - evalScript('[eval]', - shouldDefineCrypto ? ( - print ? `let crypto=require("node:crypto");{${source}}` : `(crypto=>{{${source}}})(require('node:crypto'))` - ) : source, - getOptionValue('--inspect-brk'), - print, - shouldLoadESM); + + let evalFunction; + if (inputType === 'commonjs') { + evalFunction = evalScript; + } else if (inputType === 'commonjs-typescript' && tsEnabled) { + evalFunction = parseAndEvalCommonjsTypeScript; + } else if (tsEnabled) { + evalFunction = evalTypeScript; + } else { + // Default to commonjs. + evalFunction = evalScript; + } + + evalFunction('[eval]', + shouldDefineCrypto ? ( + print ? `let crypto=require("node:crypto");{${code}}` : `(crypto=>{{${code}}})(require('node:crypto'))` + ) : code, + getOptionValue('--inspect-brk'), + print, + shouldLoadESM); } diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js index f8c410b5b25cb0..caf64728b754cc 100644 --- a/lib/internal/main/worker_thread.js +++ b/lib/internal/main/worker_thread.js @@ -49,7 +49,10 @@ const { setupMainThreadPort } = require('internal/worker/messaging'); const { onGlobalUncaughtException, evalScript, + evalTypeScript, evalModuleEntryPoint, + parseAndEvalCommonjsTypeScript, + parseAndEvalModuleTypeScript, } = require('internal/process/execution'); let debug = require('internal/util/debuglog').debuglog('worker', (fn) => { @@ -166,7 +169,29 @@ port.on('message', (message) => { value: filename, }); ArrayPrototypeSplice(process.argv, 1, 0, name); - evalScript(name, filename); + const tsEnabled = getOptionValue('--experimental-strip-types'); + const inputType = getOptionValue('--input-type'); + + if (inputType === 'module-typescript' && tsEnabled) { + // This is a special case where we want to parse and eval the + // TypeScript code as a module + parseAndEvalModuleTypeScript(filename, false); + break; + } + + let evalFunction; + if (inputType === 'commonjs') { + evalFunction = evalScript; + } else if (inputType === 'commonjs-typescript' && tsEnabled) { + evalFunction = parseAndEvalCommonjsTypeScript; + } else if (tsEnabled) { + evalFunction = evalTypeScript; + } else { + // Default to commonjs. + evalFunction = evalScript; + } + + evalFunction(name, filename); break; } diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 455166d3612aa3..62c33730ed17cb 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -435,7 +435,6 @@ function initializeCJS() { const tsEnabled = getOptionValue('--experimental-strip-types'); if (tsEnabled) { - emitExperimentalWarning('Type Stripping'); Module._extensions['.cts'] = loadCTS; Module._extensions['.ts'] = loadTS; } diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index e8c7b97d23c62a..fdecd0c926c953 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -210,9 +210,25 @@ class ModuleLoader { } } - async eval(source, url, isEntryPoint = false) { + /** + * + * @param {string} source Source code of the module. + * @param {string} url URL of the module. + * @returns {object} The module wrap object. + */ + createModuleWrap(source, url) { + return compileSourceTextModule(url, source, this); + } + + /** + * + * @param {string} url URL of the module. + * @param {object} wrap Module wrap object. + * @param {boolean} isEntryPoint Whether the module is the entry point. + * @returns {Promise} The module object. + */ + async executeModuleJob(url, wrap, isEntryPoint = false) { const { ModuleJob } = require('internal/modules/esm/module_job'); - const wrap = compileSourceTextModule(url, source, this); const module = await onImport.tracePromise(async () => { const job = new ModuleJob( this, url, undefined, wrap, false, false); @@ -232,6 +248,18 @@ class ModuleLoader { }; } + /** + * + * @param {string} source Source code of the module. + * @param {string} url URL of the module. + * @param {boolean} isEntryPoint Whether the module is the entry point. + * @returns {Promise} The module object. + */ + eval(source, url, isEntryPoint = false) { + const wrap = this.createModuleWrap(source, url); + return this.executeModuleJob(url, wrap, isEntryPoint); + } + /** * Get a (possibly not yet fully linked) module job from the cache, or create one and return its Promise. * @param {string} specifier The module request of the module to be resolved. Typically, what's diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 491dc3f450733a..a587246e329b41 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -242,7 +242,6 @@ translators.set('require-commonjs', (url, source, isMain) => { // Handle CommonJS modules referenced by `require` calls. // This translator function must be sync, as `require` is sync. translators.set('require-commonjs-typescript', (url, source, isMain) => { - emitExperimentalWarning('Type Stripping'); assert(cjsParse); const code = stripTypeScriptModuleTypes(stringify(source), url); return createCJSModuleWrap(url, code); @@ -457,7 +456,6 @@ translators.set('wasm', async function(url, source) { // Strategy for loading a commonjs TypeScript module translators.set('commonjs-typescript', function(url, source) { - emitExperimentalWarning('Type Stripping'); assertBufferSource(source, true, 'load'); const code = stripTypeScriptModuleTypes(stringify(source), url); debug(`Translating TypeScript ${url}`); @@ -466,7 +464,6 @@ translators.set('commonjs-typescript', function(url, source) { // Strategy for loading an esm TypeScript module translators.set('module-typescript', function(url, source) { - emitExperimentalWarning('Type Stripping'); assertBufferSource(source, true, 'load'); const code = stripTypeScriptModuleTypes(stringify(source), url); debug(`Translating TypeScript ${url}`); diff --git a/lib/internal/modules/typescript.js b/lib/internal/modules/typescript.js index 30e8f5ba6bca7b..6abfc707657b92 100644 --- a/lib/internal/modules/typescript.js +++ b/lib/internal/modules/typescript.js @@ -1,5 +1,8 @@ 'use strict'; +const { + ObjectPrototypeHasOwnProperty, +} = primordials; const { validateBoolean, validateOneOf, @@ -14,6 +17,7 @@ const { assertTypeScript, const { ERR_INVALID_TYPESCRIPT_SYNTAX, ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING, + ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, } = require('internal/errors').codes; const { getOptionValue } = require('internal/options'); const assert = require('internal/assert'); @@ -49,7 +53,21 @@ function parseTypeScript(source, options) { try { return parse(source, options); } catch (error) { - throw new ERR_INVALID_TYPESCRIPT_SYNTAX(error.message); + /** + * Amaro v0.3.0 (from SWC v1.10.7) throws an object with `message` and `code` properties. + * It allows us to distinguish between invalid syntax and unsupported syntax. + */ + switch (error?.code) { + case 'UnsupportedSyntax': + throw new ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX(error.message); + case 'InvalidSyntax': + throw new ERR_INVALID_TYPESCRIPT_SYNTAX(error.message); + default: + // SWC may throw strings when something goes wrong. + if (typeof error === 'string') { assert.fail(error); } + assert(error != null && ObjectPrototypeHasOwnProperty(error, 'message')); + assert.fail(error.message); + } } } @@ -113,9 +131,13 @@ function processTypeScriptCode(code, options) { * It is used by internal loaders. * @param {string} source TypeScript code to parse. * @param {string} filename The filename of the source code. + * @param {boolean} emitWarning Whether to emit a warning. * @returns {TransformOutput} The stripped TypeScript code. */ -function stripTypeScriptModuleTypes(source, filename) { +function stripTypeScriptModuleTypes(source, filename, emitWarning = true) { + if (emitWarning) { + emitExperimentalWarning('Type Stripping'); + } assert(typeof source === 'string'); if (isUnderNodeModules(filename)) { throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename); diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index 68b267b61c39b1..b5f9bec59189ca 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -2,6 +2,8 @@ const { RegExpPrototypeExec, + StringPrototypeIndexOf, + StringPrototypeSlice, Symbol, globalThis, } = primordials; @@ -17,6 +19,7 @@ const { } = require('internal/errors'); const { pathToFileURL } = require('internal/url'); const { exitCodes: { kGenericUserError } } = internalBinding('errors'); +const { stripTypeScriptModuleTypes } = require('internal/modules/typescript'); const { executionAsyncId, @@ -32,6 +35,7 @@ const { getOptionValue } = require('internal/options'); const { makeContextifyScript, runScriptInThisContext, } = require('internal/vm'); +const { emitExperimentalWarning } = require('internal/util'); // shouldAbortOnUncaughtToggle is a typed array for faster // communication with JS. const { shouldAbortOnUncaughtToggle } = internalBinding('util'); @@ -70,70 +74,29 @@ function evalModuleEntryPoint(source, print) { } function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) { - const CJSModule = require('internal/modules/cjs/loader').Module; - - const cwd = tryGetCwd(); const origModule = globalThis.module; // Set e.g. when called from the REPL. - - const module = new CJSModule(name); - module.filename = path.join(cwd, name); - module.paths = CJSModule._nodeModulePaths(cwd); - + const module = createModule(name); const baseUrl = pathToFileURL(module.filename).href; - if (getOptionValue('--experimental-detect-module') && - getOptionValue('--input-type') === '' && getOptionValue('--experimental-default-type') === '' && - containsModuleSyntax(body, name, null, 'no CJS variables')) { - return evalModuleEntryPoint(body, print); + if (shouldUseModuleEntryPoint(name, body)) { + return getOptionValue('--experimental-strip-types') ? + evalTypeScriptModuleEntryPoint(body, print) : + evalModuleEntryPoint(body, print); } - const runScript = () => { - // Create wrapper for cache entry - const script = ` - globalThis.module = module; - globalThis.exports = exports; - globalThis.__dirname = __dirname; - globalThis.require = require; - return (main) => main(); - `; - globalThis.__filename = name; - RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs. - const result = module._compile(script, `${name}-wrapper`)(() => { - const hostDefinedOptionId = Symbol(name); - async function importModuleDynamically(specifier, _, importAttributes) { - const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); - return cascadedLoader.import(specifier, baseUrl, importAttributes); - } - const script = makeContextifyScript( - body, // code - name, // filename, - 0, // lineOffset - 0, // columnOffset, - undefined, // cachedData - false, // produceCachedData - undefined, // parsingContext - hostDefinedOptionId, // hostDefinedOptionId - importModuleDynamically, // importModuleDynamically - ); - return runScriptInThisContext(script, true, !!breakFirstLine); - }); - if (print) { - const { log } = require('internal/console/global'); - - process.on('exit', () => { - log(result); - }); - } - - if (origModule !== undefined) - globalThis.module = origModule; - }; + const evalFunction = () => runScriptInContext(name, + body, + breakFirstLine, + print, + module, + baseUrl, + undefined, + origModule); if (shouldLoadESM) { - require('internal/modules/run_main').runEntryPointWithESMLoader(runScript); - return; + return require('internal/modules/run_main').runEntryPointWithESMLoader(evalFunction); } - runScript(); + evalFunction(); } const exceptionHandlerState = { @@ -238,10 +201,273 @@ function readStdin(callback) { }); } +/** + * Adds the TS message to the error stack. + * + * At the 3rd line of the stack, the message is added. + * @param {string} originalStack The stack to decorate + * @param {string} newMessage the message to add to the error stack + * @returns {void} + */ +function decorateCJSErrorWithTSMessage(originalStack, newMessage) { + let index; + for (let i = 0; i < 3; i++) { + index = StringPrototypeIndexOf(originalStack, '\n', index + 1); + } + return StringPrototypeSlice(originalStack, 0, index) + + '\n' + newMessage + + StringPrototypeSlice(originalStack, index); +} + +/** + * + * Wrapper of evalScript + * + * This function wraps the evaluation of the source code in a try-catch block. + * If the source code fails to be evaluated, it will retry evaluating the source code + * with the TypeScript parser. + * + * If the source code fails to be evaluated with the TypeScript parser, + * it will rethrow the original error, adding the TypeScript error message to the stack. + * + * This way we don't change the behavior of the code, but we provide a better error message + * in case of a typescript error. + * @param {string} name The name of the file + * @param {string} source The source code to evaluate + * @param {boolean} breakFirstLine Whether to break on the first line + * @param {boolean} print If the result should be printed + * @param {boolean} shouldLoadESM If the code should be loaded as an ESM module + * @returns {void} + */ +function evalTypeScript(name, source, breakFirstLine, print, shouldLoadESM = false) { + const origModule = globalThis.module; // Set e.g. when called from the REPL. + const module = createModule(name); + const baseUrl = pathToFileURL(module.filename).href; + + if (shouldUseModuleEntryPoint(name, source)) { + return evalTypeScriptModuleEntryPoint(source, print); + } + + let compiledScript; + // This variable can be modified if the source code is stripped. + let sourceToRun = source; + try { + compiledScript = compileScript(name, source, baseUrl); + } catch (originalError) { + try { + sourceToRun = stripTypeScriptModuleTypes(source, name, false); + // Retry the CJS/ESM syntax detection after stripping the types. + if (shouldUseModuleEntryPoint(name, sourceToRun)) { + return evalTypeScriptModuleEntryPoint(source, print); + } + // If the ContextifiedScript was successfully created, execute it. + // outside the try-catch block to avoid catching runtime errors. + compiledScript = compileScript(name, sourceToRun, baseUrl); + // Emit the experimental warning after the code was successfully evaluated. + emitExperimentalWarning('Type Stripping'); + } catch (tsError) { + // If it's invalid or unsupported TypeScript syntax, rethrow the original error + // with the TypeScript error message added to the stack. + if (tsError.code === 'ERR_INVALID_TYPESCRIPT_SYNTAX' || tsError.code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX') { + originalError.stack = decorateCJSErrorWithTSMessage(originalError.stack, tsError.message); + throw originalError; + } + + throw tsError; + } + } + + const evalFunction = () => runScriptInContext(name, + sourceToRun, + breakFirstLine, + print, + module, + baseUrl, + compiledScript, + origModule); + + if (shouldLoadESM) { + return require('internal/modules/run_main').runEntryPointWithESMLoader(evalFunction); + } + evalFunction(); +} + +/** + * Wrapper of evalModuleEntryPoint + * + * This function wraps the compilation of the source code in a try-catch block. + * If the source code fails to be compiled, it will retry transpiling the source code + * with the TypeScript parser. + * @param {string} source The source code to evaluate + * @param {boolean} print If the result should be printed + * @returns {Promise} The module evaluation promise + */ +function evalTypeScriptModuleEntryPoint(source, print) { + if (print) { + throw new ERR_EVAL_ESM_CANNOT_PRINT(); + } + + RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs. + + return require('internal/modules/run_main').runEntryPointWithESMLoader( + async (loader) => { + const url = getEvalModuleUrl(); + let moduleWrap; + try { + // Compile the module to check for syntax errors. + moduleWrap = loader.createModuleWrap(source, url); + } catch (originalError) { + try { + const strippedSource = stripTypeScriptModuleTypes(source, url, false); + // If the moduleWrap was successfully created, execute the module job. + // outside the try-catch block to avoid catching runtime errors. + moduleWrap = loader.createModuleWrap(strippedSource, url); + // Emit the experimental warning after the code was successfully compiled. + emitExperimentalWarning('Type Stripping'); + } catch (tsError) { + // If it's invalid or unsupported TypeScript syntax, rethrow the original error + // with the TypeScript error message added to the stack. + if (tsError.code === 'ERR_INVALID_TYPESCRIPT_SYNTAX' || + tsError.code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX') { + originalError.stack = `${tsError.message}\n\n${originalError.stack}`; + throw originalError; + } + + throw tsError; + } + } + // If the moduleWrap was successfully created either with by just compiling + // or after transpilation, execute the module job. + return loader.executeModuleJob(url, moduleWrap, true); + }, + ); +}; + +/** + * + * Function used to shortcut when `--input-type=module-typescript` is set. + * @param {string} source + * @param {boolean} print + */ +function parseAndEvalModuleTypeScript(source, print) { + // We know its a TypeScript module, we can safely emit the experimental warning. + const strippedSource = stripTypeScriptModuleTypes(source, getEvalModuleUrl()); + evalModuleEntryPoint(strippedSource, print); +} + +/** + * Function used to shortcut when `--input-type=commonjs-typescript` is set + * @param {string} name The name of the file + * @param {string} source The source code to evaluate + * @param {boolean} breakFirstLine Whether to break on the first line + * @param {boolean} print If the result should be printed + * @param {boolean} shouldLoadESM If the code should be loaded as an ESM module + * @returns {void} + */ +function parseAndEvalCommonjsTypeScript(name, source, breakFirstLine, print, shouldLoadESM = false) { + // We know its a TypeScript module, we can safely emit the experimental warning. + const strippedSource = stripTypeScriptModuleTypes(source, getEvalModuleUrl()); + evalScript(name, strippedSource, breakFirstLine, print, shouldLoadESM); +} + +/** + * + * @param {string} name - The filename of the script. + * @param {string} body - The code of the script. + * @param {string} baseUrl Path of the parent importing the module. + * @returns {ContextifyScript} The created contextify script. + */ +function compileScript(name, body, baseUrl) { + const hostDefinedOptionId = Symbol(name); + async function importModuleDynamically(specifier, _, importAttributes) { + const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); + return cascadedLoader.import(specifier, baseUrl, importAttributes); + } + return makeContextifyScript( + body, // code + name, // filename, + 0, // lineOffset + 0, // columnOffset, + undefined, // cachedData + false, // produceCachedData + undefined, // parsingContext + hostDefinedOptionId, // hostDefinedOptionId + importModuleDynamically, // importModuleDynamically + ); +} + +/** + * @param {string} name - The filename of the script. + * @param {string} body - The code of the script. + * @returns {boolean} Whether the module entry point should be evaluated as a module. + */ +function shouldUseModuleEntryPoint(name, body) { + return getOptionValue('--experimental-detect-module') && getOptionValue('--experimental-default-type') === '' && + getOptionValue('--input-type') === '' && + containsModuleSyntax(body, name, null, 'no CJS variables'); +} + +/** + * + * @param {string} name - The filename of the script. + * @returns {import('internal/modules/esm/loader').CJSModule} The created module. + */ +function createModule(name) { + const CJSModule = require('internal/modules/cjs/loader').Module; + const cwd = tryGetCwd(); + const module = new CJSModule(name); + module.filename = path.join(cwd, name); + module.paths = CJSModule._nodeModulePaths(cwd); + return module; +} + +/** + * + * @param {string} name - The filename of the script. + * @param {string} body - The code of the script. + * @param {boolean} breakFirstLine Whether to break on the first line + * @param {boolean} print If the result should be printed + * @param {import('internal/modules/esm/loader').CJSModule} module The module + * @param {string} baseUrl Path of the parent importing the module. + * @param {object} compiledScript The compiled script. + * @param {any} origModule The original module. + * @returns {void} + */ +function runScriptInContext(name, body, breakFirstLine, print, module, baseUrl, compiledScript, origModule) { + // Create wrapper for cache entry + const script = ` + globalThis.module = module; + globalThis.exports = exports; + globalThis.__dirname = __dirname; + globalThis.require = require; + return (main) => main(); + `; + globalThis.__filename = name; + RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs. + const result = module._compile(script, `${name}-wrapper`)(() => { + // If the script was already compiled, use it. + return runScriptInThisContext( + compiledScript ?? compileScript(name, body, baseUrl), + true, !!breakFirstLine); + }); + if (print) { + const { log } = require('internal/console/global'); + + process.on('exit', () => { + log(result); + }); + } + if (origModule !== undefined) + globalThis.module = origModule; +} + module.exports = { + parseAndEvalCommonjsTypeScript, + parseAndEvalModuleTypeScript, readStdin, tryGetCwd, evalModuleEntryPoint, + evalTypeScript, evalScript, onGlobalUncaughtException: createOnGlobalUncaughtException(), setUncaughtExceptionCaptureCallback, diff --git a/src/node_options.cc b/src/node_options.cc index 5726b749e829a4..1d81079a9b7d8a 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -109,8 +109,12 @@ void PerIsolateOptions::CheckOptions(std::vector* errors, void EnvironmentOptions::CheckOptions(std::vector* errors, std::vector* argv) { if (!input_type.empty()) { - if (input_type != "commonjs" && input_type != "module") { - errors->push_back("--input-type must be \"module\" or \"commonjs\""); + if (input_type != "commonjs" && input_type != "module" && + input_type != "commonjs-typescript" && + input_type != "module-typescript") { + errors->push_back( + "--input-type must be \"module\"," + "\"commonjs\", \"module-typescript\" or \"commonjs-typescript\""); } } diff --git a/test/es-module/test-typescript-eval.mjs b/test/es-module/test-typescript-eval.mjs index e6d841ffa07f7e..38744de7185179 100644 --- a/test/es-module/test-typescript-eval.mjs +++ b/test/es-module/test-typescript-eval.mjs @@ -1,5 +1,5 @@ import { skip, spawnPromisified } from '../common/index.mjs'; -import { match, strictEqual } from 'node:assert'; +import { doesNotMatch, match, strictEqual } from 'node:assert'; import { test } from 'node:test'; if (!process.config.variables.node_use_amaro) skip('Requires Amaro'); @@ -20,7 +20,7 @@ test('eval TypeScript ESM syntax', async () => { test('eval TypeScript ESM syntax with input-type module', async () => { const result = await spawnPromisified(process.execPath, [ '--experimental-strip-types', - '--input-type=module', + '--input-type=module-typescript', '--eval', `import util from 'node:util' const text: string = 'Hello, TypeScript!' @@ -37,17 +37,16 @@ test('eval TypeScript CommonJS syntax', async () => { '--eval', `const util = require('node:util'); const text: string = 'Hello, TypeScript!' - console.log(util.styleText('red', text));`, - '--no-warnings']); + console.log(util.styleText('red', text));`]); match(result.stdout, /Hello, TypeScript!/); - strictEqual(result.stderr, ''); + match(result.stderr, /ExperimentalWarning: Type Stripping is an experimental/); strictEqual(result.code, 0); }); -test('eval TypeScript CommonJS syntax with input-type commonjs', async () => { +test('eval TypeScript CommonJS syntax with input-type commonjs-typescript', async () => { const result = await spawnPromisified(process.execPath, [ '--experimental-strip-types', - '--input-type=commonjs', + '--input-type=commonjs-typescript', '--eval', `const util = require('node:util'); const text: string = 'Hello, TypeScript!' @@ -84,10 +83,10 @@ test('TypeScript ESM syntax not specified', async () => { strictEqual(result.code, 0); }); -test('expect fail eval TypeScript CommonJS syntax with input-type module', async () => { +test('expect fail eval TypeScript CommonJS syntax with input-type module-typescript', async () => { const result = await spawnPromisified(process.execPath, [ '--experimental-strip-types', - '--input-type=module', + '--input-type=module-typescript', '--eval', `const util = require('node:util'); const text: string = 'Hello, TypeScript!' @@ -98,10 +97,10 @@ test('expect fail eval TypeScript CommonJS syntax with input-type module', async strictEqual(result.code, 1); }); -test('expect fail eval TypeScript ESM syntax with input-type commonjs', async () => { +test('expect fail eval TypeScript ESM syntax with input-type commonjs-typescript', async () => { const result = await spawnPromisified(process.execPath, [ '--experimental-strip-types', - '--input-type=commonjs', + '--input-type=commonjs-typescript', '--eval', `import util from 'node:util' const text: string = 'Hello, TypeScript!' @@ -111,12 +110,156 @@ test('expect fail eval TypeScript ESM syntax with input-type commonjs', async () strictEqual(result.code, 1); }); -test('check syntax error is thrown when passing invalid syntax', async () => { +test('check syntax error is thrown when passing unsupported syntax', async () => { const result = await spawnPromisified(process.execPath, [ '--experimental-strip-types', '--eval', 'enum Foo { A, B, C }']); strictEqual(result.stdout, ''); + match(result.stderr, /SyntaxError/); + doesNotMatch(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/); + strictEqual(result.code, 1); +}); + +test('check syntax error is thrown when passing unsupported syntax with --input-type=module-typescript', async () => { + const result = await spawnPromisified(process.execPath, [ + '--experimental-strip-types', + '--input-type=module-typescript', + '--eval', + 'enum Foo { A, B, C }']); + strictEqual(result.stdout, ''); + match(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/); + strictEqual(result.code, 1); +}); + +test('check syntax error is thrown when passing unsupported syntax with --input-type=commonjs-typescript', async () => { + const result = await spawnPromisified(process.execPath, [ + '--experimental-strip-types', + '--input-type=commonjs-typescript', + '--eval', + 'enum Foo { A, B, C }']); + strictEqual(result.stdout, ''); + match(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/); + strictEqual(result.code, 1); +}); + +test('should not parse TypeScript with --type-module=commonjs', async () => { + const result = await spawnPromisified(process.execPath, [ + '--experimental-strip-types', + '--input-type=commonjs', + '--eval', + `enum Foo {}`]); + + strictEqual(result.stdout, ''); + match(result.stderr, /SyntaxError/); + doesNotMatch(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/); + strictEqual(result.code, 1); +}); + +test('should not parse TypeScript with --type-module=module', async () => { + const result = await spawnPromisified(process.execPath, [ + '--experimental-strip-types', + '--input-type=module', + '--eval', + `enum Foo {}`]); + + strictEqual(result.stdout, ''); + match(result.stderr, /SyntaxError/); + doesNotMatch(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/); + strictEqual(result.code, 1); +}); + +test('check warning is emitted when eval TypeScript CommonJS syntax', async () => { + const result = await spawnPromisified(process.execPath, [ + '--experimental-strip-types', + '--eval', + `const util = require('node:util'); + const text: string = 'Hello, TypeScript!' + console.log(util.styleText('red', text));`]); + match(result.stderr, /ExperimentalWarning: Type Stripping is an experimental/); + match(result.stdout, /Hello, TypeScript!/); + strictEqual(result.code, 0); +}); + +test('code is throwing a non Error is rethrown', async () => { + const result = await spawnPromisified(process.execPath, [ + '--experimental-strip-types', + '--eval', + `throw null;`]); + doesNotMatch(result.stderr, /node:internal\/process\/execution/); + strictEqual(result.stdout, ''); + strictEqual(result.code, 1); +}); + +test('code is throwing an error with customized accessors', async () => { + const result = await spawnPromisified(process.execPath, [ + '--experimental-strip-types', + '--eval', + `throw Object.defineProperty(new Error, "stack", { set() {throw this} });`]); + + match(result.stderr, /Error/); + match(result.stderr, /at \[eval\]:1:29/); + strictEqual(result.stdout, ''); + strictEqual(result.code, 1); +}); + +test('typescript code is throwing an error', async () => { + const result = await spawnPromisified(process.execPath, [ + '--experimental-strip-types', + '--eval', + `const foo: string = 'Hello, TypeScript!'; throw new Error(foo);`]); + + match(result.stderr, /Hello, TypeScript!/); + strictEqual(result.stdout, ''); + strictEqual(result.code, 1); +}); + +test('typescript ESM code is throwing a syntax error at runtime', async () => { + const result = await spawnPromisified(process.execPath, [ + '--experimental-strip-types', + '--eval', + 'import util from "node:util"; function foo(){}; throw new SyntaxError(foo(1));']); + // Trick by passing ambiguous syntax to see if evaluated in TypeScript or JavaScript + // If evaluated in JavaScript `foo(1)` is evaluated as `foo < Number > (1)` + // result in false + // If evaluated in TypeScript `foo(1)` is evaluated as `foo(1)` + match(result.stderr, /SyntaxError: false/); + strictEqual(result.stdout, ''); + strictEqual(result.code, 1); +}); + +test('typescript CJS code is throwing a syntax error at runtime', async () => { + const result = await spawnPromisified(process.execPath, [ + '--experimental-strip-types', + '--eval', + 'const util = require("node:util"); function foo(){}; throw new SyntaxError(foo(1));']); + // Trick by passing ambiguous syntax to see if evaluated in TypeScript or JavaScript + // If evaluated in JavaScript `foo(1)` is evaluated as `foo < Number > (1)` + // result in false + // If evaluated in TypeScript `foo(1)` is evaluated as `foo(1)` + match(result.stderr, /SyntaxError: false/); + strictEqual(result.stdout, ''); + strictEqual(result.code, 1); +}); + +test('check syntax error is thrown when passing invalid syntax with --input-type=commonjs-typescript', async () => { + const result = await spawnPromisified(process.execPath, [ + '--experimental-strip-types', + '--input-type=commonjs-typescript', + '--eval', + 'function foo(){ await Promise.resolve(1); }']); + strictEqual(result.stdout, ''); + match(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/); + strictEqual(result.code, 1); +}); + +test('check syntax error is thrown when passing invalid syntax with --input-type=module-typescript', async () => { + const result = await spawnPromisified(process.execPath, [ + '--experimental-strip-types', + '--input-type=module-typescript', + '--eval', + 'function foo(){ await Promise.resolve(1); }']); + strictEqual(result.stdout, ''); match(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/); strictEqual(result.code, 1); }); diff --git a/test/es-module/test-typescript.mjs b/test/es-module/test-typescript.mjs index c2db03b90fa17f..abaa2f647fbe86 100644 --- a/test/es-module/test-typescript.mjs +++ b/test/es-module/test-typescript.mjs @@ -435,3 +435,14 @@ test('execute a TypeScript loader and a .js file', async () => { match(result.stdout, /Hello, TypeScript!/); strictEqual(result.code, 0); }); + +test('execute invalid TypeScript syntax', async () => { + const result = await spawnPromisified(process.execPath, [ + '--experimental-strip-types', + fixtures.path('typescript/ts/test-invalid-syntax.ts'), + ]); + + match(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/); + strictEqual(result.stdout, ''); + strictEqual(result.code, 1); +}); diff --git a/test/fixtures/eval/eval_messages.snapshot b/test/fixtures/eval/eval_messages.snapshot index 6a37ad22634617..f6fc803e0e3ec3 100644 --- a/test/fixtures/eval/eval_messages.snapshot +++ b/test/fixtures/eval/eval_messages.snapshot @@ -5,12 +5,6 @@ with(this){__filename} SyntaxError: Strict mode code may not include a with statement - - - - - - Node.js * 42 42 @@ -20,13 +14,6 @@ throw new Error("hello") Error: hello - - - - - - - Node.js * [eval]:1 throw new Error("hello") @@ -34,13 +21,6 @@ throw new Error("hello") Error: hello - - - - - - - Node.js * 100 [eval]:1 @@ -49,13 +29,6 @@ var x = 100; y = x; ReferenceError: y is not defined - - - - - - - Node.js * [eval]:1 diff --git a/test/fixtures/eval/eval_typescript.js b/test/fixtures/eval/eval_typescript.js new file mode 100644 index 00000000000000..d16eefc8d42c9b --- /dev/null +++ b/test/fixtures/eval/eval_typescript.js @@ -0,0 +1,25 @@ +'use strict'; + +require('../../common'); + +const spawnSync = require('child_process').spawnSync; + +const queue = [ + 'enum Foo{};', + 'throw new SyntaxError("hello")', + 'const foo;', + 'let x: number = 100;x;', + 'const foo: string = 10;', + 'function foo(){};foo(1);', + 'interface Foo{};const foo;', + 'function foo(){ await Promise.resolve(1)};', +]; + +for (const cmd of queue) { + const args = ['--disable-warning=ExperimentalWarning','--experimental-strip-types', '-p', cmd]; + const result = spawnSync(process.execPath, args, { + stdio: 'pipe' + }); + process.stdout.write(result.stdout); + process.stdout.write(result.stderr); +} diff --git a/test/fixtures/eval/eval_typescript.snapshot b/test/fixtures/eval/eval_typescript.snapshot new file mode 100644 index 00000000000000..074e966e51e0f5 --- /dev/null +++ b/test/fixtures/eval/eval_typescript.snapshot @@ -0,0 +1,48 @@ +[eval]:1 +enum Foo{}; +^^^^ + x TypeScript enum is not supported in strip-only mode + ,---- + 1 | enum Foo{}; + : ^^^^^^^^^^ + `---- + +SyntaxError: Unexpected reserved word + +Node.js * +[eval]:1 +throw new SyntaxError("hello") +^ + +SyntaxError: hello + +Node.js * +[eval]:1 +const foo; + ^^^ + +SyntaxError: Missing initializer in const declaration + +Node.js * +100 +undefined +false +[eval]:1 + ;const foo; + ^^^ + +SyntaxError: Missing initializer in const declaration + +Node.js * +[eval]:1 +function foo(){ await Promise.resolve(1)}; + ^^^^^ + x await isn't allowed in non-async function + ,---- + 1 | function foo(){ await Promise.resolve(1)}; + : ^^^^^^^ + `---- + +SyntaxError: await is only valid in async functions and the top level bodies of modules + +Node.js * diff --git a/test/fixtures/eval/stdin_messages.js b/test/fixtures/eval/stdin_messages.js index 874b473be38e00..a5bac683fb6ed4 100644 --- a/test/fixtures/eval/stdin_messages.js +++ b/test/fixtures/eval/stdin_messages.js @@ -26,7 +26,7 @@ require('../../common'); const spawn = require('child_process').spawn; function run(cmd, strict, cb) { - const args = []; + const args = ['--experimental-strip-types']; if (strict) args.push('--use_strict'); args.push('-p'); const child = spawn(process.execPath, args); diff --git a/test/fixtures/eval/stdin_messages.snapshot b/test/fixtures/eval/stdin_messages.snapshot index 3c03bd64072061..66bd506f758ca9 100644 --- a/test/fixtures/eval/stdin_messages.snapshot +++ b/test/fixtures/eval/stdin_messages.snapshot @@ -2,19 +2,14 @@ [stdin]:1 with(this){__filename} ^^^^ + x The 'with' statement is not supported. All symbols in a 'with' block will have type 'any'. + ,---- + 1 | with(this){__filename} + : ^^^^ + `---- SyntaxError: Strict mode code may not include a with statement - - - - - - - - - - Node.js * 42 42 @@ -24,16 +19,6 @@ throw new Error("hello") Error: hello - - - - - - - - - - Node.js * [stdin]:1 throw new Error("hello") @@ -41,16 +26,6 @@ throw new Error("hello") Error: hello - - - - - - - - - - Node.js * 100 [stdin]:1 @@ -59,16 +34,6 @@ let x = 100; y = x; ReferenceError: y is not defined - - - - - - - - - - Node.js * [stdin]:1 diff --git a/test/fixtures/eval/stdin_typescript.js b/test/fixtures/eval/stdin_typescript.js new file mode 100644 index 00000000000000..800ff6cbcb76c4 --- /dev/null +++ b/test/fixtures/eval/stdin_typescript.js @@ -0,0 +1,38 @@ +'use strict'; + +require('../../common'); + +const spawn = require('child_process').spawn; + +function run(cmd, strict, cb) { + const args = ['--disable-warning=ExperimentalWarning', '--experimental-strip-types']; + if (strict) args.push('--use_strict'); + args.push('-p'); + const child = spawn(process.execPath, args); + child.stdout.pipe(process.stdout); + child.stderr.pipe(process.stdout); + child.stdin.end(cmd); + child.on('close', cb); +} + +const queue = + [ + 'enum Foo{};', + 'throw new SyntaxError("hello")', + 'const foo;', + 'let x: number = 100;x;', + 'const foo: string = 10;', + 'function foo(){};foo(1);', + 'interface Foo{};const foo;', + 'function foo(){ await Promise.resolve(1)};', + ]; + +function go() { + const c = queue.shift(); + if (!c) return console.log('done'); + run(c, false, function () { + run(c, true, go); + }); +} + +go(); diff --git a/test/fixtures/eval/stdin_typescript.snapshot b/test/fixtures/eval/stdin_typescript.snapshot new file mode 100644 index 00000000000000..3e209e6db2973a --- /dev/null +++ b/test/fixtures/eval/stdin_typescript.snapshot @@ -0,0 +1,97 @@ +[stdin]:1 +enum Foo{}; +^^^^ + x TypeScript enum is not supported in strip-only mode + ,---- + 1 | enum Foo{}; + : ^^^^^^^^^^ + `---- + +SyntaxError: Unexpected reserved word + +Node.js * +[stdin]:1 +enum Foo{}; +^^^^ + x TypeScript enum is not supported in strip-only mode + ,---- + 1 | enum Foo{}; + : ^^^^^^^^^^ + `---- + +SyntaxError: Unexpected reserved word + +Node.js * +[stdin]:1 +throw new SyntaxError("hello") +^ + +SyntaxError: hello + +Node.js * +[stdin]:1 +throw new SyntaxError("hello") +^ + +SyntaxError: hello + +Node.js * +[stdin]:1 +const foo; + ^^^ + +SyntaxError: Missing initializer in const declaration + +Node.js * +[stdin]:1 +const foo; + ^^^ + +SyntaxError: Missing initializer in const declaration + +Node.js * +100 +100 +undefined +undefined +false +false +[stdin]:1 + ;const foo; + ^^^ + +SyntaxError: Missing initializer in const declaration + +Node.js * +[stdin]:1 + ;const foo; + ^^^ + +SyntaxError: Missing initializer in const declaration + +Node.js * +[stdin]:1 +function foo(){ await Promise.resolve(1)}; + ^^^^^ + x await isn't allowed in non-async function + ,---- + 1 | function foo(){ await Promise.resolve(1)}; + : ^^^^^^^ + `---- + +SyntaxError: await is only valid in async functions and the top level bodies of modules + +Node.js * +[stdin]:1 +function foo(){ await Promise.resolve(1)}; + ^^^^^ + x await isn't allowed in non-async function + ,---- + 1 | function foo(){ await Promise.resolve(1)}; + : ^^^^^^^ + `---- + +SyntaxError: await is only valid in async functions and the top level bodies of modules + +Node.js * +done diff --git a/test/fixtures/typescript/ts/test-invalid-syntax.ts b/test/fixtures/typescript/ts/test-invalid-syntax.ts new file mode 100644 index 00000000000000..031bce938d27dc --- /dev/null +++ b/test/fixtures/typescript/ts/test-invalid-syntax.ts @@ -0,0 +1,3 @@ +function foo(): string { + await Promise.resolve(1); +} diff --git a/test/parallel/test-node-output-eval.mjs b/test/parallel/test-node-output-eval.mjs index 2fa60206e1ea1c..d8c52176b1c3c3 100644 --- a/test/parallel/test-node-output-eval.mjs +++ b/test/parallel/test-node-output-eval.mjs @@ -10,20 +10,27 @@ describe('eval output', { concurrency: true }, () => { } const defaultTransform = snapshot.transform( - removeStackTraces, normalize, snapshot.replaceWindowsLineEndings, snapshot.replaceWindowsPaths, - snapshot.replaceNodeVersion + snapshot.replaceNodeVersion, + removeStackTraces, + filterEmptyLines, ); function removeStackTraces(output) { return output.replaceAll(/^ *at .+$/gm, ''); } + function filterEmptyLines(output) { + return output.replaceAll(/^\s*$/gm, ''); + } + const tests = [ { name: 'eval/eval_messages.js' }, { name: 'eval/stdin_messages.js' }, + { name: 'eval/stdin_typescript.js' }, + { name: 'eval/eval_typescript.js' }, ]; for (const { name } of tests) { diff --git a/test/parallel/test-worker-eval-typescript.js b/test/parallel/test-worker-eval-typescript.js new file mode 100644 index 00000000000000..daf0de87dccccd --- /dev/null +++ b/test/parallel/test-worker-eval-typescript.js @@ -0,0 +1,67 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); +const { test } = require('node:test'); +const { once } = require('events'); + +const esmHelloWorld = ` + import worker from 'worker_threads'; + const foo: string = 'Hello, World!'; + worker.parentPort.postMessage(foo); +`; + +const cjsHelloWorld = ` + const { parentPort } = require('worker_threads'); + const foo: string = 'Hello, World!'; + parentPort.postMessage(foo); +`; + +const flags = ['--experimental-strip-types', '--disable-warning=ExperimentalWarning']; + +test('Worker eval module typescript without input-type', async () => { + const w = new Worker(esmHelloWorld, { eval: true, execArgv: [...flags] }); + assert.deepStrictEqual(await once(w, 'message'), ['Hello, World!']); +}); + +test('Worker eval module typescript with --input-type=module-typescript', async () => { + const w = new Worker(esmHelloWorld, { eval: true, execArgv: ['--input-type=module-typescript', + ...flags] }); + assert.deepStrictEqual(await once(w, 'message'), ['Hello, World!']); +}); + +test('Worker eval module typescript with --input-type=commonjs-typescript', async () => { + const w = new Worker(esmHelloWorld, { eval: true, execArgv: ['--input-type=commonjs-typescript', + ...flags] }); + + const [err] = await once(w, 'error'); + assert.strictEqual(err.name, 'SyntaxError'); + assert.match(err.message, /Cannot use import statement outside a module/); +}); + +test('Worker eval module typescript with --input-type=module', async () => { + const w = new Worker(esmHelloWorld, { eval: true, execArgv: ['--input-type=module', + ...flags] }); + const [err] = await once(w, 'error'); + assert.strictEqual(err.name, 'SyntaxError'); + assert.match(err.message, /Missing initializer in const declaration/); +}); + +test('Worker eval commonjs typescript without input-type', async () => { + const w = new Worker(cjsHelloWorld, { eval: true, execArgv: [...flags] }); + assert.deepStrictEqual(await once(w, 'message'), ['Hello, World!']); +}); + +test('Worker eval commonjs typescript with --input-type=commonjs-typescript', async () => { + const w = new Worker(cjsHelloWorld, { eval: true, execArgv: ['--input-type=commonjs-typescript', + ...flags] }); + assert.deepStrictEqual(await once(w, 'message'), ['Hello, World!']); +}); + +test('Worker eval commonjs typescript with --input-type=module-typescript', async () => { + const w = new Worker(cjsHelloWorld, { eval: true, execArgv: ['--input-type=module-typescript', + ...flags] }); + const [err] = await once(w, 'error'); + assert.strictEqual(err.name, 'ReferenceError'); + assert.match(err.message, /require is not defined in ES module scope, you can use import instead/); +});