From 174d88eab135b6f92eebfcf4bce7a5b8c319409b Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Tue, 24 Dec 2024 17:48:02 +0100 Subject: [PATCH 1/8] module: support eval with ts syntax detection PR-URL: https://github.com/nodejs/node/pull/56285 Backport-PR-URL: https://github.com/nodejs/node/pull/56912 Refs: https://github.com/nodejs/typescript/issues/17 Reviewed-By: Pietro Marchini Reviewed-By: Geoffrey Booth --- doc/api/cli.md | 19 +- lib/internal/main/eval_string.js | 53 ++-- lib/internal/modules/cjs/loader.js | 1 - lib/internal/modules/esm/loader.js | 32 ++- lib/internal/modules/esm/translators.js | 3 - lib/internal/modules/typescript.js | 6 +- lib/internal/process/execution.js | 313 ++++++++++++++++++++-- src/node_options.cc | 8 +- test/es-module/test-typescript-eval.mjs | 143 +++++++++- test/fixtures/eval/eval_messages.snapshot | 1 + 10 files changed, 511 insertions(+), 68 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index 15da9004b0e7b6..acfe77c3a9e18d 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1414,8 +1414,22 @@ 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_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 +3726,7 @@ 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 [`NODE_OPTIONS`]: #node_optionsoptions [`NO_COLOR`]: https://no-color.org [`SlowBuffer`]: buffer.md#class-slowbuffer 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/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..993fd3ff72d74d 100644 --- a/lib/internal/modules/typescript.js +++ b/lib/internal/modules/typescript.js @@ -113,9 +113,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..35d55cd7038b99 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, isError } = require('internal/util'); // shouldAbortOnUncaughtToggle is a typed array for faster // communication with JS. const { shouldAbortOnUncaughtToggle } = internalBinding('util'); @@ -70,21 +74,14 @@ 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 = () => { @@ -99,23 +96,8 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) { 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); + const compiledScript = compileScript(name, body, baseUrl); + return runScriptInThisContext(compiledScript, true, !!breakFirstLine); }); if (print) { const { log } = require('internal/console/global'); @@ -238,10 +220,283 @@ 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) { + // If it's not a SyntaxError, rethrow it. + if (!isError(originalError) || originalError.name !== 'SyntaxError') { + throw 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 its not an error, or it's not an invalid typescript syntax error, rethrow it. + if (!isError(tsError) || tsError?.code !== 'ERR_INVALID_TYPESCRIPT_SYNTAX') { + throw tsError; + } + + try { + originalError.stack = decorateCJSErrorWithTSMessage(originalError.stack, tsError.message); + } catch { /* Ignore potential errors coming from `stack` getter/setter */ } + throw originalError; + } + } + + if (shouldLoadESM) { + return require('internal/modules/run_main').runEntryPointWithESMLoader( + () => runScriptInContext(name, + sourceToRun, + breakFirstLine, + print, + module, + baseUrl, + compiledScript, + origModule)); + } + + runScriptInContext(name, sourceToRun, breakFirstLine, print, module, baseUrl, compiledScript, origModule); +} + +/** + * 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) { + // If it's not a SyntaxError, rethrow it. + if (!isError(originalError) || originalError.name !== 'SyntaxError') { + throw originalError; + } + let strippedSource; + try { + 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 its not an error, or it's not an invalid typescript syntax error, rethrow it. + if (!isError(tsError) || tsError?.code !== 'ERR_INVALID_TYPESCRIPT_SYNTAX') { + throw tsError; + } + try { + originalError.stack = `${tsError.message}\n\n${originalError.stack}`; + } catch { /* Ignore potential errors coming from `stack` getter/setter */ } + + throw originalError; + } + } + // 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, + 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..64e334e001beac 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!' @@ -117,6 +116,128 @@ test('check syntax error is thrown when passing invalid syntax', async () => { '--eval', 'enum Foo { A, B, C }']); strictEqual(result.stdout, ''); + match(result.stderr, /SyntaxError/); + doesNotMatch(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', + 'enum Foo { A, B, C }']); + 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=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_INVALID_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_INVALID_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_INVALID_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); +}); diff --git a/test/fixtures/eval/eval_messages.snapshot b/test/fixtures/eval/eval_messages.snapshot index 6a37ad22634617..4b4069219f03fb 100644 --- a/test/fixtures/eval/eval_messages.snapshot +++ b/test/fixtures/eval/eval_messages.snapshot @@ -11,6 +11,7 @@ SyntaxError: Strict mode code may not include a with statement + Node.js * 42 42 From 3ac92ef6078bf21da241b77c310f272ca56996df Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Sun, 29 Dec 2024 23:42:51 +0100 Subject: [PATCH 2/8] lib: add typescript support to STDIN eval PR-URL: https://github.com/nodejs/node/pull/56359 Backport-PR-URL: https://github.com/nodejs/node/pull/56912 Reviewed-By: Jordan Harband Reviewed-By: James M Snell Reviewed-By: Pietro Marchini --- doc/api/typescript.md | 4 +- lib/internal/main/eval_stdin.js | 35 +++- test/fixtures/eval/stdin_messages.js | 2 +- test/fixtures/eval/stdin_messages.snapshot | 8 +- test/fixtures/eval/stdin_typescript.js | 38 ++++ test/fixtures/eval/stdin_typescript.snapshot | 185 +++++++++++++++++++ test/parallel/test-node-output-eval.mjs | 1 + 7 files changed, 261 insertions(+), 12 deletions(-) create mode 100644 test/fixtures/eval/stdin_typescript.js create mode 100644 test/fixtures/eval/stdin_typescript.snapshot 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/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/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..416d6bbf3d89c2 100644 --- a/test/fixtures/eval/stdin_messages.snapshot +++ b/test/fixtures/eval/stdin_messages.snapshot @@ -2,10 +2,14 @@ [stdin]:1 with(this){__filename} ^^^^ - -SyntaxError: Strict mode code may not include a with statement + 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 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..0755075f50b5ce --- /dev/null +++ b/test/fixtures/eval/stdin_typescript.snapshot @@ -0,0 +1,185 @@ +[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/parallel/test-node-output-eval.mjs b/test/parallel/test-node-output-eval.mjs index 2fa60206e1ea1c..a8bfd730e92cf4 100644 --- a/test/parallel/test-node-output-eval.mjs +++ b/test/parallel/test-node-output-eval.mjs @@ -24,6 +24,7 @@ describe('eval output', { concurrency: true }, () => { const tests = [ { name: 'eval/eval_messages.js' }, { name: 'eval/stdin_messages.js' }, + { name: 'eval/stdin_typescript.js' }, ]; for (const { name } of tests) { From f041742400d33684c4df567a9f5248329f177481 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Tue, 24 Dec 2024 17:55:56 +0100 Subject: [PATCH 3/8] test: remove empty lines from snapshots PR-URL: https://github.com/nodejs/node/pull/56358 Backport-PR-URL: https://github.com/nodejs/node/pull/56912 Reviewed-By: James M Snell Reviewed-By: Yagiz Nizipli Reviewed-By: Jacob Smith --- test/fixtures/eval/eval_messages.snapshot | 21 ------------ test/fixtures/eval/stdin_messages.snapshot | 38 ---------------------- test/parallel/test-node-output-eval.mjs | 5 +++ 3 files changed, 5 insertions(+), 59 deletions(-) diff --git a/test/fixtures/eval/eval_messages.snapshot b/test/fixtures/eval/eval_messages.snapshot index 4b4069219f03fb..2b188a0cd71b38 100644 --- a/test/fixtures/eval/eval_messages.snapshot +++ b/test/fixtures/eval/eval_messages.snapshot @@ -21,13 +21,6 @@ throw new Error("hello") Error: hello - - - - - - - Node.js * [eval]:1 throw new Error("hello") @@ -35,13 +28,6 @@ throw new Error("hello") Error: hello - - - - - - - Node.js * 100 [eval]:1 @@ -50,13 +36,6 @@ var x = 100; y = x; ReferenceError: y is not defined - - - - - - - Node.js * [eval]:1 diff --git a/test/fixtures/eval/stdin_messages.snapshot b/test/fixtures/eval/stdin_messages.snapshot index 416d6bbf3d89c2..f0a4d4db48cd2e 100644 --- a/test/fixtures/eval/stdin_messages.snapshot +++ b/test/fixtures/eval/stdin_messages.snapshot @@ -11,14 +11,6 @@ with(this){__filename} SyntaxError: Strict mode code may not include a with statement - - - - - - - - Node.js * 42 42 @@ -28,16 +20,6 @@ throw new Error("hello") Error: hello - - - - - - - - - - Node.js * [stdin]:1 throw new Error("hello") @@ -45,16 +27,6 @@ throw new Error("hello") Error: hello - - - - - - - - - - Node.js * 100 [stdin]:1 @@ -63,16 +35,6 @@ let x = 100; y = x; ReferenceError: y is not defined - - - - - - - - - - Node.js * [stdin]:1 diff --git a/test/parallel/test-node-output-eval.mjs b/test/parallel/test-node-output-eval.mjs index a8bfd730e92cf4..cd22cbfea28cd6 100644 --- a/test/parallel/test-node-output-eval.mjs +++ b/test/parallel/test-node-output-eval.mjs @@ -11,6 +11,7 @@ describe('eval output', { concurrency: true }, () => { const defaultTransform = snapshot.transform( removeStackTraces, + filterEmptyLines, normalize, snapshot.replaceWindowsLineEndings, snapshot.replaceWindowsPaths, @@ -21,6 +22,10 @@ describe('eval output', { concurrency: true }, () => { 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' }, From 96ec862ce23b6332269a37cd023c5de433c56d33 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Tue, 24 Dec 2024 18:07:32 +0100 Subject: [PATCH 4/8] lib: refactor execution.js PR-URL: https://github.com/nodejs/node/pull/56358 Backport-PR-URL: https://github.com/nodejs/node/pull/56912 Reviewed-By: James M Snell Reviewed-By: Yagiz Nizipli Reviewed-By: Jacob Smith --- lib/internal/process/execution.js | 63 +++++++++++-------------------- 1 file changed, 22 insertions(+), 41 deletions(-) diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index 35d55cd7038b99..04f8a97b43b759 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -84,38 +84,19 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) { 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 compiledScript = compileScript(name, body, baseUrl); - return runScriptInThisContext(compiledScript, 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 = { @@ -301,19 +282,19 @@ function evalTypeScript(name, source, breakFirstLine, print, shouldLoadESM = fal } } + const evalFunction = () => runScriptInContext(name, + sourceToRun, + breakFirstLine, + print, + module, + baseUrl, + compiledScript, + origModule); + if (shouldLoadESM) { - return require('internal/modules/run_main').runEntryPointWithESMLoader( - () => runScriptInContext(name, - sourceToRun, - breakFirstLine, - print, - module, - baseUrl, - compiledScript, - origModule)); + return require('internal/modules/run_main').runEntryPointWithESMLoader(evalFunction); } - - runScriptInContext(name, sourceToRun, breakFirstLine, print, module, baseUrl, compiledScript, origModule); + evalFunction(); } /** @@ -476,7 +457,7 @@ function runScriptInContext(name, body, breakFirstLine, print, module, baseUrl, const result = module._compile(script, `${name}-wrapper`)(() => { // If the script was already compiled, use it. return runScriptInThisContext( - compiledScript, + compiledScript ?? compileScript(name, body, baseUrl), true, !!breakFirstLine); }); if (print) { From e4f133c20c82e709f9a207ce490824ac571d2bc1 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Wed, 25 Dec 2024 09:31:01 +0100 Subject: [PATCH 5/8] test: add ts eval snapshots PR-URL: https://github.com/nodejs/node/pull/56358 Backport-PR-URL: https://github.com/nodejs/node/pull/56912 Reviewed-By: James M Snell Reviewed-By: Yagiz Nizipli Reviewed-By: Jacob Smith --- test/fixtures/eval/eval_messages.snapshot | 7 -- test/fixtures/eval/eval_typescript.js | 25 ++++++ test/fixtures/eval/eval_typescript.snapshot | 48 +++++++++++ test/fixtures/eval/stdin_messages.snapshot | 1 - test/fixtures/eval/stdin_typescript.snapshot | 88 -------------------- test/parallel/test-node-output-eval.mjs | 7 +- 6 files changed, 77 insertions(+), 99 deletions(-) create mode 100644 test/fixtures/eval/eval_typescript.js create mode 100644 test/fixtures/eval/eval_typescript.snapshot diff --git a/test/fixtures/eval/eval_messages.snapshot b/test/fixtures/eval/eval_messages.snapshot index 2b188a0cd71b38..f6fc803e0e3ec3 100644 --- a/test/fixtures/eval/eval_messages.snapshot +++ b/test/fixtures/eval/eval_messages.snapshot @@ -5,13 +5,6 @@ with(this){__filename} SyntaxError: Strict mode code may not include a with statement - - - - - - - Node.js * 42 42 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.snapshot b/test/fixtures/eval/stdin_messages.snapshot index f0a4d4db48cd2e..66bd506f758ca9 100644 --- a/test/fixtures/eval/stdin_messages.snapshot +++ b/test/fixtures/eval/stdin_messages.snapshot @@ -8,7 +8,6 @@ with(this){__filename} : ^^^^ `---- - SyntaxError: Strict mode code may not include a with statement Node.js * diff --git a/test/fixtures/eval/stdin_typescript.snapshot b/test/fixtures/eval/stdin_typescript.snapshot index 0755075f50b5ce..3e209e6db2973a 100644 --- a/test/fixtures/eval/stdin_typescript.snapshot +++ b/test/fixtures/eval/stdin_typescript.snapshot @@ -7,17 +7,8 @@ enum Foo{}; : ^^^^^^^^^^ `---- - SyntaxError: Unexpected reserved word - - - - - - - - Node.js * [stdin]:1 enum Foo{}; @@ -28,17 +19,8 @@ enum Foo{}; : ^^^^^^^^^^ `---- - SyntaxError: Unexpected reserved word - - - - - - - - Node.js * [stdin]:1 throw new SyntaxError("hello") @@ -46,16 +28,6 @@ throw new SyntaxError("hello") SyntaxError: hello - - - - - - - - - - Node.js * [stdin]:1 throw new SyntaxError("hello") @@ -63,16 +35,6 @@ throw new SyntaxError("hello") SyntaxError: hello - - - - - - - - - - Node.js * [stdin]:1 const foo; @@ -80,14 +42,6 @@ const foo; SyntaxError: Missing initializer in const declaration - - - - - - - - Node.js * [stdin]:1 const foo; @@ -95,14 +49,6 @@ const foo; SyntaxError: Missing initializer in const declaration - - - - - - - - Node.js * 100 100 @@ -116,14 +62,6 @@ false SyntaxError: Missing initializer in const declaration - - - - - - - - Node.js * [stdin]:1 ;const foo; @@ -131,14 +69,6 @@ Node.js * SyntaxError: Missing initializer in const declaration - - - - - - - - Node.js * [stdin]:1 function foo(){ await Promise.resolve(1)}; @@ -149,17 +79,8 @@ 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)}; @@ -170,16 +91,7 @@ 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/parallel/test-node-output-eval.mjs b/test/parallel/test-node-output-eval.mjs index cd22cbfea28cd6..d8c52176b1c3c3 100644 --- a/test/parallel/test-node-output-eval.mjs +++ b/test/parallel/test-node-output-eval.mjs @@ -10,12 +10,12 @@ describe('eval output', { concurrency: true }, () => { } const defaultTransform = snapshot.transform( - removeStackTraces, - filterEmptyLines, normalize, snapshot.replaceWindowsLineEndings, snapshot.replaceWindowsPaths, - snapshot.replaceNodeVersion + snapshot.replaceNodeVersion, + removeStackTraces, + filterEmptyLines, ); function removeStackTraces(output) { @@ -30,6 +30,7 @@ describe('eval output', { concurrency: true }, () => { { 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) { From c658a8afdfb28df342334c31d1f026fbd5affa4c Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Fri, 3 Jan 2025 10:49:21 +0100 Subject: [PATCH 6/8] worker: add eval ts input PR-URL: https://github.com/nodejs/node/pull/56394 Backport-PR-URL: https://github.com/nodejs/node/pull/56912 Reviewed-By: James M Snell Reviewed-By: Paolo Insogna Reviewed-By: Pietro Marchini --- lib/internal/main/worker_thread.js | 27 +++++++- test/parallel/test-worker-eval-typescript.js | 67 ++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-worker-eval-typescript.js 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/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/); +}); From 1614e8e7bc463190ffa23727e8a7c013aedd3bf2 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Fri, 17 Jan 2025 13:42:50 +0100 Subject: [PATCH 7/8] module: add ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX PR-URL: https://github.com/nodejs/node/pull/56610 Backport-PR-URL: https://github.com/nodejs/node/pull/56912 Reviewed-By: James M Snell Reviewed-By: Ruben Bridgewater Reviewed-By: Geoffrey Booth Reviewed-By: Ethan Arrowood Reviewed-By: Chengzhong Wu --- doc/api/cli.md | 4 +- doc/api/errors.md | 20 ++++++++-- lib/internal/errors.js | 1 + lib/internal/modules/typescript.js | 17 +++++++- lib/internal/process/execution.js | 40 +++++++------------ test/es-module/test-typescript-eval.mjs | 38 ++++++++++++++---- test/es-module/test-typescript.mjs | 11 +++++ .../typescript/ts/test-invalid-syntax.ts | 3 ++ 8 files changed, 96 insertions(+), 38 deletions(-) create mode 100644 test/fixtures/typescript/ts/test-invalid-syntax.ts diff --git a/doc/api/cli.md b/doc/api/cli.md index acfe77c3a9e18d..ad916576654072 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1423,7 +1423,8 @@ 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_INVALID_TYPESCRIPT_SYNTAX`][], +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. @@ -3727,6 +3728,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12 [`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/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/modules/typescript.js b/lib/internal/modules/typescript.js index 993fd3ff72d74d..689788b09853c4 100644 --- a/lib/internal/modules/typescript.js +++ b/lib/internal/modules/typescript.js @@ -12,8 +12,10 @@ const { assertTypeScript, isUnderNodeModules, kEmptyObject } = require('internal/util'); const { + ERR_INTERNAL_ASSERTION, 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 +51,20 @@ 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 will throw strings when something goes wrong. + // Check if has the `message` property or treat it as a string. + throw new ERR_INTERNAL_ASSERTION(error.message ?? error); + } } } diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index 04f8a97b43b759..b5f9bec59189ca 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -35,7 +35,7 @@ const { getOptionValue } = require('internal/options'); const { makeContextifyScript, runScriptInThisContext, } = require('internal/vm'); -const { emitExperimentalWarning, isError } = require('internal/util'); +const { emitExperimentalWarning } = require('internal/util'); // shouldAbortOnUncaughtToggle is a typed array for faster // communication with JS. const { shouldAbortOnUncaughtToggle } = internalBinding('util'); @@ -254,10 +254,6 @@ function evalTypeScript(name, source, breakFirstLine, print, shouldLoadESM = fal try { compiledScript = compileScript(name, source, baseUrl); } catch (originalError) { - // If it's not a SyntaxError, rethrow it. - if (!isError(originalError) || originalError.name !== 'SyntaxError') { - throw originalError; - } try { sourceToRun = stripTypeScriptModuleTypes(source, name, false); // Retry the CJS/ESM syntax detection after stripping the types. @@ -270,15 +266,14 @@ function evalTypeScript(name, source, breakFirstLine, print, shouldLoadESM = fal // Emit the experimental warning after the code was successfully evaluated. emitExperimentalWarning('Type Stripping'); } catch (tsError) { - // If its not an error, or it's not an invalid typescript syntax error, rethrow it. - if (!isError(tsError) || tsError?.code !== 'ERR_INVALID_TYPESCRIPT_SYNTAX') { - throw 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; } - try { - originalError.stack = decorateCJSErrorWithTSMessage(originalError.stack, tsError.message); - } catch { /* Ignore potential errors coming from `stack` getter/setter */ } - throw originalError; + throw tsError; } } @@ -322,28 +317,23 @@ function evalTypeScriptModuleEntryPoint(source, print) { // Compile the module to check for syntax errors. moduleWrap = loader.createModuleWrap(source, url); } catch (originalError) { - // If it's not a SyntaxError, rethrow it. - if (!isError(originalError) || originalError.name !== 'SyntaxError') { - throw originalError; - } - let strippedSource; try { - strippedSource = stripTypeScriptModuleTypes(source, url, false); + 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 its not an error, or it's not an invalid typescript syntax error, rethrow it. - if (!isError(tsError) || tsError?.code !== 'ERR_INVALID_TYPESCRIPT_SYNTAX') { - throw tsError; - } - try { + // 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}`; - } catch { /* Ignore potential errors coming from `stack` getter/setter */ } + throw originalError; + } - throw originalError; + throw tsError; } } // If the moduleWrap was successfully created either with by just compiling diff --git a/test/es-module/test-typescript-eval.mjs b/test/es-module/test-typescript-eval.mjs index 64e334e001beac..38744de7185179 100644 --- a/test/es-module/test-typescript-eval.mjs +++ b/test/es-module/test-typescript-eval.mjs @@ -110,36 +110,36 @@ test('expect fail eval TypeScript ESM syntax with input-type commonjs-typescript 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_INVALID_TYPESCRIPT_SYNTAX/); + doesNotMatch(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/); strictEqual(result.code, 1); }); -test('check syntax error is thrown when passing invalid syntax with --input-type=module-typescript', async () => { +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_INVALID_TYPESCRIPT_SYNTAX/); + match(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/); strictEqual(result.code, 1); }); -test('check syntax error is thrown when passing invalid syntax with --input-type=commonjs-typescript', async () => { +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_INVALID_TYPESCRIPT_SYNTAX/); + match(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/); strictEqual(result.code, 1); }); @@ -152,7 +152,7 @@ test('should not parse TypeScript with --type-module=commonjs', async () => { strictEqual(result.stdout, ''); match(result.stderr, /SyntaxError/); - doesNotMatch(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/); + doesNotMatch(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/); strictEqual(result.code, 1); }); @@ -165,7 +165,7 @@ test('should not parse TypeScript with --type-module=module', async () => { strictEqual(result.stdout, ''); match(result.stderr, /SyntaxError/); - doesNotMatch(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/); + doesNotMatch(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/); strictEqual(result.code, 1); }); @@ -241,3 +241,25 @@ test('typescript CJS code is throwing a syntax error at runtime', async () => { 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/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); +} From bae4b2e20a00b31730d8c066d885882594a685d1 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Thu, 23 Jan 2025 16:17:31 +0100 Subject: [PATCH 8/8] module: use more defensive code when handling SWC errors PR-URL: https://github.com/nodejs/node/pull/56646 Backport-PR-URL: https://github.com/nodejs/node/pull/56912 Reviewed-By: Marco Ippolito Reviewed-By: James M Snell --- lib/internal/modules/typescript.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/internal/modules/typescript.js b/lib/internal/modules/typescript.js index 689788b09853c4..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, @@ -12,7 +15,6 @@ const { assertTypeScript, isUnderNodeModules, kEmptyObject } = require('internal/util'); const { - ERR_INTERNAL_ASSERTION, ERR_INVALID_TYPESCRIPT_SYNTAX, ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING, ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, @@ -55,15 +57,16 @@ function parseTypeScript(source, options) { * 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) { + 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 will throw strings when something goes wrong. - // Check if has the `message` property or treat it as a string. - throw new ERR_INTERNAL_ASSERTION(error.message ?? error); + // 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); } } }