nodejs / node-eps

Node.js Enhancement Proposals for discussion on future API additions/changes to Node core
443 stars 66 forks source link

adjusted proposal: ES module "esm": true package.json flag #60

Closed guybedford closed 6 years ago

guybedford commented 7 years ago

Update: This proposal has been updated to reflect the direction I believe to be the simplest and best for the ecosystem - an "esm": true flag in the package.json file.

The reason for this change from module is due to the interactions of module with other conditional main systems resulting in much unwanted complexity. For example browserModule was the natural next step here, which I don't think is a road that would be good for the ecosystem at all.

(added with comment at https://github.com/nodejs/node-eps/pull/60#issuecomment-343136052)

This spec provides a package.json "module" property and resolver implementation approach, to allow distinguishing ES modules in NodeJS. Unlike the In Defense of Dot JS proposal, it excludes supporting the "modules" and "modules.root" properties, while providing alternatives to the workflows these were designed for.

I've found in my own tooling work that having an active resolver spec would help a lot to ensure convergence towards where NodeJS will ultimately be heading. Webpack and Rollup both already implement this package.json "module" property, but the edge cases are not completely clear. If we have a formal active spec for "module", hopefully we can start to get everyone on board with a future-facing solution here.

ljharb commented 7 years ago

"Goal 2" there is still incredibly critical; there's far more existing packages that are maintained than the unmaintained ones, and those are the ones with the marketshare.

not-an-aardvark commented 7 years ago

To clarify, I agree that goal 2 is still important -- we should definitely try to make it easy for existing packages to migrate. I'm saying that we shouldn't make decisions that would benefit goal 2 at the expense of goal 1. For example, in response to this comment:

In that way, a small amount of friction [from having to import CJS modules and ES modules differently] is very likely to accelerate adoption.

It's possible that having two different import syntaxes would cause more packages to switch to ESM, but the "friction" would create a poorer experience for new packages and applications using ESM. I don't think it's worth intentionally causing pain for new users in order just to create an incentive for existing packages to switch.

Rich-Harris commented 7 years ago

something Node-specific like import.meta.require seems far preferable to using import for both ESM and CJS given the different semantics, even if you do eventually run into the same problem.

Could you clarify why? I understand how it would be useful to determine whether a file is browser-compatible by only looking at the file itself, and without traversing the dependencies. But since the dependencies themselves might use import.meta.require, it seems like traversing the dependencies would be necessary either way.

Because import is declarative, and import.meta.require (or require) is imperative — it seems that having a synchronous function call to bring in a CommonJS dependency is a lot more appropriate given how CommonJS modules work. I can understand how load/evaluate order works with import (dependencies are fetched and eval'd before this module runs), and I can understand how it works with require (it's just a synchronous function call), but when you import a CommonJS module it suddenly becomes rather harder to understand when a given bit of code will execute.

Requiring a different syntax to import different types of modules would hurt goal 1, because needing to distinguish whether dependencies use CJS increases cognitive load for future users.

It's possible that having two different import syntaxes would cause more packages to switch to ESM, but the "friction" would create a poorer experience for new packages and applications using ESM. I don't think it's worth intentionally causing pain for new users in order just to create an incentive for existing packages to switch.

A good analogy would be removing a band-aid or getting into a cold swimming pool — you can do it slowly, and spread the pain out over a longer time, or you can experience a short moment of slightly more intense pain and get it over and done with. If there's no real incentive for people to switch over to ESM (or provide forks/alternatives for those unmaintained packages), then we're going to be experiencing this pain for years to come.

(although it still seems inconvenient for a package to have two separate entry points).

Realistically, that's going to be the case until no-one needs to support versions of Node that don't support ESM. There's no way we can get to an ESM ecosystem without having multiple entry points (which is exactly why an incentive is required).

not-an-aardvark commented 7 years ago

something Node-specific like import.meta.require seems far preferable to using import for both ESM and CJS given the different semantics

Could you clarify why?

Because import is declarative, and import.meta.require (or require) is imperative — it seems that having a synchronous function call to bring in a CommonJS dependency is a lot more appropriate given how CommonJS modules work. I can understand how load/evaluate order works with import (dependencies are fetched and eval'd before this module runs), and I can understand how it works with require (it's just a synchronous function call), but when you import a CommonJS module it suddenly becomes rather harder to understand when a given bit of code will execute.

Does this matter from the perspective of the importer? If the import syntax is uniform, the importer module will use declarative syntax and get a reference to the module in either case. The imported module's type is an implementation detail in this case, so it's not necessary to make the execution order part of the public API.

I don't think it's worth intentionally causing pain for new users in order just to create an incentive for existing packages to switch.

A good analogy would be removing a band-aid or getting into a cold swimming pool — you can do it slowly, and spread the pain out over a longer time, or you can experience a short moment of slightly more intense pain and get it over and done with. If there's no real incentive for people to switch over to ESM (or provide forks/alternatives for those unmaintained packages), then we're going to be experiencing this pain for years to come.

I don't think it will ever realistically be "over and done with". It might convince some module authors to switch, but not all packages will end up migrating even in the long term. As a result, people will have to worry about the distinction indefinitely.

(although it still seems inconvenient for a package to have two separate entry points).

Realistically, that's going to be the case until no-one needs to support versions of Node that don't support ESM. There's no way we can get to an ESM ecosystem without having multiple entry points (which is exactly why an incentive is required).

Ideally, I think packages would continue to use CJS until they don't need to support those older Node versions, and then they would upgrade to ESM. This would allow a single entry point, and it's also consistent with how packages tend to handle other new syntax features (they use the lowest supported syntax, rather than running different files depending on which Node version is in use). With uniform import syntax, this would be a smooth transition and wouldn't cause any pain for consumers.

Rich-Harris commented 7 years ago

As a result, people will have to worry about the distinction indefinitely.

That's not something we should resign ourselves to. If in 2025 I'm teaching students about CommonJS because we made decisions in 2017 that allowed people not to bother updating their packages, I'll be very sad. Of course there'll still be a few hold-outs here and there, but we need to (and can) ensure that they're a small minority that we basically don't need to worry about.

Does this matter from the perspective of the importer? If the import syntax is uniform, the importer module will use declarative syntax and get a reference to the module in either case. The imported module's type is an implementation detail in this case, so it's not necessary to make the execution order part of the public API.

Other people here have made a big deal about the different loading/evaluation semantics between ESM and CommonJS (i.e., Babel ESM isn't real ESM). Those differences will be easier to understand and deal with if the point at which CommonJS semantics come into play is explicitly determined at the point at which you cross over from one to the other — i.e. at the point at which a CommonJS module is required from ESM. Otherwise, things get a bit confusing.

Ideally, I think packages would continue to use CJS until they don't need to support those older Node versions, and then they would upgrade to ESM. This would allow a single entry point, and it's also consistent with how packages tend to handle other new syntax features (they use the lowest supported syntax, rather than running different files depending on which Node version is in use). With uniform import syntax, this would be a smooth transition and wouldn't cause any pain for consumers.

All this does is delay the roll-out of ESM, which hurts the web. It would be a criminal shame if web developers weren't able to take advantage of ESM because packages still needed to support legacy versions of Node.

People are already shipping packages with "main" and "module" because there are tangible benefits to doing so — tree-shaking, faster bundles via scope hoisting, and better static analysis for example.

bmeck commented 7 years ago

I am back from vacation!

@hax ( https://github.com/nodejs/node-eps/pull/60#issuecomment-316644333 )

As I understand, most projects will eventually use modules for all source files, so we will not need fine-grained control over how a given file is parsed.

If any third parse goal appears like BinaryJS we will still want fine grained control and a plan to be safe for that in the future is appropriate since there is now WASM and WebPackage on the horizon.

@guybedford ( https://github.com/nodejs/node-eps/pull/60#issuecomment-317103659 )

I don't think there would be an immediate backlash at all for Node to release modules with just ".mjs". But as the bulk of users actually start writing and migrating their primary codebases to ES modules in this way, there becomes a desire for alternatives if that frustration is too much. @jdalton has already made it clear that he is working on a loader for NodeJS to retain a ".js" extension approach and will be sharing this approach widely. It only seems productive to have a plan for handling such possible outcomes.

+1 for letting people do whatever they want via userland opt-in.

The question of "if that frustration is too much" is not something that is quantifiable so much research has been done trying to find what problems exist.

It only seems productive to have a plan for handling such possible outcomes.

Clarify "possible outcomes". I assume you mean people refusing to change their file extensions. If that is the case "use module" is in discussion on TC39 side (though it is more complicated than it seems due to resolution thrashing).

@jkrems ( https://github.com/nodejs/node-eps/pull/60#issuecomment-317112091 )

P.S.: My own personal preference would be: Just drop any implicit compatibility between CommonJS and ES modules, period. Dynamic import is the only way to get ES modules from CommonJS, import.meta.require is the only way for the reverse. No magic fields, no file extensions, possible even separate path resolution rules. One can always dream... :D

Are you also suggesting we not allow ESM import for anything non-ESM like .json, and .node?

@guybedford ( https://github.com/nodejs/node-eps/pull/60#issuecomment-317165247 )

the transition I'm talking about here is specifically users who have already written js files as ES modules, with a compile step to publish CommonJS to npm (or production), who then want to transition to publishing those ES modules directly.

This will undoubtedly require code changes already. I am not clear on how this proposal prevents that. As mentioned above, I still would like a compatibility matrix of this PR and what existing tools do with "module" today.

@Rich-Harris ( https://github.com/nodejs/node-eps/pull/60#issuecomment-317177985 )

which preserves the correct behaviour around live bindings and cycles. My expectation is that the differing semantics around loading and evaluation will affect a very small minority of projects. Meanwhile, Rollup users shouldn't even have to worry about that.

Not really. This doesn't enforce the read-only on import side nor does it prevent invalid getter setups.

@unional ( https://github.com/nodejs/node-eps/pull/60#issuecomment-317209768 )

Also, the separation between .js and .mjs actually promoting two module formats.

I am not sure if promoting is the right word. "Supporting" is the word I would use.

There are two consumers of the ESM module, the machine, and human. The tool, such and Babel and TS, would like to keep the source as close to the resulting code as possible. That means the way to author the code should change and adapt. And it is in the benefit of the author to use the same semantic anyway.

I am in agreement but trying to see how this changes in any meaningful way with the suggestions provided. You still need the human to know both module systems. Tools can adapt fairly easy, but teaching many humans is hard. In both cases teaching is required.

Get transpiler to do that work in transpiling the code to CJS or ESM.

That is part of what the ESM EP standardized (see PR https://github.com/nodejs/node/pull/14369/files#diff-0ec386eadac8e6aaa87ab1075c7606a5R19 ). This could be done by a transpiler, but it requires a CJS capable Script mode to be available, it cannot be done purely using ESM.

NodeJS can support ESM in .js

Unclear on this point.

Switch over.

Unclear as well.

Of course, this is a very rough view and there are lots of challenges associated with it. But if we can fix postpublish, can we fix this in a similar manner?

Can we clarify how this relates to postpublish? Just that it would be a breaking change?

@Rich-Harris ( https://github.com/nodejs/node-eps/pull/60#issuecomment-317219412 )

Something that really doesn't get discussed nearly enough: if file foo.js can import a CommonJS file called bar.js, then foo.js will only work in Node. I cannot import it in a browser, but I can't determine that from looking at the file. By allowing the possibility that an ES module might eventually import some CommonJS, we are setting browsers up to fail — that is surely a far greater threat to the ecosystem.

Same for .json, and .node. Don't see this as a real talking point considering those aren't slated for removal nor do I see desire to remove them either.

@Rich-Harris ( https://github.com/nodejs/node-eps/pull/60#issuecomment-317253957 )

If a package has both "main" and "module", it can be consumed as either ESM or CJS, the implication being that "main" would eventually be phased out.

@Rich-Harris ( https://github.com/nodejs/node-eps/pull/60#issuecomment-317536932 )

when you import a CommonJS module it suddenly becomes rather harder to understand when a given bit of code will execute.

Can you clarify this a bit? import is very clear about when dependencies evaluate. Everything happens in ModuleEvaluation in the proper order.

A good analogy would be removing a band-aid or getting into a cold swimming pool — you can do it slowly, and spread the pain out over a longer time, or you can experience a short moment of slightly more intense pain and get it over and done with. If there's no real incentive for people to switch over to ESM (or provide forks/alternatives for those unmaintained packages), then we're going to be experiencing this pain for years to come.

Unclear on which side is the band-aid and which is the swimming pool. I think people would disagree so maybe a different analogy would be better?

@Rich-Harris ( https://github.com/nodejs/node-eps/pull/60#issuecomment-317559811 )

That's not something we should resign ourselves to. If in 2025 I'm teaching students about CommonJS because we made decisions in 2017 that allowed people not to bother updating their packages, I'll be very sad. Of course there'll still be a few hold-outs here and there, but we need to (and can) ensure that they're a small minority that we basically don't need to worry about.

As long as an environment keeps vestiges or support for CommonJS you will need to do this. Teaching is a moving target and things like WASM, BinaryJS, Webpackage, etc. are coming. I am not sure why you need to teach CommonJS in particular. Can you clarify why you need to teach it without this proposal, but not with this proposal?

All this does is delay the roll-out of ESM, which hurts the web. It would be a criminal shame if web developers weren't able to take advantage of ESM because packages still needed to support legacy versions of Node.

Can you clarify how this delays the roll out? You mean by giving a transition path for people who can't use ESM?

bmeck commented 7 years ago

I would like to keep discussion in this thread about the PR. If you want to re-litigate other issues please take them to the appropriate place.

Rich-Harris commented 7 years ago

@bmeck ok, but you just asked a bunch of questions... are you now saying you don't want the answers? 😉

bmeck commented 7 years ago

I ask questions to discuss. Most of the answers here vary based on point of view and I ask to better understand from my viewpoint

On Jul 25, 2017 5:12 AM, "Rich Harris" notifications@github.com wrote:

@bmeck https://github.com/bmeck ok, but you just asked a bunch of questions... are you now saying you don't want the answers? 😉

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/nodejs/node-eps/pull/60#issuecomment-317718475, or mute the thread https://github.com/notifications/unsubscribe-auth/AAOUowoC6TnQNv0yaDFdfmKl29Kbhhj5ks5sRduVgaJpZM4OYAqh .

chyzwar commented 7 years ago

Would module proposal allow to ship ESM in next node version(behind flag)? Maybe it would be better to ship ESM in this form and see what users(developers) need. If there is huge demand for CLJ compatibility layer then you can roll something.

@bmeck there is pragma proposal "use module" but why not "use clj"? From my perspective more JS code is for the browser, why we need to have pragma for ECMA compliant code but not for legacy CLJ ? The same follows for an extension, we could have .clj (ClojureScript :p) instead .mjs.

For WAS it is not JS and new extension would be appropriate. JS survive a new framework and major version of node twice a year . I think node.js underestimate velocity of JS community in the span of three years our code ES5->ES6 changed in a dramatical way.

Anyway, we currently have brain split. Almost all new front end code leverage ESM and then use transpiller/bundler. If node.js natively supports ESM we could drop babel for some greenfield projects. For full-stack JS projects, this would reduce cognitive load on having two module systems.

tniessen commented 7 years ago

there is pragma proposal "use module" but why not "use clj"? From my perspective more JS code is for the browser, why we need to have pragma for ECMA compliant code but not for legacy CLJ ? The same follows for an extension, we could have .clj (ClojureScript :p) instead .mjs.

Because of compatibility issues. Your suggestion would require all existing packages to be completely updated to work in newer node versions, and that is unacceptable. In other words, no existing package would work anymore as long as it is using CJS modules. We are trying to minimize backward compatibility issues.

If you want to argue with "ECMA compliant code": 'use module' could actually become ECMA compliant, see https://github.com/tc39/proposal-modules-pragma, but 'use clj' certainly won't.

Anyway, we currently have brain split. Almost all new front end code leverage ESM and then use transpiller/bundler. If node.js natively supports ESM we could drop babel for some greenfield projects. For full-stack JS projects, this would reduce cognitive load on having two module systems.

Be assured, we are working on a solution.

hax commented 7 years ago

Because of compatibility issues. Your suggestion would require all existing packages to be completely updated to work in newer node versions, and that is unacceptable. In other words, no existing package would work anymore as long as it is using CJS modules. We are trying to minimize backward compatibility issues.

That's where this proposal work. The author of new packages, or the maintainers of old packages which already use ESM with .js extensions by babel/webpack/rollup could just introduce module field. I can imagine they even could use .cjs extension for old commonjs module if two diff extensions is inevitable.

Fishrock123 commented 6 years ago

Oh my this is long. I simply do not have the capacity to consume all of this thread...


To explain the need here, there is a very definite desire from the JS community to continue to be able to use the .js extension. The efforts being made by @jdalton in this direction are very much in response to this, and I think it is important for the Node community to listen to these voices.

I would like to point out that as this is in no way a new discussion, we have been, and ultimately I'm not convinced there are any CTC members who really like having a second javascript file mode either.


I think we should frame this as an additive change for convenience of people who can't/won't use other file extensions rather than seeking a full set of support for all workflows.

@bmeck could you elaborate on "can't" use .mjs or "use module"? I mean, people will ultimately use with Node whatever we support out of the box so long as it isn't entirely convoluted.


I'm really not a fan of having more than one approach to do this, doing so will only cause a mess of confusion, especially for newcomers, down the line.

Also there seems to yet again be an implication that we will, within the foreseeable future, only have ES Modules.

... I would like to remind you that we cannot even get rid of sys.js years after deprecations. Script mode is here to stay for many years to come, whether we like it or not.

In addition, it seems that to would-be users of this, the dual-mode module implications of this as mentioned here seem quite detrimental.


If a centralized spec would be useful to packers in some form, that is a discussion we could attempt to put together but isn't really much in our realm anymore. (Not many people have Node run bundled js, after all.)

(Edit: That being said, thanks for writing an excellently put together EP.)

guybedford commented 6 years ago

Thanks @Fishrock123 for the review here. Certainly CommonJS isn't going away anytime soon, but while continuing to support CommonJS over the long term, we should also consider what the user experience will be for new NodeJS beginners using modules.

Imagine a new JavaScript developer learning to use modules in the browser with a .js extension, then being told when learning NodeJS that they need to change the extensions of their files to make them run. Perhaps all JS tutorials will follow NodeJS and use ".mjs" in the browser, but this proposal is exactly about what if that isn't the case.

In the scenario with this spec, the basic rule for getting started in Node for a new user, is always to set the package.json module property and then you can run your ".js" modules. It just becomes part of the standard process for any Node developer. Tools like npm could automate setting this property so that users just run npm init and then start writing code.

I hope my response above at https://github.com/nodejs/node-eps/pull/60#discussion_r130567765 clarifies the dual-mode considerations as well.

Yes this is another way to do the same thing, but when it comes to ecosystem conventions some level of darwinism must be at play.

I don't really mind too much where this gets specified - but my primary concern is forming a possible path here for JavaScript and tools that can be called upon when needed, forgotten if not.

mcollina commented 6 years ago

I have another objection to this proposal: what happens if a package.json file is place as the root of the filesystem? could this change the loading behavior of all .js files? I have seen inexperienced Node.js developers forgetting these files everywhere.

IMHO the proposal should be amended to remove the recursive search of package.json. If we run node server.js, we look for ./package.json, and set the "module" flags for all the files in . and deeper, if that's the case.

guybedford commented 6 years ago

@mcollina note that the first package.json file found becomes the package boundary, and the search stops at that point. So a root package.json file would only apply if the user is running a JS file that has no associated package.json. If users wanted "modules by default" when not using a package.json file that could be an interesting hack :P A normal package.json file in the root without a module property wouldn't alter the resolution at all though.

So what you describe in your amendment is effectively the exact behaviour already. It's not just about the entry point, but about every module in the app - and NodeJS permits modules loading cross-packages, so it's difficult to get around this search process.

ChALkeR commented 6 years ago

@mcollina, I disagree. Imo it should be recursive, starting with the js file being executed (not the cwd), and stopping on node_modules. There are valid usecases that would be broken otherwise, and placing a package.json at root is a clear user error that already breaks npm, so it's probably best to let user know about that.

Overall, I'm +1 on the current proposal, btw (after thinking about this), as long as this doesn't break other esm work.

tniessen commented 6 years ago

I'm not saying we should ship this approach today by any means, I'm just saying that having an approach and an active, relevant and current approach that is compatible with the way modules work in Node through the current PR implementation while being compatible with ".mjs" and "use module", is important to be able to say to those who question ".mjs" that there is an alternative, that it has been thought of, and that if they are interested there is a link to this proposal (or another), where they can engage, participate and help it become popular enough to gain adoption.

Following this argument, I would prefer to delay this discussion until node fully supports .mjs and / or 'use module'. I guess these will eventually be implemented and then we can still think about ways to make it more comfortable for users, as long as this or other proposals don't make those concepts unusable. Most of this dicussion relies on assumptions about our users and our implementation, and we won't know anything for sure until we figure out how to implement ESM properly. @guybedford is correct, it does no harm to have a proposal at hand, whether it is an open PR or an actually landed proposal, but IMO node is not ready for this proposal yet.

demurgos commented 6 years ago

Webpack and Rollup both already implement this package.json "module" property, but the edge cases are not completely clear. If we have a formal active spec for "module", hopefully we can start to get everyone on board with a future-facing solution here.

Please note that defining multiple entry points in package.json to support different configurations is already a familiar concept for library authors thanks to the "browser" field. In a similar way, I feel that the "module" field is a good solution to advertise that a library provides an ES6 version for environments supporting it. As stated in the first message, this currently allows bundlers to leverage the guarantees of ES6 modules for dead code eliminations ("tree shaking"). As a library author, this is already very convenient and I am not sure how *.mjs or "use module" would address this issue (providing an es6 version with a cjs fallback for the same package).

teppeis commented 6 years ago

@demurgos See https://github.com/nodejs/node-eps/blob/master/002-es-modules.md#44-shipping-both-esm-and-cjs You can ship both ESM and CJS without module field in package.json.

Rich-Harris commented 6 years ago

Following this argument, I would prefer to delay this discussion until node fully supports .mjs and / or 'use module'.

Realistically, if that happens then .mjs would be treated as a fait accompli and this proposal would be dead in the water regardless of its relative merits. I think this issue deserves a resolution before Node takes that decision on behalf of the entire JavaScript ecosystem.

You can ship both ESM and CJS without module field in package.json.

Note that this relies on module authors leaving off file extensions and letting Node guess at them until it finds a match. We've got away with that decision until now because we've been forced to use tooling to use CommonJS modules in the browser, but browser module loaders won't allow you to leave off file extensions for obvious reasons (AFAIK — please correct me if I'm wrong). For that reason, though it's outside the scope of this discussion, I would urge Node to consider not filling in missing file extensions when dealing with modules, since that's browser-hostile behaviour.

bmeck commented 6 years ago

@Rich-Harris note that browsers can perfectly support things without .js extensions such as:

import './foo';

It is up to the server to determine what gets served here. They can do redirects to create new Module Records or do export * from "./foo.mjs"; and/or export default from "./foo.mjs";. These also do not even need to be dynamic lookups but can be done when uploading the files to a server.

jkrems commented 6 years ago

White it's true that servers can do any kind of magic to remap / rewrite files, it's still valid to point out that browsers won't. I think realistically, if you want to have support in bundlers, editors, etc. you'll be forced into one (or at least "a limited") set of semantics. So if you want to make sure your distributed package can be used properly "no matter where", your best bet right now seems to be to specify the file extension for modules.

bmeck commented 6 years ago

@jkrems not entirely true that browsers won't, such mechanics are possible in browser with service workers etc. Though that is userland mutation.

jkrems commented 6 years ago

@bmeck I think my more general point here is "my library won't control that". So writing my (library) code to depend on the user using a special service worker- or server setup seems icky. Not saying that it's prohibitive, just yielding that it's a genuine downside. :)

bmeck commented 6 years ago

@jkrems I think a more positive path forward is to work with browsers to support path aliasing of some form. I am not clear that removing path aliasing has concrete advantages here if there are many approaches like mentioned here to solving the problem and the use of tooling is already almost mandatory. You could write code that uses the WHATWG resolution mechanics but that has limitations already like lack of support for node_modules and package.json

Rich-Harris commented 6 years ago

Why exactly is adding file extensions behaviour worth preserving? It slows down application startup, increases the complexity of every piece of tooling that has to deal with Node's resolution algorithm, makes code less explicit and more magical, and is browser hostile (counter-arguments that involve adding unnecessary complexity to servers and service workers notwithstanding).

All for the sake of saving a few keystrokes!

and the use of tooling is already almost mandatory

The beautiful thing about native modules — if Node doesn't make browser-hostile decisions — is that tooling won't be mandatory for a much larger set of cases than it currently is. It's not a binary thing — it's about how much you can do without tooling, and how much tooling you need when you do. Let's make decisions that benefit everybody, not just Node users.

Fishrock123 commented 6 years ago

re: @guybedford

Imagine a new JavaScript developer learning to use modules in the browser with a .js extension, then being told when learning NodeJS that they need to change the extensions of their files to make them run.

This isn't a correct comparison. Modules will be undetectable by .js alone in the browser. Those users will already have to change the script tags to use ~application/module (or whatever the content-type is).~ <script type="module" ... > (but top-level only).

In the scenario with this spec, the basic rule for getting started in Node for a new user, is always to set the package.json module property and then you can run your ".js" modules. It just becomes part of the standard process for any Node developer.

Assuming of course this is the only way to do it...? What happens when those people then encounter .mjs or "use module", or the people from there discover this?

Yes this is another way to do the same thing, but when it comes to ecosystem conventions some level of darwinism must be at play.

I don't really buy this, it's the same things TC39 did with promises instead of using the full set of tools at their disposal to solve the problem at hand.


re: @demurgos

As stated in the first message, this currently allows bundlers to leverage the guarantees of ES6 modules for dead code eliminations ("tree shaking").

While perhaps slightly-off-topic I would like to point out that https://github.com/indutny/webpack-common-shake exists and works.


All for the sake of saving a few keystrokes!

@Rich-Harris wait, are you saying saving keystrokes is an argument for the extension? because it really never has been and it (and "use module") have always been argued against under that pretence...

bmeck commented 6 years ago

@Rich-Harris

Why exactly is adding file extensions behaviour worth preserving?

Abstraction of implementation details while preserving the idea of modules as being interfaces. It does not matter if my implementation goes from ESM to WASM and vice versa if the interface remains the same.

It slows down application startup, increases the complexity of every piece of tooling that has to deal with Node's resolution algorithm, makes code less explicit and more magical

This is also true of this proposal. I am not convinced speed is a relevant concern if people are trying to speed things up for Node. Bundling saves both disk size and startup times for applications in Node.

is browser hostile (counter-arguments that involve adding unnecessary complexity to servers and service workers notwithstanding).

I will not accept this unless bringing in other behaviors such as package.json (they don't necessary live in node_modules as this proposal notes), and node_modules are also talked about. Browser hostility is also present from those but there doesn't seem to be the same discussion about them. Are they not browser hostile / why? What makes this one aspect different. Also I would be happy to hear about the difference in browser redirects and symlinks in this regard.

This proposal uses package.json so talking about why that browser hostile choice is different/ok seems prudent.

The beautiful thing about native modules — if Node doesn't make browser-hostile decisions — is that tooling won't be mandatory for a much larger set of cases than it currently is. It's not a binary thing — it's about how much you can do without tooling, and how much tooling you need when you do. Let's make decisions that benefit everybody, not just Node users.

I am once again stating that browsers need to support path aliasing of some sort if they are to integrate directly with the npm ecosystem. I am not sure why users would be unable to use a browser supplied aliasing mechanism.

@Fishrock123

This isn't a correct comparison. Modules will be undetectable by .js alone in the browser. Those users will already have to change the script tags to use application/module (or whatever the content-type is).

ESM and Script in browser share the same MIME type of application/javascript. However, they will need to still change their source texts (in non-trivial ways if they have a dependency which is a Script and not an ESM), and can happily use .mjs for all files (or .php for all their files if they want).

Rich-Harris commented 6 years ago

Abstraction of implementation details while preserving the idea of modules as being interfaces. It does not matter if my implementation goes from ESM to WASM and vice versa if the interface remains the same.

Really?! That seems like an incredibly niche case. If an implementation switches from ESM to WASM I would expect to have to change the file extension, and for that to be the least of the changes I would expect to have to make.

This proposal uses package.json so talking about why that browser hostile choice is different/ok seems prudent.

This proposal is about getting modules to work in Node. Not sure what your point is! Node needs the package.json in order to figure out what to do; browsers wouldn't (as long as we don't do something silly like write import declarations without file extensions).

I will not accept this unless bringing in other behaviors such as package.json (they don't necessary live in node_modules as this proposal notes), and node_modules are also talked about.

Again, this is not a binary thing. It's about making browser-friendliness a higher priority. No-one is suggesting that everything that's possible in Node should also be possible in browsers. Bare imports remain a TODO — I get that. It is no reason to abandon browser-friendliness as a goal in general.

bmeck commented 6 years ago

Really?! That seems like an incredibly niche case. If an implementation switches from ESM to WASM I would expect to have to change the file extension, and for that to be the least of the changes I would expect to have to make.

I would find import 'react'; changing implementation needing me to change things a violation of it being a black box.

This proposal is about getting modules to work in Node. Not sure what your point is! Node needs the package.json in order to figure out what to do; browsers wouldn't (as long as we don't do something silly like write import declarations without file extensions).

If this is not relevant to the browsers, I defer to the following response.

Again, this is not a binary thing. It's about making browser-friendliness a higher priority. No-one is suggesting that everything that's possible in Node should also be possible in browsers. Bare imports remain a TODO — I get that. It is no reason to abandon browser-friendliness as a goal in general.

My point remains that we are not browser friendly (due to package.json and node_modules) and browser already need to find a happy path for path aliasing. What causes the assumption that path aliasing won't work for the case of extension searching?

Rich-Harris commented 6 years ago

My point remains that we are not browser friendly and browser already need to find a happy path for path aliasing.

They need to find a solution to bare imports, eventually. They don't need to find a solution to relative imports, because that will work exactly how you'd expect. To suggest that browsers should append file extensions is no less absurd than suggesting that <img src='./thing'> should automatically resolve to thing.jpg or thing.png.

Frankly though, I'm done with this conversation. There seems to be an unwillingness to acknowledge that Node has a responsibility to the JS ecosystem beyond Node itself, and an insistence on responding to good faith attempts to suggest ways to smooth over incompatibilities with reductio-esque arguments. I tried. Unsubscribing from this issue.

Fishrock123 commented 6 years ago

Ok so, it seems two large things favor this approach so far:

  1. Node does not special case for Script mode javascript currently and any unrecognized filetype will be executed as a script.
    • (But extensions are already used for other "modes", such as JSON.)
  2. Browsers only have module mode specification at a top level.
    • (It would seem largely because Scripts have no module system in browsers.)
    • This does actually mean the extension and "use module" are more work than browser detection.

For (2) specifically this approach seems better but is still far off of what a browser does.

A proposal closest to that would probably look like node --module / node -m, and module.import() dictating module mode trees. I think this was discussed at some point.

I don't mind that idea too much but it does leave holes where we have an existing module system and browsers do not, particularly dual-mode packages.

bmeck commented 6 years ago

They need to find a solution to bare imports, eventually. They don't need to find a solution to relative imports, because that will work exactly how you'd expect. To suggest that browsers should append file extensions is no less absurd than suggesting that <img src='./thing'> should automatically resolve to thing.jpg or thing.png.

Given all of my discussions so far about realistic browser hooks needing to be synchronous, precomputation of some kind (tooling or manual) is expected for path aliasing. I would find no problem in appending .php to a specifier and am not sold on the idea that import resolution hooks only apply to bare imports.

bmeck commented 6 years ago

@Rich-Harris what about relative imports that hit a package.json like:

entry.mjs -> import('./foo')
foo/
- package.json -> main: ./lib.mjs
- lib.mjs
Rich-Harris commented 6 years ago

Right, I'm saying 'don't do that'. That's bad code. This is what I meant by reductio-esque arguments — no-one actually does that (and if they do, well, they shouldn't).

If someone says that they're unsubscribing from a conversation, please don't @ them to drag them back in.

bmeck commented 6 years ago

Might also be another thing here with how symlinks work in Node (shared singletons based upon the realpath); you would also need to hook into anything (relative/absolute/or bare) that uses symlinks since HTTP redirects create new Module Records instead of singletons.

demurgos commented 6 years ago

@demurgos See https://github.com/nodejs/node-eps/blob/master/002-es-modules.md#44-shipping-both-esm-and-cjs You can ship both ESM and CJS without module field in package.json.

@teppeis Thank you for the link, it's obvious in retrospect that Node can resolve it based on the extension, even if I tend to agree that this extension-based resolution adds a cost to consume packages in the browser. It was already discussed yesterday so I won't come back to it.

While we're still comparing the different solutions, I'd like to add a minor difference. "module" allows a more flexible directory structure than *.mjs when publishing packages supporting both modules and commonjs (= transition period). Since the *.mjs way is to use the extension, the pathname and basename for the main module must be the same: you cannot have dist/cjs/index.js and dist/es/index.mjs (distinct directories for each variant) while "module" would allow it. I guess that for hand-written libraries it wouldn't be a real issue, but for compiled / transpiled libraries having a distinct build directory for each target could be slightly easier to work with. It's not a big deal but I don't think it was mentioned earlier.

bmeck commented 6 years ago

@demurgos if there is a build directory it can be used for all things. Compiling Flow / TypeScript / JSX / babel plugins / etc. can all be placed into /dist. It is also possible to use .npmignore, or "files" in your package.json to avoid shipping the unneeded source.

/src
- main.ts
/dist
- main.js
- main.mjs  
demurgos commented 6 years ago

I still think that it is slightly less flexible, even if it should never be a problem. It's still very hypothetical since there are no packages using both systems yet. What if I have other resources that depend on the module system? For example an HTML template containing some JS (like some sort of Angular template supporting imports).

/src
- main.ts
- template.html    # Will be pre-processed because it contains some embedded JS
/dist
- /es
  - main.mjs
  - template.html  # Built to use modules
- /cjs
  - main.js
  - template.html  # Built to use commonjs

I know that this example is already pretty contrived and since it already assumes some pre-processing, it could be solved in a few lines (like renaming the templates to *.es.html and *.cjs.html) to use a single dist directory.

A more simple example why some people could prefer distinct directories is that it's easier to clear the build of one variant simply by deleting the directory.

Finally, I am aware that if I desperately want distinct directories, I can use two one liners at the root of dist:

/src
- main.ts
/dist
- /es
  - main.mjs
- /cjs
  - main.js
- main.mjs      # export { default } from "./es/main";
- main.js       # module.exports = require("./cjs/main.js");

Again, I am not sure if these examples have any real-world use, I just wanted to illustrate that "module" allows you to have independent entry points which can sometimes be more convenient (even if an *.mjs solution always exists). This flexibility seems more useful with "browser" but it could have some use with "module".

Fishrock123 commented 6 years ago

@guybedford Again, we'll need to consider something for -e regardless of other detection proposals. 😅

teppeis commented 6 years ago

@demurgos Another solution for the first example is

/dist
- main.html
- /es
  - main.mjs
- /cjs
  - main.js

and the main.html is

<script src="es/main.mjs" type="module"></script>
<script src="cjs/main.js" nomodule></script>

For the second example, this is a sample project. (working with current pr)

I just wanted to illustrate that "module" allows you to have independent entry points which can sometimes be more convenient

Yes, it seems nice in some cases. I agree with @tniessen's https://github.com/nodejs/node-eps/pull/60#issuecomment-319333156, since .mjs is the simplest way to understand and implement. After shipping .mjs as flagged feature, we can continue to discuss other proposals with the feedback.

mscdex commented 6 years ago

I'm -1 on requiring a package.json just to use an ES module. I much prefer .mjs or "use module". One benefit of .mjs is that it's very clear that something is an ES module without opening it. One benefit of "use module" is that it allows for easier sharing with browsers.

I should also say I am -1 on having multiple ways to incorporate an ES module. I think it will just create unnecessary confusion.

That's my 2 cents anyway.

guybedford commented 6 years ago

@mscdex certainly ".mjs" is better if we can get everyone behind it. But the worry is that as long as there continue exist tools, workflows and scenarios where users have ".js" containing ES modules, this is going to cause difficulty and fragmentation when these workflows brush up against NodeJS workflows.

ljharb commented 6 years ago

imo everyone will get behind whatever node supports, as long as it meets all the use cases - and "a separate file extension" does.

Trott commented 6 years ago

I'm removing the ctc-review and ctc-agenda labels for now, as I believe the approach here is expected to be "Let .mjs land and see how that goes before figuring out what if anything to do with this."

TheLarkInn commented 6 years ago

Hi!! Sean here (from webpack). Sorry for showing up so late to the show here. But on behalf of our ecosystem and team, we are very much for this prop. +1.

Wanted to respond to some things: @mcollina

The problem with this proposals is that, before loading any javascript file, Node.js would have to resolve the nearest package.json, and parse that. Currently we are doing it only for things in node_modules, and only to know which one is the main. I think it would be very hard to implement this proposal in a way that does not cause slowdowns in loading applications composed of 100s of files.

Technically we have this implemented in a package we call enhanced-resolve. This powers all of the customization for module resolution and in addition allows you to specify separate main fields and description files.

Although we collect dependencies at build time vs runtime, there is still merit to examine this resolution pattern and see if it can drive inspriation for the implementation for "module" field.

I have another objection to this proposal: what happens if a package.json file is place as the root of the filesystem? could this change the loading behavior of all .js files? I have seen inexperienced Node.js developers forgetting these files everywhere.

IMHO the proposal should be amended to remove the recursive search of package.json. If we run node server.js, we look for ./package.json, and set the "module" flags for all the files in . and deeper, if that's the case.

Here is how we have implemented the resolution pattern. Mind you ours is very pluggable so there are a lot of paths that may not be necessary but I think could address many of the concerns.

screenshot_20170809-220849

The labels which connect to boxes which represent configurable options. Ideally the resolution pattern always starts with A (CACHE) and continues through the algo and recursion only occurs if needed. Each box represents a plugin which implements that piece of the resolution algo.

Hopefully food for thought to help support implementation.

ljharb commented 6 years ago

(probably unrelated to the thread, but what is a "concord module"?)

bmeck commented 6 years ago

@ljharb another module system that was proposed by WebPack but never rose to popularity. https://github.com/webpack/concord

guybedford commented 6 years ago

I understand this thread has a lot of history, but I am withdrawing the "module" package.json proposal as discussed here, and altering instead to a "esm": true package.json flag proposal. The reason is that module just has too many edge case interactions (eg what happens for browser / electron / other platforms - we need combination main specifiers). A boolean flag handles this for us - { "esm": true, "main": "index.js" } can support an ES module main fine, and work with the existing alternative main conventions much more nicely!

I am personally moving forward with this proposal as the primary mechanism of distinguishing ES modules in my own work, based on a package.json lookup for esm: true check for all module loads.

If there is interest, I'd be happy to work on a resolver PR for NodeJS to add this, based on the careful spec here. Happy to discuss these directions anytime as well.