WICG / import-maps

How to control the behavior of JavaScript imports

Home Page:https://html.spec.whatwg.org/multipage/webappapis.html#import-maps

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Sugary defaults: can we create a simple case for advanced ahead-of-time tools?

domenic opened this issue · comments

As mentioned in "A convention-based flat mapping", it'd be ideal to also support a way of doing a very simple package map for applications which are laid out ahead of time in a conventional way on-server. For example, you could imagine something like

{
  "imports": {
    "*": "/node_modules_flattened/$1/index.js"
  }
}

In offline discussions, @nyaxt cautioned that this would add a lot of complexity to what was so far a simple format. For example, as-written this would presumably work at any level of the tree. Still, it'd sure be nice...

An alternative is to not reuse the same format, but instead come up with something specifically tailored for this use case, that only works at top level. As a straw-person, you could do:

{
  "pattern": "/node_modules_flattened/*/index.js"
}

package-community/discussions#2 is relevant

I would like to note my dislike if this would cause diverging package ecosystems.

I'll note that this also is important if we want to enable people who are not using package managers to use bare import specifiers without a lot of manual synchronization between their filesystem and their package map.

For example, imagine someone who downloads libraries as zip files and unzips them into a /js/assets/ directory. I don't think too many folks visiting this repository will still be doing that, but it's worth having sympathy with a good fraction of developers who still might. (Or do we think that any libraries that produce native modules will insist you download them via packages? I guess it's not quite clear yet...)

Or imagine a tutorial that says something like "insert these lines into your page and now you can use any package on npm:"

{
  "pattern": "https://normalized.unpkg.com/*/index.js"
}

(Here "normalized.unpkg.com" is a hypothetical variant of unpkg that ensures a top-level index.js is always the main module, somehow.)

I'm not against adding this, but I strongly prefer to not have this in the first cut. I think we can carefully add this later.

Adding .js to every import is redundant. Can this feature cover a simple way to automatically make it so all extension-less specifiers use .js?

After trying out prefix-matching with many existing packages in node.js (and based on feedback from bundlers), it looks like the bare specifier mapping there may move away from prefix and instead always suggest using a syntax that's very similar to this.

Since import maps haven't shipped widely yet, this may be an opportunity to prevent confusion on the browser side and "just" never add prefix-matching. For the cases where prefix-matching behavior is the actual desired behavior, it can be expressed with fairly limited overhead using basic pattern support:

{
  "imports": {
    // before:
    "components/": "/static/app/components/",
    // after:
    "components/*": "/static/app/components/*",
    // or with the syntax in OP:
    "components/*": "/static/app/components/$1",
  }
}

To add to @jkrems's point here, Node.js is currently considering landing effectively an analog to this feature here - nodejs/node#34718. The PR would effectively then seek to deprecate the trailing slash path exports if it lands as we're finding these pattern definitions more flexible.

I've been using a method similar to convention based flat mapping: I prefix, usually with a tool, all bare imports with /lib. Then I have both dynamic (nginx) and static (my keaton static cache builder) ways of mapping /lib to particular libraries not only from node_modules but also various other application directories. Publishing the tools and technique soon.

A workaround in the absence of this is a simple Service Worker that remaps the path based on any Javascript logic. This is less than ideal because it would require the complexity, overhead, and browser-support of a Service Worker, but it's available today, and may be useful as a thought exercise.

In the way I see ServiceWorker, they cannot be used to control import behaviour as they are installed concurrently with the main page. To me they can start to control import behaviour (if worker installed successfully) only the next time page is loaded.

@dmail This is part of the overhead and complexity, and your concern is noteworthy — it's not immediately obvious how to solve the many complex needs of ServiceWorkers. It looks like the case you are noting may be solved with ServiceWorkerContainer.ready.

The ready read-only property of the ServiceWorkerContainer interface provides a way of delaying code execution until a service worker is active. It returns a Promise that will never reject, and which waits indefinitely until the ServiceWorkerRegistration associated with the current page has an active worker. Once that condition is met, it resolves with the ServiceWorkerRegistration.

For example: importing /entry will wait for the ServiceWorker, meaning it can be rewritten/redirected:

navigator.serviceWorker.ready.then(() => import('/entry'))

However, your comment is illustrative of the fact that Service Workers are a complex mechanic, versus an import maps glob.

Waiting to execute javascript until the service worker is installed is not just complexity, but a performance limitation.

This documentation talks about service workers as an alternative that was considered: https://github.com/WICG/import-maps#service-workers

How about in a first iteration we "allow" * to be only in the end?

This would mean the functionality would be exactly like the existing trailing slash functionality.

{
  "imports": {
     "lodash/": "/node_modules/lodash-es/",
     "lodash/*": "/node_modules/lodash-es/*",     
  }
}

👆 both of these imports are exactly the same

We would however gain

  1. More intuitive API
  2. Sime alignment with how node package entry points work

What do you think?

I would suggest mimicking the behavior well-defined for the exports field of the package.json file.
https://nodejs.org/api/packages.html#packages_subpath_patterns

{
  "exports": {
    "./features/*.js": "./src/features/*.js"
  }
}

This globbing feature would be generic and would cover extensionless specifiers, while the idea in

is simpler, with no globbing, specifically for extensions in a way similar to scopes.

This would be very useful for certain libraries, for example here's an importmap for Shoelace:

          "@shoelace-style/shoelace/": "/node_modules/@shoelace-style/shoelace/",

and usage:

import '@shoelace-style/shoelace/dist/components/card/card.js'

Importing from components/ is a very common thing, so it would be nice to make a shortcut for it in the importmap like so:

          "shoelace/*": "/node_modules/@shoelace-style/shoelace/dist/components/*/*.js",

and the usage would then be:

import 'shoelace/card'

Wildcards would also play well with TypeScript tsconfig.json, and we can write the same sort of maps in tsconfig such that the types are still picked up from the customized import statements.

I hope import-map can provide a Subpath patterns function similar to node exports, so that I can run the module in the browser without adding the .js suffix myself.

I think the core reasons from the npm world to support subpath patterns are:

  1. To enable much more compact import maps, given that Node shipped subpath patterns and many people are using extension-less imports.
  2. To enable simpler import map generation for npm projects that can just look at the package.json and doesn't have to scan the file system. You can get the package.json with a call to the registry, so you wouldn't have to install a package. I imagine module CDNs could use this to serve long-cached modules that work against any compatible dependency versions.

It's a little unfortunate that Node shipped a pattern capability that import maps don't support, but here we are and I think it'd be a pragmatic thing to do.