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

Export commonjs named exports in ESM #442

Open
remorses opened this issue Oct 8, 2020 · 11 comments
Open

Export commonjs named exports in ESM #442

remorses opened this issue Oct 8, 2020 · 11 comments

Comments

@remorses
Copy link
Contributor

remorses commented Oct 8, 2020

Currently a module that uses commonjs features like exports and require will translate to a default export exporting the module.exports object.

But some packages like react rely on bundlers like webpack to allow you to use named imports like import { useEffect } from 'react', event thought react is a commonjs module.

It would be cool to detect the cjs named exports and reexport them in the ESM output.

Code

currently executing esbuild --bundle --outdir=out --format=esm x.json

exports.x = 9

becomes

...
var require_x = __commonJS((exports, module) => {
  module.exports.x = 9;
});
export default require_x();

what i am asking for is to have this output

...
var require_x = __commonJS((exports, module) => {
  module.exports.x = 9;
});
export default require_x();

export {
  x: require_x().x
}

Nodejs is trying to allow the same interopability and they are using a lexer to detect commonjs named exports: https://github.com/guybedford/cjs-module-lexer

That package implements the lexer in C and uses WASM to execute it in js, we could use the same code nodejs uses to detect commonjs named exports and reexport them as ESM

@remorses
Copy link
Contributor Author

remorses commented Oct 8, 2020

I could do this in a js plugin that changes the entrypoint in a fake ESM module that exports from the commonjs module (we are already doing the same thing in snowpack with rollup) but it would be cool to have this feature built into the bundler

@ije
Copy link

ije commented Oct 8, 2020

this is useful for me, i created a cdn using esbuild to bundle npm package to esm version:
https://github.com/postui/esm.sh

to get export names of a commonjs module, i created apeer.js like:

const mod = require('xxx')
fs.writeFileSync('./peer.exports.json', JSON.stringify({exports: Object.keys(mod)}))

then run node peer.js.

@shrinktofit
Copy link

shrinktofit commented Oct 9, 2020

I wonder why my module exports only a default binding even if I passed --format=esm and my module(in esm) exports many named bindings. Does it because I import commonjs module?

@vinsonchuong
Copy link

@remorses, I noticed that you're doing a lot of work in exporting npm packages as ESM. Have you been able to work out a fully working solution?

I've also been looking to accomplish the same. I started with esinstall, but it's way too slow to experiment with. So, I've been trying to do the same with esbuild.

So far, it seems to more or less just work except for named exports from CJS.

I like the workaround from @ije, in just evaluating the module to get its export names. But, that doesn't account for packages that export different code for browsers.

After reading #532, I found guybedford/cjs-module-lexer. Combining this with webpack/enhanced-resolve does the trick for me:

import {promisify} from 'util'
import enhancedResolve from 'enhanced-resolve'
import * as moduleLexer from 'cjs-module-lexer'

const resolve = promisify(
  enhancedResolve.create({
    mainFields: ['browser', 'module', 'main']
  })
)

let lexerInitialized = false
async function getExports(modulePath) {
  if (!lexerInitialized) {
    await moduleLexer.init()
    lexerInitialized = true
  }

  try {
    const exports = []
    const paths = []
    paths.push(await resolve(process.cwd(), modulePath))
    while (paths.length > 0) {
      const currentPath = paths.pop()
      const results = moduleLexer.parse(await fs.readFile(currentPath, 'utf8'))
      exports.push(...results.exports)
      for (const reexport of results.reexports) {
        paths.push(await resolve(path.dirname(currentPath), reexport))
      }
    }
    return `{ ${exports.join(', ')} }`
  } catch (e) {
    return '*'
  }
}

Then, since plugins aren't applied to entry points (#546), I use this to write export statements in temporary files. Passing these temporary files as entry points into esbuild allows me to accomplish what esinstall does.

@ije
Copy link

ije commented Dec 5, 2020

@vinsonchuong it's amazing, i will try your reslution!

@zheeeng
Copy link

zheeeng commented Sep 13, 2021

Any progress on this?

@ije
Copy link

ije commented Oct 17, 2021

since the guybedford/cjs-module-lexer @vinsonchuong suggested above can't handle some edge cases, i created another cjs export parser that uses swc ast walker as a wasm module, it is used by esm.sh CDN, for now it is good: https://www.npmjs.com/package/esm-cjs-lexer

@ruanyl
Copy link

ruanyl commented Nov 14, 2021

@ije Nice work! Could you share your setup of using cjs-esm-exports to compile cjs module to esm module? I checked esm.sh and I believe that's what I'm looking for, but I wanna run it locally.

@paralin
Copy link

paralin commented Aug 23, 2023

@ije @ruanyl What was the last word on this? What's the best way to fix transforming packages like "react" erasing the esm named exports? Thanks!

@ije
Copy link

ije commented Aug 24, 2023

@paralin check https://github.com/esm-dev/esm.sh/blob/main/packages/esm-node-services/cjs-lexer.js

@paralin
Copy link

paralin commented Sep 1, 2023

@ije That works great, thanks, with one issue: esm-dev/esm.sh#713 - exportDefault: true is not set for React for some reason.

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

7 participants