module: add ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX

PR-URL: https://github.com/nodejs/node/pull/56610
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
Marco Ippolito 2025-01-17 13:42:50 +01:00 committed by GitHub
parent 22f1518d2f
commit cee63dcf35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 93 additions and 38 deletions

View File

@ -1390,7 +1390,8 @@ Node.js will try to detect the syntax with the following steps:
1. Run the input as CommonJS. 1. Run the input as CommonJS.
2. If step 1 fails, run the input as an ES module. 2. If step 1 fails, run the input as an ES module.
3. If step 2 fails with a SyntaxError, strip the types. 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, throw the error from step 2, including the TypeScript error in the message,
else run as CommonJS. else run as CommonJS.
5. If step 4 fails, run the input as an ES module. 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 [`Buffer`]: buffer.md#class-buffer
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man3.0/man3/CRYPTO_secure_malloc_init.html [`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_INVALID_TYPESCRIPT_SYNTAX`]: errors.md#err_invalid_typescript_syntax
[`ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`]: errors.md#err_unsupported_typescript_syntax
[`NODE_OPTIONS`]: #node_optionsoptions [`NODE_OPTIONS`]: #node_optionsoptions
[`NO_COLOR`]: https://no-color.org [`NO_COLOR`]: https://no-color.org
[`SlowBuffer`]: buffer.md#class-slowbuffer [`SlowBuffer`]: buffer.md#class-slowbuffer

View File

@ -2095,11 +2095,13 @@ does not consist of exactly two elements.
added: added:
- v23.0.0 - v23.0.0
- v22.10.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. The provided TypeScript syntax is not valid.
This could happen when using TypeScript syntax that requires
transformation with [type-stripping][].
<a id="ERR_INVALID_URI"></a> <a id="ERR_INVALID_URI"></a>
@ -3116,6 +3118,18 @@ try {
} }
``` ```
<a id="ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX"></a>
### `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`
<!-- YAML
added: REPLACEME
-->
The provided TypeScript syntax is unsupported.
This could happen when using TypeScript syntax that requires
transformation with [type-stripping][].
<a id="ERR_USE_AFTER_CLOSE"></a> <a id="ERR_USE_AFTER_CLOSE"></a>
### `ERR_USE_AFTER_CLOSE` ### `ERR_USE_AFTER_CLOSE`

View File

@ -1838,6 +1838,7 @@ E('ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING',
E('ERR_UNSUPPORTED_RESOLVE_REQUEST', E('ERR_UNSUPPORTED_RESOLVE_REQUEST',
'Failed to resolve module specifier "%s" from "%s": Invalid relative URL or base scheme is not hierarchical.', 'Failed to resolve module specifier "%s" from "%s": Invalid relative URL or base scheme is not hierarchical.',
TypeError); TypeError);
E('ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX', '%s', SyntaxError);
E('ERR_USE_AFTER_CLOSE', '%s was closed', Error); E('ERR_USE_AFTER_CLOSE', '%s was closed', Error);
// This should probably be a `TypeError`. // This should probably be a `TypeError`.

View File

@ -12,8 +12,10 @@ const { assertTypeScript,
isUnderNodeModules, isUnderNodeModules,
kEmptyObject } = require('internal/util'); kEmptyObject } = require('internal/util');
const { const {
ERR_INTERNAL_ASSERTION,
ERR_INVALID_TYPESCRIPT_SYNTAX, ERR_INVALID_TYPESCRIPT_SYNTAX,
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING, ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX,
} = require('internal/errors').codes; } = require('internal/errors').codes;
const { getOptionValue } = require('internal/options'); const { getOptionValue } = require('internal/options');
const assert = require('internal/assert'); const assert = require('internal/assert');
@ -49,7 +51,20 @@ function parseTypeScript(source, options) {
try { try {
return parse(source, options); return parse(source, options);
} catch (error) { } 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);
}
} }
} }

View File

@ -35,7 +35,7 @@ const { getOptionValue } = require('internal/options');
const { const {
makeContextifyScript, runScriptInThisContext, makeContextifyScript, runScriptInThisContext,
} = require('internal/vm'); } = require('internal/vm');
const { emitExperimentalWarning, isError } = require('internal/util'); const { emitExperimentalWarning } = require('internal/util');
// shouldAbortOnUncaughtToggle is a typed array for faster // shouldAbortOnUncaughtToggle is a typed array for faster
// communication with JS. // communication with JS.
const { shouldAbortOnUncaughtToggle } = internalBinding('util'); const { shouldAbortOnUncaughtToggle } = internalBinding('util');
@ -254,10 +254,6 @@ function evalTypeScript(name, source, breakFirstLine, print, shouldLoadESM = fal
try { try {
compiledScript = compileScript(name, source, baseUrl); compiledScript = compileScript(name, source, baseUrl);
} catch (originalError) { } catch (originalError) {
// If it's not a SyntaxError, rethrow it.
if (!isError(originalError) || originalError.name !== 'SyntaxError') {
throw originalError;
}
try { try {
sourceToRun = stripTypeScriptModuleTypes(source, name, false); sourceToRun = stripTypeScriptModuleTypes(source, name, false);
// Retry the CJS/ESM syntax detection after stripping the types. // 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. // Emit the experimental warning after the code was successfully evaluated.
emitExperimentalWarning('Type Stripping'); emitExperimentalWarning('Type Stripping');
} catch (tsError) { } catch (tsError) {
// If its not an error, or it's not an invalid typescript syntax error, rethrow it. // If it's invalid or unsupported TypeScript syntax, rethrow the original error
if (!isError(tsError) || tsError?.code !== 'ERR_INVALID_TYPESCRIPT_SYNTAX') { // with the TypeScript error message added to the stack.
throw tsError; if (tsError.code === 'ERR_INVALID_TYPESCRIPT_SYNTAX' || tsError.code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX') {
originalError.stack = decorateCJSErrorWithTSMessage(originalError.stack, tsError.message);
throw originalError;
} }
try { throw tsError;
originalError.stack = decorateCJSErrorWithTSMessage(originalError.stack, tsError.message);
} catch { /* Ignore potential errors coming from `stack` getter/setter */ }
throw originalError;
} }
} }
@ -322,28 +317,23 @@ function evalTypeScriptModuleEntryPoint(source, print) {
// Compile the module to check for syntax errors. // Compile the module to check for syntax errors.
moduleWrap = loader.createModuleWrap(source, url); moduleWrap = loader.createModuleWrap(source, url);
} catch (originalError) { } catch (originalError) {
// If it's not a SyntaxError, rethrow it.
if (!isError(originalError) || originalError.name !== 'SyntaxError') {
throw originalError;
}
let strippedSource;
try { try {
strippedSource = stripTypeScriptModuleTypes(source, url, false); const strippedSource = stripTypeScriptModuleTypes(source, url, false);
// If the moduleWrap was successfully created, execute the module job. // If the moduleWrap was successfully created, execute the module job.
// outside the try-catch block to avoid catching runtime errors. // outside the try-catch block to avoid catching runtime errors.
moduleWrap = loader.createModuleWrap(strippedSource, url); moduleWrap = loader.createModuleWrap(strippedSource, url);
// Emit the experimental warning after the code was successfully compiled. // Emit the experimental warning after the code was successfully compiled.
emitExperimentalWarning('Type Stripping'); emitExperimentalWarning('Type Stripping');
} catch (tsError) { } catch (tsError) {
// If its not an error, or it's not an invalid typescript syntax error, rethrow it. // If it's invalid or unsupported TypeScript syntax, rethrow the original error
if (!isError(tsError) || tsError?.code !== 'ERR_INVALID_TYPESCRIPT_SYNTAX') { // with the TypeScript error message added to the stack.
throw tsError; if (tsError.code === 'ERR_INVALID_TYPESCRIPT_SYNTAX' ||
} tsError.code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX') {
try {
originalError.stack = `${tsError.message}\n\n${originalError.stack}`; 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 // If the moduleWrap was successfully created either with by just compiling

View File

@ -102,33 +102,33 @@ test('expect fail eval TypeScript ESM syntax with input-type commonjs-typescript
strictEqual(result.code, 1); 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, [ const result = await spawnPromisified(process.execPath, [
'--eval', '--eval',
'enum Foo { A, B, C }']); 'enum Foo { A, B, C }']);
strictEqual(result.stdout, ''); strictEqual(result.stdout, '');
match(result.stderr, /SyntaxError/); match(result.stderr, /SyntaxError/);
doesNotMatch(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/); doesNotMatch(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/);
strictEqual(result.code, 1); 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, [ const result = await spawnPromisified(process.execPath, [
'--input-type=module-typescript', '--input-type=module-typescript',
'--eval', '--eval',
'enum Foo { A, B, C }']); 'enum Foo { A, B, C }']);
strictEqual(result.stdout, ''); strictEqual(result.stdout, '');
match(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/); match(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/);
strictEqual(result.code, 1); 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, [ const result = await spawnPromisified(process.execPath, [
'--input-type=commonjs-typescript', '--input-type=commonjs-typescript',
'--eval', '--eval',
'enum Foo { A, B, C }']); 'enum Foo { A, B, C }']);
strictEqual(result.stdout, ''); strictEqual(result.stdout, '');
match(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/); match(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/);
strictEqual(result.code, 1); strictEqual(result.code, 1);
}); });
@ -140,7 +140,7 @@ test('should not parse TypeScript with --type-module=commonjs', async () => {
strictEqual(result.stdout, ''); strictEqual(result.stdout, '');
match(result.stderr, /SyntaxError/); match(result.stderr, /SyntaxError/);
doesNotMatch(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/); doesNotMatch(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/);
strictEqual(result.code, 1); strictEqual(result.code, 1);
}); });
@ -152,7 +152,7 @@ test('should not parse TypeScript with --type-module=module', async () => {
strictEqual(result.stdout, ''); strictEqual(result.stdout, '');
match(result.stderr, /SyntaxError/); match(result.stderr, /SyntaxError/);
doesNotMatch(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/); doesNotMatch(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/);
strictEqual(result.code, 1); 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.stdout, '');
strictEqual(result.code, 1); 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);
});

View File

@ -321,3 +321,13 @@ test('execute a TypeScript loader and a .js file', async () => {
match(result.stdout, /Hello, TypeScript!/); match(result.stdout, /Hello, TypeScript!/);
strictEqual(result.code, 0); 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);
});

View File

@ -0,0 +1,3 @@
function foo(): string {
await Promise.resolve(1);
}