-
Notifications
You must be signed in to change notification settings - Fork 43
Divergent specifier hazard #371
Comments
I, for one, would really like to just resolve the same specifier to the same module in both resolvers - that would go a long way to making esm feel like a gradually adoptable improvement, rather than a hard break - I really hate the idea of leaving cjs users in the cold with respect to new esm libraries (and no, dynamic import isn't practical in the real world because the promise wrapper forces an unrealistic code structure compared to how code is written and consumed today). It's worth noting that a similar mechanism to what I've looked at to support require of esm could in theory be used to support top level await in cjs without creating excessive event loop turns (just like how the TLA spec specifies esm TLA should behave, with the same caveats about possible deadlocks). I started looking at that, but am suuuuper unfamiliar with how the cjs wrapper is formed and, from what I can tell, there's currently no equivalent of the v8 create function call that supports marking the function as async, which would mean degrading to the older style wrapper (as is done when the wrapper is patched). |
It's worth noting that I also think the same-specifier-resolves-to-the-same-thing-in-both-resolvers also isn't actually predicated on require in esm - it's just a matter of the cjs loader prioritizing esm files in the same way as the esm loader and throwing if an esm file is found first (barring actually being able to import it). In effect, the cjs loader should be a superset of the esm loader in this scenario, except it throws when the module that is resolved to is esm. In the It just comes down to both loaders need to resolve a specifier to the same thing, whatever tweaks that takes. |
I've mentioned this in another issue, but I think it should be mentioned here. If two separate packages require/import a third package, they should not assume they will both get the same singleton. This is a dubious assumption, which can already lead to issues even without getting into ESM vs CJS. With NPM, version conflicts are resolved by giving each package a version it requires, even if that means installing two different versions. With a current version of NPM, a different copy will be given to one of the packages to import, and I believe older versions would simply have installed two copies regardless of version. That means that if
And
Both are going to get a different This isn't a new issue, and I don't think that it's a compelling reason to not support dual packages. If a package wants to be a dual package, it should move to not assuming there will only be one version of a package... actually, all packages should do this anyway. |
@AlexanderOMara the issue is not only across package boundaries but within the same package if you are moving between CJS and ESM (which is likely to happen as teams transition code bases). Packages like react which have global state that is shared will break in very mysterious and hard to debug ways if you end up with a different singleton in ESM + CJS. |
@MylesBorins that's true, but that's already the case with duplicates or different versions. The solution is peer deps for "across package boundaries"; there's no solution node can provide that would be any different (npm or another package manager could perhaps provide one; bower for example's solution was "pick which one you want and that's the only one you'll get", but that was untenable for many use cases). Within the same package, I don't see why that's a hazard we're concerned with; that seems like something that package needs to test for? |
As you said in the other thread - without a package, the issue can be fixed with peer dependencies - within a package, it's pretty much impossible as long as two side by side "seperate but equivalent" builds of the package are there (currently). Forcing only one implementation to be active (for all usages) would be my prefered solution~ |
I remember asking @guybedford about this when I was struggling to get dual packages to work, and trying to find a workaround for this hazard. My suggestion was basically “well once He can surely explain it better than I can, but assuming that Node remaining deterministic is a requirement of the project, then I think throwing on this hazard isn’t a solution.
This is the current behavior of the @weswigham would it be possible to produce a PR that implements |
You seem to misunderstand - basing which you get on how it's first imported is the issue there. Just dont do that. (None of the logic in the esm resolver at least is dependent on the parent module kind, and neither is the cjs core algorithm, nor should it be). The key is just to make it resolve to the same thing every time, regardless of how it is accessed. |
There was a branch with that already linked in the proposal issue. :S |
Wouldn’t
So then why don’t you rebase it off the latest |
As per the updated spec for top-level await, synchronous graphs can stay synchronous, so In order to evaluate an esm graph synchronously one would just need to check |
Nah, throwing with an error like |
We don't even need to do that - the built-in ones can all be sync in practice (even if they're marked async right now), and any user provided loaders can be waited for synchronously akin to how child_process.execSync works (since they should be executing in a different realm/on a different event loop), even when they're defined with promises and |
I meant, isn't it required if we want something other than an error to happen. |
Well.... Yeah. But an error is, imo, a fine way to enforce a module only having one relative identity. Yes, we could do better, but that's the baseline.
Yes but no - it means that both esm and cjs need to interpret |
@weswigham The current Whereas with regard to dual packages, this hazard is the only objection. So getting |
Fwiw, I wouldn't call it "dual packages" anymore at that point, as there's nothing dual about it (which is the point). |
The packages are still dual in that they still contain both CommonJS and ESM sources, even if the CommonJS files are only used in old Node. Are you interested in pursuing this or is your interest in |
I'd still consider that better. FWIW, I think there's room for both extension searching and non-extension searching resolver modes on a per-package boundary, tbh - that way a package author can opt for either one (or possibly in the browser compat package mode side even stricter checks on browser compatibility, like ensuring no usage of
I mean.... sure. Technically. But using dual meanings for "dual packages" is exactly the kind of doublespeak that causes us to talk past one another so often, so I'd not attribute the same term to that. |
Okay, so @weswigham do you mind please opening a PR against core that enables |
So far, I haven't heard any explanation of a hazard that:
I look forward to that PR being opened :-) |
The problem illustrated by the I also don’t see how peer dependencies would solve the |
@GeoffreyBooth as has been stated elsewhere tho, that's because the singleton is being created in two places. Instead, it should be created in one place, and re-exported from the other. This is a package design issue. It could still happen if you had both Prohibiting dual-mode packages doesn't solve the issue, it just means an author has to create two packages, and still has the same hazard if they architect their code in the same incorrect way. |
This is currently only possible for CommonJS files and packages, since CommonJS can be
As I understand things, prohibiting the same specifier from resolving to different things in each module system solves the issue. See the readme in the example repo. Perhaps you could produce an example showing how the hazard is still present if an author creates two packages? |
I don't think it's up to node.js to prevent this, it is the module maintainers responsibility (publishing a dual mode package would always be opt-in by the maintainer). The current plan will block the vast majority of the ecosystem from offering ES modules any time soon. When maintainers eventually do publish ES modules it will be in a semver-major release. That semver-major release is likely to cause the singleton issue by forcing npm to install multiple copies of the module. I've seen the suggestion about |
Just adding this here, in #401 (comment) @MylesBorins cited three examples of where the hazard surfaced in real projects during the Node 7-11 days: |
I find all of these decent examples of why this rare "hazard" perhaps should require explicit opt-in, but none of them are examples of why it should be utterly prohibited for packages to replicate what dep duplication allows already. |
This prevents the singleton hazard in the same way that
What difference does it make that something is opt in? All bugs are opt in by the authors who wrote the bugs. Allowing the hazard is enabling a new class of bugs; and clearly some very competent teams (graphql, webpack, and facebook) fell into it. It doesn’t matter how explicit we make the opt-in; package authors will add Deduplication could theoretically solve this if we could somehow tell via static analysis that both the CommonJS and ESM versions of I also think the premise is wrong here: the burden should be on the folks who want to enable something unsafe to demonstrate why it’s safe enough to allow (and why the benefit is worth the risk) not the other way around. The section “Publishing Dual CommonJS / ES Module Packages” in https://github.com/nodejs/node/compare/273d38b…guybedford:91bcf79?short_path=8e67f40#diff-8e67f407bc32a0569e25d7ecaff6e494 comes closest to this in my opinion, as it actually demonstrates a pattern that allows divergent specifiers while avoiding the hazard. The issue is making sure people use it, when shipping the bug is much easier. |
|
TSC meeting did not result in any consensus. Folks did not have enough context to make informed decisions. I'm going to send an email to the TSC that give more context around this and we'll invite TSC members who are interested in this to attend the Modules Team. The general sentiment from the TSC was that we should hold off on unflagging for now while individuals get ramped up. |
Permitting the hazard and allowing both See https://github.com/jkrems/singleton-issue/tree/master/dual-esm-commonjs-package. That example is running under It sounds like what you really want is |
Not all commonJS - just the single file that contains the singleton. The rest can be dual, and can import/require the shared singleton from the same place. Using "exports", that internal file can even be kept internal. |
I want the documentation to specify conditions for which the hazard exists, leave it to the module maintainers to not publish modules with bugs. You've given examples of major projects which published broken versions but this happened under a lack of proper documentation about the hazard. I trust properly informed module maintainers to generally avoid creating broken releases. Being a known hazard means that it won't be as difficult to identify in the future. Some solutions for maintainers that I can think of:
If this were possible it could be interesting. I'm unclear if/how it's possible, specifically my concern is that loading ESM is async and the |
Well that’s why some people think it can’t be done 😄 This thread is probably the place where it’s been discussed the most; see #371 (comment) or farther up. |
Certainly if we permit the hazard, the docs will try to guide people as clearly as possible to avoiding it. The issue though is that it’s not just an author thing; consumers also need to be aware of it. Consider the |
The same way as one explains peer dep issues. Anything that depends on identity - This is solved with separate packages by ensuring that the singleton-containing-package is always a peer dep of everything that depends on it - this is how Babel, React, eslint, etc, all solve this issue already, and it's widely understood. The new hazard is the same, just within a package - it's that if you have anything that depends on identity, it must not be duplicated (as in, not duplicated between a CJS and an ESM file). This piece would have to remain CJS to be usable in both module systems; or it could be ESM to be usable in only ESM. In the graphql case, graphql should be a peer dep of everything in its ecosystem already, since it's the core of its ecosystem, and the onus remains on the graphql developers to ensure that there's only one file that conceptually represents a given stateful singleton (altho that doesn't even apply here). Their bug is the identical bug as what would have happened if graphql was duplicated in someone's dep graph, it's just in a new place. By duplicating their files (presumably automated, with a build process), the developers opted in to having to know this hazard. Also, given that |
How so? If |
I think we should careful separate three scenarios:
Your comment seems to assume we're talking about (1) or it doesn't address the issue with (3): It requires perfect coordination between all packages in the dependency tree. For example:
In a world where an app owner should now figure out what to do, this isn't great. Should they use The situation would be a lot easier if the onus would be on |
@weswigham if the entry points are different, i mean, but there’s still a conceptual duplication. I’m saying that the bug is only avoidable by eschewing back compat, or properly understanding how to avoid it - in the former case, it’s hostile to many users; in the latter, there’s no hazard regardless. |
@GeoffreyBooth this is a point of disagreement we have. IMV the maintainers of each module are responsible for not producing releases which cause the singleton issue. I mentioned a few suggestions above - #371 (comment). I'm sure other ways to mitigate exist. This is something which consumers generally should not need to be concerned about. The only thing end-users should need to be concerned about is how to write an import vs require. Take |
I didn’t mean that consumers should need to be aware of it, just that I don’t see how they wouldn’t need to be. Just look at @jkrems’ examples and how many of them involve the consumer needing to be aware of the module format of the various modules they’re installing. Now some of that awareness will remain even in a
Agreed, but unfortunately I don’t see how this is possible in any form of dual packages. See the various examples above.
You can always import the default export, regardless of wrapper. There’s no situation where a named export |
There seemed to be some confusion about my point above: I believe that we currently have a dual copy hazard within the |
Closing per #408 (comment). |
@jkrems and I created a repo to demonstrate the hazard mentioned by @MylesBorins in #273 (comment) and #273 (comment) where a single specifier resolves to separate things in ESM and CommonJS environments within the same runtime. This hazard is currently blocking dual packages, and logically would also block
--es-module-specifier-resolution=node
from becoming the default behavior.Hopefully the repo illustrates the issue clearly enough that everyone can get a solid grasp on it. It seems to me that we have three options:
Accept that the hazard is a serious concern and that it therefore rules out supporting dual packages and automatic extension resolution. The current modules implementation is what ships with regard to those two features, and presumably we would remove the
--es-module-specifier-resolution
flag (since arguably the flag’s existence invites users to stumble into the hazard).Acknowledge the hazard but decide that user education can overcome it. @MylesBorins and others concerned about the hazard would need to be persuaded as to why it isn’t a blocker, especially considering that it’s already come up in the real world. If the persuasion effort succeeds, we would then need to decide how far to lean into enabling features that invite the hazard (such as automatic extension resolution and dual packages).
Find a technical solution to prevent the hazard, such as making
require
of ESM work in a way that everyone is comfortable with (cc @weswigham). As with the previous option, we would then need to decide whether or not to support dual packages and/or automatic extension resolution.Deciding on any of these options would lead to a resolution of the dual packages item in Phase 3 of our roadmap.
The text was updated successfully, but these errors were encountered: