javascriptember.jspolyfillsintl

Add Intl.Locale polyfill only when needed (How to block script tag with async functions)


I am trying to add Intl polyfill to an ember app, but have run into the issue that I need to add a script tag that executes async functions before evaluating other script tags.

In ember I can add a new <script> tag to index.html, that is placed before the emberjs tags:

<body>
  <script src="assets/polyfills.js"></script>   
  <script src="assets/vendor.js"></script> <-- this throws an exception if polyfill is not loaded for iOS < 14
</body>

Everything works fine when assets/polyfills.js looks like this:

import '@formatjs/intl-locale/polyfill';

However, the issue then is that the polyfill is loaded for all devices - no matter if needed or not. But according to the docs there is way to check wether the polyfill is actually needed https://formatjs.io/docs/polyfills/intl-locale/:

import {shouldPolyfill} from '@formatjs/intl-locale/should-polyfill'
async function polyfill() {
  // This platform already supports Intl.Locale
  if (shouldPolyfill()) {
    await import('@formatjs/intl-locale/polyfill')
  }
}

The problem now is, that I am dealing with an async function and I can't find a way to load the polyfill before any other js code is executed.

I have tried to modify polyfills.js to use top level awaits and enabled the experimental webpack feature topLevelAwait: true, but subsequent code is executed before the polyfill is loaded:

await import('@formatjs/intl-getcanonicallocales/polyfill');

I also tried to wrap it in a function, but that also didn't change anything:

async function load() {
  await import('@formatjs/intl-locale/polyfill');
};
await load();

I also tried things like this, which had exactly the same effect:

(async () => {
  await import('@formatjs/intl-locale/polyfill');
})();

Pretty much the thing that I need would look like this:

if (shouldPolyfill) {
  import '@formatjs/intl-locale/polyfill';
}

However, that is not valid and leads to this error: An import declaration can only be used at the top level of a module.

How do I solve that issue?

EDIT (adding more ember details)

The error appears in one of embers chunk.*.js files, so I think it is caused by a dependency loaded with auto-import. If I look at the content, it looks like it is ember-intl.

I configured auto-import to add the polyfill before the other dependencies:

ember-cli-build:

autoImport: {
      insertScriptsAt: 'auto-import-script',
      webpack: {
        target: 'web',
        entry: {
          polyfills: './lib/polyfills.js',
        },

index.html:

<auto-import-script entrypoint="polyfills"></auto-import-script>
    <script src="{{rootURL}}assets/vendor.js"></script>
    <auto-import-script entrypoint="app"></auto-import-script>
    <script src="{{rootURL}}assets/app.js"></script>
  </body>

targets.js

'use strict';

const browsers = [
  'last 2 Chrome versions',
  'last 2 Firefox versions',
  'last 4 Safari versions',
  'last 1 Edge versions',
  'last 2 ChromeAndroid versions',
  'last 4 iOS versions',
];

module.exports = {
  browsers,
  node: '12'
};

stacktrace

TypeError: undefined is not a constructor (evaluating 'new Intl.Locale(a[0])')
1
File "https://static.app.com/app/assets/chunk.367.65428fe8e29cd2560eec.js", line 1404 col 34 in resolveLocale
2
File "https://static.app.com/app/assets/chunk.367.65428fe8e29cd2560eec.js", line 1396 col 296 in c
3
File "addon-tree-output/ember-intl/-private/formatters/format-message.js", line 61 col 1 in [anonymous]
return new _intlMessageformat.default(ast, locales, formatConfig, {
4
File "https://static.app.com/app/assets/chunk.367.65428fe8e29cd2560eec.js", line 873 col 30 in e
5
File "[native code]", line (unknown) in e
6
File "addon-tree-output/ember-intl/-private/formatters/format-message.js", line 84 col 1 in format
const formatterInstance = this.createNativeFormatter(ast, locale, this.readFormatConfig());
7
File "@ember/-internals/glimmer/index.js", line 2808 col 24 in getValue
let ret = instance.compute(positional, named);
8
File "@glimmer/reference.js", line 121 col 35 in [anonymous]
lastValue = ref.lastValue = compute();
9
File "@glimmer/validator.js", line 677 col 5 in track
callback();
10
File "@glimmer/reference.js", line 120 col 21 in m
tag = ref.tag = track(() => {
11
File "@glimmer/runtime.js", line 3777 col 31 in [anonymous]
vm.stack.push(toContentType(valueForRef(reference)));
12
File "@glimmer/runtime.js", line 1205 col 17 in evaluate
operation.evaluate(vm, opcode);
13
File "@glimmer/runtime.js", line 4882 col 20 in evaluateSyscall
APPEND_OPCODES.evaluate(vm, opcode, opcode.type);
14
File "@glimmer/runtime.js", line 4838 col 12 in evaluateInner
this.evaluateSyscall(opcode, vm);
15
File "@glimmer/runtime.js", line 4830 col 12 in evaluateOuter
this.evaluateInner(opcode, vm);
16
File "@glimmer/runtime.js", line 5790 col 22 in next
this[INNER_VM].evaluateOuter(opcode, this);
17
File "@glimmer/runtime.js", line 5774 col 21 in _execute
result = this.next();
18
File "@ember/-internals/glimmer/index.js", line 5194 col 43 in render
let result = this.result = iterator.sync(); // override .render function after initial render
19
File "@ember/-internals/glimmer/index.js", line 5513 col 16 in [anonymous]
root.render();
20
File "@glimmer/runtime.js", line 4725 col 7 in Nt
cb();
21
File "@ember/-internals/glimmer/index.js", line 5492 col 7 in _renderRoots
inTransaction(runtime.env, () => {
22
File "@ember/-internals/glimmer/index.js", line 5545 col 12 in _renderRootsTransaction
this._renderRoots();
23
File "@ember/-internals/glimmer/index.js", line 5479 col 10 in _renderRoot
this._renderRootsTransaction();
24
File "@ember/-internals/glimmer/index.js", line 5385 col 10 in _appendDefinition
this._renderRoot(rootState);
25
File "@ember/-internals/glimmer/index.js", line 5367 col 10 in appendOutletView
this._appendDefinition(view, curry(0
26
File "backburner.js", line 275 col 24 in invokeWithOnError
method.apply(target, args);
27
File "backburner.js", line 182 col 21 in flush
invoke(target, method, args, onError, errorRecordedForStack);
28
File "backburner.js", line 341 col 27 in flush
if (queue.flush(false /* async */) === 1 /* Pause */) {
29
File "backburner.js", line 784 col 38 in _end
result = currentInstance.flush(fromAutorun);
30
File "backburner.js", line 582 col 14 in end
this._end(false);
31
File "backburner.js", line 827 col 22 in _run
this.end();
32
File "@ember/application/lib/application.js", line 430 col 9 in e
run(this, 'domReady');

Solution

  • WARNING: This solution no longer works and came with unintended consequences as outlined here for instance: https://www.theregister.com/2024/06/25/polyfillio_china_crisis/

    I think for cross-browser functionality, using CDNs is the best approach instead of installing and importing a polyfill library on your own. Polyfill.io's CDN automatically checks if the polyfill requested is necessary or not using the User-Agent HTTP header and sends the polyfill script conditionally.

    The normal behavior of the HTML <script> tag is to load and execute a script as soon as it is found while parsing an HTML document and each script is loaded and executed sequentially, So adding the polyfill script on the top of other script tags makes sense and should work as expected:

    <head>
      <script src="https://polyfill.io/v3/polyfill.min.js?features=Intl.Locale"></script>
      <script src="assets/vendor.js"></script>
    </head>