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

767 Views Asked by At

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)

2

There are 2 best solutions below

1
user3840170 On BEST ANSWER

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.

9
VonC On

import.meta is always a syntax error in a direct eval().

True: import.meta is restricted to module code, while eval() executes in a script context.

Conditional code depending on import.meta support is rather tricky to implement directly in the browser due to the parsing-time nature of syntax errors.

An alternative approach might involve using separate script files, but, as commented:

The module and fallback scripts are still a reasonable idea, only they are available without reinventing the wheel by using <script nomodule> which only gets executed as a fallback when modules are not available.

That would mean:

  • use type="module" for the script that utilizes import.meta. This script will only be executed in modern browsers that support ES6 modules and consequently import.meta.

  • Use nomodule for the fallback script that employs location.href. This script will only be executed in browsers that do not support ES6 modules.

<!-- For modern browsers that support ES6 modules and import.meta -->
<script type="module" src="module-script.js"></script>

<!-- Fallback for older browsers -->
<script nomodule src="fallback-script.js"></script>

In module-script.js:

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

In fallback-script.js:

let currentPath = location.href.substring(0, location.href.lastIndexOf("/"));
// Rest of your code

They are trying to specifically target the few browsers that did support type="module" but not import.meta, which was added a couple of browser shipments later.

If you are specifically targeting the rare set of browsers that support ES modules but not import.meta, I do not know of a browser-side solution. These browsers would execute the type="module" script and encounter a syntax error when they come across import.meta.

You might consider using a server-side user-agent string check to identify these specific browser versions and serve different scripts accordingly.

It would involve examining the User-Agent header sent by the browser in the HTTP request to identify the browser type and version, and, based on this information, serving different JavaScript files.

Something like (using Node.js and the Express framework for this example):

const express = require('express');
const app = express();

app.get('/your-script.js', (req, res) => {
  const userAgent = req.headers['user-agent'];

  // You would replace the following conditions with the specific user-agent strings 
  // or patterns that identify the browsers that support `type="module"` but not `import.meta`
  if (userAgent.includes('SomeOldBrowser')) {
    res.sendFile(__dirname + '/fallback-script.js');
  } else {
    res.sendFile(__dirname + '/module-script.js');
  }
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000/');
});

In your HTML file, you would then simply link to this script:

<script type="module" src="/your-script.js"></script>

The server will serve either module-script.js or fallback-script.js based on the user-agent string.

User-agent string checking is generally not recommended for feature detection due to various issues like browser spoofing, but it can serve as a workaround for this specific use-case.


Interesting approach, but import type="module" is needed because there is a webcomponent in the code

If you are using web components, you likely have a build process where you are using tools like Webpack or Rollup. In that case, you might consider feature detection at build time, producing two different builds. You could then use server-side logic to serve the correct build based on the user agent.

Another idea is to isolate the specific logic that depends on import.meta into a separate JavaScript module file. You can then serve this isolated module conditionally based on server-side user-agent string checks. Once the appropriate module is loaded, you can import it dynamically into your main web component module script.

Create two separate JavaScript files:

  • importMetaLogic.js for browsers that support import.meta

    export const getCurrentPath = () => {
      return import.meta.url.substring(0, import.meta.url.lastIndexOf("/"));
    };
    
  • locationHrefLogic.js for browsers that do not

    export const getCurrentPath = () => {
      return location.href.substring(0, location.href.lastIndexOf("/"));
    };
    

On the server-side, conditionally serve one of these files based on the user-agent. Using Node.js and Express, the code might look like:

const express = require('express');
const app = express();

app.get('/logic.js', (req, res) => {
  const userAgent = req.headers['user-agent'];

  if (/* condition identifying browsers that support import.meta */) {
    res.sendFile(__dirname + '/importMetaLogic.js');
  } else {
    res.sendFile(__dirname + '/locationHrefLogic.js');
  }
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000/');
});

In your main web component module script, dynamically import this logic:

let getCurrentPath;

import('/logic.js')
  .then((module) => {
    getCurrentPath = module.getCurrentPath;
  })
  .catch((error) => {
    console.error("An error occurred:", error);
  });

// Rest of your web component code

That uses the dynamic import() syntax, which returns a Promise. Unlike static import, dynamic import() can be used in any context and is not subject to the same restrictions as static import. That means you should be able to load modules conditionally at runtime.

import('/logic.js') returns a Promise that resolves into the imported module. Once the Promise is resolved, the getCurrentPath function from the dynamically loaded module is assigned to the local getCurrentPath variable. That makes the function available for use within the rest of your web component code.

That approach ensures that your main web component code remains largely unchanged. Only the logic that depends on import.meta or location.href is isolated into a separate module and conditionally loaded.