diff --git a/doc/api/cli.md b/doc/api/cli.md index da2506326fc5bd..dd058306aa26f7 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1390,7 +1390,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. @@ -3708,6 +3709,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 24e2290e472b6b..fcd351b6993cb5 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -2095,11 +2095,13 @@ does not consist of exactly two elements. added: - v23.0.0 - v22.10.0 +changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/56610 + description: This error is no longer thrown on valid yet unsupported syntax. --> -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. @@ -3116,6 +3118,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 bda50797124758..d6b2ceb5962351 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1838,6 +1838,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 f5b19d5a7e8c9c..d4d7a604851ef1 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 5c6f25bec4df7d..bbbed8863de25a 100644 --- a/test/es-module/test-typescript-eval.mjs +++ b/test/es-module/test-typescript-eval.mjs @@ -102,33 +102,33 @@ 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, [ '--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, [ '--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, [ '--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); }); @@ -140,7 +140,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); }); @@ -152,7 +152,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); }); @@ -222,3 +222,23 @@ 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, [ + '--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, [ + '--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 81aed880bdcf51..74c4a0f120b758 100644 --- a/test/es-module/test-typescript.mjs +++ b/test/es-module/test-typescript.mjs @@ -321,3 +321,13 @@ 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, [ + 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); +}