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

Default import is broken when bundling JS files compiled from TypeScript #1295

Closed
rhysd opened this issue May 19, 2021 · 7 comments
Closed

Comments

@rhysd
Copy link

rhysd commented May 19, 2021

Versions

  • TypeScript: v4.2.4
  • esbuild: 0.12.1
  • Node.js: v15.10.0
  • OS: macOS 10.15

Repro

Put tsconfig.json, index.ts and app.ts in current directory as follows:

tsconfig.json:

{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "removeComments": true,
    "preserveConstEnums": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noEmitOnError": true,
    "noFallthroughCasesInSwitch": true,
    "noPropertyAccessFromIndexSignature": true,
    "strictNullChecks": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "target": "es2017",
    "sourceMap": true,
    "esModuleInterop": true,
    "resolveJsonModule": true
  },
  "include": [
    "index.ts",
    "app.ts"
  ]
}

app.ts:

export default class App {
    start() {
        console.log('start');
    }
}

index.ts:

import App from './app';

new App().start();

Compile TypeScript files with tsc command:

tsc -p .

Confirm the compiled files work fine:

node ./index.js

It should output:

start

Then bundle the JS files with esbuild:

esbuild --bundle index.js --platform=node --outfile=out.js

Run the bundled file:

node ./out.js

Expected behavior

node out.js outputs

start

Actual behavior

node out.js causes an exception:

/Users/rhysd/tmp/out.js:34
new app_1.default().start();
^

TypeError: app_1.default is not a constructor
    at Object.<anonymous> (/Users/rhysd/tmp/out.js:34:1)
    at Module._compile (node:internal/modules/cjs/loader:1091:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1120:10)
    at Module.load (node:internal/modules/cjs/loader:971:32)
    at Function.Module._load (node:internal/modules/cjs/loader:812:14)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)
    at node:internal/main/run_main_module:17:47

Note

Directly bundling TypeScript files with:

esbuild --bundle index.ts --platform=node --outfile=out.js

does not cause this issue. It works as expected.

@evanw
Copy link
Owner

evanw commented May 20, 2021

What's happening here is that the import from ./app in index.js ends up loading app.ts in esbuild because with esbuild's default behavior, an implicit .ts extension takes precedence over an implicit .js extension.

Here are some ideas for how to avoid this but still do what you're trying to do:

  • Import from ./app.js explicitly in your source code
  • Disable esbuild's implicit .ts extension with --resolve-extensions=.js
  • Delete the .ts files before running esbuild
  • Use TypeScript's outDir setting to generate the .js files into a separate directory, then run esbuild on those files instead

@rhysd
Copy link
Author

rhysd commented May 21, 2021

@evanw Thank you for your answers. 2nd and 4th options work fine for me.

with esbuild's default behavior, an implicit .ts extension takes precedence over an implicit .js extension.

I wander that imports in .js files without file extension should prioritize implicit .js file extension. Is there any case where it's better to prioritize .ts extension than .js extension in .js file? Since TypeScript compiler emits .js file in the same directory as .ts file by default, it might be better to avoid this kind of confusion by prioritizing .js implicit file extension.

@evanw
Copy link
Owner

evanw commented May 27, 2021

I think converting .js to .ts inside a .js file could be considered a bug. I can restrict that to inside .ts and .tsx files only.

@rhysd
Copy link
Author

rhysd commented May 27, 2021

Thank you for taking a look. Is the issue already being tracked anywhere? If not, I'll keep this issue open to track it.

@evanw
Copy link
Owner

evanw commented May 27, 2021

Actually I was wrong. The behavior I was thinking of is the one where if you import ./app.js with an explicit .js extension and that file is missing but ./app.ts exists, then it will use the app.ts file instead. But that's not what is happening here since both files exist. So restricting that behavior to TypeScript files doesn't help in this case.

I think using --resolve-extensions=.js to tell esbuild to not import TypeScript files is the solution that makes the most sense here.

@rhysd
Copy link
Author

rhysd commented May 27, 2021

I confirmed

esbuild --bundle index.js --platform=node --outfile=out.js --resolve-extensions=.js

fixed the out.js file.

Is the correct behavior that TypeScript file is prioritized on resolving imports in JavaScript file when file extension is omitted? (It looked confusing for me.) If it is correct, this issue is actually an intended behavior so we can happily close this. Otherwise I think this issue would be kept to be open.

@evanw
Copy link
Owner

evanw commented Jun 28, 2021

Yes, that is what I consider to be correct behavior. The default resolve order is documented as .tsx,.ts,.jsx,.js,.css,.json, which is why TypeScript is prioritized, and that is usually more correct because that refers to the source file that people are trying to build. Closing as by design.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants