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

Isomorphic code is no longer isomorphic #348

Closed
SupernaviX opened this issue Apr 29, 2020 · 17 comments
Closed

Isomorphic code is no longer isomorphic #348

SupernaviX opened this issue Apr 29, 2020 · 17 comments

Comments

@SupernaviX
Copy link

  • Rollup Plugin Name: commonjs
  • Rollup Plugin Version: 11.1.0
  • Rollup Version: 2.7.3
  • Operating System (or Browser): Windows 10
  • Node Version: 13.9.0

How Do We Reproduce?

Repro repo at https://github.com/SupernaviX/rollup-plugin-commonjs-broken-isomorphic-require

Bundle a module which checks if require is defined, and requires a module if it is.

Expected Behavior

Since the require statement will never be called in the browser, it should ideally be transpiled out. Since the original code worked in the browser, the new code should work too.

Actual Behavior

The require statement is converted into a static import. Even though the module is never actually used, it becomes a hard dependency and the file no longer works in the browser.

Relevant snippet of code:

// Add isomorphic support for TextDecoder
let TextDecoder = undefined;
if (typeof window === "object") {
  TextDecoder = window.TextDecoder;
} else if (typeof self === "object") {
  TextDecoder = self.TextDecoder;
} else if (typeof require === "function") {
  TextDecoder = require("util").TextDecoder;
}

This might be a duplicate of #342, I'm not sure.

@lukastaegert
Copy link
Member

@rollup/plugin-commonjs does not support mixing the ES module format (which uses export) with the CommonJS module format (which uses require). As soon as an import or export statement is used, this plugin assumes this is an ES module and it will not be transpiled.

@SupernaviX
Copy link
Author

If this file wasn't getting transpiled, it would work; i'm pretty sure the source code is a valid es module even though it "uses" require. Could there be a bug in the logic to skip es modules?

@lukastaegert
Copy link
Member

It is a valid ES module, but in a valid ES module require is just a function without any meaning, and this is what happens. There is no specification of a module format that supports mixing those styles even though Webpack makes people believe there is. So in a sense you could call this a "Webpack style module". What keeps you from replacing your export statements with assignments to exports?

Also, how is this isomorphic if you expect export statements to work in your target environment? In browsers, you can only place this in a script tag if the type is module.

@lukastaegert
Copy link
Member

Ah sorry, reading your issue again, maybe I slightly misunderstood the problem. The thing is, require is an unknown global function that might have side-effects. And even if it were not, there might be a side-effect when accessing a property on its return value. And even if this was not the case, you are assigning the return value to a variable you are using. This is actually not related to the plugin but how Rollup sees it. Not sure if there is a way to achieve what you want as you would need to tell Rollup in a way that you will never reach the last if case

@TrySound
Copy link
Member

@lukastaegert Looks like the behaviour of detection was changed. Now this issue appears here and there because of it.
#304

@manucorporat
Copy link

I can confirm we can't longer compile stencil with rollup because the commonjs plugin is trying to wrap ESmodule code around the commonjs wrapper, leading to Error: 'import' and 'export' may only appear at the top level

@SupernaviX
Copy link
Author

SupernaviX commented Apr 29, 2020

Ah sorry, reading your issue again, maybe I slightly misunderstood the problem. The thing is, require is an unknown global function that might have side-effects. And even if it were not, there might be a side-effect when accessing a property on its return value. And even if this was not the case, you are assigning the return value to a variable you are using. This is actually not related to the plugin but how Rollup sees it. Not sure if there is a way to achieve what you want as you would need to tell Rollup in a way that you will never reach the last if case

@lukastaegert I think this is achievable; I can see that the plugin is already replacing references to typeof module and such with "object". We tried using the replace plugin to change typeof require to "undefined" in FredKSchott/snowpack#286, and it convinced rollup to remove the unused import. Doing it from the commonjs plugin would be preferable, because that has access to the AST and can do it more robustly (and because this case would work out of the box). Would you accept a PR for that?

@lukastaegert
Copy link
Member

A PR would surely be welcome but the question is what it should actually do. So if by default, typeof require is changed to "undefined", it would actually break UMD modules. What I am saying is, usually changing it to "function" is correct, which is then turned into a static import but removes the unneeded alternatives. What is the correct thing to happen here? Take TextDecoder from the global object? And why not the import?

@arlac77
Copy link

arlac77 commented Apr 30, 2020

I can confirm we can't longer compile stencil with rollup because the commonjs plugin is trying to wrap ESmodule code around the commonjs wrapper, leading to Error: 'import' and 'export' may only appear at the top level

Same for me when using 'symbol-observable'

[!] Error: 'import' and 'export' may only appear at the top level
node_modules/symbol-observable/es/index.js (2:0)
1: /* global window */
2: import ponyfill from './ponyfill.js';

@shellscape shellscape changed the title [commonjs] - Isomorphic code is no longer isomorphic Isomorphic code is no longer isomorphic Apr 30, 2020
@SupernaviX
Copy link
Author

Hmm, looking at the problem again I see your point, turning typeof require into "undefined" would fix my specific problem but cause weird issues in other places. I also realize I explained my issue poorly.

The file that's causing me trouble is a valid ES module, which works (without transpilation) in both the browser and Node 8+. It's trying to use a TextDecoder class, which is available as window.TextDecoder in the browser and require('util').TextDecoder in Node. If window.TextDecoder exists (i.e. if we're in the browser), it doesn't even try following the node codepath and require never gets called (or even accessed). In other words, this file only has a dependency on util when run in node.

My problem is that the commonjs plugin is transpiling that conditional require('util') into a static import util from 'util'. Even though the file will use window.TextDecoder in the browser, now it will try importing that node builtin no matter where it runs.

Tricking the commonjs transform tool into thinking that require was undefined solved the issue for me, because the block that actually called require got optimized out instead of turned into a static import. Thinking about it, a better solution would be if the system worked as you described earlier, where files with import or export statements weren't affected by the plugin at all; after all, the whole point of the code snippet I linked is that it should work in the browser without any extra modifications.

@lukastaegert
Copy link
Member

The thing I overlooked is that my comment #348 (comment) is actually no longer valid because #206 changed semantics to always process modules if they contain global require calls.

So what we would need here would be a toggle to switch back to the old behaviour which was to not process any file that contains mixed ESM and CJS syntax. Would this work for you?@danielgindi as you are more intimate with that code, what do you think? Or maybe you have another idea?

@lukastaegert
Copy link
Member

As such, it would actually be a duplicate of #342

@danielgindi
Copy link
Contributor

The thing I overlooked is that my comment #348 (comment) is actually no longer valid because #206 changed semantics to always process modules if they contain global require calls.

So what we would need here would be a toggle to switch back to the old behaviour which was to not process any file that contains mixed ESM and CJS syntax. Would this work for you?@danielgindi as you are more intimate with that code, what do you think? Or maybe you have another idea?

I think that the latest PR should have fixed it. Should try and see. If all hell breaks loose sure we can revert to the old behavior where we don’t even look in ES module files- but that will break behavior where an ES module actually uses require.

We could also easily add a check for “typeof require”- which will make it much smarter, and will be very easy to implement.

@lukastaegert
Copy link
Member

My understanding is that the latest PR just avoided adding a wrapper but would still bundle the file in question. If require is just ignored, it would work in the browser because even though the call is not removed, typeof require will avoid this case. Setting typeof require would of course be nicer but as I said, it could have the opposite effect if bundling the optional import is actually intended.

@danielgindi
Copy link
Contributor

@lukastaegert Yes you raise a good point. We probably have no good way of knowing whether the user intend for the module to be transpiled for the browser (hence typeof require should resolve to undefined) or for a node env, or whether they actually want to keep the typeof require to keep the code isomorphic to some extend. So manual control over this can't be avoided, until everyone stops using these hacks of typeof require.

@danielgindi
Copy link
Contributor

Well for this specific case the right path would be to use include/exclude to mark code that should not be transformed and code that should.
The fact that it worked previously when no require calls were transformed in ES modules - is just a lucky (unlucky?) accident. A combination of modules that were using UMD in just the right way to play nice with the specific situation.

We will have a kill switch for this soon anyway, so any breakage will be fixed. But people should really start sorting out their mess.

danielgindi added a commit to danielgindi/rollup-official-plugins that referenced this issue May 1, 2020
@SupernaviX
Copy link
Author

If this code is inside of a dependency which is explicitly an ES module (i.e. package.json has a "module" field), does it make sense to automatically exclude it? Or would that mess with its (potentially non-ES-module) dependencies?

LarsDenBakker pushed a commit to LarsDenBakker/plugins that referenced this issue Sep 12, 2020
BREAKING CHANGE:

Reverts default behavior of mixed es and cjs modules.
Marked as a "breaking change", but the PR that raised this need was actually a breaking change for people who did not utilize include/exclude correctly and have variety of dependencies with mixed UMD mechanisms.

* Implemented kill-switch for mixed commonjs in mixed es-cjs (Closes rollup#348, rollup#342)

`transformMixedEsModules = false`

* Update packages/commonjs/README.md

Co-authored-by: Andrew Powell <shellscape@users.noreply.github.com>

* Update index.d.ts

Co-authored-by: Andrew Powell <shellscape@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants