Repository: nodejs/node
Pull request: module: wrap swc error in ERR_INVALID_TYPESCRIPT_SYNTAX

This PR wraps swc errors in a Node error to provide a more consistent error handling.

Before this PR
```bash
marcoippolito@marcos-MacBook-Pro node % node --experimental-strip-types -p "enum Foo{}"

node:internal/deps/amaro/dist/index:226
          throw takeObject(r1);
          ^
  x TypeScript enum is not supported in strip-only mode
   ,----
 1 | enum Foo{}
   : ^^^^^^^^^^
   `----

(Use `node --trace-uncaught ...` to show where the exception was thrown)

Node.js v22.8.0
```

After this PR
```bash
marcoippolito@marcos-MacBook-Pro node % ./node --experimental-strip-types -p "enum Foo{}"                      
node:internal/modules/helpers:390
    throw new ERR_INVALID_TYPESCRIPT_SYNTAX(error);
    ^

SyntaxError [ERR_INVALID_TYPESCRIPT_SYNTAX]:   x TypeScript enum is not supported in strip-only mode
   ,----
 1 | enum Foo{}
   : ^^^^^^^^^^
   `----

    at stripTypeScriptTypes (node:internal/modules/helpers:390:11)
    at node:internal/main/eval_string:27:3 {
  code: 'ERR_INVALID_TYPESCRIPT_SYNTAX'
}
```

Changed files:
filename: doc/api/errors.md
patch: @@ -2094,6 +2094,18 @@ An element in the `iterable` provided to the [WHATWG][WHATWG URL API]
 represent a `[name, value]` tuple – that is, if an element is not iterable, or
 does not consist of exactly two elements.
 
+<a id="ERR_INVALID_TYPESCRIPT_SYNTAX"></a>
+
+### `ERR_INVALID_TYPESCRIPT_SYNTAX`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+The provided TypeScript syntax is not valid or unsupported.
+This could happen when using TypeScript syntax that requires
+transformation with [type-stripping][].
+
 <a id="ERR_INVALID_URI"></a>
 
 ### `ERR_INVALID_URI`
@@ -4203,4 +4215,5 @@ An error occurred trying to allocate memory. This should never happen.
 [stream-based]: stream.md
 [syscall]: https://man7.org/linux/man-pages/man2/syscalls.2.html
 [try-catch]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch
+[type-stripping]: typescript.md#type-stripping
 [vm]: vm.md

filename: lib/internal/errors.js
patch: @@ -1524,6 +1524,7 @@ E('ERR_INVALID_SYNC_FORK_INPUT',
   TypeError);
 E('ERR_INVALID_THIS', 'Value of "this" must be of type %s', TypeError);
 E('ERR_INVALID_TUPLE', '%s must be an iterable %s tuple', TypeError);
+E('ERR_INVALID_TYPESCRIPT_SYNTAX', '%s', SyntaxError);
 E('ERR_INVALID_URI', 'URI malformed', URIError);
 E('ERR_INVALID_URL', function(input, base = null) {
   this.input = input;

filename: lib/internal/modules/helpers.js
patch: @@ -15,6 +15,7 @@ const {
 const {
   ERR_INVALID_ARG_TYPE,
   ERR_INVALID_RETURN_PROPERTY_VALUE,
+  ERR_INVALID_TYPESCRIPT_SYNTAX,
 } = require('internal/errors').codes;
 const { BuiltinModule } = require('internal/bootstrap/realm');
 
@@ -312,44 +313,37 @@ function getBuiltinModule(id) {
   return normalizedId ? require(normalizedId) : undefined;
 }
 
-/**
- * TypeScript parsing function, by default Amaro.transformSync.
- *  @type {Function}
- */
-let typeScriptParser;
 /**
  * The TypeScript parsing mode, either 'strip-only' or 'transform'.
  * @type {string}
  */
-let typeScriptParsingMode;
-/**
- * Whether source maps are enabled for TypeScript parsing.
- * @type {boolean}
- */
-let sourceMapEnabled;
+const getTypeScriptParsingMode = getLazy(() =>
+  (getOptionValue('--experimental-transform-types') ? 'transform' : 'strip-only'),
+);
 
 /**
  * Load the TypeScript parser.
- * @param {Function} parser - A function that takes a string of TypeScript code
  * and returns an object with a `code` property.
  * @returns {Function} The TypeScript parser function.
  */
-function loadTypeScriptParser(parser) {
-  if (typeScriptParser) {
-    return typeScriptParser;
-  }
+const loadTypeScriptParser = getLazy(() => {
+  const amaro = require('internal/deps/amaro/dist/index');
+  return amaro.transformSync;
+});
 
-  if (parser) {
-    typeScriptParser = parser;
-  } else {
-    const amaro = require('internal/deps/amaro/dist/index');
-    // Default option for Amaro is to perform Type Stripping only.
-    typeScriptParsingMode = getOptionValue('--experimental-transform-types') ? 'transform' : 'strip-only';
-    sourceMapEnabled = getOptionValue('--enable-source-maps');
-    // Curry the transformSync function with the default options.
-    typeScriptParser = amaro.transformSync;
+/**
+ *
+ * @param {string} source the source code
+ * @param {object} options the options to pass to the parser
+ * @returns {TransformOutput} an object with a `code` property.
+ */
+function parseTypeScript(source, options) {
+  const parse = loadTypeScriptParser();
+  try {
+    return parse(source, options);
+  } catch (error) {
+    throw new ERR_INVALID_TYPESCRIPT_SYNTAX(error);
   }
-  return typeScriptParser;
 }
 
 /**
@@ -364,14 +358,13 @@ function loadTypeScriptParser(parser) {
  */
 function stripTypeScriptTypes(source, filename) {
   assert(typeof source === 'string');
-  const parse = loadTypeScriptParser();
   const options = {
     __proto__: null,
-    mode: typeScriptParsingMode,
-    sourceMap: sourceMapEnabled,
+    mode: getTypeScriptParsingMode(),
+    sourceMap: getOptionValue('--enable-source-maps'),
     filename,
   };
-  const { code, map } = parse(source, options);
+  const { code, map } = parseTypeScript(source, options);
   if (map) {
     // TODO(@marco-ippolito) When Buffer.transcode supports utf8 to
     // base64 transformation, we should change this line.

filename: test/es-module/test-typescript-eval.mjs
patch: @@ -110,3 +110,13 @@ test('expect fail eval TypeScript ESM syntax with input-type commonjs', async ()
   match(result.stderr, /Cannot use import statement outside a module/);
   strictEqual(result.code, 1);
 });
+
+test('check syntax error is thrown when passing invalid syntax', async () => {
+  const result = await spawnPromisified(process.execPath, [
+    '--experimental-strip-types',
+    '--eval',
+    'enum Foo { A, B, C }']);
+  strictEqual(result.stdout, '');
+  match(result.stderr, /ERR_INVALID_TYPESCRIPT_SYNTAX/);
+  strictEqual(result.code, 1);
+});