WICG / import-maps

How to control the behavior of JavaScript imports
https://html.spec.whatwg.org/multipage/webappapis.html#import-maps
Other
2.68k stars 69 forks source link

Detecting support / extensible web? #171

Closed remy closed 2 years ago

remy commented 5 years ago

I've found that importmap has landed in Chrome stable (not sure exactly when or if it's an experimental flag that's turned on, but bear with me).

This makes me very pleased that this code works seamlessly:

<script type="importmap">
{
  "imports": {
    "vue": "/vendor/vue.js"
  }
}
</script>
<script type="module" src="./index.js"></script>
<script nomodule src="./bundle.js"></script>

Where index.js is doing import Vue from 'vue and the nomodule bundle is able to use webpack to bundle up Vue as required.

Except as soon as I ran this in Firefox the code failed hard. Firefox has support for type="module" but knows nothing about import maps, so it failed to import Vue.

I'm happy to target only browsers that have type="module" and type="importmap" as I have my nomodule fallback support - but how?

I can't see any way that I can state this code only works if importmap is supported. Is this included in the specification anywhere?

domenic commented 5 years ago

Great to hear you're liking this! It is still behind an experimental flag/origin trial right now, but hopefully soon it will make it to unflagged-stable :).

I'm a little unsure what you're asking, especially with the last paragraph. But I'll try to answer the question of how to set up a page that uses modules if modules + import maps are supported, or bundles if not. Here's what I've got off the top of my head:

<script type="importmap">
{
  "imports": {
    "vue": "/vendor/vue.js"
  }
}
</script>

<script nomodule src="./bundle.js"></script>
<script type="module" src="./index.js"></script>
<script>
import ("vue").catch(() => {
  // We'll get here if we're in a browser that doesn't support import maps,
  // because import "vue" will fail (with no evaluation performed). In that case
  // the <script type=module> was also a no-op (mostly).

  const s = document.createElement('script');
  s.src = "./bundle.js";
  document.body.append(s);
});
</script>

This solution is a little sub-par, because in the case of browsers that support modules but not import maps, it will fetch (but not evaluate) your module graph up until it sees a bare specifier and fails. I see a couple ways to rejigger it, but they all move that kind of small delay around, instead of eliminating it. It's quite possible there's a solution if I think harder though...

I'll think a bit harder about if there's something more elegant, and /cc @philipwalton and @hiroshige-g who might have better ideas for this particular case.

If we were to add something for this case, one idea is to add the stub of an imperative API, e.g. an empty window.importMap class instance with no properties for now, that in the future we can expand per #128. That would allow cleaning up the above code in a way that involves no extra fetches:

<script nomodule src="./bundle.js"></script>
<script type="module">
if (window.importMap) {
  import("./index.js");
} else {
  const s = document.createElement('script');
  s.src = "./bundle.js";
  document.body.append(s);
}
</script>
matthewp commented 5 years ago

One thing you can also do today is avoid using bare specifiers in your code, but instead use something like /vue which is valid in all module supporting browsers. You'd then need to configure your web server to also take the import map as the fallback case. I know that using bare specifiers is the goal, but this is the smoothest option I've found so far.

philipwalton commented 4 years ago

For some use cases, you can use Import Maps as their own feature detect. For example, you can build an app with two different entry points (e.g. main-map and main-nomap), and then you can determine whether the browser support Import Maps based on which entry module loads.

<script type="importmap">
{
  "imports": {
    "./main-nomap.mjs": "./main-map.mjs"
  }
}
</script>

With this techinque, both main-map and main-nomap would import a common main module, and anything extra that you need to do to handle the no-map case you can run in main-nomap. (e.g. fall back to SystemJS or something like that).

But this only solves part of the problem.

When loading modules (at least at the moment) you need to preload/modulepreload them in order to get good load performance, and there's not really a good way to conditionally preload modules based on whether you have Import Map support, so it's hard to see a strong adoption story until that's addressed (or I guess until most browsers support Import Maps).

(Note: bundled exchanges would probably help address the modulepreload issue, but it'd also be nice to have something that works now. And of course then there'd be the issue of detecting bundled exchange support.)

guybedford commented 4 years ago

@philipwalton on that performance note, I've been pushing for some time for a generic web preload spec as discussed in https://github.com/systemjs/systemjs/issues/1953. Exactly about dealing with this lazy / conditional graph preloading.

trusktr commented 3 years ago

Is it implied above that any browser that supports type="module" also supports type="importmap" too? I can't find a browser support matrix for type="importmap" anywhere, but I do see support for type="module" is easy to find.

ljharb commented 3 years ago

It is not; i believe only chrome has import maps support at this time.

LarsDenBakker commented 3 years ago

Did we add something for detecting import maps in the end?

mrdoob commented 3 years ago

I'm also interested. How do we know if the browser supports import maps?

dmail commented 3 years ago

Once I tried to write code to detect importmap support. I just found back the relevant file, I guess my past self wrote something usable 🤞 . I was apparently too lazy to put explicit comments but code might be enough to explain what is going on 🤞

<html>
  <head></head>

  <body>
    <script>
      //import-map feature detection
      const supportsImportmap = async () => {
        const specifier = jsToTextUrl(`export default false`)

        const importMap = {
          imports: {
            [specifier]: jsToTextUrl(`export default true`),
          },
        }

        const importmapScript = document.createElement("script")
        importmapScript.type = "importmap"
        importmapScript.textContent = JSON.stringify(importMap, null, "  ")
        document.body.appendChild(importmapScript)

        const scriptModule = document.createElement("script")
        scriptModule.type = "module"
        scriptModule.src = jsToTextUrl(`import supported from "${specifier}"; window.__importmap_supported = supported`)

        return new Promise((resolve, reject) => {
          scriptModule.onload = () => {
            const supported = window.__importmap_supported
            delete window.__importmap_supported
            resolve(supported)
          }
          scriptModule.onerror = () => {
            reject()
          }
          document.body.appendChild(scriptModule)
        })
      }

      const jsToTextUrl = (js) => {
        return `data:text/javascript;base64,${window.btoa(js)}`
      }

      supportsImportmap().then((has) => {
        console.log(has)
      })
    </script>
  </body>
</html>

Edit: Don't know in what scenario one could want to use this piece of code but there is maybe something to learn from it. Edit2: I guess it does not work because data: url cannot be remapped anymore since https://github.com/WICG/import-maps/issues/166

mrdoob commented 3 years ago

Edit: Don't know in what scenario one could want to use this piece of code

Better than if ( /apple/i.test(navigator.vendor) ) return false;...

Edit2: I guess it does not work because data: url cannot be remapped anymore since #166

So it doesn't work?

dmail commented 3 years ago

I wanted to verify if remapping data: urls is possible so I wrote a little Node.js code and runned it against the reference implementation available in this repository.

My test code

const { parseFromString } = require("./parser.js");
const { resolve } = require("./resolver.js");

const from = `data:text/javascript,false`;
const to = `data:text/javascript,true`;

const importmapObject = {
  imports: {
    [from]: to,
  },
};
const importmap = parseFromString(
  JSON.stringify(importmapObject),
  "file:///directory/"
);

console.log(resolve(from, importmap, "file:///directory/file.js").href);

Link to reference implementation

https://github.com/WICG/import-maps/blob/75fcc9287dad1bb808c4cde25e0ca432a1dc82ee/reference-implementation/lib/resolver.js#L5

Screenshot of the result

image

So it should work.

dmail commented 3 years ago

Running the code in https://github.com/WICG/import-maps/issues/171#issuecomment-786577380 twice in Chromium Version 90.0.4421.0 logs the following error in the console:

image

It means the code should be run before injecting any script type "module" in the page or it becomes non reliable (promise resolved to undefined instead of true)