7

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.

rugk
  • 4,055
  • 2
  • 22
  • 51

5 Answers5

8

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).

Joyce Babu
  • 18,005
  • 11
  • 60
  • 93
  • Note Edge 41.16299.15.0 (EdgeHTML 16) accepts the import() syntax, fooling the eval or Function constructor detection methods! – Markus Jul 22 '20 at 09:53
  • 1
    @Markus It may accept the the `import` statement, but unless it returns a Promise,(which I believe it doesn't), the above code will still work correctly. So, in EdgeHTML16, it should throw an exception due to the `.then` chain, which will be caught by the `catch` block. – Joyce Babu Jul 23 '20 at 12:17
  • Also, it's a promise. Can't be used with sync code. – Stefan Steiger Oct 18 '21 at 08:00
4

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).

T.J. Crowder
  • 959,406
  • 173
  • 1,780
  • 1,769
  • Okay, so this has the disadvantage of doing a network request, anyway, (that takes time/causes latency; what happens if the server is temporarily offline in PWA contexts e.g.?) and it also uses eval, [which we know is evil](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#Never_use_eval!). – rugk Feb 20 '20 at 10:09
  • @rugk - I can't think of a way to do it without the network request (a blob maybe?). `eval` is not evil when **you** control the content, that's just common hyperbole. :-) – T.J. Crowder Feb 20 '20 at 10:11
  • 1
    Eval is simply not possible e.g. for websites that do [use a CSP](https://stackoverflow.com/q/10944794/5008962). Anyway, arguing does not matter, it's certainly a disadvantage. – rugk Feb 20 '20 at 10:13
  • 1
    @rugk - Okay granted about some CSPs. :) The second approach doesn't use `eval`, and I found a way around the network request (note that the request didn't cause any delays or latency, though; it runs in *parallel* to the detection; detection is immediate). Updated the answer. – T.J. Crowder Feb 20 '20 at 10:19
  • Well, what you do there is effectively bypassing the CSPs "unsafe-eval" restriction... – rugk Feb 20 '20 at 10:24
  • @rugk - Okay, you can do it with three script tags. (Updated.) Or are you going to add a further restriction? – T.J. Crowder Feb 20 '20 at 10:27
1

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

Lcf.vs
  • 1,742
  • 1
  • 11
  • 15
0

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)
rugk
  • 4,055
  • 2
  • 22
  • 51
  • That doesn't seem right, correct me if I am wrong, but that will always return true, I mean try renaming "import" to "foobar" and it will still return true, even though "foobar" doesn't exist. It doesn't actually execute the script, does it? – Seivan May 29 '20 at 08:31
  • 1
    I was wrong, apparently `import` is a reserved keyword on the browsers that doesn't support them and would in fact throw, indicating that it's not available. – Seivan May 29 '20 at 23:54
-2

How about loading your JS within a type='module' script, otherwise load the next script with a nomodule attribute, like so:

<script type="module" src="main.mjs"></script>
<script nomodule src="fallback.js"></script>

Browsers that understand type="module" ignore scripts with a nomodule attribute.

Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import & https://v8.dev/features/modules#browser

Beto
  • 743
  • 2
  • 11
  • 26
  • 2
    This does detect whether the browser supports **modules at all**. However, the question was about _dynamic_ modules, i.e. these imported via `import`. Static module support can be checked in the way you do, that's easy. And as you can see in the linked MDN doc, the browser support for static vs dynamic modules varies a lot, partially… – rugk Apr 03 '20 at 11:32
  • 2
    Correct, the idea was that if modules are supported then support for static AND dynamic imports can be checked using type="module" because both features came to be supported at relatively the same time, here is an example with dynamic imports: https://v8.dev/features/dynamic-import#dynamic – Beto Apr 06 '20 at 19:37
  • 3
    @beto Not sure that's valid. Some browsers support ESModules, but do not support dynamic import. Safari 10.1 (10.3 on iOS) support `module` but does not support ESModules. This is a poor way of distinguishing between these two types OP is looking for. – Seivan May 29 '20 at 08:32