Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

module: add ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX #56610

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
16 changes: 13 additions & 3 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2097,9 +2097,7 @@ added:
- v22.10.0
marco-ippolito marked this conversation as resolved.
Show resolved Hide resolved
-->

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.

<a id="ERR_INVALID_URI"></a>

Expand Down Expand Up @@ -3135,6 +3133,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][].
Comment on lines +3148 to +3150

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be more explicit and immediately suggest to the user that a potential solution is to run using --experimental-transform-types?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a more forward-looking suggestion would be to suggest they use the TS 5.8 option (once it's out) so they avoid using that syntax in the first place (or perhaps both suggestions)

Copy link
Member Author

@marco-ippolito marco-ippolito Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add it in the error message itself?
I will followup with a PR after swc updates since I have to refactor error messages anyways


<a id="ERR_USE_AFTER_CLOSE"></a>

### `ERR_USE_AFTER_CLOSE`
Expand Down
1 change: 1 addition & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1853,6 +1853,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`.
Expand Down
17 changes: 16 additions & 1 deletion lib/internal/modules/typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
40 changes: 15 additions & 25 deletions lib/internal/process/execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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.
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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
Expand Down
36 changes: 28 additions & 8 deletions test/es-module/test-typescript-eval.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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);
});

Expand All @@ -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);
});

Expand Down Expand Up @@ -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);
});
10 changes: 10 additions & 0 deletions test/es-module/test-typescript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
3 changes: 3 additions & 0 deletions test/fixtures/typescript/ts/test-invalid-syntax.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function foo(): string {
await Promise.resolve(1);
}
Loading