You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Jul 31, 2018. It is now read-only.
Repository navigation
This repository was archived by the owner on Jul 31, 2018. It is now read-only.
I obviously can't speak for the whole community, but it seems like a lot of people are not happy with .mjs.
One of the main arguments to keep .js is that if we can detect 99% of cases where we CAN tell if is it CJS or ESM (or where we just know what to do), we may just call rest 1% edge cases and deal with it.
We can even come up with some linter rules and/or workarounds to simply teach people to do the right thing.
I want to start off with the 99% fallacy here. We are talking about module graphs. If 1% of your graph is wrong it can affect 100% of your graph. Let us keep that in mind.
Lets also try and enumerate where things differ between CJS, Script, and ESM whenever we talk about the problematic situations.
Lets also try and enumerate use cases where those situation might occur whenever we accept or dismiss them as being valid or invalid.
Let's start with simple stuff.
I think one of the most common patterns in node is to require() a bunch of core modules and export either one object with some API, or just export a plain function:
// Full CJSconstfs=require('fs');constpath=require('path');constapi=function(...params){// ... };module.exports={api: api};
Which in ESM I guess translates to something like:
// Full ESMimportfsfrom'fs';importpathfrom'path';constapi=function(...params){// ... };export{api: api};
Are there any problems so far in that particular example?
A path for CJS to being using ESM modules must exist.
It must be possible for CJS files to import() ESM.
Since this is available in the Script goal, this is automatically fulfilled.
Backwards Path
A path for ESM to be created that uses legacy or CJS files must exist.
It must be possible to import CJS files.
Some things such as Node core are unable to be upgraded to ESM due to design or usage differences. Other Modules such as meow use things that are not present/do not translate into ESM like module.parent (due to parallel loading, idempotency requirement, etc.).
Some things like "deep" linking is in use and necessary to support like "lodash/chunk"
There must be an upgrade path for ESM to be safe against CJS becoming ESM.
Generally achieved by ensuring CJS files are a facade with only a single default export
Mixed mode situations (both ESM and CJS in same app/package) must be supported. Note: again, there may be cases where code can never be updated to ESM.
ESM only future
It should be possible for the ecosystem to move to be ESM only for newly written code.
Whatever path is taken, it should be considered debt if files are still easily or accidentally able to be CJS.
Whatever path is taken, it should be ready for the so called "3rd goal" problem.
@YurySolovyov can you clarify what you mean by "Are there any problems so far in that particular example?" I mean, they do act similar but aren't the same.
I mainly meant that they don't introduce ambiguity, right?
Even if we'll have to parse them >1 times, we can with 100% certainty tell which is which, right?
I am not asking in this issue if ESM can fulfill a use case, I'm asking about use cases where ambiguity is possible / exists. Take for example a simple prefetching script:
We can make a good guess that it is CJS if we parse for require() but not all CJS uses require(). We need to define what mechanism are you using to perform these guesses.
If import and export declarations do not exist, it could be either ESM, [Script], or CJS. It is better to say import and export declarations (not import()) signifies something that currently is only able to parse in ESM.
So we can't just always start with ESM and then fallback to CJS if that failed?
Looking at all these parse errors in the table above, I don't think many of them are useful or just make a lot of sense in general.
I have a question though about let, yield, return and await: is this about top-level ones or what?
I can't imagine a module system where you can't use these in function's body.
No, default needs to be CJS since that is what existing backwards compat needs.
I have a question though about let, yield, return and await: is this about top-level ones or what?
I can't imagine a module system where you can't use these in function's body.
return works in CJS at top level.
let, yield, await, etc. are reserved words in ESM but not in the other goals.
Looking at all these parse errors in the table above, I don't think many of them are useful or just make a lot of sense in general.
To note as well: the guess cannot change over time. Once you ship the guessing mechanism it will stay backwards compat (so if something guesses CJS, it will always guess CJS even 10 years from now)
Again, I must be able to import CJS - otherwise users of my module will be forced to deal with a breaking change were I to migrate to ESM, which means I won't migrate to ESM - and neither will many module authors. That will kill the future we all want where ESM is the only module format.
I do not wish to endorse a specific path forward, but perhaps I can clarify a point where I see parties talking past one another.
These two claims seem to keep coming up, without a resolution.
Whatever node does, it should allow a package author to upgrade to ESM with breaking consumers.
With ".mjs", an author can't simply transpile because consumers still have to know to get the default property of the package exports.
Making it possible for a consumer not to know the format of a module is difficult. It is unreasonable to expect this without some work from authors. ESM distinguishes between the default export and the module namespace while CJS does not.
The following pattern requires a little bit of work, but address the issue for some cases.
index.js
// Notice that the default export is lifted here, for CJS consumers.// This is taking care of interopRequireDefault for non-babel, CJS consumers.module.exports=require('./lib/index.js').default;
index.mjs
export*from'./lib/index.mjs';
This approach does require that the author ensure that the default export of ./lib is its namespace, otherwise the results are different.
The situation is a little trickier when consumers want to do deep imports/requires. My intuition is that there is a pattern that would work for this, but would need to thinking further to see exactly how.
Well said. For deep imports, it'd be the same thing; for each entry point, include a .js file that requires and extracts .default - or for maximum backwards compat, author in ES5 CJS and manually create a .mjs file for each entry point.
Again, I must be able to import CJS - otherwise users of my module will be forced to deal with a breaking change were I to migrate to ESM, which means I won't migrate to ESM - and neither will many module authors. That will kill the future we all want where ESM is the only module format.
Right now CJS cannot take the shape of all ESM when imported thats why efforts are going on with tooling about how to fully support this. There is some push back because it would encourage people to just ship CJS+pragma in some people's eyes.
With ".mjs", an author can't simply transpile because consumers still have to know to get the default property of the package exports.
This isn't related to .mjs but to the fact that import is allowed to load CJS. There are similar effect to JSON files and C++ addons as well. I think this relates to my previous comment, but your solution seems workable.
I would like to see better first class support for generating Module Namespaces so that people can implement various module types land as we get user feedback from the loader hooks, as this would allow more rich integration with other module systems. That will take time though.
Given the following: index.js, es6moduleA.js
If I symlink es6moduleA.js as es6moduleA.mjs, then index.js can successfully import "es6moduleA.mjs"
(nodejs --harmony --experimental-modules --preserve-symlinks ) and es6moduleA.js is still usable by browsers.
But as soon as es6moduleA.js itself import 'es6moduleB.js', you're stuck:
Either it's imported using the .js extension (code works with browsers, file is served application/javascript by Apache, Nginx, php built-in webserver, ... ...)
Either it's renamed and imported using .mjs and javascript-code start to be node.js-only because most tooling is not compliant with this new extension and because JS-engines must not consider ES6 modules served using a wrong mimetype
Currently, a brand-new purely ES6 project, following the language syntax (using *.js file extension and working in most browsers) would not only just fail under node.js but there is currently no command-line parameter to make it work.
Extensions are not part of the language; there’s nothing “correct” about using .js except for what it conveys by convention: that it’s a JavaScript Script.
Browsers ignore extensions entirely; the point of them is to inform the server how to parse the file (to tell it what kind of file it is, to inform the mime type etc).
Following this reasoning servers are in charge of dealing with extensions -> mimetype (w.r.t to IETF) but they do not support mjs (and from the PoV I guess there is a few reason to support it).
Still node.js, which is not a server does not even offer an option to be extension-agnostic.
Given that there’s nothing “incorrect” in not using .js, a simple --esm is a must have so that even script.jpeg could be interpreted as an ES6 module.
They don't support it yet; at one time, long ago, they didn't support .js either. As soon as node ships .mjs unflagged, all the servers that want to stay relevant will ship support for it.
node certainly could take any extension - or even an extensionless file - and choose to parse it however it wants (including obeying a command-line argument). However, that wouldn't be very useful, and it would almost certainly hide bugs and mistakes.
Minor note that several MIME DBs (and so servers like express by proxy)
have the updated MIME that has gone through IETF with the new extension.
Also the goal parameter has been added to further disambiguate .js which
now has 3 well known meanings for IANA.
On Jan 27, 2018 11:48 AM, "Jordan Harband" ***@***.***> wrote:
They don't support it *yet*; at one time, long ago, they didn't support
.js either. As soon as node ships .mjs unflagged, all the servers that
want to stay relevant will ship support for it.
node certainly *could* take any extension - or even an extensionless file
- and choose to parse it however it wants (including obeying a command-line
argument). However, that wouldn't be very useful, and it would almost
certainly hide bugs and mistakes.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#57 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAOUo0UUV0Ouv0vmtuCLzZkocTgE_93Kks5tO2FdgaJpZM4NYUQA>
.
Reacted by Jordan Harband, Charles Samborski, Jared Kantrowitz, Nikita Skovoroda and J. S. Choi
Activity
bmeck commentedon May 11, 2017
I want to start off with the 99% fallacy here. We are talking about module graphs. If 1% of your graph is wrong it can affect 100% of your graph. Let us keep that in mind.
Lets also try and enumerate where things differ between CJS, Script, and ESM whenever we talk about the problematic situations.
Lets also try and enumerate use cases where those situation might occur whenever we accept or dismiss them as being valid or invalid.
YurySolovyov commentedon May 11, 2017
Let's start with simple stuff.
I think one of the most common patterns in node is to
require()a bunch of core modules and export either one object with some API, or just export a plain function:Which in ESM I guess translates to something like:
Are there any problems so far in that particular example?
bmeck commentedon May 11, 2017
Parsing (focus on when ESM is not a parse error but others are):
var argumentsvar evalimport("x")import "x";export {};with({}){}<!--\n-->\n0777delete xtry {} catch (eval) {}try {} catch (arguments) {}(function (_, _) {})eval = evalarguments = []implementsinterfaceletpackageprivateprotectedpublicstaticyieldreturnawaitEval differences (overlap parses and runs, but diff results)
thisvar x(function (){return this}())(function (x) {x = 1; return arguments[0];})()(function () {return typeof this;}).call(1)var x = 0; eval('var x = 1'); x__filename__dirnamerequireexportsmoduleargumentsTiming differences (not a problem, listed for posterity)
require('foo');import "foo";import('foo');Potential relevant use cases w/o ESM specific declarations:
In all cases where only
importdeclarations are usedimport()may be used instead. This list really is about things that do notexportvalues.source-map-support, some config scripts)import())vm.runInContext)Potential relevant use cases that may combine source texts
Relevant to when
importorexportdeclarations may be added/removed.Potential relevant use cases w/o
package.jsonnpmbin scripts have similar but not same)"-e","-p", REPL)node myapp.jsPotential tooling that lacks
package.jsoncapabilitiesnvmin particularPotential relevant use cases w/o a file extension
"-e","-p", REPL)node myapp.mjsPotential tooling that lacks file extension capabilities
None known yet.
Mandates
Resolution
Resolution in ESM is URL based and should be 100% compatible in non-error cases with the web specification. This is known to have minor differences with CJS.
Forward Path
A path for CJS to being using ESM modules must exist.
import()ESM.Backwards Path
A path for ESM to be created that uses legacy or CJS files must exist.
importCJS files.module.parent(due to parallel loading, idempotency requirement, etc.)."lodash/chunk"defaultexportESM only future
It should be possible for the ecosystem to move to be ESM only for newly written code.
bmeck commentedon May 11, 2017
@YurySolovyov can you clarify what you mean by "Are there any problems so far in that particular example?" I mean, they do act similar but aren't the same.
YurySolovyov commentedon May 11, 2017
I mainly meant that they don't introduce ambiguity, right?
Even if we'll have to parse them >1 times, we can with 100% certainty tell which is which, right?
bmeck commentedon May 11, 2017
I am not asking in this issue if ESM can fulfill a use case, I'm asking about use cases where ambiguity is possible / exists. Take for example a simple prefetching script:
Such text is either a Script, CJS, or a Module, but could rely on something pretty easily that causes it to cease functioning:
bmeck commentedon May 11, 2017
@YurySolovyov for
We can make a good guess that it is CJS if we parse for
require()but not all CJS usesrequire(). We need to define what mechanism are you using to perform these guesses.bmeck commentedon May 11, 2017
It also would need to ensure no local variable named
requireexists.bmeck commentedon May 11, 2017
Also in theory that CJS can parse and eval just fine in Module.
YurySolovyov commentedon May 11, 2017
Will the absence of
import/exportbe a better sign of CJS than trying to parserequires ?bmeck commentedon May 11, 2017
If
importandexportdeclarations do not exist, it could be either ESM, [Script], or CJS. It is better to sayimportandexportdeclarations (notimport()) signifies something that currently is only able to parse in ESM.bmeck commentedon May 11, 2017
Detecting CJS really won't work due to amount of overlap in that direction, detecting [something that is only] ESM is possible though.
YurySolovyov commentedon May 11, 2017
So we can't just always start with ESM and then fallback to CJS if that failed?
Looking at all these parse errors in the table above, I don't think many of them are useful or just make a lot of sense in general.
I have a question though about
let,yield,returnandawait: is this about top-level ones or what?I can't imagine a module system where you can't use these in function's body.
bmeck commentedon May 11, 2017
No, default needs to be CJS since that is what existing backwards compat needs.
returnworks in CJS at top level.let,yield,await, etc. are reserved words in ESM but not in the other goals.Indeed! thats why https://github.com/bmeck/UnambiguousJavaScriptGrammar went to TC39!
Eval errors are much more insidious. Too many chats going on right now for me to complete.
bmeck commentedon May 11, 2017
To note as well: the guess cannot change over time. Once you ship the guessing mechanism it will stay backwards compat (so if something guesses CJS, it will always guess CJS even 10 years from now)
224 remaining items
ljharb commentedon Oct 15, 2017
Again, I must be able to import CJS - otherwise users of my module will be forced to deal with a breaking change were I to migrate to ESM, which means I won't migrate to ESM - and neither will many module authors. That will kill the future we all want where ESM is the only module format.
tilgovi commentedon Oct 16, 2017
I do not wish to endorse a specific path forward, but perhaps I can clarify a point where I see parties talking past one another.
These two claims seem to keep coming up, without a resolution.
defaultproperty of the package exports.Making it possible for a consumer not to know the format of a module is difficult. It is unreasonable to expect this without some work from authors. ESM distinguishes between the default export and the module namespace while CJS does not.
The following pattern requires a little bit of work, but address the issue for some cases.
index.js
index.mjs
This approach does require that the author ensure that the default export of
./libis its namespace, otherwise the results are different.The situation is a little trickier when consumers want to do deep imports/requires. My intuition is that there is a pattern that would work for this, but would need to thinking further to see exactly how.
ljharb commentedon Oct 16, 2017
Well said. For deep imports, it'd be the same thing; for each entry point, include a .js file that requires and extracts .default - or for maximum backwards compat, author in ES5 CJS and manually create a .mjs file for each entry point.
bmeck commentedon Oct 16, 2017
Right now CJS cannot take the shape of all ESM when imported thats why efforts are going on with tooling about how to fully support this. There is some push back because it would encourage people to just ship CJS+pragma in some people's eyes.
This isn't related to
.mjsbut to the fact thatimportis allowed to load CJS. There are similar effect to JSON files and C++ addons as well. I think this relates to my previous comment, but your solution seems workable.I would like to see better first class support for generating Module Namespaces so that people can implement various module types land as we get user feedback from the loader hooks, as this would allow more rich integration with other module systems. That will take time though.
loader,docs,test: set named exports based on keys from `module.exports`
cztomsik commentedon Dec 12, 2017
Is this --esm flag ever going to happen before somebody will just fork node again?
BTW: I am currently using reify as a workaround but
.mjsis wrong decision in the long term.drzraf commentedon Jan 27, 2018
Given the following:
index.js,es6moduleA.jsIf I symlink
es6moduleA.jsases6moduleA.mjs, thenindex.jscan successfullyimport "es6moduleA.mjs"(
nodejs --harmony --experimental-modules --preserve-symlinks) andes6moduleA.jsis still usable by browsers.But as soon as
es6moduleA.jsitselfimport 'es6moduleB.js', you're stuck:application/javascriptby Apache, Nginx, php built-in webserver, ... ...)Currently, a brand-new purely ES6 project, following the language syntax (using *.js file extension and working in most browsers) would not only just fail under
node.jsbut there is currently no command-line parameter to make it work.ljharb commentedon Jan 27, 2018
Extensions are not part of the language; there’s nothing “correct” about using .js except for what it conveys by convention: that it’s a JavaScript Script.
Browsers ignore extensions entirely; the point of them is to inform the server how to parse the file (to tell it what kind of file it is, to inform the mime type etc).
drzraf commentedon Jan 27, 2018
Following this reasoning servers are in charge of dealing with extensions -> mimetype (w.r.t to IETF)
but they do not support
mjs(and from the PoV I guess there is a few reason to support it).Still
node.js, which is not a server does not even offer an option to be extension-agnostic.Given that there’s nothing “incorrect” in not using .js, a simple
--esmis a must have so that evenscript.jpegcould be interpreted as an ES6 module.ljharb commentedon Jan 27, 2018
They don't support it yet; at one time, long ago, they didn't support
.jseither. As soon as node ships.mjsunflagged, all the servers that want to stay relevant will ship support for it.node certainly could take any extension - or even an extensionless file - and choose to parse it however it wants (including obeying a command-line argument). However, that wouldn't be very useful, and it would almost certainly hide bugs and mistakes.
bmeck commentedon Jan 27, 2018
drzraf commentedon Jan 29, 2018
Nevermind, I just discovered the (much awaited) PR nodejs/node#18392
caub commentedon May 13, 2018
just on a lighter aesthetic aspect, I'd have liked
.estbranyen commentedon May 14, 2018
@caub Folks are already using mjs, IMO if Node chooses to use a new extension, it's far too late to try proposing a change.
cztomsik commentedon May 15, 2018
@tbranyen I personally don't know anybody who does, they use reify/esm