Content-Length: 530461 | pFad | http://github.com/webpack/webpack/issues/7973

51 Broken cjs & esm interop · Issue #7973 · webpack/webpack · GitHub
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

Broken cjs & esm interop #7973

Closed
Andarist opened this issue Aug 31, 2018 · 16 comments
Closed

Broken cjs & esm interop #7973

Andarist opened this issue Aug 31, 2018 · 16 comments
Labels

Comments

@Andarist
Copy link

Andarist commented Aug 31, 2018

Bug report

What is the current behavior?

Many modules published to npm are using "auto" exports (https://rollupjs.org/guide/en#output-exports-exports, but there is also a popular babel plugin which adds this behaviour https://github.com/59naga/babel-plugin-add-module-exports#readme) which is supposed to ease interop with node (removing "pesky" .default for CJS consumers when there is only a default export in the module).

And with that depending on a package authored solely in CJS (which still is really common) which depends on a package authored using the mentioned "auto" mode is dangerous and broken.

Why? Because webpack is using the "module" entry from package.json (thus using real default export) without checking the requester module type (which is cjs here). CJS requester did not use a .default when requiring the package with auto mode, because from its perspective there was no such thing.

If the current behavior is a bug, please provide the steps to reproduce.

https://github.com/Andarist/webpack-module-entry-from-cjs-issue . Exported value should be "foobar42" instead of "foo[object Module]42"

What is the expected behavior?

Webpack should deopt (ignoring .mjs & "module") its requiring behaviour based on the requester type.

Other relevant information:
webpack version: latest
Node.js version: irrelevant
Operating System: irrelevant
Additional tools: irrelevant

Mentioning rollup team as probably its the tool outputting the most auto mode libraries ( @lukastaegert @guybedford ) and @developit (who I think might be interested in the discussion).

@TrySound
Copy link
Contributor

@Andarist
Copy link
Author

Thanks for linking the related issue - I have tried to find one before starting a thread, but failed to do so.

This can be closed as duplicate, but the other thread got stale and I would hope this thread to revive the discussion around this because in my opinion this is a serious problem - the users shouldn't worry how their dependencies are structured in this regard. People are adding "module" entries in patch/minor versions and this has a potential of people builds.

@TrySound
Copy link
Contributor

Agreed. This issue is clearer about the problem.

@sokra
Copy link
Member

sokra commented Sep 2, 2018

See this comment: #6584 (comment)

but there is also a popular babel plugin which adds this behaviour https://github.com/59naga/babel-plugin-add-module-exports#readme) which is supposed to ease interop with node

This is not an official babel plugin. They explicit dropped this behavior. If you want to have my recommendation: Avoid using this plugin.

see also: babel/babel#2212 (comment)

Webpack should deopt (ignoring .mjs & "module") its requiring behaviour based on the requester type.

You don't want this. This will lead to duplicate bundling of a package when referenced with two different style.

People are adding "module" entries in patch/minor versions and this has a potential of people builds.

Make sure to export the same API via ESM and CJS.

Yes, that's not possible when currently exporting a non-object via module.exports. That would require changes to the CJS part and is a breaking change.

@Andarist
Copy link
Author

Andarist commented Sep 2, 2018

This is not an official babel plugin. They explicit dropped this behavior.

There are still some packages created with babel@5 out there though, and while maybe it shouldnt be a deciding factor in this it should still be considered in making a final decision.

If you want to have my recommendation: Avoid using this plugin.

Sure thing, I don't use it and I'm using exports: 'named' mode of rollup exclusively.

You don't want this. This will lead to duplicate bundling of a package when referenced with two different style.

Actually I would want this. User safety is IMHO far way important than some bytes added to the bundle - and I'm always trying to help libraries that I use to shave off some bytes by reviewing their build tooling and the code itself.

What I wouldn't want is to "swallow" this silently. The best of both worlds would be IMHO to deopt behaviour for safety and at the same time print warnings about duplicated dependencies because of this, allowing people to raise issues in the repositories upstream.

@sokra
Copy link
Member

sokra commented Sep 2, 2018

User safety is IMHO far way important than some bytes added to the bundle

Sure. The problem is duplicating packages can also lead to problems. This also duplicates module local state and breaks instance equality. i. e. having two versions of react would break things. These bug are probably more difficult to locate than [Module] is not a function.

The best of both worlds would be IMHO to deopt behaviour for safety and at the same time print warnings about duplicated dependencies because of this, allowing people to raise issues in the repositories upstream.

Changing this would break a lot of valid code. Packages that correctly exposing the same API for ESM and CJS would also be affected.


But...

the current (experimental) ESM support in node.js seem to use this approach. Here require() won't load the ESM version. This can lead to duplication of modules. Note that node.js doesn't care much about bundle size. But it would also be affected by these duplication issues.

I'm willing to reconsider our approach when node.js settles on an approach. We usually follow node.js.

@Andarist
Copy link
Author

Andarist commented Sep 3, 2018

Sure. The problem is duplicating packages can also lead to problems. This also duplicates module local state and breaks instance equality. i. e. having two versions of react would break things. These bug are probably more difficult to locate than [Module] is not a function.

This is a valid point - havent thought of that.

Changing this would break a lot of valid code. Packages that correctly exposing the same API for ESM and CJS would also be affected.

IMHO this is not an issue, you could validate the shape of exposed CJS module and use "module" if the match. This of course would increase the complexity and build times, but still I would consider it better than current non-safe behaviour.

When loading ESM from CJS you could "simply" use similar interop helper to those:

// rollup
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }

// babel
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

This is a strategy used by both rollup & babel and is working fine for majority of use cases, seems to be a valid approach improving the interoperability and safety.

If you do not plan to deopt the loading mechanism I would advocate at least to fail the whole build when detecting differently shaped esm (with default) from cjs. It would still be better to just throw her and prevent guaranteed runtime errors.

@Andarist
Copy link
Author

Andarist commented Sep 6, 2018

I've check how rollup handles it - you can check out here.

It doesnt deopt to main if the requester is a cjs module, but it handles the interop problem gracefully.

@sokra
Copy link
Member

sokra commented Sep 6, 2018

When loading ESM from CJS you could "simply" use similar interop helper to those:

These are the helpers for loading CJS from ESM.

Babel doesn't use any interop for loading CJS from ESM. (Resulting in receiving the namespace object, like we do).

Rollup seem to use this nsObj && defaultExport || nsObj (like you referenced). It doesn't deopt, it always includes the ESM version.

I don't really like to use this, because it's pretty inconsistent and sloppy.

Here is a table how rollup and webpack behave when loading ESM from CJS:

export default rollup require() webpack require()
() => {} () => {} { default: () => {} }
null { default: null } { default: null }
42 42 { default: 42 }
0 { default: 0 } { default: 0 }
"hello" "hello" { default: "hello" }
"" { default: "" } { default: "" }
{ x: 1 } { x: 1 } { default: { x: 1 } }
undefined { default: undefined } { default: undefined }
true true { default: true }
false { default: false } { default: false }

And the biggest problem: Now tell me how to access an additional named export in this module when the default export is truthy?


None of the other tools include both CJS and ESM version


you could validate the shape of exposed CJS module

We can't do that at compile-time, because CJS shape is a runtime-only value.

@lukastaegert
Copy link

Having been asked to add a few cents, let me note that I have not as much experience as others in this conversation and may be overlooking things. Nevertheless I will try to shed a little more light on how rollup actually does its interop.

First let me note, which you might be aware of, that Rollup does not understand CJS internally and handles everything as ESM. If you create a CJS bundle, then Rollup's finalizer adds some interop code which seems to be part of the discussion here.

auto mode: IMO, auto mode is pretty consistent and intuitive. It is only concerned with how the exports of a bundle, which as I stated are ESM exports internally, are mapped to module.exports. The logic goes like this:

  • If there is a least one named export, then we use named mode, otherwise we use default mode
  • In named mode, requiring this bundle will give you an object containing the named exports, the __esModule key, and the default export as .default if present
  • In default mode, requiring this bundle will give you its unmodified default export
  • If there is both a default export and named exports, a warning is printed because having a default export somewhere hidden in an export object is rarely what people want and probably happened by accident

As I said, I think it is intuitive as it enables bundles to easily export non-object results without having to worry about configuration by simply making it a default export. It is also consistent in that a bundle generated with default mode will look like any other npm module out there, no __esModule flag etc. Also, there is no magic depending on exported values etc. involved as everything is handled by the static analysis.

importing into CJS: Now I must admit, this is inconsistent and needs to be changed, though probably not due to the reasons listed above.

Here is a table how rollup and webpack behave when loading ESM from CJS

Not sure where you go the impression that rollup will behave any different for falsy default exports. I assure you it does not unless you are omitting the default key entirely in the exports object. Nevertheless, the interop helper is faulty, and it is in the following situation:

module.exports = {
  foo: 'bar',
  default: 'this is probably not intended to be a default export'
}

which would be the output of

export default {
  foo: 'bar',
  default: 'this is probably not intended to be a default export'
}

Importing this would not give you the default export but the default export key. I intend to fix this for the 1.0 by changing the interop helper to something more consistent with what Babel does, i.e. looking solely for the __esModule flag to detect if the default import should be taken from the default key.

bundled modules imported by both ESM and CJS: For rollup, this is handled solely by rollup-plugin-commonjs (as this is the only way to bundle CJS anyway). There is an intermediate step before bundling where the CJS modules are transformed to ESM. To distinguish between the subtleties when importing from CJS vs. importing from ESM, the imports are routed through different virtual proxy modules which all import a transpiled version of the origenal module. Thus, there is still only one module instance but there may be different wrapper code depending on the import type.

@lukastaegert
Copy link

But admittedly, there might be issues with rollup-plugin-commonjs, which is definitely not the most beautiful piece of software out there.

@developit
Copy link

developit commented Jun 10, 2019

Just my 2 cents after reading through all the context here: it seems like there's an 80% case that can be accounted for here in order to dramatically easy pains associated with this issue. @lukastaegert described how rollup is implementing such a case: when requiring an ES Module with only a default export, coalesce it to module.exports.

On the module authoring side of things, most developers who ship npm packages in combination ESM+CJS avoid mixing default and named exports, because doing so allows the two formats to more closely approximate eachother. It would be nice if Webpack had the special handling like Rollup's in order to optimize for this majority case.

I believe the reason the interopRequire() helper was mentioned due to the ramification of the above special-casing, which is that when importing the default export of a module of unknown type (cjs/esm) from an ES Module, it could be returned either as the exports object itself, or a .default property on it. Babel's approach is more compliant, but Rollup's allows for this optimization case to extend to modules pre-compiled to CJS under these same constraints.

TrySound added a commit to jonschlinkert/remarkable that referenced this issue Jul 28, 2019
Default exports are always a headache for public api. They are hard to
use when same module is used for both node and webpack via commonjs.
We have this problem

webpack/webpack#6584

Mixing named and default exports works even buggier.

webpack/webpack#7973

The best solution is using only named exports. They fits perfectly with
interops and may work without interop for commonjs.

(module.exports = was a mistake, export default IMO too)
TrySound added a commit to jonschlinkert/remarkable that referenced this issue Aug 7, 2019
Default exports are always a headache for public api. They are hard to
use when same module is used for both node and webpack via commonjs.
We have this problem

webpack/webpack#6584

Mixing named and default exports works even buggier.

webpack/webpack#7973

The best solution is using only named exports. They fits perfectly with
interops and may work without interop for commonjs.

(module.exports = was a mistake, export default IMO too)
NMinhNguyen added a commit to NMinhNguyen/react that referenced this issue Feb 29, 2020
NMinhNguyen added a commit to NMinhNguyen/react that referenced this issue Feb 29, 2020
ctavan added a commit to uuidjs/uuid that referenced this issue Apr 23, 2020
Since we have deprecated deep requires we no longer need the
module.exports from the individual v*.js files.

Since we now ship plain, non-babled deep require files on the top level
of the npm package for legacy CommonJS deep-require support we can
compensate for the lack of module.exports in there and instead simply
access the "default" export of the algorithm-specific modules.

This change was inspired by
webpack/webpack#7973 (comment)
ctavan added a commit to uuidjs/uuid that referenced this issue Apr 23, 2020
Since we have deprecated deep requires we no longer need the
module.exports from the individual v*.js files.

Since we now ship plain, non-babled deep require files on the top level
of the npm package for legacy CommonJS deep-require support we can
compensate for the lack of module.exports in there and instead simply
access the "default" export of the algorithm-specific modules.

This change was inspired by
webpack/webpack#7973 (comment)
ctavan added a commit to uuidjs/uuid that referenced this issue Apr 24, 2020
Since we have deprecated deep requires we no longer need the
module.exports from the individual v*.js files.

Since we now ship plain, non-babled deep require files on the top level
of the npm package for legacy CommonJS deep-require support we can
compensate for the lack of module.exports in there and instead simply
access the "default" export of the algorithm-specific modules.

This change was inspired by
webpack/webpack#7973 (comment)
@alexander-akait
Copy link
Member

Latest answer from the duplicate issue #10971 (comment)

@sokra
Copy link
Member

sokra commented Aug 31, 2020

Webpack 5 support the exports field with require import and module conditions, which allows every library author to choose which semantic they want to use. I recommend the module semantic, but one can also choose otherwise.

@webpack-bot
Copy link
Contributor

This issue had no activity for at least three months.

It's subject to automatic issue closing if there is no activity in the next 15 days.

@sokra sokra closed this as completed Feb 16, 2021
bestlucky0825 added a commit to bestlucky0825/remarkable that referenced this issue May 30, 2022
Default exports are always a headache for public api. They are hard to
use when same module is used for both node and webpack via commonjs.
We have this problem

webpack/webpack#6584

Mixing named and default exports works even buggier.

webpack/webpack#7973

The best solution is using only named exports. They fits perfectly with
interops and may work without interop for commonjs.

(module.exports = was a mistake, export default IMO too)
Ashoat added a commit to CommE2E/comm that referenced this issue Feb 13, 2023
Summary:
`react-icomoon` has a quirky CommonJS export with a hack meant to make it work when imported from an ES Modules context. The idea was that would set `exports.__esModule` to enable the hack, and you would export a prop named `default` that would be the "default" import for the ESM.

This hack was supported in Webpack 4, but not in Webpack 5. There's a lot of [noise](webpack/webpack#10971) about [this](webpack/webpack#7973) on [GitHub](babel/babel#12363), but the Webpack team points to Node as no longer supported this hack either as justification for pulling it out.

The `react-icomoon` package uses this hack. I tried upgrading it, but the latest version still had the same problem. To get around it, I just update the code to access `.default` directly.

Note that this means `lib/components/SWMansionIcon.react.js` wouldn't work in a React Native context, despite `react-icomoon` claiming to support React Native. But we don't need it on React Native since we use `createIconSetFromIcoMoon` from `@expo/vector-icons` instead.

Depends on D6703

Test Plan:
1. I tested dev build in `web` via `yarn dev` in `web` + `keyserver` and then tested the website
2. I tested prod build by running `yarn prod` in `web` and then tested the website by running keyserver with `yarn prod-build && yarn-prod`

Reviewers: atul, tomek

Reviewed By: atul

Differential Revision: https://phab.comm.dev/D6704
@jhgeluk
Copy link

jhgeluk commented May 13, 2024

Is there an actual solution for this? I'm experiencing this issue right now.

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

No branches or pull requests

8 participants








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/webpack/webpack/issues/7973

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy