egoist / esbuild-register

Transpile JSX, TypeScript and esnext features on the fly with esbuild
MIT License
982 stars 52 forks source link

.js extension import raises MODULE_NOT_FOUND error #77

Open n1ru4l opened 2 years ago

n1ru4l commented 2 years ago

See the following reproduction: https://codesandbox.io/s/esbuild-register-js-module-not-found-reproduction-gyogmj

run yarn start

I am pretty sure this should be allowed.

romanown commented 2 years ago

use 'foo' instead 'foo.js'. although 'foo' is 'foo.ts'. but use need as 'foo'. to try please. https://codesandbox.io/s/esbuild-register-js-module-not-found-reproduction-forked-y85vvn?file=/index.ts

n1ru4l commented 2 years ago

foo.js is what TypeScript node16 moduleResolution expects (for importing from a .ta file). Also in Node.js you can attach the .js extension in any version and it behaves the same way as if you did not attach the extension. Thus, to me it seems like esbuild-register should be able to follow this behavior.

justinhelmer commented 1 year ago

I am experiencing the same issue in a pure ESM package, trying to use esbuild-register/loader to run mocha for my TS files with JS extensions.

janus-reith commented 1 year ago

Im experiencing the same issue with both esbuild-register and swc-node which also provides an experimental esm loader. I think I have all the necessary settings in my tsconfig, since tsc is able to build with the same input in ESM mode. Omitting the .js extension is not an option, TSC which I still depend on for type-checking would fail in that case.

I had a similar issue trying to get swc/jest to work some time ago https://github.com/swc-project/jest/issues/64, that worked eventually with the solution provided in the comments there, that however was specific to jest. Hope I'm not going too much off topic here, it seems to me like the underlying technical issues are the same and revolve around the discrepancy between ESM explicit extension requirements and tsc + other build tools usually omitting them. Last time I checked how deno does it, I realized they simply had a patched version of tsc to make it not complain about imports ending with ".ts";

I'm still wondering if there's some way the authors of esbuild-register and swc-node have this working in ESM mode which I simply didn't notice yet, since using them on .ts files and then using tsc for typechecking seems to be the by far most common usecase I guess?

brookback commented 6 months ago

I am experiencing the same issue in a pure ESM package, trying to use esbuild-register/loader to run mocha for my TS files with JS extensions.

Having the exact same use case, @justinhelmer.

Using esbuild-register to transpile our test .ts files before running Mocha on them with this conf:

// .mocharc.cjs
module.exports = {
    extension: ['ts'],
    'node-option': ['loader=esbuild-register/loader'],

    require: [
        'esbuild-register',
    ],
}

Moving to ts-node and their ESM loader works:

// .mocharc.cjs
module.exports = {
    extension: ['ts'],
    'node-option': ['experimental-specifier-resolution=node', 'loader=ts-node/esm'],
}
markandrus commented 3 months ago

In my project, I only have ".ts" files that I import as ".js". Based on this, I made a very dumb patch for esbuild-register, and applied it with yarn patch:

diff --git a/dist/node.js b/dist/node.js
index e117be6904a8145923dce5a75b35dfe174666861..12e7be4aba3359fcd71098c85618ae618a98e076 100644
--- a/dist/node.js
+++ b/dist/node.js
@@ -4793,10 +4793,26 @@ function registerTsconfigPaths() {
       const found = matchPath(request);
       if (found) {
         const modifiedArguments = [found, ...[].slice.call(arguments, 1)];
-        return originalResolveFilename.apply(this, modifiedArguments);
+        try {
+          return originalResolveFilename.apply(this, modifiedArguments);
+        } catch (error) {
+          if (error.code === 'MODULE_NOT_FOUND' && found.endsWith('.js')) {
+            modifiedArguments[0] = found.replace(/\.js$/, '.ts');
+            return originalResolveFilename.apply(this, modifiedArguments);
+          }
+          throw error;
+        }
+      }
+    }
+    try {
+      return originalResolveFilename.apply(this, arguments);
+    } catch (error) {
+      if (error.code === 'MODULE_NOT_FOUND' && arguments[0].endsWith('.js')) {
+        arguments[0] = arguments[0].replace(/\.js$/, '.ts');
+        return originalResolveFilename.apply(this, arguments);
       }
+      throw error;
     }
-    return originalResolveFilename.apply(this, arguments);
   };
   return () => {
     Module._resolveFilename = originalResolveFilename;

There is probably a smarter version of this that handles the other typical file suffixes, avoids try/catch, etc.