Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 34.2k
esm: handle extension-less "bin" files#41552
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
esm: handle extension-less "bin" files #41552
Uh oh!
There was an error while loading. Please reload this page.
Conversation
JakobJingleheimer commented Jan 16, 2022 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
nodejs-github-bot commented Jan 16, 2022
Review requested:
|
| functiongetFileProtocolModuleFormat(url,ignoreErrors){ | ||
| constext=extname(url.pathname); | ||
| if(ext==='.js'){ | ||
| if(ext==='.js'||!ext){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has a broader impact than just "bin" files.
I could artificially limit it with something like the parentDir check I put together as a quick-fix to work around this issue for my own projects, so that it affects only "bin" files. I'm not sure whether the extra complexity would be worthwhile.
Is there a particular reason other extension-less files should specifically be excluded?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about if extensionless files are wasm in the future?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exclusively or additionally?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exclusive isn't possible; it'd have to be additionally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When more than 2 options are possible, I would probably just convert the logic to some kind of Map or Set.
ljharb commented Jan 16, 2022
type module should only impact .js files; extensionless files either need to be only CJS forever, or, should be able to be any format node supports, which is 2 now but will be 3 soon. |
GeoffreyBooth commented Jan 16, 2022
Search for |
JakobJingleheimer commented Jan 16, 2022
@ljharb oh, sure. I wasn't intending to limit it to only CJS or ESM (that either/or logic here already existed—I just enabled it when there is no extension).
@GeoffreyBooth do you mean as a general principle or as a blanket rule? It's common practice for executables to have no extension, so I think that should at least not be applied here to "bin". I'd be happy to limit to just bin 🙂 (I just figured it would be less work to add if desired rather than remove if undesired). |
ljharb commented Jan 16, 2022
@JakobJingleheimer right but it's not common practice to have extensionless ESM files, because that's not supported yet. We shouldn't allow it to become common practice if doing so would prevent extensionless WASM files. |
JakobJingleheimer commented Jan 16, 2022
Yes, I agree it would be an undesirable outcome if this did preclude extensionless wasm. I can't think of how it could though: {"name": "project-a", "bin": "./bin/wasm-executable", "type": "wasm" }// test.mjsimportprojectAfrom"project-a"// all good in the neighbourhoodThe above would actually fail without this PR. |
ljharb commented Jan 16, 2022
|
JakobJingleheimer commented Jan 16, 2022 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
For now. Maybe it won't in future when WASM is fully supported 🤷♂️ Or maybe there will be some other flag, in which case we'd have to check both anyway since CommonJS files can already be extension-less: Pandora's Box is already open (this problem already exists). |
aduh95 commented Jan 16, 2022
WASM files start with a magic word (00 61 73 6d), ideally that would be how Node.js detect them. However if we expect extentionless files to be JavaScript, that certainly complicate things. Should we impose a magic phrase for exentionless files to be treated as JS? (e.g. iif the file starts with |
ljharb commented Jan 16, 2022 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
All these questions are exactly why no changes should be made to extensionless files yet - to keep the design space maximally open for when wasm arrives and gets ecosystem adoption and we can consider use cases. Indeed i suspect it would have to be another flag; the “type” flag unfortunately forced us into a position where the flag name has insufficient actual relation to what the flag does, and it’s not extensible to other formats and extensions. |
JakobJingleheimer commented Jan 16, 2022
Oof, I hope not 😳 that's like how a dog determines whether it should put something in his mouth by putting it in his mouth. IMO, checking for that magic word should be considered the same as checking for the magic word in an image file to know what kind it is: A last resort to absolutely confirm. Since the problem this PR is intending to address occurs only when an ESM loader hook is used, would it be sufficient to also gate this fix behind that experimental CLI flag? Thus the experimental introduction of the problem and the experimental fix to the problem are tethered and atomic. |
GeoffreyBooth commented Jan 16, 2022
I think this is the problem that should be addressed: whether the This would also solve the original issue, and would preserve the status quo where extensionless files in a CommonJS package scope are always treated as CommonJS, and extensionless files in an ESM package scope are unknown/unloadable. |
GeoffreyBooth commented Jan 16, 2022
This isn’t a bad idea, and if it applies only to the entry point it wouldn’t present a performance concern (because we’re only checking a single file) but it would lock us into requiring that all future entry points are distinguishable based on magic words/phrases. I think for now, because the future is so uncertain regarding potential new module types, we want to keep the design space here as open as possible. The workaround of a one-line extensionless CommonJS wrapper that just has an For now we should probably document (if it isn’t in the docs already) that extensionless ESM entry files are unsupported, with an example of the CommonJS wrapper workaround. |
targos commented Jan 17, 2022
Even for CLI packages, there is no need for a CommonJS wrapper. You can publish the bin entry points with an extension, because npm already creates an extensionless shell wrapper for them. |
JakobJingleheimer commented Jan 17, 2022 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
ah, that's true. Sorry, Antoine, I thought you were suggesting this as a way to determine all (extension-less) wasm files.
The problem with that is the the
Do all the major package managers do that? (If so, it sounds like we should specifically say "don't publish an extension-less bin entry point" and possibly throw if they do, similar to @aduh95's idea above.) |
GeoffreyBooth commented Jan 17, 2022
Right, this is what I think we should change. The flag should not cause a change in which loader is used for the entry point. If necessary, we could have an “entry loader” that only determines what should be done with the entry point, and then passes it off to the appropriate loader. This new loader should also be unaffected by whether or not |
ljharb commented Jan 17, 2022
npm does that, which means that any alternative that doesn’t is broken ¯\_(ツ)_/¯ |
JakobJingleheimer commented Jan 17, 2022
Lions, and tigers, and bears—oh my! Entry-point loader, ambient loader, (lay) loader 😵💫 I like the idea of that if we can somehow avoid a maelstrom of loaders and let the loader control its own destiny, but without creating a whole new concept. What if the existing loader somehow signalled whether it should affect entry-point flow? Ex
Isn't Node.js supposed to be package manager agnostic now? (If yarn also does it, I think we're sufficiently covered?) |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
JakobJingleheimer commented Jan 17, 2022
Actually, nevermind. It should be the user's decision, not the loader's. If we can't go the throw on extension-less entry point route (which sounds like the better option), could we use the |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
bmeck commented Jan 17, 2022
I think solutions are coming before constraints here. The goals are separated into:
These have been discussed at length multiple times and I personally don't think it is a high priority or worth the heated debate since a simple workaround exists and is even used in CJS to load things like .node files since they similarly cannot be extension-less. Answering how to solve these goals is actually prone to a lot of bike-shedding vs just showing a solution like the wrapper script done by yarn/npm/pnpm/etc. or manually writing your own. Using various magic like WASM magic bytes is equally problematic for forwards compat since not all magic byte combinations are mutually exclusive (particularly with various archive formats like .zip)! I think before jumping on a complex solution with heated debate we should just decide if this is not solvable already with a simple/easily learned workaround or if a capability is not reasonably easy and needs to be expanded. The capability we add doesn't necessarily need to be allowing extension-less files directly through the ESM loader, just allowing the goals to be achieved. |
GeoffreyBooth commented Jan 18, 2022
For reasons stated, I think this is a non-goal at the moment. We should document it as such if it isn't already documented.
Or put another way, |
guybedford commented Jan 18, 2022
Agreed! This used to be because |
guybedford commented Jan 19, 2022
One of the big problems is that the Node.js bootstrapping into CJS absolutely needs to be synchronous for timing requirements - a bunch of tests will fail if the event loop is yielded before the CJS loader is initiated. This ties us to needing to synchronously determine the module system upfront to be able to ensure a sync CJS bootstrap. Node.js having an async bootstrap would be preferable IMO but eg many async hooks test cases would fail under the invariant change. |
cspotcode commented Jan 19, 2022
You're saying that the presence of |
aduh95 commented Jan 19, 2022
The presence of |
cspotcode commented Jan 19, 2022
Coming back to the core of the bugfix: "a flag can't affect or determine which loader is used" Supposing we update the ESM loader so that node's treatment of extensionless entrypoints is identical whether or not you pass Is that acceptable? Would it adequately fix the bug today without too much effort nor breaking tests, and still allow for improvements to be made in the future? |
JakobJingleheimer commented Jan 19, 2022
I believe that's what this PR currently does.
I think your suggestion above will not result in this as they are unrelated. Also, CJS loader cannot be async, so I don't understand what this second bit is positing. |
JakobJingleheimer commented Jan 19, 2022
I think (assuming the flag checks were removed from |
cspotcode commented Jan 20, 2022
Another use-case is |
GeoffreyBooth commented Jan 20, 2022
I think that’s a case where the user might want the loader to apply to |
guybedford commented Jan 20, 2022
Agreed this is likely the more general fix to what is yet another sync / async issue. |
guybedford commented Jan 20, 2022
Just moving loaders to a separate thread allowing syncified loader hook calls could likely unblock the loader from entirely controlling the main entry point process. |
cspotcode commented Jan 20, 2022
I'm coming from the perspective of users who know this is already true today: if you use Since we're already requiring users to pass flags to node 1, you can imagine that the user is sufficiently motivated to pass these flags. So in the future, if loaders are moved to a separate thread, we can assume the user will read the appropriate documentation and learn that they must do the following: Point being that package.json type is not sufficient to determine the entrypoint's type.
My comments about sync vs async were attempting to understand this constraint, since stable node can already load a CJS entrypoint async in the presence of Footnotes
|
guybedford commented Jan 20, 2022
The issue is this is eg a bunch of Node.js timing and async hooks test cases. Definitely asyncifying the main Node.js bootstrap is another route here, it's just that those async hooks test cases would have to break.... |
bmeck commented Jan 20, 2022
To clarify a bit on async bootstrapping, altering the tests themselves is fine to do and they are there mostly for stability not invariants; but doing so adds a few complexities since async_hooks etc. don't want to have incomplete tasks when the application starts up. It is non-trivial to fix, but if someone wants to do so they would be quite the hero. However, like @guybedford seems to say, that would best be done as a separate PR as it doesn't directly affect the goals discussed above. |
JakobJingleheimer commented Jan 20, 2022 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
@cspotcode is there a compelling, practical reason to omit @arcanis@bmeck could this whole "use CJS vs ESM loader" be handled by an "ambient loader" we talked about in the most recent Loaders team meeting? This seems too widespread an issue to offload to users to solve though 🤔 |
ljharb commented Jan 20, 2022
@JakobJingleheimer |
GeoffreyBooth commented Jan 20, 2022
This thread is getting a little unmanageable. I think it’s clear that we won’t be merging this PR in as is, and we need a discussion to address the issue of how to handle entry points when |
JakobJingleheimer commented Jan 20, 2022
@ljharb ah but typescript does not play by the rules, does it 🙃 if the biggest annoyance of my day was having to also specify |
cspotcode commented Jan 20, 2022
TypeScript compiles If |
JakobJingleheimer commented Jan 20, 2022 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
@GeoffreyBooth I don't have the power to convert this into a discussion. Last ditch idea before this gets converted:
{"type": "commonjs" }
constassert=require('assert');import('./bar.mjs').then((mod)=>{assert(typeofmod.default,'function')});node --loader ./whatever.mjs --require ./whatever.cjs ./foo.jsIn the above, AFAIK, there is no documented rule that When the flag checks are removed from The documented purpose of the Whether and how an entry-point is processed by a loader or require whatever is a different, probably larger issue. I think removing the |
guybedford commented Jan 21, 2022
I don't think we should make Another option for extensionless bins to be made to work with loaders could be having the |
GeoffreyBooth commented Jan 21, 2022
Neither do I, I think only issues can convert into discussions. Regardless, I think we should close this PR and you can open a discussion thread. |
cspotcode commented Jan 21, 2022
To do this without passing an User loaders will still be able to probe it by passing an extensionless URL to guess if they're resolving the entrypoint, even though they won't be given an explicit Is this loosely the implementation you had in mind? |
guybedford commented Jan 21, 2022
@cspotcode that certainly sounds like a good approach if wanting to avoid isMain or a parentURL sentinel. |
JakobJingleheimer commented Jan 26, 2022
Per the above (since this is getting quite long): https://github.com/nodejs/node/discussions/41711 |
There have been several issues reported recently around extension-less files failing when
--experimental-loaderis used, and I myself encountered this.I think the circumstances under which the failures occurred are legitimate use-cases specifically supported in the design of the relevant core module(s). Specifically in regard to "bin" (executable) files, this is likely to affect a non-trivial amount of users (ex users of Mocha which has 5.6M weekly downloads as of the opening of this PR).
Note that this PR does not affect specifier resolution, only determining format (when the filename on disk literally has no extension): when the specifier is not properly resolved (under the current rules), this has no effect.
cc @nodejs/loaders @nodejs/modules
Resolves#33226, resolves#41275, resolves#41465