-
Notifications
You must be signed in to change notification settings - Fork 66
.mjs extension trade-offs revision #57
Comments
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. // Full CJS
const fs = require('fs');
const path = require('path');
const api = function(...params) {
// ...
};
module.exports = {
api: api
}; Which in ESM I guess translates to something like: // Full ESM
import fs from 'fs';
import path from 'path';
const api = function(...params) {
// ...
};
export {
api: api
}; Are there any problems so far in that particular example? |
Parsing (focus on when ESM is not a parse error but others are):
Eval differences (overlap parses and runs, but diff results)
Timing differences (not a problem, listed for posterity)
Potential relevant use cases w/o ESM specific declarations:In all cases where only
Potential relevant use cases that may combine source textsRelevant to when
Potential relevant use cases w/o
|
@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? |
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: import('./something-for-later-1')
import('./something-for-later-2')
import('./something-for-later-3')
import('./something-for-later-4') Such text is either a Script, CJS, or a Module, but could rely on something pretty easily that causes it to cease functioning: import(`${__dirname}/something-for-later-1`) |
@YurySolovyov for
We can make a good guess that it is CJS if we parse for |
It also would need to ensure no local variable named |
Also in theory that CJS can parse and eval just fine in Module. |
Will the absence of |
If |
Detecting CJS really won't work due to amount of overlap in that direction, detecting [something that is only] ESM is possible though. |
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 |
No, default needs to be CJS since that is what existing backwards compat needs.
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. |
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) |
Ok, I remember it was proposed at some point that we might want to have |
A file need not have |
@ljharb yup, which is why it needed standardization to remove wrong guesses |
What do you mean by "break" ? If the file is valid in some mode, you'll guess until you succeed, or you just report that file is invalid in all of them. |
The modes have different behaviors during execution time, it is not just about guessing how to parse the file. If you guess that something is an ESM, then Similarly, guessing something is a module and successfully parsing it means that code will now run in strict mode, but it's just as possible that a given file is a script that will fail when executed in strict mode. |
If we start with CJS, and that's indeed a module written with CJS in mind, we're ok. I don't think we should try to "make the most sense" of invalid modules. |
@YurySolovyov I think the point being made is that suddenly valid ESM is being treated as CJS. The implication being, ESM and CJS ambiguity shouldn't change how code evaluates. Hence attempt to pass a standard to remove the ambiguity. |
Ah, now I get it, I think. |
A polyfill, that is imported for side effects, that relies on implicit strict mode. |
Can you just import it with |
@YurySolovyov yes but then that means the consumer has to know what kind of module it is - and you shouldn't have to know that. |
@YurySolovyov that was one approach that was discussed in great depth, it means CJS permanently exists in all ESM of Node though, so no path towards a import "dep"; If the mode of "dep" must be known:
If the mode is not specified by how it is loaded:
It is safe to move to ESM even if your dependencies are not known if we provide a safe facade like the single |
We should also list other things outside of polyfills like:
the prefetch use / anything that only uses |
It gets into more trouble once source texts are combined when taking the parsing approach, that means in some 5000 line file, like 1337 might be an You might even remove that line and accidentally change how the whole file works as well. Or you might accidentally add an [ /me thinks of stack overflow mode poisoning w/ |
Problemspace comment I think is in a decent place now |
What exactly is going to break?
That's because Node supports CJS modules. |
Two things:
ProofI am the author of module export default function ok(cond) {
console.assert(cond);
}; The Node author of module import ok from 'ok';
function notok(cond) {
console.assert(!cond);
}
export {ok, notok}; Now QuestionWhat do you expect from the following files? index.mjs import {ok, notok} from 'test';
ok(true); index.js console.log(Math.random()); Question 2Assuming import {ok, notok} from 'test';
ok(true); I really would like to see how much ambiguity
AFAIK Node is moving away from CJS, isn't that the long term plan? Keep |
That is not a breaking change. Browsers never were and never will be able to run arbitrary Node code.
For module loader, it does.
Transpiling has absolutely nothing to do with this discussion. Babel's output is inherently not spec-compliant. |
Backward compatibility is about not breaking any existing code. If you use the new features, it is not existing code but new code. It sounds a bit as if you were complaining that What existing code would be broken by the introduction of Edit: And I approve the message above. As I told in an earlier message, one of the main reason for native support of ES modules is that transpilation + cjs will never be able to be spec-compliant. |
nobody can because they all have to rename their files, their imports, and their code that works already in every browsers and target Node too, because NPM is used for both browsers and node and I swear you can write Node targeting code without needing any of the Node core.
The problem is not that my code breaks in Node if I write I have to publish two damned identical ES2015 files and I've no idea what to put in the Do you guys ever write cross platform code? I target Node, browsers, and internet of Things engines. I don't live in a Node-only bubble. How is it even possible nobody is understanding that the real issue is that Node is incapable of doing what every other environment does by default? |
Anyway. I'm done here. As previously mentioned, my ideal world solution is here and it solves every real-world issue. If you don't like Best Regards. |
It's going to end up being pretty easy to make a foo.mjs (ESM) and foo.js (CJS ES5) file alongside one another; either manually (if you insist) or via a build process - bundlers will be able to transparently pick up whichever format they want, The story for all these use cases would not be nearly so simple with a different approach. |
I don't want that. I want a I don't use transpilers, I don't like them, I don't want to be force by Node to use them, neither I like to be forced by Node to write
bundlers have never been an issues. Modules shouldn't require bundlers by defaults.
The PR with If you don't like |
If a switch is provided that makes all .js files be treated as ESM, it will at best break on most of the npm ecosystem, and at worst it will silently do the wrong thing. It's the same reason that the flag to enable strict mode everywhere breaks horribly; because sloppy code is written to be sloppy, and needs to remain so. Similarly, CJS needs to remain CJS, even if it was published on npm 5 years ago and will never be updated (for example) |
I am trying to understand your examples in your previous comment (with the two questions): For both cases I expect it to run successfully: load the Does it corresponds to the code in the following repo: https://github.com/demurgos/mjs-example ? I can either run it with ES modules enabled, or "transpile" # Use ES entry point (with ES support)
node --experimental-modules index.mjs
# Simulate transpilation of `ok.mjs`
mv _ok.js ok.js
# Use cjs entry point (without ES support)
node index.js What is the problem? Could you provide an example repo if I understood your scenario wrongly? Edit: Just clone the repo and run the commands: it does work. |
no, it's a flag. developers know what they are doing (at least some of them). nothing breaks, already tried that.
no, you require CJS modules via CJS. Only core and real ESM modules are allowed as ESM imports.
You load that via
no, everything breaks always. I'm done with this thread. I am unsubscribing. |
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.
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.mjsexport * from './lib/index.mjs'; This approach does require that the author ensure that the default export of 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. |
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 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. |
I know a lot of discussion went into the original decision on how mjs would handle cjs modules but after using the system for a while, and talking with a lot of other people in the community, it just seems like the expected behavior and the wanted behavior is to export named based on the keys. This PR implements that in what is hopefully a performant enough solution, although that shouldn't be too much of a problem since this code only runs during initial module loading. This implementation remains safe with regard to named exports that are also reserved keywords such as `class` or `delete`. Refs: nodejs/node-eps#57
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 |
Given the following: But as soon as
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 |
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) Given that there’s nothing “incorrect” in not using .js, a simple |
They don't support it yet; at one time, long ago, they didn't support 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>
.
|
Nevermind, I just discovered the (much awaited) PR nodejs/node#18392 |
just on a lighter aesthetic aspect, I'd have liked |
@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. |
@tbranyen I personally don't know anybody who does, they use reify/esm |
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.
The text was updated successfully, but these errors were encountered: