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

Converting ESM to CJS: module wrapped twice #2023

Closed
valeriangalliat opened this issue Feb 17, 2022 · 5 comments
Closed

Converting ESM to CJS: module wrapped twice #2023

valeriangalliat opened this issue Feb 17, 2022 · 5 comments

Comments

@valeriangalliat
Copy link

Hey! I'm trying to convert ESM files to CJS using esbuild (0.14.22) and I'm running into an issue when they reference each other:

// src/foo.js
export default 'foo'

// src/index.js
import foo from './foo.js'
console.log(foo)
node_modules/.bin/esbuild src/*.js --outdir=dist --format=cjs

When running node dist/index.js this logs { default: [Getter] } where I would expect it to be foo (so it is consistent to the output of running src/index.js).

FWIW the built dist/index.js is:

var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __markAsModule = (target) => __defProp(target, "__esModule", { value: true });
var __reExport = (target, module2, copyDefault, desc) => {
  if (module2 && typeof module2 === "object" || typeof module2 === "function") {
    for (let key of __getOwnPropNames(module2))
      if (!__hasOwnProp.call(target, key) && (copyDefault || key !== "default"))
        __defProp(target, key, { get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable });
  }
  return target;
};
var __toESM = (module2, isNodeMode) => {
  return __reExport(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", !isNodeMode && module2 && module2.__esModule ? { get: () => module2.default, enumerable: true } : { value: module2, enumerable: true })), module2);
};
var import_foo = __toESM(require("./foo.js"), 1);
console.log(import_foo.default);

If I set isNodeMode to 0 in the __toESM function call, then the code checks the __esModule that's present in the imported module and gets the default property instead of exposing the whole { default: ... } object, but I couldn't figure how to get esbuild to not set isNodeMode to 1. Is that possible?

Thanks so much for a great tool and for your help! ❤️

@hyrious
Copy link

hyrious commented Feb 17, 2022

To switch to node mode, you have to rename your filename extension to .mjs/.mts/.cts. I cannot reproduce your issue. (see it alive) i.e. I don't see the isNodeMode being set to 1.

Did you name your file to end with .mjs/.mts/.cts? These extensions will actually make that param being set to 1.

@valeriangalliat
Copy link
Author

Oh that's a good catch, I realize isNodeMode was being set to 1 because I have {"type": "module"} in my package.json. If I remove it I get the same output as in your REPL link!

I did that in the first place so that I can export both for ESM and CommonJS:

{
  "exports": {
    ".": {
      "import": "./src/index.js",
      "require": "./dist/index.js"
    },
    "./foo": {
      "import": "./src/foo.js",
      "require": "./dist/foo.js"
    }
  }
}

I can fix the issue by duplicating src/ to a esm/ dir where I have {"type":"module"} only in esm/package.json, so that esbuild doesn't set isNodeMode when building from src/... that's a bit redundant but it works.

Do you think there's a better way to achieve that with esbuild?

@hyrious
Copy link

hyrious commented Feb 17, 2022

The simplest way to prevent unexpected default import is -- not using it. In practice we only export names and a default one to simulate the commonjs behavior that includes all names. i.e. Write code like this:

// esm/foo.js
export const foo = 'foo'
export default { foo }   // never use other things as default

Another approach is you can manually write a cjs wrapper to "fix" the exports:

// package.json
"exports": {
  "import": "./esm/foo.js",
  "require": "./shim/foo-wrapper.cjs"
}

// dist/foo.js
(generated by esbuild --format=cjs, like having:)
exports.default = 'foo'

// shim/foo-wrapper.cjs   (written by yourself)
module.exports = require('../dist/foo.js').default

@valeriangalliat
Copy link
Author

That makes a lot of sense, thank you @hyrious! 🙏

@unicornware
Copy link

@valeriangalliat i recently encountered the same issue. if you're still using default exports, try this! it's a cli tool to toggle type in package.json. running the tool pre build led to isNodeMode not being set (and my project working as expected 🤠)

gabro added a commit to buildo/bento-design-system that referenced this issue Jun 14, 2023
Due to how esbuild works, setting type: module will cause default
imports to be wrapped incorrectly.
See evanw/esbuild#2023 for more information.
The easiest fix seems to be remove "type: module" from the package.json,
which makes esbuild behave properly again
gabro added a commit to buildo/bento-design-system that referenced this issue Jun 14, 2023
Due to how esbuild works, setting type: module will cause default
imports to be wrapped incorrectly.
See evanw/esbuild#2023 for more information.
The easiest fix seems to be remove "type: module" from the package.json,
which makes esbuild behave properly again
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