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

fix(commonjs): support CJS modules re-exporting transpiled ESM modules #1165

Merged
merged 34 commits into from Apr 24, 2022

Conversation

fwouts
Copy link
Contributor

@fwouts fwouts commented Apr 16, 2022

Rollup Plugin Name: commonjs

This PR contains:

  • bugfix
  • feature
  • refactor
  • documentation
  • other

Are tests included?

  • yes (bugfixes and features will not be merged without tests)
  • no

Breaking Changes?

  • yes (breaking changes will not be merged unless absolutely necessary)
  • no

List any relevant issue numbers:
#1116
vitejs/vite#2139

Description

Rollup with the CommonJS plugin currently cannot handle the following scenario (real example with next@12.1.4):

Example ESM code:

import Link from 'next/link`  // default import

Definitions in next@12.1.4 NPM package (CJS):

// next/link.js
module.exports = require('./dist/client/link');
// next/dist/client/link.js
Object.defineProperty(exports, "__esModule", {
    value: true
});

const Link = // ...

exports.default = Link;

This happens because the __esModule property is not defined in next/link.js, but inside next/dist/client/link.js instead. Technically, next/link.js does have this property transitively, but that is not apparent with static analysis.

In order for this to work, we need to check the value __esModule at runtime (unless we prefer to analyze files recursively with Rollup, which appears difficult given Rollup's architecture). For that to happen, shouldWrap and detectWrappedDefault must both be true.

This PR implements this additional check. As a side-effect, one existing snapshot for produces optimized code when importing esm with a known default export is arguably not as optimized anymore. I'm not sure it's possible to fix this bug while keeping that particular example as simple, which means some Rollup builds may end up slightly larger. I'd argue this is better than Rollup incorrectly wrapping default exports!

Note 1: This PR should merge relatively cleanly with #1038. I'm happy to submit a patch for that branch as well.

Note 2: I am new to this codebase, and I could obviously have missed an important use case. I'd love a bit of guidance to make sure it's top-notch before it gets merged.

@lukastaegert FYI

lukastaegert and others added 25 commits April 3, 2022 07:27
BREAKING CHANGES: requires Node 12
No longer supports require.cache
BREAKING CHANGES: Requires at least rollup@2.68.0
Copy link
Member

@lukastaegert lukastaegert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it looks good and is rather minimal.

Two things:

  • Could you convert the tests as I outlined?
  • Could you rebase to and change your PR target directly to the commonjs/strict-require-order branch? Then I will make sure they are released together and I do not need to resolve the merge conflicts :) I really want to release that thing now.

@@ -258,6 +259,78 @@ test('import CommonJS module with esm property should get default export ', asyn
t.is(result2.error.message, 'lib is not a function');
});

test('import CommonJS module with re-exported esm default should get default export', async (t) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you do me a favour and instead of adding to this huge test file (which I would really hope to get rid of eventually, but we need it for some tricky caching things) and instead convert them to "function" tests? The reason is that for those tests, everything is neatly together in a separate folder and you do not have to scroll and search around for your fixtures.
Basically for each test, you add a folder to test/fixtures/function with your test fixtures (entry should always be main.js) and a _config.js file as well with something like

module.exports: {
  description: '...some text to describe the test case',
  // options for the commonjs plugin
  pluginOptions: {
    defaultIsModuleExports: 'auto'
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, will do in the next day or two! Thanks for the speedy review :)

@fwouts fwouts changed the base branch from master to commonjs/strict-require-order April 19, 2022 01:03
@@ -0,0 +1,11 @@
import * as entry from './proxy';

// Note: There ideally shouldn't be a default generated property.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note this particular bug that the change introduces. I'm not sure it's possible to avoid it, but would love suggestions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, will need to thing about this. In any case your observation

In order for this to work, we need to check the value __esModule at runtime (unless we prefer to analyze files recursively with Rollup, which appears difficult given Rollup's architecture).

is not correct. Rollup can "look into a module before including it" vie this.load, and as a matter of fact, it already does extensively in resolve-require-sources. So it should actually be possible to extend this logic to figure out e.g. if we want to create a default export I guess (currently we only know if we are requiring CJS or ES), but this will make things MUCH more complicated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! That's useful info. I'd be willing to explore that if you prefer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, have a look, but the logic is slightly involved, feel free to ask questions. I guess the current logic will be good enough as well, but I will need to think a little more about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, still exploring and not quite understanding everything yet but I do have a question!

Is the definition of this test intentionally replacing the module.exports object—which has its __esModule property set to true—with a string? Is the idea to have the static analysis kick in but have __esModule missing at runtime?

Copy link
Contributor Author

@fwouts fwouts Apr 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After exploring a bit more, I found out that this spurious default export is already expected in other tests such as this one, so it's not actually new behaviour, and not something we need to worry about in the context of this PR.

I think there is a more general refactor required, which like you suggested would look at the meta data collected in required modules and only generate a default export if we expect there to be one. This could even allow us to remove the getDefaultExportFromCjs helper entirely (it would always be export default x["default"]).

I'm now of the opinion that this general refactor would be better done in a separate PR, to be implemented after #1038. It may also be alright to leave it unchanged :)

I propose to merge this PR as-is.

@fwouts
Copy link
Contributor Author

fwouts commented Apr 19, 2022

Ready for another round of review!

@fwouts fwouts changed the title fix(commonjs): support CJS modules re-exporting ESM modules fix(commonjs): support CJS modules re-exporting transpiled ESM modules Apr 20, 2022
Copy link
Member

@lukastaegert lukastaegert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! Will look into getting this one released.

@lukastaegert lukastaegert merged commit 1e66132 into rollup:commonjs/strict-require-order Apr 24, 2022
lukastaegert added a commit that referenced this pull request Apr 24, 2022
#1165)

* fix(commonjs): attach correct plugin meta-data to commonjs modules

* feat(commonjs): reimplement dynamic import handling

BREAKING CHANGES: requires Node 12
No longer supports require.cache

* feat(commonjs): add strictRequires option to wrap modules

* feat(commonjs): automatically wrap cyclic modules

* feat(commonjs): Infer type for unidentified modules

* feat(commonjs): make namespace callable when requiring ESM with function default

* fix(commonjs): handle relative external imports

* feat(commonjs): limit ignoreTryCatch to external requires

* feat(commonjs): auto-detect conditional requires

* feat(commonjs): add dynamicRequireRoot option

* feat(commonjs): throw for dynamic requires from outside the configured root

* refactor(commonjs): deconflict helpers only once globals are known

* feat(commonjs): expose plugin version

* fix(commonjs): do not transform "typeof exports" for mixed modules

resolves #1014

* fix(commonjs): inject module name into dynamic require function

* fix(commonjs): validate node-resolve peer version

* fix(commonjs): use correct version and add package exports

* fix(commonjs): proxy all entries to not break legacy polyfill plugins

* fix(commonjs): add heuristic to deoptimize requires after calling imported function

* fix(commonjs): make sure type changes of esm imports are tracked

BREAKING CHANGES: Requires at least rollup@2.68.0

* fix(commonjs): handle external dependencies when using the cache

* fix(commonjs): Do not change semantics when removing requires in if statements

* fix(commonjs): Allow to dynamically require an empty file

* Add a test to illustrate problematic re-exported module

This is exhibited for example in Next.js, see https://github.com/vercel/next.js/blob/5feb400aff8e7b8968174b4e339b98ce48412180/packages/next/link.js#L1 which doesn't specify __esModule but re-exports a module that does.

See https://www.runpkg.com/?next@12.1.4/link.js and https://www.runpkg.com/?next@12.1.4/dist/client/link.js for the compiled example.

* fix(commonjs): support CJS modules re-exporting ESM modules

* fix(commonjs): Warn when plugins do not pass options to resolveId

* Update snapshots post-merge

* Update tests

* Update snapshot

* Remove unnecessary fixtures

* Remove comment given other tests do exactly the same

See:
- https://github.com/rollup/plugins/blob/d637611a79a2701c359f5f2f6ffb49978070da38/packages/commonjs/test/fixtures/function/transpiled-esm-entry-named/main.js#L3
- https://github.com/rollup/plugins/blob/d637611a79a2701c359f5f2f6ffb49978070da38/packages/commonjs/test/fixtures/function/transpiled-esm-namespace-named/main.js#L5

* Update snapshots

Co-authored-by: Lukas Taegert-Atkinson <lukas.taegert-atkinson@tngtech.com>
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

Successfully merging this pull request may close these issues.

None yet

2 participants