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

Problem with 'import * as' when importing a set of class #1073

Closed
JerryLeam opened this issue Mar 28, 2021 · 7 comments
Closed

Problem with 'import * as' when importing a set of class #1073

JerryLeam opened this issue Mar 28, 2021 · 7 comments

Comments

@JerryLeam
Copy link

JerryLeam commented Mar 28, 2021

I want to use typescript and its dynamic feature to do something but I realized that the esbuild didn't compile the 'import * as'
For example:

//src/main.ts
import * as lib from "./lib"
Object.values(lib).forEach(l => new l());
//src/lib.ts just a empty class
export class Test{
    constructor(something:string) {        
    }
}

and I build the thing with following configuration:

//build.js
require('esbuild').build({
  entryPoints: ['src/main.ts',"src/lib.ts"],
  bundle: false,
    format:"cjs",
    outdir: './dist',
    platform:"node"
}).catch((err) => {
  console.log(err);
  process.exit(1)
})

it is built successfully but when I ran dist/main.js error came out.
TypeError: l is not a constructor
this behavior is different from using tsc because esbuild warp the lib in main.ts like this:
esbuild
so the values function gets the default and it goes error
but tsc warp like this
tsc
the default is gone and the class is still a class rather than a getter

and I've read https://esbuild.github.io/content-types/#es-module-interop and realized that I should explicitly indicate the thing I was importing like import lib from "./lib" rather than "import *", but is there any way to maintain the original behavior from tsc to keep the dynamic import functional?

@evanw
Copy link
Owner

evanw commented Mar 28, 2021

I believe this is basically happening because esbuild is trying to faithfully emulate the way node works, and the node team made unfortunate decision that breaks a common interop scenario with cross-compiled ES modules. There is a big discussion about this here: #532.

They decided that when you import a CommonJS module using an import statement in node, the value of module.exports is always assigned to the default import even if that CommonJS module uses the well-established __esModule marker convention to indicate that it has been cross-compiled from an ES module and that ES module didn't have a default import (or even worse, had a different default import!).

You can observe how node behaves in this situation for yourself here:

$ cat lib.cjs
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.Test = class { }

$ cat main.mjs
import * as lib from "./lib.cjs"
Object.values(lib).forEach(l => new l());

$ node main.js
main.mjs:2
Object.values(lib).forEach(l => new l());
                                ^

TypeError: l is not a constructor

If node respected the __esModule marker, then this module would not have a default export. But node does not interpret the __esModule marker and generates the default import.

This is happening because you are using esbuild to compile the file one-at-a-time so esbuild must treat ./lib as a CommonJS module. And as the previous example shows, importing a CommonJS module using an ESM import statement always results in a default import with the value of module.exports in node. So this is the result of esbuild trying to faithfully represent node's behavior on a file-by-file level.

I can think of a few options:

  • You can filter out the default import.
  • You can use require() instead of import().
  • You can use the esm output format instead of the cjs output format.
  • You can use esbuild to bundle everything together. In that case esbuild will make this work, since this means esbuild is now emulating node's ESM support instead of node's CommonJS support.

@JerryLeam
Copy link
Author

Thank you very much indeed! I think you're right and I have got to make a workaround to adopt different tools. The reason for compiling the file one-at-a-time is that I want to keep the original file structure while at the same time, I want to make use of the efficiency of esbuild. And I know it's a little bit tricky 'cause esbuild is essentially a bundler rather than an alternative for babel.

@evanw
Copy link
Owner

evanw commented Apr 7, 2021

I'm closing this issue as esbuild matches node's behavior when it's run as ESM so this is working as intended. I agree that this interop scenario is unfortunate, but that's how node works.

@evanw evanw closed this as completed Apr 7, 2021
@mhart
Copy link

mhart commented May 10, 2021

Oof, just ran into this too trying to update our esbuild version (on a very large project).

Can you explain the "filter out" option you mentioned?

@evanw
Copy link
Owner

evanw commented May 10, 2021

Can you explain the "filter out" option you mentioned?

Object.values(lib).forEach(l => {
  if (l === 'default') return;
  new l();
});

@mhart
Copy link

mhart commented May 10, 2021

Hmmm. Is there any option to get the legacy behavior back? So that it continues to match tsc or tsnode behavior?

@mhart
Copy link

mhart commented May 10, 2021

And FWIW, it only appears to be import * as statements that are tripping us up (with the sudden appearance of default properties) – everything else (so far) seems compatible with how it was pre 0.8.55

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

3 participants