Skip to content
This repository has been archived by the owner on Sep 2, 2023. It is now read-only.

Module Interop (CommonJS resources from ESModules) #10

Closed
MylesBorins opened this issue Feb 5, 2018 · 42 comments
Closed

Module Interop (CommonJS resources from ESModules) #10

MylesBorins opened this issue Feb 5, 2018 · 42 comments

Comments

@MylesBorins
Copy link
Member

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 esm

  • import.meta.require
  • import {require} from 'nodejs'

There has also been a suggestion of offering scoped variables in esm, similar to what we currently do with cjs.

What do people think?

@weswigham
Copy link
Contributor

There has also been a suggestion of offering scoped variables in esm, similar to what we currently do with cjs.

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 require, exports, __dirname, and __filename in their codebase all at once. A userspace commonjs package, today, can provide shims for these using a global require and getters on the global object for the others, by inspecting the stack of an error thrown within them - not good for perf, but good for interop. It's bad enough that you may have to change your file extension and change all of your exports all at once - I think it's appropriate to make as many commonjs features available in a module scope as possible. Module "purity" is, in my opinion, not as importing as lowering adoption barriers as much as possible. And again, as I mentioned in the other thread - a userspace package can offer this today, so we should own the experience in node proper, instead, to make sure that behaviors with respect to loaders and hooks are respected, and that the way it behaves with respect to eval and vm contexts is well-documented.

@TimothyGu
Copy link
Member

See a part of my response here: #7 (comment)

When I was referring to vm module, I meant more about vm.runInThisContext.

@weswigham
Copy link
Contributor

weswigham commented Feb 5, 2018

Nobody migrating older code wants to do a big bang migration where you need to replace every reference to require, exports, __dirname, and __filename in their codebase all at once.

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.

@TimothyGu
Copy link
Member

There has also been a suggestion of offering scoped variables in esm, similar to what we currently do with cjs.

This is infinitely better than having a require hanging off of global, but as of right now it is not technically feasible, either in V8 or by spec.

@weswigham
Copy link
Contributor

And we (Node.js core) wouldn’t have to deal with the issue of tricky edge cases with eval or new Function that may result from doing that.

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).

@TimothyGu
Copy link
Member

most projects that I've met with that adopt new features do so very gradually

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.

@zackschuster
Copy link
Contributor

zackschuster commented Feb 5, 2018

+1 on import.meta.require, if only because feature detection would be easier: if ((const { require } = import.meta) != null) { }, for instance. afaict, dedicated modules would need to rely on exception handing to accomplish the same thing.

wildly incorrect

@zackschuster
Copy link
Contributor

zackschuster commented Feb 5, 2018

@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 import.meta without a suitably "magic" equivalent cropping up (like injected imports, tho idk if that's even technically possible).

@weswigham
Copy link
Contributor

@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.

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.

@devsnek
Copy link
Member

devsnek commented Feb 5, 2018

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.

@MylesBorins
Copy link
Member Author

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

@zackschuster
Copy link
Contributor

@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!

@giltayar
Copy link

giltayar commented Feb 6, 2018

@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 module.exports to their ESM equivalent (otherwise why make the change?). Making them also change __dirname to some form of import.meta.url and all require-s to import.meta.require isn't that big of a deal once they've choosen to do some amount of work.

@giltayar
Copy link

giltayar commented Feb 6, 2018

@devsnek - the mjs proposal definitely works, and moving incrementally from CJS to ESM is aan exercise in technical refactoring that could easily be helped by a codemod, except for one place that needs real code refactoring: if some code in a CJS module dynamically require-ed another CJS module, then moving that importing CJS module to ESM means transforming the require code that is synchronous to import code that is now async, and that may mean big refactorings in other places in the code.

I believe import.meta.require will alleviate that pain by not requiring that move to async. I'm still ambivalent about this proposal, because the pain alleviation is temporary—once the imported CJS module becomes ESM, the move from sync require code to async import code needs to happen anyway.

@weswigham
Copy link
Contributor

weswigham commented Feb 6, 2018

BTW, re @TimothyGu :

This is infinitely better than having a require hanging off of global, but as of right now it is not technically feasible, either in V8 or by spec.

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 __dirname and friends feels arbitrary)

@devsnek
Copy link
Member

devsnek commented Feb 6, 2018

you can also just type const __dirname = ... or const require = ... at the top of your file and not poison the new system. we don't leave behind people who depend on these variables by not having them attached to the scope, it's a pretty simple refactor step. I don't think we should have to keep our nasty past to suit people who are refactoring if it isn't absolutely necessary. honestly i find it absurd that we would have any intention to keep these scoped variables around, esm is a chance to learn from our mistakes, lets please please please take it.

@bmeck
Copy link
Member

bmeck commented Feb 6, 2018

@zackschuster

+1 on import.meta.require, if only because feature detection would be easier: if ((const { require } = import.meta) != null) { }, for instance. afaict, dedicated modules would need to rely on exception handing to accomplish the same thing.

import.meta is not available in the script goal so it is not available in CJS. I'm not sure I understand.

@MylesBorins

import {require} from 'nodejs'

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 import.meta exists. import.meta should be used over any idea of a contextual module when possible.

There has also been a suggestion of offering scoped variables in esm, similar to what we currently do with cjs.

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 '' specifier). Doing this requires the ability to insert references and cannot be done purely with source text.


I mostly agree with @giltayar here:

I believe import.meta.require will alleviate that pain by not requiring that move to async. I'm still ambivalent about this proposal, because the pain alleviation is temporary—once the imported CJS module becomes ESM, the move from sync require code to async import code needs to happen anyway.

I would even go further, stating that after porting to ESM, import.meta.require will be a hinderance and only provide technical debt, both to node and those trying to make their modules work in ecosystems without it.

@bmeck
Copy link
Member

bmeck commented Feb 6, 2018

@weswigham

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 getRelativeRequire which adds another thing to worry about.

@bmeck
Copy link
Member

bmeck commented Feb 6, 2018

@zackschuster When not in a Module goal import.meta throws an error during parsing.

@zackschuster
Copy link
Contributor

@bmeck ah, i see. my mistake. thank you!

@weswigham
Copy link
Contributor

At this point we are exposing the creation of require functions via getRelativeRequire which adds another thing to worry about.

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.

I don't think we should have to keep our nasty past to suit people who are refactoring if it isn't absolutely necessary. honestly i find it absurd that we would have any intention to keep these scoped variables around, esm is a chance to learn from our mistakes, lets please please please take it.

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).

@bmeck
Copy link
Member

bmeck commented Feb 6, 2018

@weswigham

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.

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 util module as mentioned above.

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).

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 __filename to pass around as a way to pass your module reference as a specifier since it would not contain search or hash fragments.

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).

@weswigham
Copy link
Contributor

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 util module as mentioned above.

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.

@bmeck
Copy link
Member

bmeck commented Feb 6, 2018

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.

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.

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.

I'm not sure I understand this, are you talking about wanting to put a bundler/build tool into core?

@weswigham
Copy link
Contributor

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 npm asset proposal is. I'd prefer to have a blessed tool in core rather than making it a community problem.

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 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.

@bmeck
Copy link
Member

bmeck commented Feb 6, 2018

That's effectively what the npm asset proposal is. I'd prefer to have a blessed tool in core rather than making it a community problem.

I have had many long discussions about why I believe we should never use such behavior or bless it.

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.

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 / can reroute to /index.html). Today, you can use all of the Node proposed path of ESM in the browser for ESM<->ESM compatibility in both environments except for 2 points:

  1. Node allows loading some module formats that browsers currently do not ~
  2. Node has a path resolution algorithms that was designed to take over in the cases where the WHATWG algorithm would produce errors and performs path searching.

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.

@weswigham
Copy link
Contributor

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...

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).

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".

I have had many long discussions about why I believe we should never use such behavior or bless it.

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.

@bmeck
Copy link
Member

bmeck commented Feb 6, 2018

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.

Yes, it is pushing it down to a different level than the environment.

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...

The same is the general review for trying to ship ESM without a bundler. It works fine in DEV but not in PROD.

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".

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 import.meta. The ability to load other formats should not be treated as a means to state that existing well known formats should not be compatible across environments. I would be more comfortable not using text/javascript to represent Node's usage of ESM if we start making custom local variables that are not to spec.

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.

Seems fine, I just lean on compatibility extreme, which includes not trying to bring legacy behavior that starts to break down in odd ways.

@WebReflection
Copy link
Member

WebReflection commented Feb 6, 2018

This is why I really want to know what the foundation's priorities are here

To me the foundation's priorities here can be summarized in 2 points:

  • ESM that works in browsers should work out of the box in NodeJS too
  • not vice versa, let tooling solve that for browsers

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.

Node's es module experience should be the best way to write code for node

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.

require in ESM? why not!

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.

@ljharb
Copy link
Member

ljharb commented Feb 6, 2018

"best" is subjective, however, and there's clearly some disagreement on that.

(to clarify; I don't have any objection to require working in ESM; I have an objection to forcing require for bringing in a CJS module)

@WebReflection
Copy link
Member

I have an objection to forcing require for bringing in a CJS module

the moment a file is not fully qualified as relative or absolute path, including fully qualified URLs, the import dflt from "shenanigans" can do whatever it wants to understand the target.

The KISS approach would be to use require to bring in CJS and import to bring in ESM, like you use obj.then() in native ES6 and await obj in ES2018 but we can complicate the non qualified import as much as we want because that should never compromise other environments.

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:

  • ESM that works in browsers (or other standard env) should work out of the box in NodeJS too
  • not vice versa, NodeJS can do by itself whatever it wants

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.

@bmeck
Copy link
Member

bmeck commented Feb 6, 2018

@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.

@evanplaice
Copy link

evanplaice commented Feb 6, 2018

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 import.meta.require for ESM compatible built-ins. Make require() async by wrapping it with await once top-level await is available. Not before. Am I missing something essential here, does require need to be completely re-engineered to work async?

Leave the existing built-ins as is. Don't screw with the existing Node.js ecosystem. Even when import.meta is implemented, the existing built-ins aren't likely to go anywhere for years anyway.

ESM support in CJS is already available via @std/esm.

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? @std/esm exists already. CJS/ESM interop isn't intended to be a 'permanent solution'. The FE ecosystem has used disposable polyfills for years with a great deal of success, why should Node.js be any different?

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.

@bmeck
Copy link
Member

bmeck commented Feb 6, 2018

@evanplaice that is a big proposal and would probably be best split into a separate issue of discussing the reasoning for all those decisions.

@evanplaice
Copy link

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.

@WebReflection
Copy link
Member

We can rehash those discussions if we desire.

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.

@bmeck
Copy link
Member

bmeck commented Feb 6, 2018

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.

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.

@WebReflection
Copy link
Member

I am willing to scrap implementations and create new things that are not compatible with the existing loader

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.

@Jamesernator
Copy link

Previously I was opposed to .mjs, however after the release of @std/esm I've been using .mjs for internal tools that share code between both a node server and the browser and frankly I can't say I have a single complaint with it.

I have had precisely no issues with using import fs from "fs" and the like to import CommonJS in the Node parts of my application (though @bmeck's proposal for named exports would be nice to have for the core libraries at least) so I no longer really understand the opposition to .mjs when it's such a simple to use solution.

@WebReflection
Copy link
Member

after the release of @std/esm I've been using .mjs

I don't understand this sentence ... but to quickly answer your question: .mjs is a work around for node only because every other environment that already shipped with ESM, including browsers, don't need a different extension to work.

Accordingly, interoperability and portability across all other established client/server JavaScript environments is the reason .mjs is not always welcome.

"works for me" is also usually not a great indication that a solution is valid for everyone else.

@ljharb
Copy link
Member

ljharb commented Feb 14, 2018

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.

@MylesBorins
Copy link
Member Author

I'm closing this thread for now. I think when we get back to this subject a fresh thread isn't a bad idea 😄

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests