Repository: mochajs/mocha (JavaScript)

Issue:
🐛 Bug: original require() errors are masked with an unrelated `ERR_UNKNOWN_FILE_EXTENSION`
### Bug Report Checklist

- [x] I have read and agree to Mocha's [Code of Conduct](https://github.com/mochajs/mocha/blob/main/.github/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/mochajs/mocha/blob/main/.github/CONTRIBUTING.md)
- [x] I have searched for [related issues](https://github.com/mochajs/mocha/issues?q=is%3Aissue) and [issues with the `faq` label](https://github.com/mochajs/mocha/issues?utf8=%E2%9C%93&q=is%3Aissue%20label%3Afaq%20), but none matched my issue.
- [x] I have 'smoke tested' the code to be tested by running it outside the real test suite to get a better sense of whether the problem is in the code under test, my usage of Mocha, or Mocha itself.
- [x] I want to provide a PR to resolve this (but after community discussion first)

### Expected

in 11.7.0, if `require()` fails, the error is thrown and output to console. e.g.:

```
 $ mocha --require ts-node/register --require source-map-support/register 'a.ts'
 
  Exception during run: .../a.ts:61:66 - error TS2345: Argument of type '{ address: string; }' is not assignable to parameter of type ...
 
 $ mocha --version
 11.7.0
```

### Actual

after this change https://github.com/mochajs/mocha/pull/5384#diff-30714d24f4a4acd4826e5f7e92f58922ed0ab41b1f0062ae2a46df6c5c899eb4L93-L102
the `err` is masked and returned an `ERR_UNKNOWN_FILE_EXTENSION` which is misleading. 

```
 $ mocha --require ts-node/register --require source-map-support/register 'a.ts'
 
  Exception during run: TypeError: Unknown file extension ".ts" for /.../a.ts
     at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:219:9)
     at defaultGetFormat (node:internal/modules/esm/get_format:245:36)
     at defaultLoad (node:internal/modules/esm/load:120:22)
     at async ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:580:32)
     at async ModuleJob._link (node:internal/modules/esm/module_job:116:19) {
   code: 'ERR_UNKNOWN_FILE_EXTENSION'
 }
 
 $ mocha --version
 11.7.1
```

### Minimal, Reproducible Example

1. install `ts-node` and require it along with the mocha command i.e. `mocha --require ts-node/register abc.ts`
2. try to run with a giblish ts file. e.g.

```
bla@#&#_@$
```
what ever contents in it causing a ts not compilable will output the same ERR_UNKNOWN_FILE_EXTENSION error

### Versions

11.7.1 not work
11.7.0 works

### Additional Info

I think this is open to discussion whether this should be classified as bug or not, but I just want to point out the breaking behavioral change in this patch version release which might potentially affect people who rely on the error output for debugging purpose or experiencing surprises suddenly. 


Discussion:
Ah, fun, this is something of a duplicate-ish of #5235. The fact that you're filing this only a few months after that was closed as _"not many people are asking for this"_ might mean we should reconsider the 'wontfix' label. 

> Minimal, Reproducible Example

Could you please post a full [reproducible project or playground](https://antfu.me/posts/why-reproductions-are-required#reproducible-projects-or-playgrounds)? In particular it'd be good to see your exact package lockfile and Node.js version, since we saw in #5235 that Mocha's behavior changes across Node.js versions.
https://codesandbox.io/p/devbox/adoring-butterfly-ypftzk
tried in node 20, 22
👍 thanks! Confirmed that this is a bug. Marking as accepting PRs for specifically improving one reported error message. 

I'm not clear on what the right heuristic would be for knowing to report the `import()` vs. `require()` message. _Maybe_ a good starting change would be to only switch to the original error if the new one is `ERR_UNKNOWN_FILE_EXTENSION`? A file extension complaint might be a reliable enough indication that the user's config intends to load through an older method.
Just adding some more information here. 

I was having an issue with `sinonJS` failing because i was creating a stub on an already stubbed line of code in my global.ts file. 


Mocha/ts-mocha was reporting this as `Exception during run: TypeError: Unknown file extension ".ts" for global.ts`

I was able to track it down by debugging the `node_modules/mocha/lib/nodejs/esm-utils.js` file 

```
const formattedImport = async (file, esmDecorator = forward) => {
  if (path.isAbsolute(file)) {
    try {
      return await exports.doImport(esmDecorator(url.pathToFileURL(file)));
    } catch (err) {
      // This is a hack created because ESM in Node.js (at least in Node v15.5.1) does not emit
      // the location of the syntax error in the error thrown.
      // This is problematic because the user can't see what file has the problem,
      // so we add the file location to the error.
      // TODO: remove once Node.js fixes the problem.
      if (
        err instanceof SyntaxError &&
        err.message &&
        err.stack &&
        !err.stack.includes(file)
      ) {
        const newErrorWithFilename = new SyntaxError(err.message);
        newErrorWithFilename.stack = err.stack.replace(
          /^SyntaxError/,
          `SyntaxError[ @${file} ]`
        );
        throw newErrorWithFilename;
      }
      throw err;
    }
  }
  return exports.doImport(esmDecorator(file));
};
```

Perhaps a simple solution would be to just join all the errors that occur here. Not sure at which point the orignal errors are getting swallowed/ignored. 


Just leaving this here for googlers going crazy trying to fix unrelated `Exception during run: TypeError: Unknown file extension ".ts" for... errros`




> Mocha/ts-mocha was reporting this as `Exception during run: TypeError: Unknown file extension ".ts" for global.ts`
> 
> Perhaps a simple solution would be to just join all the errors that occur here. Not sure at which point the orignal errors are getting swallowed/ignored.
> 
> Just leaving this here for googlers going crazy trying to fix unrelated `Exception during run: TypeError: Unknown file extension ".ts" for... errros`

yes you have exactly hit the situation like me whenever your test files are not compilable for whatever reasons.

i think the original error is somewhere inside the next recursive call (as you see should be the line `exports.doImport(...)` which again triggers another loop into one of the `exports.requireOrImport` implementations. 
Have you ever made `tsconfig.json`? 
If you make tsconfig.json and run the test, it will run smoothly. 
Usually, when you build a test with a typescript, you will make tsconfig.json unconditionally. 

So I don't think there's anything else to do at mocha on this issue, and I think it's good to do PR of a tip to the user on this part. 

The first part is done after creating tsconfig, and the second part is done after deleting tsconfig and testing it.
<img width="1810" height="868" alt="Image" src="https://github.com/user-attachments/assets/ea884707-e303-41b5-8924-a4e653d35a24" />
my folder
<img width="804" height="264" alt="Image" src="https://github.com/user-attachments/assets/92427cb5-d9f2-466e-9f3c-4651998fd8a5" />

Please feel free to comment, thank you.
@JoshuaKGoldberg @lkho 

Some updates on my testing, wanted to isolate some variables, so many edge cases.

### Node 24
Node 24 vanilla: ✅  `ERR_INVALID_TYPESCRIPT_SYNTAX`
Node 24 `-r ts-node/register`  (probably not doing anything): ✅  `ERR_INVALID_TYPESCRIPT_SYNTAX`
Node 24 `-r tsx`: ✅  `Error: Transform failed with 1 error`

Error is not reproducible on node 24! Probably due to the built-in type stripping

### Node 20 & 22
Node 22 vanilla: ✅  `ERR_UNKNOWN_FILE_EXTENSION`
Node 22 `-r ts-node/register`  (probably not doing anything): ❌   `ERR_UNKNOWN_FILE_EXTENSION`
Node 22 `-r tsx`: ✅  `Error: Transform failed with 1 error`

ts-node still seems to be dead weight, the presence of a tsconfig did not help ts-node

So conclusions for me:
1. TS-Node registration does not seem to be working with the new `require(esm)` / `import(esm)`. Its a bug on their side.
    - P.S.: ts-node has been abandoned for over 2 years now I don't think anyone should use it anymore
3. TSX is working and is a drop in replacement for ts-node
4. Node.js 24 and onwards has [built-in](https://nodejs.org/api/typescript.html) type stripping by default and type transform is just around the corner
5. The fact that ts-node half patches only require and not import, its only natural that would also break our ESM fallback code, I personally don't think consider it a bug, [Garbage In Garbage Out](https://en.wikipedia.org/wiki/Garbage_in,_garbage_out)
I agree with you very much. @samuelhnrq 
However, to add to this, it is as follows. 
This seems to be a problem with default tsconfig.json in ts-node. 
If you do not create a separate tsconfig.json, the ts-node runs based on the default tsconfig.json inside the ts-node, which causes an error. 

Then, where will the error occur? 
That's the tsconfig.json - types 
```
{
    "$schema": "./tsconfig.schemastore-schema.json",
    "compilerOptions": {
      // `target` and `lib` match @tsconfig/bases for node14, since that's the oldest node LTS, so it's the oldest node we support
      "target": "es2020",
      "lib": ["es2020"],
      "rootDir": ".",
      "outDir": "temp",
      "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo",
      "noEmit": true,
      "module": "commonjs",
      "moduleResolution": "node",
      "strict": true,
      "declaration": true,
      "sourceMap": true,
      "inlineSources": true,
      "types": ["node"],
...
```
If you add mocha over there, it works fine with ts-node as well. 
```
      "types": ["node", "mocha"],
```


The following video is a video that runs well when you make tsconfig.json an empty file. 
This is because when I create the tsconfig.json file locally
ts-node will read the local file rather than the ts-node's default tsconfig.json.
https://github.com/TypeStrong/ts-node/blob/main/tsconfig.json

video: https://youtu.be/4bcSZlJkMiI
@JoshuaKGoldberg I think we arrived at a stalemate here I dont know what else is there to do in this issue.

- TSNode require seems not to cover `require(esm)` properly and that's whats causing the issue. (naturally it has been abandoned for 2 years)
- There is a easy and obvious workaround (node 24 or tsx)

Max I can see adding another note in the docs advising against ts-node? Maybe related to #5311 ?
@samuelhnrq - May improve developer ergonomics and reduce future reports if mocha would raise an error/warning like 
`ts-node is deprecated please migrate to tsx or node >= 24` when running specs. 
> docs advising against ts-node
> error/warning like `ts-node is deprecated`

That's an interesting idea, but I don't think Mocha should add library-specific warnings like this. The bug isn't specific to `ts-node` - it's just most commonly triggered by it. Any userland `-r` library could fail in a similar way.

Plus, what happens if Mocha back falls out of maintenance and ts-node gets back into maintenance? We'd then have a situation where the out-of-maintenance tool is pointing a finger at the in-maintenance one. 😄 

As interesting as this discussion is, I don't think there's much more to cover? This issue is accepting PRs. The right next step would be to send a PR that switches reporting to the original error if the new one is `ERR_UNKNOWN_FILE_EXTENSION`.



Patch:
diff --git a/lib/nodejs/esm-utils.js b/lib/nodejs/esm-utils.js
index fd26665595..77541d7f51 100644
--- a/lib/nodejs/esm-utils.js
+++ b/lib/nodejs/esm-utils.js
@@ -101,9 +101,24 @@ const requireModule = async (file, esmDecorator) => {
     try {
       return dealWithExports(await formattedImport(file, esmDecorator));
     } catch (importErr) {
+      // If a --require module throws in a Node.js version that doesn't yet support .ts files,
+      // the fallback import() will throw an uninformative error about the file extension.
+      // What we actually care about is the original require() error.
+      // See: https://github.com/mochajs/mocha/issues/5393
+      if (
+        /\.(cts|mts|ts)$/.test(file) ||
+        importErr.code === 'ERR_UNKNOWN_FILE_EXTENSION'
+      ) {
+        throw requireErr;
+      }
+
+      // Similarly, for an exports/imports mismatch such as a missing 'default',
+      // the require() error will be more informative for users.
+      // See: https://github.com/mochajs/mocha/issues/5411
       if (importErr.code === 'ERR_INTERNAL_ASSERTION') {
         throw requireErr;
       }
+
       throw importErr;
     }
   }


Test patch:
diff --git a/test/node-unit/esm-utils.spec.js b/test/node-unit/esm-utils.spec.js
index e751ce3caf..2ca4cbfc34 100644
--- a/test/node-unit/esm-utils.spec.js
+++ b/test/node-unit/esm-utils.spec.js
@@ -20,6 +20,21 @@ describe('esm-utils', function () {
         }
       );
     });
+
+    it('should show a syntax error message when importing a TypeScript file with invalid syntax', async function () {
+      return expect(
+        () =>
+          esmUtils.requireOrImport(
+            '../../test/node-unit/fixtures/broken-syntax.ts'
+          ),
+        'to be rejected with error satisfying',
+        {
+          name: 'SyntaxError',
+          message:
+            /Invalid or unexpected token|Expected ident/
+        }
+      );
+    });
   });
 
   describe('loadFilesAsync()', function () {
diff --git a/test/node-unit/fixtures/broken-syntax.ts b/test/node-unit/fixtures/broken-syntax.ts
new file mode 100644
index 0000000000..550b2f0afd
--- /dev/null
+++ b/test/node-unit/fixtures/broken-syntax.ts
@@ -0,0 +1,1 @@
+@!