guybedford / ecmascript-modules-mode

ECMAScript Modules esm boundary proposal for Node.js
0 stars 0 forks source link

Does "mode" help or hinder? #1

Open robpalme opened 5 years ago

robpalme commented 5 years ago

I agree that by slicing and dicing this way, we are indeed making the mode switch more orthogonal to the other concerns, such as defining explicit ESM package entrypoints and encouraging encapsulation of packages. This proposal permits reuse of the existing CJS "main" field to now refer to ESM-mode files.

In my opinion, this undermines the clarity and integrity of the existing "main" field. The user-facing story for migration to ESM is already quite complicated. I'd suggest we don't confuse the "main" field by making it conditionally serve a second purpose based on the value of another field ("mode"). Over time, this allows "main" to be left behind as a compatibility-only feature for CJS consumers, with "exports" serving as the unambiguous preferred field to use.

A secondary reason for not introducing "mode" is that by guiding users towards "exports" we also facilitate encapsulation. This is healthy for promoting an ecosystem where packages can evolve over time without breaking their dependents.

guybedford commented 5 years ago

You've actually touched on the two gotchas with exports here:

  1. If I'm testing my package locally and haven't yet set "exports" I might be confused by "file.js" is being loaded as CommonJS when every tutorial I've followed set "exports" first then used ".js" and I just forgot to add exports. mode makes it very clear what the intention is when you write it. There is no hidden feature.

  2. While encapsulation is good and healthy we aren't in the business here of strong arming best practices, we're in the business of giving users what they want. This is certainly debatable though, but in general we are in a free market system not a single dominant all-powerful entity (especially given growing alternatives), so the best interest of Node lies in capturing its users wants and needs (even if those are bad for them), as the less pain users experience the more Node will prosper. Basically, I don't think we should force encapsulation as a side effect of achieving the ".js" as ESM user goal, as there is a potential for user frustration there.

Now I certainly get your point about the "main" here. I've specified the exact behaviours in https://github.com/guybedford/ecmascript-modules/commit/f4817c8bddeb42db6886f572a726ccbbf9052833. Please do let me know your thoughts further.

guybedford commented 5 years ago

I'd suggest we don't confuse the "main" field by making it conditionally serve a second purpose based on the value of another field ("mode")

It's worth noting that there is no conditional behaviour on the main from a user perspective.

robpalme commented 5 years ago

Response to Gotcha 1

This ordering hazard (forgetting to define "exports" first and triggering CJS interpretation) equally applies to "mode".

Response to Point 2

Overall I agree where best practices are unclear or controversial, clearly we should not force anyone's hand.

Encapsulation on the other hand is less controversial. Or at least I would argue that, if you want to use modules, you are implicitly asking for encapsulation. There's a reason the "export" keyword exists: to express your module's public entrypoints.

In this case I would also like to draw an analogy with strict-mode being force-enabled in ES modules. One could argue that introducing a new module system is orthogonal to tightening up the language specification. So why were they coupled? The reason is that the majority of developers recognize the value that a more constrained language provides to developers - especially providing it by default. If we really wanted to follow the principles of catering to everyone's wishes, we would be offering a "esm-strict-mode" flag to allow users to toggle it off; yet I doubt we would prioritize such a feature. Again, if you're opting into using modules, probably you already buy into the idea of opting-into stricter worlds.

Ryan Dahl recently acknowledged lack of package encapsulation as a design flaw in the original design of module.

Which leaves a diminishingly small population (or set of use-cases) that might actively want to avoid package encapsulation. I would argue that if these use-cases are real, we could support them in an opt-in fashion, e.g. we define "exports": "*" to means that every file is considered an entrypoint.

By default, I expect most users of modules will want package encapsulation just as much as they want module encapsulation and strict-mode.

guybedford commented 5 years ago

This ordering hazard (forgetting to define "exports" first and triggering CJS interpretation) equally applies to "mode".

From a semantic point of view, yes. But from a user perspective there is very different user understanding of the two.

Two examples:

  1. I will often start working on a package without setting the "main". The thought of setting the "main" does not correspond to the thought of wanting to use "js as ESM". And if I wanted "js as ESM" I now need to consider what my main is. It's a small thing, but it's a difference.
  2. When telling someone about "exports" there is a lot to digest: package encapsulation, main entry point handling, js as esm. There is a greater chance of misunderstanding when there is the added cognitive overhead. On the other hand "mode": "esm" is simple to explain because (1) it solves the problem the user was already having only when they want it (2) it just solves their problem, and doesn't come bundled with any additional cognitive overhead. Again, a small thing, but these things are important to user experience.

By default, I expect most users of modules will want package encapsulation just as much as they want module encapsulation and strict-mode.

This is an excellent argument, and I was almost convinced. But this argument only works when considering the "general user". We need to consider every user. Imagine one user who specifically wants an unencapsulated package (for whatever reason, it may be due to a large number of subpaths, instrumentation, internal tooling etc). Now this user will want to opt-out of encapsulation.

So their workflow is now the following:

  1. Wants ".js as ESM"
  2. Sets "exports"
  3. Now needs to set it as an object
  4. Now needs to set "exports": { "./": "./" } to opt-out of encapsulation

Again, it doesn't seem like much, but bear in mind that the opt-out syntax above is a little jarring to the eye. This sort of thing can easily feel like an over-encumbered Node.js organization placing unnecessary ceremony on users due to an inability to get consensus on simple proposals that optimize user experience :P

guybedford commented 5 years ago

Another thing to mention here is that the use case of publishing a dual mode package does imply encapsulation as you're after.

To publish a package that has a different main in CommonJS and in ESM one can do:

{
  "main": "./index-cjs.js",
  "exports": "./index-esm.mjs",
}

where using exports does opt-in to the encapsulation.

So that a very large swath of user workflows will be pushed into encapsulation here anyway.

guybedford commented 5 years ago

(the rule being exports always take precedence over the main)

robpalme commented 5 years ago

Agreed that "exports" does the job. That was not my concern.

It seems like the core argument is that "mode" is a toggle to cater for users that (a) don't use mjs, and (b) don't understand or like "exports", and (c) don't want encapsulation.

It feels odd to create a highly visible feature targeting just this set of users. It's like using an extreme court case to determine the law. It may get overused.

weswigham commented 5 years ago

I think they should be separate. Especially because I want to see exports support for masking cjs packages. I especially don't want to actually see "exports": { ".": "." } as an opaque way to indicate that .js should be treated as es modules - all other things equal, nobody would inherently think from reading that that that does that without knowing a lot of details about the implementation. I am much happier targeting exports at all users (current cjs library authors included!) and having a separate flag to control a separate aspect of resolution.

robpalme commented 5 years ago

If"exports" stops meaning "this package is ESM" because we backport it to CJS, then I buy the argument that we need an orthogonal toggle. Let's see if it lands in CJS.

I especially don't want to actually see "exports": { ".": "." } as an opaque way to indicate that .js should be treated as es modules

My argument above is that, whilst this syntax is possible, the set of people who want this use-case is very small. If you think of encapsulation as the default user desire, it's a hazard for people to accidentally disable encapsulation just by specifying "mode": "esm" and forgetting to add "exports".

weswigham commented 5 years ago

If you think of encapsulation as the default user desire, it's a hazard for people to accidentally disable encapsulation just by specifying "mode": "esm" and forgetting to add "exports".

I think an appropriate middle ground for this mindset is to have the split options, and ask npm to update --init's default template when it ships to set both. That way the "default" at an ecosystem level is still encapsulation, without mucking with mixing semantics in configs or relying on if it does or does not have cjs support.