diff --git a/doc/api/cli.md b/doc/api/cli.md index 2425c504156575..3e61b248b94fd9 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1369,8 +1369,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"`. +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"`. + +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 @@ -3648,6 +3663,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12 [`AsyncLocalStorage`]: async_context.md#class-asynclocalstorage [`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 6518c93430a28e..ee402f50fbdd2b 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,18 +28,19 @@ 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') { - evalModuleEntryPoint(source, print); +const inputType = getOptionValue('--input-type'); +const tsEnabled = getOptionValue('--experimental-strip-types'); +if (inputType === 'module') { + 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 = RegExpPrototypeExec(/\bcrypto\b/, source) !== null; + const isUsingCryptoIdentifier = RegExpPrototypeExec(/\bcrypto\b/, code) !== null; const shouldDefineCrypto = isUsingCryptoIdentifier && internalBinding('config').hasOpenSSL; if (isUsingCryptoIdentifier && !shouldDefineCrypto) { @@ -49,11 +55,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 0779190e1c9070..c453f2b403e89d 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -449,7 +449,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 c52f388754d5f1..19eac728623939 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -213,9 +213,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); @@ -235,6 +251,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 37611a4332c541..678659aacaad3e 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -291,7 +291,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, isMain, 'commonjs-typescript'); @@ -536,7 +535,6 @@ translators.set('addon', function translateAddon(url, source, isMain) { // 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}`); @@ -545,7 +543,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 2f8f61266b5d03..5a240a6a5403b7 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 a5afd44ca9ca06..e8a1a29efcc8a6 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') === '' && - 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('--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 d6c2e5bbb4525c..60d869de042bd8 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -108,8 +108,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