javascriptes6-modulesbrowser-feature-detection

How to detect if import.meta is supported by a browser?


In JavaScript running in a browser, to get the current script URI, one can write like in https://stackoverflow.com/a/57364525/3102264:

let currentPath = import.meta.url.substring(
  0, import.meta.url.lastIndexOf("/"));

This works fine in a modern browser, however in an old browser it raises a SyntaxError.

I would like to detect if import.meta is supported in order to use location.href when it is not available.

I tried using try catch like

let currentPath;
try {
  currentPath  = import.meta.url.substring(
    0, import.meta.url.lastIndexOf("/"));
}
catch(e) {
  currentPath  = location.href.substring(
    0, location.href.lastIndexOf("/"));
}

But this is not working, it still throws

Uncaught SyntaxError Unexpected token import

Is there a way to make conditional code depending on import.meta support?

Note: using location.href is an acceptable fallback when served from same based url (even it did not solve what allow import.meta)


Solution

  • As noted in the comments, if you want a fallback for browsers not supporting import.meta, it’s not going to be as simple as replacing import.meta.url with location.href. One will return the URI of the executing script, but the other will return the address of the page on which it runs, which may even be on a completely different domain.

    That said, I managed to come up with a feature-detector that relies neither on user-agent sniffing, nor on dynamic import() expressions:

    const isImportMetaSupported = () => {
      const execModule = (src) => {
        const sc = document.createElement('script');
        sc.type = 'module';
        sc.textContent = src;
        document.body.appendChild(sc);
        sc.remove();      
      };
    
      const js = (pieces, ...values) =>
        String.raw({ raw: pieces }, ...values.map(x => JSON.stringify(x)));
    
      const gensym = () => {
        let ident;
        do {
          ident = `${2 * Math.random() / Number.EPSILON}$$`;
        } while (ident in window);
        return ident;
      };
    
      return new Promise((ok, ko) => {
        const $ok = gensym();
        window[$ok] = ok;
        execModule(js`
          window[${$ok}](true);
          import.meta;
        `);
        execModule(js`
          window[${$ok}](false);
          delete window[${$ok}];
        `);
        setTimeout(
          () => ko(new TypeError("modules unsupported?")), 1000);
      });
    };
    
    (async () => {
      const haveImportMeta = await isImportMetaSupported();
      console.log(
        `import.meta is ${haveImportMeta ? 'supported' : 'unsupported'}`);
      /*
        if (haveImportMeta)
          [load a script that uses import.meta]
        else
          [load a script that uses a fallback solution]
      */
    })()

    How it works: the detector attempts to execute two scripts. If import.meta is supported, the first script executes and resolves the promise with a true value; if import.meta is not supported, the first script triggers a syntax error and does not execute. The second script always executes and attempts to resolve the promise with a false value; if the first script had already executed, this has no effect. I do use a few other advanced features, but those should be much easier to replace or polyfill than import.meta itself.

    It’s a bit finicky: it relies on module scripts being executed in the order they are injected, which I am not sure is actually guaranteed. If you worry about that, you can insert a couple of setTimeouts and let sleepsort deal with it.