Open wesleytodd opened 4 years ago
@wesleytodd the concern isn't that loaders or policies make import maps undesirable, but how import maps can be picked up and properly mesh with those features. I don't think either loaders or policies are a means to dissuade import maps, but without a story on how things like loaders can instrument/reflect upon the import maps loaded it seems hard to add import maps as they don't have a clear integration plan. Same for policies.
Speaking of the spec work: It's not like we have to implement import maps right now to be actively involved in that spec work. And it's not like a non-browser implementing something called "import maps" will necessarily influence a web spec with the same name. Import maps aren't a JavaScript spec and they're unlikely to become one (because neither the people behind the spec nor the people on the TC39 side would want that afaik).
There are multiple people from the node.js side talking to people involved in the import maps spec and implementation in browsers. There's an important distinction between "is node influencing the shape of import maps" and "is node implementing import maps today using the exact spec that may or may not (!) one day ship unflagged in browsers".
There are expectations in node that plain don't exist in browsers. And at the same time, the import maps spec is in a very specific part of its lifecycle: There's an attempt to find a minimal spec that can be standardized for the web and then built upon. We already know that this minimal spec is a) not sufficient for the features we'd like to support for module loading in node and b) potentially going to be extended in the future.
I have every plan to continue to engage with import maps as it evolves, as do other node collaborators. So far I don't have a reason to believe that node implementing the current spec would sway the future direction any more than those current engagements. I don't think we're a follower here just because we don't have the minimal spec implemented in node core.
i'll add that "implementing specs before they're stabilized" has historically led to significant problems elsewhere (e.g. typescript, babel) and imo should not be considered viable due to those risks.
@jkrems builtin modules will require an import-map-like facility to be able to proceed.
@ljharb You mean within Ecmascript? If so, how would that fit into the spec? I personally would be surprised if that level of resolution detail would leak into builtin modules on the ecmascript level. I would expect that to stay a host concern.
Yes, and that's a difficult challenge for the champions of that proposal.
It is for the host to resolve the module-specifier
but not to dictate the shape of it (as the module may be generic/host-invariant).
For example, if some library imports the function add
from a module MyMath
, it doesn't know if the resolve will be file based, network based or already loaded in a game executable. So it must simply import {add} from "MyMath"
.
So, to for a host to REALLY support ESM, it should:
In Chromium and Deno, the solution to (1) and (2) is imports: { "MyMath", "/whatever/my-math.js" }
.
So, what is the solution in Node?
(imperative loader hooks fails at (1) and other suggestions at least fails at (2)).
I don't think either your point 1 or 2 is failing currently, but it certainly isn't the same as import maps. Improvements to UX are always possible, but I don't see how the last comment is a claim that import maps are the way to solve things.
@bmeck
I don't think either your point 1 or 2 is failing
I don't understand. The example wants to import add
from MyMath
using Node. What method is not failing on 1 and 2?
I don't see how the last comment is a claim that import maps are the way to solve things.
I agree. It is merely a way. What is the Node way with respect to the import {add} from "MyMath"
example?
"implementing specs before they're stabilized" has historically led to significant problems elsewhere (e.g. typescript, babel)...
It is a balance. The specs that are created in a vacuum have a tendency to fail miserably. So somebody has to lead. Specs that are created in a vacuum can have no feedback loop. This does not mean that everyone has to lead. For example, when IE did not lead, others browsers had to.
What is the Node way with respect to the import {add} from "MyMath" example?
The same as with almost every other tool (bundlers, linters, etc.): Create node_modules/MyMath
and export it from there. :)
Specs that are created in a vacuum can have no feedback loop.
I think multiple people have pointed out that node collaborators are actively involved with the import map spec work. So I'm not sure I follow why you suggest that it's happening in a vacuum..?
Create
node_modules/MyMath
and export it from there
It was the generic libary that imported MyMath. The user code imports the generic library. The user is not the author of the generic library. Both the generic library and MyMath follows ESM and are host agnostic. Would not your suggestion require me to alter the foreign library source code?
The same as with almost every other tool (bundlers, linters, etc.)
So node will require bundlers and linters to be able to consume host agnostic ES modules? This clearly a disadvantage compared to tools supporting import-maps.
@Starcounter-Jack
I don't understand. The example wants to import add from MyMath using Node. What method is not failing on 1 and 2?
https://nodejs.org/api/policy.html#policy_dependency_redirection looks very similar to import maps except that the policy file doesn't do scoping currently; as does a loader resolve()
hook.
--loader
and --policy
are both CLI arguments as would any such --importmap
CLI argument. I am unclear on what makes any of them more discoverable than the other.
So node will require bundlers and linters to be able to consume host agnostic ES modules? This clearly a disadvantage compared to tools supporting import-maps.
No, it just uses the same way to resolve bare specifiers so that the code works consistently when used in node or outside of node. Just because they all share the same resolution logic doesn't mean that you have to use all of them.
The user is not the author of the generic library. Both the generic library and MyMath follows ESM and are host agnostic. Would not your suggestion require me to alter the foreign library source code?
No need to change any of the code. Let's say the library was downloaded from https://example.com/my-math.mjs
:
mkdir -p node_modules/MyMath
curl -O node_modules/MyMath/my-math.mjs https://example.com/my-math.mjs
echo '{"exports": "./my-math.mjs"}' >node_modules/MyMath/package.json
There's no change to my-math.mjs required. And if the library is coming from the npm registry (to get reliable versioning etc.), then it's enough to npm install my-math
.
Apologies if we are being a bit one sided; I don't think we are trying to state that import maps are not a UX improvement, but they are still in the realm of concerns above and understanding why improving our existing features is not a desirable alternative is needed in my viewpoint.
I am unclear on what makes any of them more discoverable than the other. Of course, I might be mistaking. This is clearly subjective. BUT....
...I consider most contributors to this thread to be above average in Javascript experience. And it is not clear to me of loaders, export fields, or policies is the way to run the ESM.
[EDIT: After some input later in this issue thread, it is clear that the NodeJs solution is export fields. I believe they solve the challenge quite well]
Imagine a scenario where you give an assignment such as the one below to some Javascript developers. Try to rid yourself of bias and guess the outcome. What is your belief the outcome would be?
You are the author of usercode.js. You are the consumer of module someoneslib
. The module someonelib
and its dependencies are written without any specific host in mind. Their only dependency is the Ecmascript specifications. Your task is to run usercode.js. You are allowed to alter usercode.js, but not the foreign libraries. Please solve the task in Node, Chromium and Deno.
Please report back on 1) time required; 2) discoverability; 3) easy-of-use; 4) performance; and 5) maintainability with respect to each host.
import { trippleadd } from "someoneslib"
console.log( trippleadd(1,2,3)
import {add} from "MyMath"
export function trippleadd( a, b, c ) {
return add(add(a,b),c)
}
export function add(x,y) {
return x+y;
}
Apologies if we are being a bit one sided; I don't think we are trying to state that import maps are not a UX improvement, but they are still in the realm of concerns above and understanding why improving our existing features is not a desirable alternative is needed in my viewpoint.
Maybe it is just me not knowing what the existing features
really is. So I took the time to formalize the problem (see challenge above) such that the solution could be presented in a more precise way. Otherwise the discussion may bend towards politics and bias (me saying that I "like import maps" or someone else saying "Firefox is not using it" is not really a constructive computer science based critique of a concept or a Spec).
@Starcounter-Jack I'm keen to understand your needs but you are starting to make leading questions and seem to have a desired answer rather than talking about the feature and how it could be integrated or how existing workflows need to be improved/why existing workflows shouldn't be the way to approach things. All of your last comment is personal and even theoretical, a lot of it merely could be finding the right docs and improve those.
Maybe it is just me not knowing what the existing features really is.
This is a good bit of feedback that we need to add more workflow docs. Right now our docs don't compare our features against other runtimes and maybe they could.
I'm keen to understand your needs
The need is not really mine. The motivation is simply to be able to run pure ESM code. If the above problem description is not enough to outline the problem, it must be due to my shortcomings in explaining them. Maybe the background section (by @domenic) does a better job (it is just 17 lines of text). It can be found here (https://github.com/WICG/import-maps). I really have nothing more to add.
@Starcounter-Jack
It is a balance. The specs that are created in a vacuum have a tendency to fail miserably. So somebody has to lead. Specs that are created in a vacuum can have no feedback loop. This does not mean that everyone has to lead. For example, when IE did not lead, others browsers had to.
agreed. consensus seeking is the appropriate way to find that balance here.
...you are starting to make leading questions and seem to have a desired answer
Sometimes, that's how we feel when someone is arguing their case. My intent, however, is for the discussion be technical rather than emotional. I don't really care what the solution is and I am not emotionally attached to import-maps, but I strongly agree with @domenic about the problem definition. I have provided a simple example in code and maybe a simple configuration file that illustrates the difference between the two approaches would be more constructive. Consider my 2 cents delivered for whatever it was worth.
The module
someonelib
and its dependencies are written without any specific host in mind.
☝️ This is the key part, as far as I'm concerned. How are someonelib
's dependencies specified?
someonelib
is intended for Node, it would be via a package.json
file with a "dependencies"
key.someonelib
is intended for Deno, the dependencies would either be inlined in the files themselves—import {add} from "https://somewhere.com/my-math.ts"
—or defined via an import map.someonelib
is intended for browsers, it would be inlined (but only as JS, not TypeScript) or defined via an import map.In my opinion it's an unfortunate part of the ECMAScript Modules spec that specifier resolution is left to each runtime to decide. I assume this was done to allow Node to keep its bare specifiers ("mymath"
) and probably its legacy automatic extensions ("./file"
) but it means in practice that there's no standard defining allowable specifiers across runtimes. Import maps are a stab at defining such a standard, and that's why the effort is valuable.
It'll be a long time before import maps are not only standardized but also supported in all not-EOL runtimes (between browsers, Node and Deno); but fortunately it's possible to write cross-compatible code today, using a specifier like "mymath"
that gets defined via package.json
for Node and via import maps for Deno and browsers. Yes, when/if Node adds support for import maps that would mean that you could drop the package.json
and just use import maps instead, at least for specifying dependencies; that's why we're interested in supporting import maps. But it would basically be a subset or complement of what Node has now, not necessarily an enhancement; its value is that it would be cross-compatible.
@GeoffreyBooth
If someonelib is intended for Node...
[EDIT: Rereading you post, you did not really refer to someonelib being intended for node but rather meant that someonelib as configured for node, right? then the below comment is moot]
That would defy the purpose of moving modules to the language specification rather than the host specification.
The challenge states that:
The module someonelib and its dependencies are written without any specific host in mind.
@GeoffreyBooth
In my opinion it's an unfortunate part of the ECMAScript Modules spec that specifier resolution is left to each runtime to decide.
I agree.
but fortunately it's possible to write cross-compatible code today
You are probably right. I have not tried if export fields (I assume that that is what you are referring to) solves the problem. If so, our team managed to miss them when trying to run things similar to the example above but managed to find import-maps in Deno. So maybe it is just UX as @bmeck suggested.
If the problem is solved without the need for loader hooks or tooling by a mere set of maps in package.json, why not wait with import maps? At least until one more browser vendor hops onboard.
Maybe making us developers stumble over export fields when reading on how to use ESM a little bit earlier would suffice. I will let you know if that works as a replacement as we have a significant amount of ESM code on other hosts.
Mapping should be the very next thing you learn how to do after groking export
and import from
as hard-coded resolution should be considered bad practice.
The challenge states that:
The module someonelib and its dependencies are written without any specific host in mind.
As far as I'm aware, this is not currently possible. As Node doesn't support import maps and other runtimes don't support package.json
, there's currently no universal way to define a module's dependencies.
I assume you are referring to export fields?
Yes, "exports"
was written specifically to be compatible with import maps. The idea is that the "exports"
field could be converted into an import map by a tool. https://github.com/jkrems/proposal-pkg-exports#1-exports-field
@GeoffreyBooth
Yes, "exports" was written specifically to be compatible with import maps. The idea is that the "exports" field could be converted into an import map by a tool. https://github.com/jkrems/proposal-pkg-exports#1-exports-field
I learn everyday. This is great.
@GeoffreyBooth
there's currently no universal way to define a module's dependencies.
Good enough. It should be trivial to translate maps. We have a fair amount, so I'll give a field report from userland later on for what it is worth (we are planning to move a fairly large code base running on Chromium and Deno over to Node).
@GeoffreyBooth
In my opinion it's an unfortunate part of the ECMAScript Modules spec that specifier resolution is left to each runtime to decide.
Yeah... it is kinda messy right now. At least a set of best practices for resolution patterns and formats covering methods for file, network and embedded resources in declarative, static, dynamic and imperative settings. Maybe even expanded semantics on versioning etc. The Ecmascript language spec basically states that a module-specifier
is a string literal. Leaves rather much to the imagination as a source for disagreement...
If Node.js is ever to support import maps natively, this would definitely be a longer term goal for the project - there are quite a few hurdles first - http/https module fetching and possible fetch
support (a lot of the caching, agent sharing, HTTP/2 transparency would be really nice to just build on top of fetch work). In addition a good security model for fetch and module execution would be important to define.
I do really like the idea that Node.js or the OpenJS foundation should participate in the import maps specification process though - it does seem a little skewed for these specs to be without participation from other engines.
That said I'm not sure I see a way for this to be realistically possible given the history of web spec participation here.
I don't think it's a given that node's implementation would support http/https URLs at all; there's no reason import maps has to be blocked on that.
I believe there is another important reason for Node.js (and/or the npm package manager people) to be involved in import maps, and it has nothing to do with Node.js implementing import maps: I believe we should strive towards a future where any npm install
(or yarn/pnpm
) will create a directory structure that can both be executed by the Node.js runtime and an import map can be generated from all the package dependencies that allows an import-map compliant runtime to use that same directory/package structure. Theoretically, this can probably happen without Node.js' (or deno, or...) involvement, but Node's involvement can make sure that this will work.
From my perspective, this discussion is heavily premature, distracting from the important work that first needs to be done on the loader API before picking additional resolution "standards" to be agreed upon. This, in turn, hurts implementers waiting for those APIs to be stabilized.
If you want import maps, I'd suggest to first prove the point by implementing a loader for them and giving it traction (like the esm
module did). If it's worthwhile, it'll be time to decide whether it should be in Node or not.
we should strive towards a future where any [
yarn install
] will create a directory structure that can both be executed by the Node.js runtime and an import map can be generated from all the package dependencies that allows an import-map compliant runtime to use that same directory/package structure
This is already something that we keep in mind and might happen later in the future once we've clearly identified the pros and cons. It won't, however, happen anytime soon, and there are half a dozen other ways to reach this state - some of them having nothing to do with import maps.
Again, we don't need import maps. We need something to experiment with import maps and other strategies.
@GeoffreyBooth
I agree that there are questions of how useful import maps really would be to Node. The new package.json "exports" implements a lot of the same functionality, with additional features...
Import maps is about code consumption. I struggle to find a way to configure node to run these two simple files in a root directory.
import { foo } from "bar"
console.log( foo() );
export let foo = ( ) => "Hello World"
In Deno, Edge, Chromium and system.js I would provide the configuration { imports: { "bar", "./some-resolve-for-bar-123.js" } }
, but it appears to me that node cannot be configured to resolve the "bar" bare import without forcing the files to be moved or altered. Is this true or am I missing some aspect for the exports
field? The consumer of "bar" should not have to commit to "bar" ending up in the same package or a different one. After all, modules are not necessarily packages.
From my perspective, this discussion is heavily premature
To early to follow a final spec, but almost too late to lead. If the spec needs to change due to Node input, it is better to have the discussion before a fait-a-compli. As implementations start popping up, that appears to be about now.
If the spec needs to change due to Node input, it is better to have the discussion before a fait-a-compli.
This presumes that the spec didn't already change because of input from node. It did. Afaik the existence of scopes for example is a direct result of node influencing the spec.
As someone who was involved in coming up with the proposal I would like to state that Node.js, and specifically our support for bare specifiers, was the initial motivation for the entire specification.
I have been an advocate for import-maps themselves as well as ensuring node's input was being shared with the folks working on the spec for the last 3 years.
So while this thread may make it seem like Node.js has not been involved, the project has been active and influential in this spec since its inception.
On Mon, Jun 1, 2020 at 12:10 PM Jan Olaf Krems notifications@github.com wrote:
If the spec needs to change due to Node input, it is better to have the discussion before a fait-a-compli.
This presumes that the spec didn't already change because of input from node. It did. Afaik the existence of scopes for example is a direct result of node influencing the spec.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/nodejs/node/issues/49443, or unsubscribe https://github.com/notifications/unsubscribe-auth/AADZYVZKOUP4BVRLWXCP7NTRUPHI5ANCNFSM4KLAZCJQ .
@MylesBorins Great to hear about Nodes.js involvement in the spec. That feels comforting. If anyone is interested, we will probably polyfill import-maps using loader hooks while waiting for native support or another standard.
For any interested, I have polyfilled import maps via Node loaders at https://github.com/node-loader/node-loader-import-maps. I don't offer this in attempt to sway the conversation about whether NodeJS itself should support import maps - just as an option for people wanting to try import maps in NodeJS now.
I see a lot of comments in this thread about generating import maps from the exports
field in package.json
, but from my time working on the tooling for Rush, the main thing I would be looking for from an import map is how to find entire packages; where to find a file within the package is a solved problem with multiple extant solutions. The package manager should ideally be able to generate an import map without any knowledge of the exports
field or indeed any information about the contents of specific packages.
Imagine, if you will:
import { FileSystem} from '@rushstack/node-core-library/lib/FileSystem';
This is really two problems:
1) In scope my-package
, where is @rushstack/node-core-library
?
2) Once I have found @rushstack/node-core-library
, resolve ./lib/FileSystem
.
The package.json
exports
field provides a robust facility for solving problem (2), and there is no particular reason that import maps need be concerned with it at all in the Node environment. If the performance overhead of loading those package.json
files once for each package during the runtime of the tool is an issue, then the metadata from the exports
field could be dumped into a single cache file alongside or integrated with the import map for performance.
Problem (1) is what I really need an import map for, so that the relative positioning of my-package
and @rushstack/node-core-library
can be completely abstracted away from the runtime or build tooling. Information about conditions is for the most part irrelevant to this stage, since completely replacing an entire package depending on environment is rare and not generally supported by the package manager to begin with. One major use case for which I need this abstraction is to be able to consume the same monorepo project from two or more other monorepo projects with different peer dependency configurations.
A custom loader may work, certainly I already poke enough into the various parts of the toolchain to be able to enforce it and inject whatever necessary overrides into the various tools (SASS, TypeScript, Jest, webpack, Node itself) to make them behave, but I always prefer cleaner solutions where they exist.
The node resolution algorithm already answers the first part, for both CJS and ESM.
Every single time I work on optimizing the performance of tooling, step 1 is invariably "how can I do less of the incredibly slow node resolution algorithm?"
The slow part is the part that exports helps with - the package part isn’t slow unless you have to traverse upwards for node_modules folders :-)
The big problem of the Node resolution algorithm is that it requires a concrete node_modules tree to traverse.
npm link
).
In fact, every package manager has to follow one of the two strategies below, which are both broken.
Slow progress is being made in the area of policy compatibility but since policies have more features we still need a translation layer to be written up. Newest node has an example of emulating import maps: https://nodejs.org/dist/latest-v17.x/docs/api/policy.html#example-import-maps-emulation . It is lacking robust import maps tests and patterns which have slightly different rules from the string list that import maps use. If anyone wants a mentor in helping with this, I can mentor them through the process.
Please educate me if I'm missing something, but package duplication is the biggest and unavoidable problem of the NodeJs ecosystem.
There's one simple use case that NONE of the existing package managers (npm, yarn 1, yarn 2, and pnpm) can handle. It is as follows
You're developing two local packages a
and b
simutaneously
c
is a published packagea
depends on b
and c
b
depends on c
At anytime, there are two instances of c
a/node_modules/c
b/node_modules/c
This undesirable behavior is due to an inherent design flaw with the node_modules
folder. To solve this issue, we need to
node_modules
foldera
and b
in this case), create a mapping file that tells NodeJS where to find the source files of bare module imports. This file should NOT be checked in as it is user-specific.This solution is what Import Maps is trying to do.
PS:
You may say npm dedup
can help, but there are two problems with it
PPS:
In Dart's ecosystem, package duplication never happens. Dart's Pub package manager stores published dependencies in a global cache and generates a package_config.json
file for each local package. This file is similar to an import map. There is no local folder similar to node_modules
or .yarn/cache
.
@jolleekin that's not what import maps is trying to do at all; import maps is explicitly trying to allow bare specifiers to work in browsers, and only that.
Duplication is a necessary part of a non-broken package management system; one that doesn't allow it is inherently broken.
@ljharb
that's not what import maps is trying to do at all; import maps is explicitly trying to allow bare specifiers to work in browsers, and only that.
What I mean is if NodeJS and NPM implement something similar to import maps, package duplication can be completely avoided.
Duplication is a necessary part of a non-broken package management system; one that doesn't allow it is inherently broken.
Did you mean maintaining backward compatibility? NodeJS and NPM can support something similar to import maps and fall back to the legacy node_modules
if no import maps exist.
In my opinion, if a package management system allows duplication, it is broken. It just doesn't make sense to have two instances of a package at both design time and runtime. Package duplication is the biggest road block for me when developing JS packages.
It does, when an inherent part of the module system is that you can edit a dependency on disk.
Anything where duplication is a problem, is meant to be a peer dep - that solves the problem.
I was hoping to start the conversation around getting Import Map support into core. I figured starting here with a proposal was better than starting right off with a PR.
What Are Import Maps
The proposal was mainly targeted at browsers where URL imports have many downsides without bare specifier support. It enables a standardized map file format to be used when resolving import specifiers, giving developers more control of the behavior of import statements.
Here is an example of the format:
Proposal link: https://github.com/WICG/import-maps
Why should they be supported in Node.js?
Currently we use the structure of the filesystem and a resolution algorithm to map import specifiers to files. These files are typically installed by a package manager in a structure which the Node.js resolution algorithm knows how to resolve. This has some drawbacks:
node_modules
at your filesystem root for exampleIf we had import maps we could circumvent most of this. Package mangers can generate an import map, enabling perf improvements and simplicity in their implementations. It increases startup performance of apps because they don't have to do as much filesystem access.
One interesting example of what import maps enables is filesystem structure independent workspaces (think yarn workspaces but without the requirement of the modules being under the same root directory).
Implementation
As an example implementation I created this gist:
https://gist.github.com/wesleytodd/4399b2351c59438db19a8ffb1f3fcdca
To run it:
This uses an experimental loader which loads an
importmap.json
and falls back to the original filesystem lookup if it fails. I am also pretty sure this very naive implementation is missing edge cases, but in these simple cases it appears to work.Obviously for support in core we would not use a loader, but I think the rest would be similar. Open questions I have are:
Where would node find the
importmap.json
?My first idea is at
node_modules/importmap.json
. I think this would be expected behavior from users if their package managers started writing this file.How would users override the
importmap.json
?For this I think a reasonable approach would be a cli flag (
--importmap
) and a package.json ("importmap": "./importmap.json"
) field.Do we want to wait for browsers to standardize import maps?
https://github.com/nodejs/node/issues/49443
I think that this is a very valid concern. Is there a way to help push that forward? The benefits are pretty large in node IMO, and it would superseded all the work currently being done on yarn pnp and npm 8. If we let those go forward but then this spec lands it might be even worse for shifting the community.
Could we release it as flagged & experimental until browsers standardize? This would mean users could opt in, but as it changed we could follow along.
Thoughts? Next steps?