-
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
Discussion: New “ESM by default” mode #49432
Comments
I don't think this is feasible to change the default if package.json is present due to the massive amount of modules available on npm. My 2 cents on how to solve this in a much easier way: let's make |
I’ve considered this and I don’t think it’s a blocker. As discussed in #49295 (comment), there are many potential solutions, the simplest of which is to just crawl through subfolders adding Also, the proposal isn’t to go straight to changing Node’s default. That will be months if not years away, once this flag has shipped for a long time and issues have been resolved. The time for considering the impact of changing Node’s default is once this flag has been available for a while and we can see how it works. We may never change Node’s default, and that’s fine; this flag and mode has merit regardless. The point of mentioning that this might lead to changing Node’s default is so that we design with that potential goal in mind, so that whatever we add has a clear way to opt out if/when it becomes the new default. |
@GeoffreyBooth how does adding that field work for people using other languages/processes that spawn child processes for existing codebases? I'm mostly curious since it is a much larger breaking change than most we see and affects all sorts of runners of node applications in non-simplistic ways. |
First off, adding a flag isn’t a breaking change. The breaking change would only be if we make the flag the new default behavior, which I don’t think we need to worry about just yet. It’s premature to consider larger ecosystem effects of making this new mode the default when we haven’t designed, much less implemented, the flag to enable this new mode. The flag is no different than achieving these effects via module customization hooks (the changes that the hooks can achieve, anyway). Just as other languages/processes wouldn’t know how you’re customizing the files in your project when using hooks, they wouldn’t know whether your project is meant to run under this new flag or not. This is a problem we already have, and it’s on those other tools to provide a way for the user to signal to them that they should expect Node to be in this new mode, like how One thing we can do to help ease the transition is to start making the |
@GeoffreyBooth I'm not opposing the flag in any way, I'm just concerned about the default swap which has been attempted in the past in #32394 |
I'm more skeptical on this due to files being loaded for config/init like ~/.*rc files outside of the application directory. |
I think the idea of having multiple releases in general is not great - if we want to do it, builds with pointer compression enabled would’ve been a thing already and then we would have another headache about what combinations of releases we want to maintain…. A runtime flag that makes ESM the default sounds good to me though, why can’t we just have that and allow it in NODE_OPTIONS? That should be enough for the use cases mentioned in the OP already |
There’s an infinitely long tail of ecosystem tools and libraries that don’t support whatever latest and greatest features we ship. Several years ago there were presumably tools that relied on the fact that
What do you mean by “multiple releases”? |
multiple binaries, e.g. if you can have node-esm, then you can also also have node-pc (pointer compression), then you can also have a combination of node-esm-pc, then you can have…other combinations |
Yes, that’s perhaps an argument against shipping an alternate binary (see #49407). I think we should worry about the flag first before contemplating binaries; I think the flag stands on its own, and maybe there’s a solution for a binary with that flag enabled that doesn’t involve us needing to ship or maintain it, but we can worry about it later.
Shebangs can’t include flags on many platforms. See #49295 (comment) and linked and following comments. I don’t know if shell scripts have much need for pointer compression, but assuming they don’t, then the shebang case is an argument for a separate binary, as shell scripts being unable to specify flags is a particular constraint that only a separate binary can solve, and hopefully this is the only Node option that such scripts need control over. Anyway to your larger point, yes, let’s ship the flag first and go from there. Many platforms do support flags in shebangs, and there are other use cases solved by the flag, so we’re still making a lot of progress just with the flag by itself at first. |
We are not in agreement. The ecosystem concern is something that needs to be at the center of this design. Changing the default of how a module from node_modules is evaluated is so massively breaking that should be off the table. A node.js that cannot run most of the registry is close to useless. Note that I'm not opposed of changing the default when a |
A new flag will not cause a break. Full stop. There is no breaking change proposed here. The proposed behavior under the new flag will require an extra patch step until package managers catch up and insert the missing In #49295 (comment) and following comments we discussed the pros and cons of preserving the “missing There are other approaches. We could treat
Or at the very least, it should start including |
What's the point of creating a mode of running Node.js that cannot run the majority of the registry? I expect the
I think this developer experience is unfeasible. What will happen when new packages that assumes are running in esm-first will be mixed with commonjs packages? Are users supposed to edit them by hand? This would worsen Node.js developer experience significantly and it is a very big deal, considering that the majority of the registry is (still) commonjs. Moreover, this new mode will break quite a lot of The ecosystem is already complex enough, and this new mode will actually worsen the situation. We can avoid completely avoid this friction and having
I agree. The default should not be changed actually: it has no friction whatsoever for developers. Let's not break everyone. |
It seems like there's some confusions there. ShebangsIMHO most of linked prior discussions are a bit mislead.
The solution for portable enough shebang without separate binary is to write I don't recall any issues with that; even though very same thing always applied to shebangs with Separate binariesThere is a difference between separate binary that is compiler with different source code and/or different compile options, and a copy of the very same binary that has different name. The first comes with downsides such as increased build time and increased release size. The second (which is proposed in #49407) comes with restriction that the second binary must have specific filename. For common real life example of it, one may run The implementation comes with almost no overhead, and Breaking changeNo, adding new optional command line flag that is disabled by default does not break anything for anyone by itself. And yes, blindly running everything in this mode with Node.js ecosystem that we have right now is likely to break. This is not only intentional, this is the intent. We must be able to enable it and see what happens. We must ship it so any developer can enable it and see how it works with their projects. We must ship it so any user can enable it and see what breaks in the packages they use. We must ship it so any maintainer can see which dependencies are breaking. Only after that, we can decide if it's feasible or not to give everyone time to migrate, then introduce |
This is where we are not in agreement, and any plans that include massive ecosystem breakage should be avoided. I'm strongly -1 on this proposal as currently framed. Moreover, I think any plan involving the discontinuation of commonjs should be avoided. |
To be precise, shebang on Linux supports only a single argument. Anything beyond that needs a modern |
One thing not mentioned here as well is tooling needing out of band info for flags/separate binaries like this; tsconfig/eslintrc/babelrc etc. would need to account for this flag since it wouldn't be statically analyzable and/or can be varied per entrypoint or in multiple modes for a given directory:
These need not be in shebangs either, these can be sources out of band just by invocation of the executable, package.json#scripts, etc. This tooling problem should probably be considered with them in this discussion as going completely out of sync of tools was a struggle for |
Any suggestions then? |
@LiviaMedeiros you are demonstrating that passing a single argument works, it would be more interesting to show wether passing a second argument doesn't work.
I guess we'd need to find a way to either restrict the effect of this flag to dev-only (a bit like the |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
I think asking the package managers to add |
@tniessen @aduh95 my bad, shebang indeed interprets parameters single argument with spaces. Thanks for pointing this out!
Dev-only might work, I guess we can sacrifice performance for that on the first stage.
I think this part is feasible on its own, e.g. marking existing packages without What about files outside of scope? How can we launch non-symlink |
@mcollina If I’m understanding your full comments correctly, this was the only part that you opposed, right? I have conflicting thoughts on this myself. On the one hand, the more that the flag breaks, the less adoption it’ll get and the longer the timeline to potentially making it the default. On the other hand, like @LiviaMedeiros mentioned, the more that it breaks behind a flag, the more potential gain: if people try out the changes and like them, that builds momentum for the ecosystem adapting. So I guess the question is, what’s the benefit of changing how “typeless There’s no way this flag won’t land without
I was thinking that in the new mode, Though I acknowledge that this breaks the symmetry, so maybe there’s no value in flipping how typeless scopes are treated. It would be an unfortunate “this is always this way for legacy reasons” thing.
I don’t think this is much of an issue. Most tools that I’m familiar with have moved away from the |
@GeoffreyBooth they interpret the type of files. I'm not concerned about tsconfig.json being interpreted in a new way but about TS not being able to determine the type of a module without "yet another config option". Even with more config options the entrypoint of a program could alter the type of a file to superimpose the 2 modes of operation. |
|
@aduh95 I've done some digging, and I think I have tentative answers for the questions you posed above regarding parsing the entry as a URL:
So today, running The There’s also
I think so. I’m not sure what the use case would be using the full resolution algorithm. Doing so would mean that you could never run Disallowing bare specifiers would also be consistent with legacy
I did some testing. At least on my Mac, the OS passes whatever string was used as the command. I made a file When run via At least on my system and maybe all POSIX systems, these pre-replacement strings are parseable as URLs: both
Agreed, I’ve updated the top post. |
I think we've already established that parsing paths as URLs is not an acceptable solution: it breaks as soon as the path contains |
My examples above aren’t parsing paths, at least for the non-shebang case. They’re parsing relative URLs, based on the file URL of Just so we’re clear, my goal is to eventually allow So that leaves the shebang case. When I run If I do |
Folks subscribed to this issue, please tell me what you think. Which is a better UX under the new mode:
Assume for the purposes of this question that both can be made to work: we can figure out ways to properly error on ambiguous special characters, etc. |
I would follow the latter case and minimize the breakage. Please use paths, folks are used to use paths to manipulate files in the filesystem. |
I think the terminology is mixed up. We don't "parse the main entry point" here, we parse first positional argument in something that points to main entry point. My answer is: we absolutely must keep parsing first positional argument only as path, and we should be able to provide main entry point as URL in a different way than as first positional argument. |
I agree with @LiviaMedeiros and @mcollina. We must not change how the first positional argument is interpreted by default. We don't want to break the use case and user expectations. What we can do is create a new flag that either:
|
Okay, so it sounds like people want the second option, where if you want to pass an entry point that’s an URL you do it via We could ship a semver-major change where |
And just so we’re clear, it seems like we don’t want the first option, but are people asking for something other than the second option? I was planning to use |
I don't like the behaviour of not being able to enter the REPL with an Personally I think what would make the most sense is an option to change how to interpret that first arg. So your |
You would just need to pass
I intended The issue with |
It shouldn’t be an issue, the ESM loader is able to load CJS, why would we error? |
I was thinking how would we handle something like Anyway getting back to the UX question first though, which do we want:
|
I know people are still debating the best approach for supporting URL entry points, but since one of the options under consideration is a semver-major change and the others aren’t, I opened a PR for the semver-major one in case that wins consensus: #49946. Let’s hopefully decide whether or not to go with that approach before the 21.0.0 cutoff on Tuesday. |
I think it would be better to open spinoff issue so we can have consensus on more details. For the UX question, it might be worth mentioning that third option: providing main entry URL as named argument. As for when it should land, I see it as a feature tied to |
We can, but since the first PR implemented everything else I think the URL entry stuff is all that’s left to design, so maybe we can just finish it here?
Yes. The input would be relative to the file URL of
I don’t think so. That’s what
What is a shortcut specifier?
From an HTTPS URL, you mean? I think not as part of the CLI, that can be achieved via a module customization hook.
Yes and yes. It would be loaded by the ESM loader, the same as if you had referenced a relative or absolute file URL via |
Subpath imports, the ones starting from
From any non- |
No, I think we shouldn’t implement the ESM resolution algorithm here.
We already have |
The more I look into this, the more complex it seems, and the less sure I am with both the way I have things now, as well as whether I like the other options instead. I think I may just stick with TSC for compiling my package code in general, and if anything I can still do/try out the dual-build setup with ESM/CJS, but maybe just use TSC, since I can ensure that things are built consistently. Should I provide synchronous APIs from NBTify? I think learning about how to work with async has really helped me progress, and I don't want to prevent someone else from learning that if I go with providing a sync option to my API. Or rather, if anything, that would only work in Node, because the Compression Streams API is only an asynchronous implementation. I'd have to use Node Zlib as a fallback to allow for a synchronous implementation, and it would be for Node only. I'm not sure I like the splintering of that either. I think I have realized afterall, that I don't specifically want to use ESBuild for building my packages, since it's meant to bundle code, and I think I would only want to use that in a situation of building an ESM bundle for the browser. I may look into that as well though. Maybe I can use pkgbuild for that instead though? It kind of works a bit nicer for plain TS type generation support out of the box, as well as the configuation for things just plain working. Maybe I can look into making my own tool kind of like that, since it feels like all of the tools for this that I find, they all don't quite work exactly like how I want them to, unfortunately. https://www.reddit.com/r/node/comments/14os7zv/the_esmcjs_situation/ https://github.com/wooorm/npm-esm-vs-cjs https://stackoverflow.com/questions/46515764/how-can-i-use-async-await-at-the-top-level https://stackoverflow.com/questions/71517624/because-i-cant-run-await-on-the-top-level-i-have-to-put-it-into-an-async-funct https://commerce.nearform.com/blog/2021/node-esm-and-exports/ https://dev.to/a0viedo/nodejs-typescript-and-esm-it-doesnt-have-to-be-painful-438e https://www.reddit.com/r/node/comments/14rg9ym/esm_not_gaining_traction_in_backend_node/ nodejs/node#49432 https://github.com/azu/tsconfig-to-dual-package https://www.breakp.dev/blog/simple-library-package-setup-with-esbuild/ https://esbuild.github.io/api/#build evanw/esbuild#263 evanw/esbuild#618 microsoft/TypeScript#46005 https://guitar.com/news/music-news/devin-townsend-shredding-dreams-steve-vai/ https://www.reddit.com/r/DevinTownsend/comments/17s332s/devin_and_the_vai_years/ https://www.reddit.com/r/DevinTownsend/comments/k9jot4/devin_townsend_i_was_unable_to_articulate_my/ https://www.kerrang.com/devin-townsend-i-was-unable-to-articulate-my-discontent-so-i-tended-to-act-up-i-even-took-a-sh-t-in-steve-vais-guitar-case https://www.reddit.com/r/DevinTownsend/comments/6tia0r/is_vai_comparing_devin_to_zappa/
The more I look into this, the more complex it seems, and the less sure I am with both the way I have things now, as well as whether I like the other options instead. I think I may just stick with TSC for compiling my package code in general, and if anything I can still do/try out the dual-build setup with ESM/CJS, but maybe just use TSC, since I can ensure that things are built consistently. Should I provide synchronous APIs from NBTify? I think learning about how to work with async has really helped me progress, and I don't want to prevent someone else from learning that if I go with providing a sync option to my API. Or rather, if anything, that would only work in Node, because the Compression Streams API is only an asynchronous implementation. I'd have to use Node Zlib as a fallback to allow for a synchronous implementation, and it would be for Node only. I'm not sure I like the splintering of that either. I think I have realized afterall, that I don't specifically want to use ESBuild for building my packages, since it's meant to bundle code, and I think I would only want to use that in a situation of building an ESM bundle for the browser. I may look into that as well though. Maybe I can use pkgbuild for that instead though? It kind of works a bit nicer for plain TS type generation support out of the box, as well as the configuation for things just plain working. Maybe I can look into making my own tool kind of like that, since it feels like all of the tools for this that I find, they all don't quite work exactly like how I want them to, unfortunately. https://www.reddit.com/r/node/comments/14os7zv/the_esmcjs_situation/ https://github.com/wooorm/npm-esm-vs-cjs https://stackoverflow.com/questions/46515764/how-can-i-use-async-await-at-the-top-level https://stackoverflow.com/questions/71517624/because-i-cant-run-await-on-the-top-level-i-have-to-put-it-into-an-async-funct https://commerce.nearform.com/blog/2021/node-esm-and-exports/ https://dev.to/a0viedo/nodejs-typescript-and-esm-it-doesnt-have-to-be-painful-438e https://www.reddit.com/r/node/comments/14rg9ym/esm_not_gaining_traction_in_backend_node/ nodejs/node#49432 https://github.com/azu/tsconfig-to-dual-package https://www.breakp.dev/blog/simple-library-package-setup-with-esbuild/ https://esbuild.github.io/api/#build evanw/esbuild#263 evanw/esbuild#618 microsoft/TypeScript#46005 https://guitar.com/news/music-news/devin-townsend-shredding-dreams-steve-vai/ https://www.reddit.com/r/DevinTownsend/comments/17s332s/devin_and_the_vai_years/ https://www.reddit.com/r/DevinTownsend/comments/k9jot4/devin_townsend_i_was_unable_to_articulate_my/ https://www.kerrang.com/devin-townsend-i-was-unable-to-articulate-my-discontent-so-i-tended-to-act-up-i-even-took-a-sh-t-in-steve-vais-guitar-case https://www.reddit.com/r/DevinTownsend/comments/6tia0r/is_vai_comparing_devin_to_zappa/
Building off of #49295 (comment), we’re considering a new mode where all of the current places where Node defaults to CommonJS would instead default to ESM. I think this should be enabled by flag, and once it ships we can potentially ship a separate binary where it’s enabled by default; and/or we can someday make the new mode Node’s default in a semver-major change.
The use cases solved by such a mode are (and I’ll edit this comment to update this list as people think of more):
I want to write ESM syntax by default without needing to opt into it, such as via
package.json
file or file extension or--input-type=module
.I want to write what are typically called shell scripts in ESM JavaScript, where I can have a single extensionless file anywhere on my disk that uses ESM syntax and it doesn’t need a nearby
package.json
file or a symlink or wrapper file in order to run as ESM.I want consistency in how the CLI handles references to files; currently
--import
accepts URL strings but the main entry point must be a path string (and therefore can’t be adata:
URL orhttps:
URL, such as to work with--experimental-network-imports
).If this mode becomes enabled by default in a future version of Node, when using that version of Node I want to be able to start a new project and write ESM syntax without needing to take any steps to opt into enabling ESM support.
To handle these use cases, the changes that this mode should make are (and likewise I’ll edit this to reflect consensus):
When a
.js
file is outside of any definedpackage.json
scope, because there are nopackage.json
files in its folder or any folders above it, it will be treated as ESM JavaScript.When a
.js
file is in a package scope where its nearest parentpackage.json
lacks a"type"
field, it will be interpreted per the conditions we establish in Discussion: In an ESM-first mode, how should apackage.json
file with notype
field be handled? #49494: when the package scope is under a folder namednode_modules
, the file will be treated as CommonJS; otherwise it will be treated as ESM.When an extensionless file is outside of any defined
package.json
scope, it can be run as an entry point or imported per the conditions we establish in Discussion: In an ESM-first mode, how should extensionless entry points be handled? #49431: if the file begins with the Wasm magic bytes, it will be evaluated as Wasm, and likewise for any other future file types with defined headers that don’t conflict with JavaScript; otherwise it will be evaluated as ESM JavaScript. (Still subject to--experimental-wasm-modules
until that stabilizes.)Input from STDIN or
--eval
would be treated as ESM unless--input-type=commonjs
is passed. Or in other words, the default value of--input-type
would flip fromcommonjs
tomodule
.The CLI will parse the main entry point as an URL string, similar to how the value of
--import
is parsed.process.argv[1]
will not be parsed by the CommonJS loader.This new flag will be named per the discussion in #49541.
Prior art: #32394, #49295, #49407
@LiviaMedeiros @nodejs/loaders @nodejs/wasi @nodejs/tsc
The text was updated successfully, but these errors were encountered: