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

[core] incorrect output of exporting of reference #4049

Closed
JounQin opened this issue Apr 20, 2021 · 21 comments · Fixed by #4064
Closed

[core] incorrect output of exporting of reference #4049

JounQin opened this issue Apr 20, 2021 · 21 comments · Fixed by #4064

Comments

@JounQin
Copy link

JounQin commented Apr 20, 2021

  • Rollup Version: 2.45.2
  • Operating System (or Browser): macOS
  • Node Version (if applicable): v14.16.0
  • Link to reproduction (IMPORTANT, read below): REPL

Expected Behavior

import { noop } from 'ts-essentials'

export * from 'ts-essentials';
export const badNoop = noop;

Actual Behavior

export * from 'ts-essentials';
export { badNoop } from 'ts-essentials';
@JounQin
Copy link
Author

JounQin commented Apr 20, 2021

related: angular/angular#40959

@JounQin
Copy link
Author

JounQin commented Apr 29, 2021

@lukastaegert Hi, is there any news, it's a really serious problem...

@lukastaegert
Copy link
Member

I will have a look into this the next days. Sorry there was no news as I was busy on another front.

@lukastaegert
Copy link
Member

Ok, this is not as simple as it looks. The code Rollup produces is not correct, but it is not immediately clear how to really hand this correctly. The core of the problem is that in main.js, you first reexport * from './types' and then * from './utils'. If you switch the order of these statements, everything will be fine.

Why is that?

The core of the problem is that ts-essentials is external. So Rollup does not know which exports it contains, and currently it blindly guesses that it contains all possible exports. Therefore when Rollup tries to determine what an export of badNoop will export, it looks at main.js, sees the first star reexport, follows that, finds the star reexport from ts-essentials and thus assumes the export is satisfied directly via ts-essentials. And this is correct IF ts-essentials actually contains an export of that name. In that scenario, your suggested code change would be wrong, because the named export would falsely have precedence over the star reexport.

Note that the reason that badNoop becomes an explicit export is what Rollup could improve here, because that is useless and confusing.

HOWEVER, if there is no export named badNoop, then the bundled code is wrong. Because in that situation, it surely should favour the export via utils, but it does not.

The problem is, I have no clue how to fix this well except by creating an additional chunk, because as exports in ESM are static, we cannot determine them at runtime. And dynamically creating a chunk here is quite difficult as well.

@JounQin
Copy link
Author

JounQin commented May 1, 2021

It seems it worked previously, at least on ng-packagr@10 which was using rollup@2.8. (I'm not on a computer, so I can't ensure if it works on rollup@2.8)

@JounQin
Copy link
Author

JounQin commented May 1, 2021

ESM is static, so of course rollup should know about whether badNoop is available in ts-essentials, right?

And also, I don't know if it's valid to override export * exports with a same identifier?

export * from 'ts-essentials' // it contains `noop`
export const noop = {
  console.log('my nopp')
}

If it is valid, rollup should always skip assuming * contains all exports, but should keep it as is.

If it is invalid, rollup should always emit an error to report instead.

@JounQin
Copy link
Author

JounQin commented May 1, 2021

According to tc39/proposal-export-default-from#11, rollup should not care whether badNopp or even noop has been exported via export * from 'ts-essentials' at all.

@lukastaegert
Copy link
Member

Yes, the problem is really the competing namespace reexports in main.js. Nevertheless, there is an argument to be made to favour the latter export: In case there is more than one namespace reexport in a file and both reexport the same name, then neither would take precedence (not sure if an error would be thrown, but something in that direction). So in the interest of the user it is always a good guess that there is no conflict, in which case we should favour the definitely existing export from the second namespace reexport over the only potentially existing on of the first. Will see if I can implement something here.

@lukastaegert
Copy link
Member

Fix at #4064

@lukastaegert
Copy link
Member

I think it should prefer the later one, instead of locally

Not sure I fully understand. What would you expect to happen in your example?

@JounQin
Copy link
Author

JounQin commented May 2, 2021

I think it should prefer the later one, instead of locally

Not sure I fully understand. What would you expect to happen in your example?

const noop = () => {
  console.log('my loop');
};

export { noop };

export * from 'ts-essentials'; // it contains `noop`, maybe should override above `noop`, not sure

But it should leave it as-is IMO.

@lukastaegert
Copy link
Member

Here is a small example that you can run in your browser if you put index.js in a script tag with "type"="module":

// index.js
import * as ns from './reexport.js';
console.log(ns.foo, ns.bar, ns.baz);

// reexport.js
export * from './first.js';
export * from './second.js';

// first.js
export const foo = 1;
export const bar = 1;

// second.js
export const foo = 2;
export const baz = 2;

If you run this, the system will log undefined 1 2. That means the conflicting reexport foo is just removed from the namespace, there is no preference in real runtimes. If you replace index.js with

import { foo, bar, baz } from './reexport.js';
console.log(foo, bar, baz);

then the code will immediately throw Uncaught SyntaxError: The requested module './reexport.js' contains conflicting star exports for name 'foo'.

So no, it should not prefer any one of them.

@lukastaegert
Copy link
Member

So the logic that is implemented in the PR is a heuristic that assumes that there will be no conflicts, which means if you explicitly export a name in one of the modules the namespace of which is reexported, then we assume this name does not exist in any of the other namespaces, especially not in external namespaces.

@lukastaegert
Copy link
Member

But it will not replicate real browser errors like the one listed above.

@JounQin
Copy link
Author

JounQin commented May 2, 2021

I mean we should keep the exporting order, so that the JavaScript engine will emit the error.

Or, we should provide an option to:

  1. keep order, then it will emit error at runtime
  2. emit error on building
  3. reorder to prefer local one

@lukastaegert
Copy link
Member

I do not think it is possible to emit any error at runtime when bundling a single file as the error only exists when there are actually several conflicting namespace reexports from different files, so you need at least two local chunks to simulate it. Beyond that, I do not think this error is really useful to many, so I would prefer to only add such an option if there is actual demand.

@lukastaegert
Copy link
Member

Note that explicit named exports or reexports will always override the same names in namespace reexports and will not throw even in case of conflict.

@guybedford
Copy link
Contributor

#4049 (comment)

This actually seems to me to be an implementation bug as ResolveExport per https://tc39.es/ecma262/#sec-resolveexport is still called on namespace exotic object Get access, which should then throw the ambiguous error. So it seems this is a bug in Firefox AND Chrome. If someone can please test Safari as well that would be a great help. Will verify further and post browser bugs as appropriate.

@guybedford
Copy link
Contributor

Posted tc39/ecma262#2399 it turns out this is a spec thing, and will likely be just an editorial change to codify the behaviour at this point as a dynamic error is likely not web compatible.

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

Successfully merging a pull request may close this issue.

3 participants