-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
esm: handle extension-less "bin" files #41552
Conversation
Review requested:
|
@@ -74,7 +74,7 @@ function getLegacyExtensionFormat(ext) { | |||
|
|||
function getFileProtocolModuleFormat(url, ignoreErrors) { | |||
const ext = 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.
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. |
Search for |
@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). |
@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. |
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.mjs
import projectA from "project-a" // all good in the neighbourhood The above would actually fail without this PR. |
|
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). |
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 |
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. |
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. |
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. |
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. |
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. |
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.) |
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 |
npm does that, which means that any alternative that doesn’t is broken ¯\_(ツ)_/¯ |
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.
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.
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. |
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, |
Agreed! This used to be because |
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. |
You're saying that the presence of |
The presence of |
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? |
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. |
I think (assuming the flag checks were removed from |
Another use-case is |
I think that’s a case where the user might want the loader to apply to |
Agreed this is likely the more general fix to what is yet another sync / async issue. |
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. |
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
|
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.... |
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. |
@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 🤔 |
@JakobJingleheimer |
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 |
@ljharb ah but typescript does not play by the rules, does it 🙃 if the biggest annoyance of my day was having to also specify |
TypeScript compiles If |
@GeoffreyBooth I don't have the power to convert this into a discussion. Last ditch idea before this gets converted:
{ "type": "commonjs" }
const assert = require('assert');
import('./bar.mjs')
.then((mod) => { assert(typeof mod.default, 'function') }); node --loader ./whatever.mjs --require ./whatever.cjs ./foo.js In 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 |
I don't think we should make Another option for extensionless bins to be made to work with loaders could be having the |
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. |
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? |
@cspotcode that certainly sounds like a good approach if wanting to avoid isMain or a parentURL sentinel. |
Per the above (since this is getting quite long): #41711 |
There have been several issues reported recently around extension-less files failing when
--experimental-loader
is 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