-
Notifications
You must be signed in to change notification settings - Fork 4
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
Will module modes be non-deterministic? #1
Comments
@bmeck should probably answer this as he’s more the expert, (and please chime in to correct me), but my thinking is that a “dual-mode” package should really be thought of as two packages that happen to share the same package name/bare specifier. So say Most of the time one of those subfolders will be just the transpiled output of the other, like For a package like Lodash consisting entirely of static functions, that doesn’t really matter. But a package where a singleton object stores state in the object will have a really hard time with this behavior, as there’s really two copies of the object and two copies of the state. (@bmeck please correct me if I’m screwing this up.) This is the danger inherent in dual-mode packages, something that dual-mode package authors will need to be aware of; it might be a reason to not release a package as dual-mode, or to release the ESM version under a new name (or deprecate the CJS version behind a breaking change version bump). If it’s enough of an issue Node could decide to not support dual-mode packages at all. Another thing to consider is different packages in the module graph loading the same package. Say your project |
This question is not really about dual-mode packages. We face this problem even with pure ESM packages into which people can perform a deep Whilst we can say "don't do that", I just want to be clear on what we are specifying will happen in that case. I expect it will arise in practice if well-known packages attempt to convert to ESM without renaming. |
So the “deep import "dual-mode-package/ambiguous.js";
// const require = createRequire...
require("dual-mode-package/ambiguous.js"); Where I guess Node would have no choice but to throw in such a circumstance, because like you say it would be a crapshoot as to whether the |
Yes. The code example captures the problematic case. And it can all be side-stepped if users are careful to stick to public package entrypoints. When you say, "Node would have to throw", would that throw be:
|
This means absolute filepath on disk, right? Does that have any browser implications? |
@Fishrock123 It's the resolved location, which for Node will be a fully qualified file path, yes. |
Ok I have another question here too: Given this case: import "dual-mode-package/is-dual-mode.js";
// const require = createRequire...
require("dual-mode-package/is-dual-mode.js"); Does If so I think that is possibly problematic because suddenly one case does not work out of the box. |
I don’t know when that throw would be. Perhaps @bmeck can answer. I’m not sure how I think this is an issue somewhat unrelated to this proposal, that both implementations have now, since both implementations have both @Fishrock123 I don’t think browsers are affected by this because browsers can’t import CJS and they also don’t have <script src="ambiguous.js"></script>
<script type="module" src="ambiguous.js"></script> I don’t know how browsers handle that, but presumably we should do something similar. |
I don’t think so, because that would be a breaking change in terms of how |
@GeoffreyBooth To be clear, my feeling is that module type selection ought to be statically analysable. This is an important invariant. Races are bad news for a module system. Whilst we could debate which repo this issue goes into, the reason we are discussing it is because the choices/constraints declared by this proposal lead to the question. I don't think we can declare this proposal accepted/coherent until we answer it. |
@Fishrock123 |
@GeoffreyBooth I'll try be more constructive and direct. This proposal chooses to use package definitions to determine the types of the modules contained within the package. This seems reasonable. I think we should take that to the logical conclusion: if someone violates the static type designation by attempting to dynamically load the module using a different mode, we ought to guarantee that fails, i.e. the This preserves the integrity of the static claims and makes the module graph statically analysable. |
Much appreciated 😄 Again I’m not the expert on how Node’s module loading system works, I haven’t contributed code to it, so someone else should address this. If you feel confident, would you mind submitting a PR to this proposal describing the behavior you expect to happen and under what conditions? And others who know the loading system well can review it and we can discuss the best way to handle the issue. |
@GeoffreyBooth Yes, I will send you the PR tomorrow morning. It is late in London now 😉 |
import "dual-mode-package/is-dual-mode.js";
// const require = createRequire...
require("dual-mode-package/is-dual-mode.js"); In this case, wouldn't that still be non-deterministic? Say it is a "dual mode" package. If you If you |
@robpalme please see #3 before you start your PR, I’m assuming you’ll probably want to base off of that (assuming the rest of the team likes it 😄) @Fishrock123 The proposal has I did a little experiment. I created an <html>
<head>
<script src="./ambiguous.js"></script>
<script src="./ambiguous.js" type="module"></script>
</head>
<body></body>
</html> And var mode = this === undefined ? 'In strict mode' : 'In sloppy mode';
console.log(mode); And served them via
So it seems to me that the browser loads the same file twice, once as a Script and again as a Module, and executes both. Oddly, whether I put the |
Well, what I was getting at is that, as I understand it, if // const require = createRequire...
require("dual-mode-package"); // loads cjs
import "dual-mode-package"; // now loads same cjs import "dual-mode-package"; // loads esm
// const require = createRequire...
require("dual-mode-package"); // error? or, replace those |
Implementing a stateful error like this seems incredibly brittle if it is relying on "first load" or similar. Also bear in mind that if a dependency already loads a dual mode package as CommonJS, that stopping an app user from loading modules seems like an unnecessary restriction. The whole point of the constraint of having a single interpretation per file is that we get predictable loading. I haven't yet see anyone propose a deterministic way to do this that won't lead to any of the above issues. And without that I personally don't see a problem with having the ESM loader having its own classification of modules (and which absolute paths to defer to the CJS loader), while the CJS loader is treated as separate legacy loader which isn't changed to support backwards compatibility. The above gives us the guarantee that for the ESM loader, there is only one interpretation of each file. While yes dual mode packages in the CJS loader might result in duplicate interpretations in some cases, but that seems preferable to sacrificing backwards compatibility or forward compatibility even. |
So I did some reading after my experiment above. According to this article, browsers treat Since @Fishrock123 in your most recent example, you have |
From @guybedford in #3 (comment):
I think there’s a case to be made for giving users the rope to potentially hang themselves if that’s what they choose. Node need not catch every mistake. I think what we should focus on is ensuring that if a race condition occurs because of dual loading, it should be the result of an intentional user action (like There is also a middle ground between allowing and banning dual-mode packages: ban import interoperability of dual-mode packages. So a CommonJS file can |
I'd like to separate the ideas of implementation preventing improper usage vs static definition of formats. We have a proposal only covering
Those two combine to make it so that any API that acts as a module loading system inherently collides with The browser has a similar problem with The solution is to define statically what formats things are. We cannot replace/"fix" Now, preventing usage in which a module is loaded in the wrong format. Currently require has no knowledge of formats. We could add warnings or throw or even abort when it loads something that is not in the expected format but we encounter problems that make any such approach brittle as @guybedford talks about.
{
"main": "cjs.js",
"exports": {
"": "esm.js",
"cjs.js": "esm.js"
}
} Since I would rather we just let people see the errors and see how people are working around them than trying to make these trade offs our selves. We cannot prevent some format problems due to how |
Resolve the open issue of what happens when a file is requested to be loaded in both modes: ESM and CommonJS. See GeoffreyBooth#1 for discussion.
Resolve the open issue of what happens when a file is requested to be loaded in both modes: ESM and CommonJS. See GeoffreyBooth#1 for discussion.
Thanks for all the feedback. From @guybedford it seems clear that we don't want the ambiguous case to result in a brittle non-deterministic runtime error. I also buy @bmeck 's argument that we also cannot forbid To solve these constraints, I propose permitting dual-instantiation of files. Effectively each module system (ESM and CJS) gets its own registry/namespace to store instantiated modules. In my opinion, this is a relatively small change purely to provide well-defined semantics. The wording for this can be found in #11 |
Resolved via #11 (I think, please reopen if otherwise). |
The doc says:
(a) a given module location can only be instantiated in one mode (CJS or ESM)
(b) the mode observed by
"import"
is deterministic based on the package that contains the module(c) we're not changing the behaviour of
require()
So far so good. Now assume we have an ESM package containing a leaf-node module (no dependencies).
require()
s the module (perhaps via a deep-import), it will be loaded as CJS.import()
s the module, it will be loaded as ESM.Only one of these can succeed due to (a). Presumably the other will throw. Now we have a module whose mode is determined by whoever dynamically loads it first. The seems like it could be non-deterministic and subject to races.
Are we introducing the possibility of non-deterministic mode selection?
The text was updated successfully, but these errors were encountered: