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

Empty init functions generated for side-effect-free ESM modules #3771

Open
brettwillis opened this issue May 17, 2024 · 4 comments
Open

Empty init functions generated for side-effect-free ESM modules #3771

brettwillis opened this issue May 17, 2024 · 4 comments

Comments

@brettwillis
Copy link

brettwillis commented May 17, 2024

ESBuild seems to generate empty "module init" functions even for side-effect free ESM modules.

Given a module src/utils/is-equal.mts:

export function isEqual(a, b) {
  // ...
}

What ends up in the generated bundle is (in the root scope):

// src/utils/is-equal.mts
function isEqual(a, b) {
  // ...
}
var init_is_equal = __esm({
  "src/utils/is-equal.mts"() {
    "use strict";
  }
});

And then init_is_equal is called everywhere that isEqual is imported. What is causing this? I would not expect init_is_equal to be present. It does this for some modules but not others.

Config

  • esbuild@0.21.3
  • platform: 'node', format: 'esm', target: 'node20'
  • Entire application is ESM and side-effect free ("sideEffects: false" in package.json) but some dependencies are CJS
@hyrious
Copy link

hyrious commented May 17, 2024

The __export(is_equal_exports, { and __esm() helpers are generated for any CommonJS-style imports. For example import('./is-equal.mjs') require('./is-equal'). This is because they captures the namespace object and esbuild doesn't know if outside code used or not used the object.

Example of namespace capturing in ESM context. // However, esbuild does know the namespace object is PURE, so if you never used the object it will be tree-shaked.

Example of using import(). // Not only it generates a namespace object, but also it has to generate a promise to preserve the semantic of import(). The promise obviously is a side-effect, which cannot be tree-shaked.

@brettwillis
Copy link
Author

I see, so it depends on the style of import where the module is imported, not so much the module itself.

Here is a full representation of my scenario, btw.

When you say "namespace object", I think you are referring to the "module object" in the case of ESM and the value of module.exports in the case of CJS, right?

I still don't quite understand, though, because init_is_equal is not the module/namespace object, it is a function generated by ESBuild to initialise the module, and esbuild inserts it where the module is require()'d and only every calls the function. init_is_equal is not passed around to application code as a reference to the module.

Furthermore, it is a complete no-op. It was generated by esbuild as an empty function so surely esbuild can know it is a no-op and omit it?

@hyrious
Copy link

hyrious commented May 17, 2024

When you say "namespace object", I think you are referring to the "module object" in the case of ESM and the value of module.exports in the case of CJS, right?

Yes, my wording might not be very correct.


esbuild bundle works by scanning the code in 2 phases. The first phase is gathering and transforming input files in parallel (that's why it is fast). The second phase is linking these individual modules, performing scope hoisting and generating esm-cjs interop helpers.

I guess when esbuild see your wrapper.cts, it knows:

  1. This is a commonjs module because it has export = ....
  2. This module imports something outside using es modules syntax import { ....

Therefore, it can prepare such template for wrapper.cts:

// prepare a commonjs module object
var require_wrapper = __commonJS {
  // evaluating dependencies to make sure
  // any possible side effect evaluates before this module
  init_is_equal();

  ... // rest of the module's content
};

During the second phase, the linker just has to make sure these variables are bound. There's no further side-effect tracking algorithm performed on them.

It might be diffcult to adding such optimization during the linking phase without hurting the speed.

@brettwillis
Copy link
Author

Ok while there is opportunity to optimise the output I understand your explanation, thanks!

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

No branches or pull requests

3 participants