-
Notifications
You must be signed in to change notification settings - Fork 44
Module Interop (CommonJS resources from ESModules) #10
Comments
As I mentioned in the other issue, I think this is critical. Nobody migrating older code wants to do a big bang migration where you need to replace every reference to |
See a part of my response here: #7 (comment) When I was referring to |
And to expand on this, most projects that I've met with that adopt new features do so very gradually - if a file is changed for a business reason, then some modernization will happen at that time as a matter of applying best-practices where possible, otherwise it will remain untouched (as there is no reason to change it). Huge migrations to the "latest and greatest" are difficult to justify from this perspective. |
This is infinitely better than having a |
If we think those edge cases are so tricky, we shouldn't leave it up to a userland package to solve them! Because what's in core becomes written in stone, it is up to us to solve the hard problems adequately. Punting because it's difficult or unclear just pushes the problem into the ecosystem, and then people have to rely on word of mouth like "oh, you use package XXX, it doesn't handle Y like you'd expect, maybe use package ZZZ instead - but it has the following caveats: ...", and you have it wait years for a "winner" to shake out, and in the meantime everyone is using a bunch of potentially conflicting options (especially in this case, since these are globals - conflicts are super likely). |
Indeed, Node.js’ current support for CJS interop is designed for such (unlike npm’s proposal). The key difference between the status quo and your proposal is that we encourage converting an entire file at once, while you seem to want the ability to convert one specific feature in all files. IMO interop for converting an entire file at once is sufficient, but do let me know if you think differently. |
|
@weswigham i'm confused, if the primary audience for your concern doesn't touch working code & the default behavior accomodates them, is there really a problem? i mean, most people don't change working code, period, even if one way is "strictly better", and QoL affordances won't change that. i think a bigger concern should be the massive amount of confusion coming if all the scoped variables are moved to |
From people I've interviewed/sat with, a lot of "upgrades" on large codebases are highly incremental (down to the logical unit, usually, not even file). So say I need to go update some business logic in some function to fix a bug - when that change goes through CI/CD, those lines (and those only) get checked for new style issues - including issues related to modern best practices (like adopting more es6 features). Then I need to go through those lines and update them (putting in a little extra effort on top of my bugfix). As it stands today, "a little extra effort" here means rewriting a large group of identifiers and all imports and exports in an entire file - that's not a small burden. Many larger projects also accidentally end up with larger files with too much functionality in them (grown naturally over time), which would need a lot of effort to disentangle. Migrating a 2000-line+ file with conditional requires or file-specific identifier references is going to be nearly impossible without refactoring the entire project, or at least the entire file as it currently stands. Minimizing the minimal work required to start adopting es6 modules should be a goal. |
this argument keeps coming up because of people who are willing to leave certain things they don't believe are needed to users in the dust. I did very much the same thing with my pr to make cjs modules get named exports, and it took a few weeks and tc39 to unravel that mess. in the end I think bmeck put it best with his phrase "the worst thing about mjs is that it works". that phrase I think also applies to this situation very well. the worst thing about the current interop is that it works, with all these other behaviours somehow leaving some functionality behind or broken. imo meta.require is a nice utility and would be cool to have but it is nonessential and I wouldn't feel that node is lacking without it. |
So one thing that I personally optimize for, perhaps to a fault, is Node + Browser interop. It is dawning on me that we may need to align on a design / architecture philosophy before we can really dig into the meat of some of these implementation details. I've opened #11 to discuss the matter |
@weswigham thanks for explaining! 😄 i hadn't heard of conditional linting like that. sounds like performing a minor fix could require a whole-hog rewrite of a huge chunk of the program. totally understandable why your position is that way! |
@weswigham - I'm guessing that people that have legacy codebases would probably leave most of the files in them in CJS land. And if they decide to move a file or two (or ten) to ESM land, they would anyway need to change all the |
@devsnek - the I believe |
BTW, re @TimothyGu :
Here: const source = `let require = import.meta.getRelativeRequire("${url}"); let __dirname = "${getDirname(new URL(url))}"; /*more injected locals*/; ${internalCJSModule.stripShebang((await readFileAsync(new URL(url)))}`; Nothing's impossible here, just less "nicely" integrated. And it's worth noting that since I can incorporate a dynamic loader that does exactly this, overriding the builtin esm loader, you can be sure that it will be done, and there will be multiple implementations for it, since this is a desirable behavior for anyone transitioning from commonjs to modules (since you want to change as little as possible, and the removal of |
you can also just type |
This should be off the table as we went to TC39 about this and talked to browser vendors, both did not like having this contextual module idea. It is exactly the reason that
TC39 objected to this and I object to this due to the mechanisms we tried early on in the original implementation poisoning the local Module Map (eg. occupying a hidden I mostly agree with @giltayar here:
I would even go further, stating that after porting to ESM, |
const source = `let require = import.meta.getRelativeRequire("${url}");
let __dirname = "${getDirname(new URL(url))}";
/*more injected locals*/;
${internalCJSModule.stripShebang((await readFileAsync(new URL(url)))}`; At this point we are exposing the creation of require functions via |
@zackschuster When not in a Module goal |
@bmeck ah, i see. my mistake. thank you! |
The source of the require function doesn't matter - you can write a package that implements that function using cjs require and import it, it needn't come from some magical import.meta property. My point still stands that this can be done in userland as is, so it will be done. Choosing not to own this experience in core is probably wrong, since then you cede control over it to whoever's "cjs-compat-shim-loader" is the most popular.
Except fixing all the small "mistakes" of the past doesn't seem like it's really a stated goal of this es module implementation, and invalidating many years of tribal knowledge, documentation, and stackoverflow answers just to make a "cleaner" or "more modern" API, is, imo, more shortsighted. Everyone prefers to live in an idealized world with clean breaks and no acknowledgement of the past, but by doing so you discard so much of what has gotten you to that point. The less differences there are between modules and cjs, the better, as the less surprising it will be, and the easier to incrementally migrate it will be. I think that pretending module syntax gives carte blanche to start tabula rasa is rather ignoring a very long history and increasing the likelihood of a harder fork of the ecosystem (down the line of the new vs the old), and definitely introduces more cognitive burden on every author (as now they contextually must choose between two ways to do everything). |
No, it does matter. If Node supports this out of the box it must continue to support it. If it is done in userland the support matrix is not Node's responsibility and it will not be treated like the
This is certainly part of the design goals with so much being about web compatibility. This is not a tabula rasa or carte blanche. These things are not coming to the web and there are good reasons to get people to using workflows that work everywhere rather than giving them a gun of tech debt over time. Importantly, some of these variables don't even work the same once put into ESM. You cannot reliably use We cannot treat the past as immediately a necessary good for the future, especially when the future was designed without the past in mind (we have spent a long time on ESM due to this and I am not saying that is a good way to design things). |
Exactly. Every compat thing that gets punted on, every implementable old thing that gets slashed - it pushes the issue into the community ecosystem and is another entry in the compat matrix of things that can cause fragmentation within the community. We already get to have great conversations about our favorite bundlers and build tools (at least for browser projects) and I don't particularly look forward to adding loaders and cjs compat layers to that discussion. By having it in core, it's a know factor, with known tests, a known lifetime, and no ambiguity about where to find it or how to manage it, and the experience gets to be owned by core - making it a community problem feels quite lacking. |
Unless we can use these old things exactly the same across environments you are suggesting we have 3 compatibility modes (CJS, ESM with CJS parts/variables [Node ESM], ESM [Web ESM]) instead of 2 (CJS, ESM). The general idea that is presented is to keep a single forward thinking ESM implementation that does not implement an incomplete compatibility with CJS. If we expose CJS things to ESM we now have to deal with the incompatibilities of those primitives not working to the same affect in ESM.
I'm not sure I understand this, are you talking about wanting to put a bundler/build tool into core? |
That's effectively what the
If that's the case then all we can do is hem to the browser module implementation to a t (as a defacto standard), and have no interop whatsoever, no path remapping or module identifiers, and then really hope they amend their runtime to support these niceties. (Destroying their ergonomics in node, in the meantime) IMO, es modules as a universal platform have failed when everything loader related got labeled as host specific behavior, and tossing away everything else to try to chase after that dream is giving up on reinforcing the strengths of the node platform and it's history. This is why I wanted to identify what stakeholders are perceived as the most important here, and what they need to be productive and successful, because hemming to the browser implementation would be like "frontend devs without a build tool" are the most important stakeholders to the project which would be... well... very odd for a server runtime implementation; but as it stands it's not even that, since you still need a tool to map all the browser incompatible paths (and maybe extensions) in your code, so you've still got the "3 compatibility modes" problem. |
I have had many long discussions about why I believe we should never use such behavior or bless it.
This assumes that the code is using browser incompatible paths and your HTTP server is not smart enough to correct for those paths (just like how
Point 1 is probably always going to be the case across the environments and will likely only grow with the advent of browser specific module formats like HTML modules. I don't think it is a solvable problem as long as ESM supports loading many kinds of formats (WASM, WebPackage, etc. are also coming down the pipe). Point 2 can be removed somewhat easily but leans heavily on the idea of HTTP Servers, Service Workers, or browser hooks as a means to support existing workflows using path searching. This seems like a possible route to take to me and would prevent this 3rd compatibility mode from existing. |
This is just pushing the compat layers farther down the pipe (and trying to place it closer to the browser to make it their responsibility) - it's a preprocessing tool by another name. You can load commonjs in the browser today, too, with a bit of shim js that could be provided by the runtime and an intelligent webserver. But not many people advocate for that work flow, I think, since you can transform your static files and get a similar effect at lower runtime cost...
So, then, why can't we acknowledge there are host differences, and optimize this implementation for node, and not browsers, or some perfect world where they can be the same? Node's es module experience should be the best way to write code for node, full stop. Without caveats like "as long as you don't use older packages, code, or idioms".
Not having a blessed browser development workflow but implementing with baseline browser compat as a requirement is just pretending not to have a horse in the race, IMO. This is why I really want to know what the foundation's priorities are here, because as is it's a slightly crippled experience out of the box for all stakeholders involved. Caveats exist for all developers, no kind of developer really gets a zero thought "it all just works" out of the box experience. IMO, either modules aught to be strictly browser compatable (without any caveats about needing intelligent webservers or service workers to act as compat layers, without the ability to access cjs at all, since that won't work in a browser), and most people can keep writing their packages in commonjs unless they need browser compat, or modules should be optimized for writing the best code you can for the node ecosystem, complete with existing idioms and patterns, with browser compat being a nice-to-have that a discerning package author could enforce in their own codebase, or could use a transform or tool to achieve (just like commonjs today). I'm all for either extreme as long as it's clearly stated and followed, what I'm not happy with is an exception-filled middleground. |
Yes, it is pushing it down to a different level than the environment.
The same is the general review for trying to ship ESM without a bundler. It works fine in DEV but not in PROD.
There is a difference in host and format considerations that I think are being conflated. We can and should keep interpretation of well known formats identical across environments. We should not diverge in format runtime behavior except using
Seems fine, I just lean on compatibility extreme, which includes not trying to bring legacy behavior that starts to break down in odd ways. |
To me the foundation's priorities here can be summarized in 2 points:
That means that every other environment compatible with strict ESM that shipped already will work with code meant to work in current ESM out of the box.
Agreed. So we can omit file extensions, use package.json, whatever makes developing experience great in NodeJS, because so far it did a pretty good job with just CommmonJS. However, it should be at least capable of running out of the box fully qualified URL as ESM import as well as ESM modules that already works on other envs (browser, jsc, spidermonkey, etc) Everything else will lead to infinite discussions about how much Node should lose, what's the benefit, let's solve everything with extensions that we never wrote in NodeJS anyway, etctera ... all arguments already discussed infinite times that brought nowhere if not right here. Also, let's keep it simple if we'd like to see this happen for node 10.
migration pattern? Yes, write ESM and use NodeJS as naturally as you can, it can work out of the box simply because it did already for the last 2 years. |
"best" is subjective, however, and there's clearly some disagreement on that. (to clarify; I don't have any objection to |
the moment a file is not fully qualified as relative or absolute path, including fully qualified URLs, the The KISS approach would be to use require to bring in CJS and import to bring in ESM, like you use When WHATWG will decide how to resolve unqualified imports then NodeJS will follow so that the basic two requirements I've mentioned will be preserved:
This is NodeJS dev friendly, it doesn't bother the Web with undesired extension, and there is the freedom to focus in only one problem instead of solving ESM for everyone, which I believe is out of this team scope. |
@WebReflection we have had this talk many times and I disagree on your conclusion to what is simplest; also on the the nature of what both of those points are at their core. We can rehash those discussions if we desire. |
What @WebReflection says +1 for ESM in Node 10 sans interop If ESM worked in Node.js today without flags or .mjs extension, I would immediately start writing code that targets both browser and Node. Provide Leave the existing built-ins as is. Don't screw with the existing Node.js ecosystem. Even when ESM support in CJS is already available via Don't force anything. Just provide ESM as an option, and see how the ecosystem responds. Why are we discussing building what amounts to a polyfill into Node's core to begin with? Implement CJS hooks if it makes preloading @std/esm easier. Make it work but don't bend over backward trying to make it work well. This shouldn't be the preferred approach anyway. Side Effects of this approach: If ESM support sans CJS interop was available today, devs who have been chomping at the bit to use ESM in Node can start experimenting with it. Early access ESM won't provide CJS interop but maybe that's a good thing. Encourage the ecosystem to experiment with writing new libs/tools in pure ESM. Import.meta + import.meta.require can come later once async/await support has been worked out. This is when mature codebases that depend on CJS can start to make their transition. ESM in CJS support is a low priority but baseline support is already available via a polyfill. If hooks ease this adoption path, cool. Don't screw with the existing ecosystem. Make sync require() in its current state feature complete, then lock it into long-term maintenance. Maybe one day, it'll be reasonable to kill it off but who knows when that'll happen. 10 is a good transition point to make big changes. Yes, there's a ton of supporting documentation/resources showing how to use CJS in Node but the best case is where users can differentiate when ESM was introduced as pre-10 and post-10. |
@evanplaice that is a big proposal and would probably be best split into a separate issue of discussing the reasoning for all those decisions. |
Noted. Give me a few to 'stew on it' and I'll try to break it up into separate issues that can be better discussed in detail. |
See @bmeck , I am not here to rehash anything because that didn't bring us anywhere, that didn't solve anything, that wasn't beneficial for the community. You've already rehashed your arguments and linked old discussions, I didn't, and I'll keep doing that way. If you are here, please do change attitude and try to open your mind, like everyone else should do. If you are here to keep promoting and restating your last 9 months of discussions many others disagree with, then we won't move any forward and I rather do something else with my time. Let's not rehash, and let's try to move forward instead through understanding and compromises. Thank you. |
I am not ready to give up multiple years of discussions and work to start from scratch. I am willing to scrap implementations and create new things that are not compatible with the existing loader, but will continue to state my opinions and refer to previous discussions since I've been at this for a long time and expect to continue these discussions for a year or two more at least. |
good, then there's no need to rehash anything, specially with me. I'll try to keep as relevant and forward thinking as I can, I hope you will manage to do the same. |
Previously I was opposed to I have had precisely no issues with using |
I don't understand this sentence ... but to quickly answer your question: Accordingly, interoperability and portability across all other established client/server JavaScript environments is the reason "works for me" is also usually not a great indication that a solution is valid for everyone else. |
To be specific; browsers don’t need any extension to work, extensions are irrelevant to browsers themselves. They’re only relevant in that they can be a signal to webservers and build tools, many of which aren’t node and/or don’t have a JS parser available to them. |
I'm closing this thread for now. I think when we get back to this subject a fresh thread isn't a bad idea 😄 |
Kicking off this thread to discuss interop, specifically getting CommonJS (cjs) resources from inside of an ESModule(esm)
We currently have a transparent interop.
.js
is reserved for cjs and.mjs
is reserved for esm. When you import, statically or dynamically, the file extension is used to determine the resource type.There is a pull request to introduce modes which would change the behavior regarding file extensions depending on meta data in the package.json
There are two other proposals for how to get cjs resources that are non transparent, and would be necessary to if
.js
were to be aliased to esmThere has also been a suggestion of offering scoped variables in esm, similar to what we currently do with cjs.
What do people think?
The text was updated successfully, but these errors were encountered: