esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

bundling in ESM with chunk splitting is confusing an ESM dependency with a commonJS dependency

Open paztis opened this issue 9 months ago • 3 comments

I'm doing a bundling with splitting in ESM I have a lazy loading of the dependency react-slick, This one is using an export way that mx commonJs and ESM

In the bundling it is undestood as a commonJs and wrapped with __commonJs but the export was already having a "default". It result with an export like this { default: { default: ReactSlick component } }

See below code

esbuild config:

const buildResult = await esbuild.build({
    entryPoints: ['src/index.ts'],
    outdir: 'dist',
    write: false,
    bundle: true,
    splitting: true,
    minify: false,
    format: 'esm',
    sourcemap: true,
    external: ['react', 'react-dom'],
    plugins: [
        sassPlugin()
    ],
    loader: {
        '.svg': 'file',
        '.woff2': 'file',
    },
    assetNames: 'assets/[name]-[hash]'
});

parent file:

const Slider = lazy(() => import('react-slick'));

build result:

var Slider3 = lazy7(() => import("./lib-CCBX26FZ.js"));

react-slice/lib/index.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports["default"] = void 0;
var _slider = _interopRequireDefault(require("./slider"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
var _default = exports["default"] = _slider["default"];

chunk result (lib-CCBX26FZ.js):

// ../../../node_modules/react-slick/lib/index.js
var require_lib = __commonJS({
  "../../../node_modules/react-slick/lib/index.js"(exports) {
    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports["default"] = void 0;
    var _slider = _interopRequireDefault(require_slider());
    function _interopRequireDefault(obj) {
      return obj && obj.__esModule ? obj : { "default": obj };
    }
    var _default = exports["default"] = _slider["default"];
  }
});
export default require_lib();
//# sourceMappingURL=lib-CCBX26FZ.js.map

_interopRequireDefault is injecting the __esModule: true to explain it is an ESM export require_lib() here returns {__esModule: true, default: ƒ}. And we do export default of this, that adds another default level. The __commonJs function might detect it to remove the "default" level.

paztis avatar Mar 20 '25 09:03 paztis

Playground

Although some of @paztis's word wasn't correct, the main issue regards to using splitting on a commonjs entry point (which was created by a dynamic import).

esbuild only handles code splitting on ESM entries. So if one entry is in commonjs, it is converted to ESM first. How does it convert? esbuild simply wraps the cjs code in a callback and assigns the whole module.exports to the ESM's default export. That is to say, a commonjs entry could only have only one export in ESM context (which is known as the Node.js behavior).

_interopRequireDefault is injecting the __esModule: true to explain it is an ESM export

  1. The __esModule annotation means that if possible, the bundler should treat module.exports as the same meaning as an es-module's namespace object. This is known as the babel behavior.

    But it isn't work in this case because the whole commonjs module is the entrypoint instead of being bundled in some chunk. You can think of re-exporting every named exports from a random commonjs code, which requires a static analyze on the code.

  2. _interopRequireDefault is simply a helper function to extract the default export from either a normal commonjs module or a module that adopts the babel style exports. This is the trick to simulate an es-module when there's no native ESM support.


There're some further steps could take:

  • For esbuild, since the code splitting function is still experimental, it is fine to live with such small issues. But if any improvement could make, it would be:

    • If a commonjs module is the only one default export in an ESM chunk:

      export default require_lib()
      

      ...then extract the default export like babel:

      export default __toESM(require_lib()).default
      
  • For users, you can either turn off the code splitting, or manually extract the exports by using a wrapper when you already know the module is in commonjs and you only want some names from it, example.

  • For library authors, just move to ESM!

hyrious avatar Mar 25 '25 12:03 hyrious

Here i'm a consumer of a 3rd part library. I'm not responsable of their code or their lazy imports. I also don't want to ask them to do code change while their code is correct because esbuild cannot process them.

I want code splitting otherwise all lazy mecanism became useless.

I want esbuild to support thèse side case by itself without any modification on my side, like webpack do for example. It is just a security they can add on hier side

Le mar. 25 mars 2025, 13:58, hyrious @.***> a écrit :

Playground https://esbuild.github.io/try/#YgAwLjI1LjEALS1idW5kbGUgLS1mb3JtYXQ9ZXNtIC0tc3BsaXR0aW5nIC0tb3V0ZGlyPS4AZQBlbnRyeS5qcwBsYXp5KCgpID0+IGltcG9ydCgnLi9saWInKSkAAGxpYi5qcwBleHBvcnRzLl9fZXNNb2R1bGUgPSB0cnVlCmV4cG9ydHMuZGVmYXVsdCA9IDQy

Although some of @paztis https://github.com/paztis's word wasn't correct, the main issue regards to using splitting on a commonjs entry point (which was created by a dynamic import).

esbuild only handles code splitting on ESM entries. So if one entry is in commonjs, it is converted to ESM first. How does it convert? esbuild simply wraps the cjs code in a callback and assigns the whole module.exports to the ESM's default export. That is to say, a commonjs entry could only have only one export in ESM context (which is known as the Node.js behavior https://esbuild.github.io/content-types/#default-interop).

_interopRequireDefault is injecting the __esModule: true to explain it is an ESM export

The __esModule annotation means that if possible, the bundler should treat module.exports as the same meaning as an es-module's namespace object. This is known as the babel behavior.

But it isn't work in this case because the whole commonjs module is the entrypoint instead of being bundled in some chunk. You can think of re-exporting every named exports from a random commonjs code, which requires a static analyze on the code. 2.

_interopRequireDefault is simply a helper function to extract the default export from either a normal commonjs module or a module that adopts the babel style exports. This is the trick to simulate an es-module when there's no native ESM support.


There're some further steps could take:

For esbuild, since the code splitting function is still experimental, it is fine to live with such small issues. But if any improvement could make, it would be:

  If a commonjs module is the only one default export in an ESM chunk:

  export default require_lib()

  ...then extract the default export like babel:

  export default __toESM(require_lib()).default

  -

For users, you can either turn off the code splitting, or manually extract the exports by using a wrapper when you already know the module is in commonjs and you only want some names from it, example https://esbuild.github.io/try/#YgAwLjI1LjEALS1idW5kbGUgLS1mb3JtYXQ9ZXNtIC0tc3BsaXR0aW5nIC0tb3V0ZGlyPS4AZQBlbnRyeS5qcwBsYXp5KCgpID0+IGltcG9ydCgnLi9saWItd3JhcHBlcicpKQAAbGliLmpzAGV4cG9ydHMuX19lc01vZHVsZSA9IHRydWUKZXhwb3J0cy5kZWZhdWx0ID0gNDIAAGxpYi13cmFwcGVyLmpzAGltcG9ydCBsaWIgZnJvbSAnLi9saWInCmV4cG9ydCBkZWZhdWx0IGxpYg .

For library authors, just move to ESM!

— Reply to this email directly, view it on GitHub https://github.com/evanw/esbuild/issues/4118#issuecomment-2751177473, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABIKMMIBRA7L6TFREQVM5CD2WFHIDAVCNFSM6AAAAABZM4RVX2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDONJRGE3TONBXGM . You are receiving this because you were mentioned.Message ID: @.***> [image: hyrious]hyrious left a comment (evanw/esbuild#4118) https://github.com/evanw/esbuild/issues/4118#issuecomment-2751177473

Playground https://esbuild.github.io/try/#YgAwLjI1LjEALS1idW5kbGUgLS1mb3JtYXQ9ZXNtIC0tc3BsaXR0aW5nIC0tb3V0ZGlyPS4AZQBlbnRyeS5qcwBsYXp5KCgpID0+IGltcG9ydCgnLi9saWInKSkAAGxpYi5qcwBleHBvcnRzLl9fZXNNb2R1bGUgPSB0cnVlCmV4cG9ydHMuZGVmYXVsdCA9IDQy

Although some of @paztis https://github.com/paztis's word wasn't correct, the main issue regards to using splitting on a commonjs entry point (which was created by a dynamic import).

esbuild only handles code splitting on ESM entries. So if one entry is in commonjs, it is converted to ESM first. How does it convert? esbuild simply wraps the cjs code in a callback and assigns the whole module.exports to the ESM's default export. That is to say, a commonjs entry could only have only one export in ESM context (which is known as the Node.js behavior https://esbuild.github.io/content-types/#default-interop).

_interopRequireDefault is injecting the __esModule: true to explain it is an ESM export

The __esModule annotation means that if possible, the bundler should treat module.exports as the same meaning as an es-module's namespace object. This is known as the babel behavior.

But it isn't work in this case because the whole commonjs module is the entrypoint instead of being bundled in some chunk. You can think of re-exporting every named exports from a random commonjs code, which requires a static analyze on the code. 2.

_interopRequireDefault is simply a helper function to extract the default export from either a normal commonjs module or a module that adopts the babel style exports. This is the trick to simulate an es-module when there's no native ESM support.


There're some further steps could take:

For esbuild, since the code splitting function is still experimental, it is fine to live with such small issues. But if any improvement could make, it would be:

  If a commonjs module is the only one default export in an ESM chunk:

  export default require_lib()

  ...then extract the default export like babel:

  export default __toESM(require_lib()).default

  -

For users, you can either turn off the code splitting, or manually extract the exports by using a wrapper when you already know the module is in commonjs and you only want some names from it, example https://esbuild.github.io/try/#YgAwLjI1LjEALS1idW5kbGUgLS1mb3JtYXQ9ZXNtIC0tc3BsaXR0aW5nIC0tb3V0ZGlyPS4AZQBlbnRyeS5qcwBsYXp5KCgpID0+IGltcG9ydCgnLi9saWItd3JhcHBlcicpKQAAbGliLmpzAGV4cG9ydHMuX19lc01vZHVsZSA9IHRydWUKZXhwb3J0cy5kZWZhdWx0ID0gNDIAAGxpYi13cmFwcGVyLmpzAGltcG9ydCBsaWIgZnJvbSAnLi9saWInCmV4cG9ydCBkZWZhdWx0IGxpYg .

For library authors, just move to ESM!

— Reply to this email directly, view it on GitHub https://github.com/evanw/esbuild/issues/4118#issuecomment-2751177473, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABIKMMIBRA7L6TFREQVM5CD2WFHIDAVCNFSM6AAAAABZM4RVX2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDONJRGE3TONBXGM . You are receiving this because you were mentioned.Message ID: @.***>

paztis avatar Mar 25 '25 23:03 paztis

I have same error use @langchain/community/document_loaders/fs/docx code=>langchain=>import('mammoth')(dynamic import commonjs package)

mammoth export with {default:xxx} but import without default

In fact, there are currently many packages that mix esm/cjs imports, and I think a solution should be developed

wszgrcy avatar Apr 05 '25 02:04 wszgrcy