nodejs / modules

Node.js Modules Team

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Feature: No refactoring

GeoffreyBooth opened this issue · comments

In the vein of “fast is my favorite feature,” I want to propose a feature that I consider important:

Existing code written using standard ES2015 syntax should continue to work without refactoring, without requiring a separate transpilation build step.

We may not produce an implementation that honors this 100%, but I think it’s important that we get as close as we possibly can. For most users, the only “spec” they’re aware of is ES2015+ and JavaScript syntax generally, and they’ve been told how to write import and export statements a certain way since 2014. If Node’s implementation doesn’t run code written “according to the spec,” this will feel like a breaking change to users, and many will argue that Node isn’t following the spec. They might not be correct, in many members’ view, but there’s a lot of logic to such an argument and it would be very harmful to adoption. It’s in the same vein as the uproar over .mjs; nowhere back in 2014 were we told that we would need to use the .mjs extension for our import statements to work, so therefore Node isn’t honoring the implicit usability contract that a standardized syntax is supposed to provide between users and a programming language: follow these rules, and the language will behave as it’s defined to.

The actual way to achieve this ultimately doesn’t matter. If vanilla Node does one thing but a loader plugin enables “legacy” mode, that’s fine. As long as the last four years’ worth of projects that use what was publicized as the “correct” ES2015 module syntax continue to work without refactoring, or with very minimal refactoring, this goal will be achieved.

Some might say, well, if users want their old code to work they should just keep using Babel. But by that logic, Node already supports import and export statements, and has for years—you just need to use Babel (or esm etc.). The whole point of “native” support is so that users can simplify or eliminate their build chains, to avoid the need for transpilation build steps and .gitignored dist folders and so on. Part of the reason for JavaScript’s explosive popularity is that it’s platform-agnostic and requires no compilation, and the need for transpilers in recent years has been a drag against JavaScript’s appeal. Put simply, transpilers are what users have now. The point of our work is to give them something better.

Use cases 40, 42, 43, 44, 48, and probably others.

This thread could become a catchall for any difficult-to-achieve feature, from #81 to plenty we haven’t discussed yet, so if people don’t mind could we discuss specific things like named exports in their own threads? And this one can stay more general.

Just so we're clear - this means code that uses ESM syntax through a transpiler should keep working without said transpiler eventually?

I'd love this - but we need to address the late binding issue somehow and currently babel transpiled ESM code gives you named exports for all cjs.

Does this mean that you believe we should do that?

@benjamingr as @GeoffreyBooth hints at, this desire to require 0 change is fairly comprehensive. It deals with cache strategies, disambiguation mechanisms, module shapes, etc.

I think we would need to discuss a lot of things regarding those. I'm not sure that this is a single feature exactly since it is so all encompassing and somewhat vague in terms of what is required to create the feature. Truly creating this feature would just be shipping a transpiler from the ESM Syntax to various existing forms instead of using native ESM stuff.

I'm not sure how to really approach this issue and converse over it, because I do think it would be ideal. However, I do see some issues with the language of it currently. Namely:

follow these rules, and the language will behave as it’s defined to

Assumes that transpilers have always been following the rules and semantics of ESM for whatever their output is. I think this is not true in a variety of ways and is not a valid conclusion of what happens from the goals provided. Providing the behavior of a transpiler that does not always match the specification is tantamount just shipping a runtime transpiler.

If we do that, we don't even need to address spec or binding issues since we won't be compiling to/from ESM. We will just be reusing the syntax.

this means code that uses ESM syntax through a transpiler should keep working without said transpiler eventually? . . . Does this mean that you believe we should do that?

@benjamingr Well . . . why not? What's the advantage for users in not supporting what they're already used to? Like I said, if we can't get there then we can't get there, but shouldn't this be our goal? Every other ES6+ feature that Node has supported has started life as a Babel transform, then users turned off that transform and their code continued to work.

@bmeck Babel isn't just some userland thing with no relationship to the spec. The process for getting new ECMAScript syntax approved includes creating a Babel transform or polyfill for the new syntax. That transform therefore is at least an artifact of the standardization process, if not part of the spec itself (as a working prototype for runtimes to emulate). It's not unreasonable for people to think that a Babel transform for a TC39 feature behaves according to the TC39-approved spec for that feature. And following from that, if my code works in that transform it should work in a runtime that implements that transform's feature.

Again, I'm not pushing an implementation here. Maybe vanilla Node supports import/export statements only in an all-ESM environment, and mixing in CommonJS involves adding a loader or configuring legacy/interop mode or something like that, whatever people feel comfortable with. But providing users with a way to preserve their current experience without a build step should be part of our scope. We shouldn't ship something less and then punt to userland to figure the rest out.

@bmeck thanks for that, I feel strongly about actually doing ESM rather than "reusing the syntax" while I also want to provide late bindings for CJS.

I do like your metadata solution for named exports (and the one Myles presented in #82 (comment) ).

It's also worth stating clearly that these goals (browser compatibility, works with native addons and late binding) are at odds by definition. I believe strongly that users should have a way to opt out of things that won't work when they try to run their code in a non-node environment.

The migration path for users can also be done with cooperation and coordination of the webpack babel and TypeScript teams who can provide one time transforms for the correct named export semantic and deprecate the non ESM behavior from their end. For example one thing we can consider for the 99% case is testing a tool that converts CJS to ESM automatically for those projects so that we don't have late bindings to begin with.

@GeoffreyBooth

Well . . . why not? What's the advantage for users in not supporting what they're already used to? Like I said, if we can't get there then we can't get there, but shouldn't this be our goal? Every other ES6+ feature that Node has supported has started life as a Babel transform, then users turned off that transform and their code continued to work.

Well yes, and we do have two things that are at odds here that are both important - we want to ship ESM with ESM semantics and we want users to not have to use a tool to transpile their existing code.

Without metadata (and maybe a tool generating it) or specifying multiple loaders (which would still mean they use babel or std/esm just not webpack) I'm not sure how that can be done.

@MylesBorins

and that some people may need
to refactor to use whatever default loader we ship

Due to this I think support for custom loaders with a clear and minimal ux
is the solution most likely to be successful.

See @GeoffreyBooth's opening comment

The actual way to achieve this ultimately doesn’t matter. If vanilla Node does one thing but a loader plugin enables “legacy” mode, that’s fine.

So there's agreement :)

Yes, that’s why I’m so excited by the loader plugin proposal: it can bridge these gaps. I would add that we shouldn’t stop at just shipping support for loaders, but also make one or more of our own, to handle all the interoperability and Babel backward compatibility concerns we can think of. Shipping these loaders at the same time as modules support, even if the loaders are just other modules installed via NPM, will hopefully forestall user backlash. And the process of creating loaders for all these use cases will toughen up the loader implementation itself, as we may have to tweak it so that loaders can be created to handle all these cases.

If we enable a pluggable loader for "legacy mode" (synchronous loading in primis), I think we can just be non-compatible from the start. The net result would be that everybody will use a custom loader that is non-compliant, creating the same problem we want to avoid (Node.js running a flavoured version of ESM).

was there ever consensus reached about what to do with magic variables in esm mode?

commented

hopefully no one is in favour of them 🤞