Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to feature-detect whether a browser supports dynamic ES6 module loading?

Background

The JavaScript ES6 specification supports module imports aka ES6 modules.

The static imports are quite obvious to use and do already have quite a good browser support, but dynamic import is still lacking behind.
So it is reasonably possible that your code uses static modules (when these would be not supported the code would not even execute), but the browser may miss support for dynamic import. Thus, detecting whether dynamic loading works (before trying to actually load code) may be useful. And as browser detection is frowned upon, of course I'd like to use feature detection.

Use cases may be to show an error, fallback to some other data/default/loading algorithm, provide developers with the advantages of dynamic loading (of data e.g. in a lazy-mode style) in a module, while allowing a fallback to passing the data directly etc. Basically, all the usual use cases where feature detection may be used.

Problem

Now, as dynamic modules are imported with import('/modules/my-module.js') one would obviously try to just detect whether the function is there, like this:

// this code does NOT work
if (import) {
    console.log("dynamic import supported")
}

I guess, for every(?) other function this would work, but the problem seems to be: As import is, respectively was, a reserved keyword in ECMAScript, and is now obviously also used for indicating the static import, it is not a real function. As MDN says it, it is "function-like".

Tries

import() results in a syntax error, so this is not really usable and import("") results in a Promise that rejects, which may be useful, but looks really hackish/like a workaround. Also, it requires an async context (await etc.) just for feature-detecting, which is not really nice.
typeeof import also fails directly, causing a syntax error, because of the keyword ("unexpected token: keyword 'import'").

So what is the best way to reliably feature-detect that a browser does support dynamic ES6 modules?

Edit: As I see some answers, please note that the solution should of course be as generally usable as possible, i.e. e.g. CSPs may prevent the use of eval and in PWAs you shall not assume you are always online, so just trying a request for some abitrary file may cause incorrect results.

like image 246
rugk Avatar asked Feb 20 '20 09:02

rugk


4 Answers

The following code detects dynamic import support without false positives. The function actually loads a valid module from a data uri (so that it works even when offline).

The function hasDynamicImport returns a Promise, hence it requires either native or polyfilled Promise support. On the other hand, dynamic import returns a Promise. Hence there is no point in checking for dynamic import support, if Promise is not supported.

function hasDynamicImport() {
  try {
    return new Function("return import('data:text/javascript;base64,Cg==').then(r => true)")();
  } catch(e) {
    return Promise.resolve(false);
  }
}

hasDynamicImport()
  .then((support) => console.log('Dynamic import is ' + (support ? '' : 'not ') + 'supported'))
  • Advantage - No false positives
  • Disadvantage - Uses eval

This has been tested on latest Chrome, Chrome 62 and IE 11 (with polyfilled Promise).

like image 195
Joyce Babu Avatar answered Oct 22 '22 16:10

Joyce Babu


Three ways come to mind, all relying on getting a syntax error using import():

  • In eval (but runs afoul some CSPs)
  • Inserting a script tag
  • Using multiple static script tags

Common bits

You have the import() use foo or some such. That's an invalid module specifier unless it's in your import map, so shouldn't cause a network request. Use a catch handler to catch the load error, and a try/catch around the import() "call" just to catch any synchronous errors regarding the module specifier, to avoid cluttering up your error console. Note that on browsers that don't support it, I don't think you can avoid the syntax error in the console (at least, window.onerror didn't for me on Legacy Edge).

With eval

...since eval isn't necessarily evil; e.g., if guaranteed to use your own content (but, again, CSPs may limit):

let supported = false;
try {
    eval("try { import('foo').catch(() => {}); } catch (e) { }");
    supported = true;
} catch (e) {
}
document.body.insertAdjacentHTML("beforeend", `Supported: ${supported}`);

Inserting a script

let supported = false;
const script = document.createElement("script");
script.textContent = "try { import('foo').catch(() => { }); } catch (e) { } supported = true;";
document.body.appendChild(script);
document.body.insertAdjacentHTML("beforeend", `Supported: ${supported}`);

Using multiple static script tags

<script>
let supported = false;
</script>
<script>
try {
    import("foo").catch(() => {});
} catch (e) {
}
supported = true;
</script>
<script>
document.body.insertAdjacentHTML("beforeend", `Supported: ${supported}`);
</script>

Those all silently report true on Chrome, Chromium Edge, Firefox, etc.; and false on Legacy Edge (with a syntax error).

like image 36
T.J. Crowder Avatar answered Oct 22 '22 16:10

T.J. Crowder


During more research I've found this gist script, with this essential piece of JS for dynamic feature detection:

function supportsDynamicImport() {
  try {
    new Function('import("")');
    return true;
  } catch (err) {
    return false;
  }
}
document.body.textContent = `supports dynamic loading: ${supportsDynamicImport()}`;

All credit for this goes to @ebidel from GitHub!

Anyway, this has two problems:

  • it uses eval, which is evil, especially for websites that use a CSP.
  • according to the comments in the gist, it does have false-positives in (some version of) Chrome and Edge. (i.e. it returns true altghough these browser do not actually support it)
like image 2
rugk Avatar answered Oct 22 '22 16:10

rugk


My own solution, it requires 1 extra request but without globals, without eval, and strict CSP compliant:

Into your HTML

<script type="module">
import('./isDynamic.js')
</script>
<script src="/main.js" type="module"></script>

isDynamic.js

let value

export const then = () => (value = value === Boolean(value))

main.js

import { then } from './isDynamic.js'

console.log(then())

Alternative

Without extra request, nor eval/globals (of course), just needing the DataURI support:

<script type="module">
import('data:text/javascript,let value;export const then = () => (value = value === Boolean(value))')
</script>
<script src="/main.js" type="module"></script>
import { then } from 'data:text/javascript,let value;export const then = () => (value = value === Boolean(value))'

console.log(then())

How it works? Pretty simple, since it's the same URL, called twice, it only invokes the dynamic module once... and since the dynamic import resolves the thenable objects, it resolves the then() call alone.

Thanks to Guy Bedford for his idea about the { then } export

like image 1
Lcf.vs Avatar answered Oct 22 '22 16:10

Lcf.vs