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/process/execution.js b/lib/internal/process/execution.js index 5e609fb650f3eb..52db57042499fc 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -313,8 +313,8 @@ function evalTypeScript(name, source, breakFirstLine, print, shouldLoadESM = fal /** * Wrapper of evalModuleEntryPoint * - * 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 + * 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 @@ -329,34 +329,39 @@ function evalTypeScriptModuleEntryPoint(source, print) { return require('internal/modules/run_main').runEntryPointWithESMLoader( async (loader) => { + const url = getEvalModuleUrl(); + let moduleWrap; try { - // Await here to catch the error and rethrow it with the typescript error message. - return await loader.eval(source, getEvalModuleUrl(), true); + // 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 { - const url = getEvalModuleUrl(); - const strippedSource = stripTypeScriptModuleTypes(source, url, false); - const result = await loader.eval(strippedSource, url, true); - // Emit the experimental warning after the code was successfully evaluated. + 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'); - return result; } 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); }, ); }; diff --git a/test/es-module/test-typescript-eval.mjs b/test/es-module/test-typescript-eval.mjs index c741ac586ba35e..03bafffcaa118f 100644 --- a/test/es-module/test-typescript-eval.mjs +++ b/test/es-module/test-typescript-eval.mjs @@ -217,3 +217,17 @@ test('typescript code is throwing an error', async () => { 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 trigger 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); +});