es-module-shims 1.6.3 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,909 +1,912 @@
1
- # ES Module Shims
2
-
3
- Shims modern ES Modules features like import maps on top of the baseline modules support in browsers supported by [95% of users](https://caniuse.com/#feat=es6-module).
4
-
5
- When running in polyfill mode, [the 72% of users](https://caniuse.com/import-maps) with import maps entirely bypass the shim code entirely.
6
-
7
- For the remaining 28% of users, the highly performant (see [benchmarks](#benchmarks)) production and [CSP-compatible](#csp-support) shim kicks in to rewrite module specifiers driven by the [Web Assembly ES Module Lexer](https://github.com/guybedford/es-module-lexer).
8
-
9
- The following modules features are polyfilled:
10
-
11
- * [Import Maps](#import-maps) support.
12
- * Dynamic `import()` shimming when necessary in eg older Firefox versions.
13
- * `import.meta` and `import.meta.url`.
14
- * [JSON](#json-modules) and [CSS modules](#css-modules) with import assertions (when enabled).
15
- * [`<link rel="modulepreload">` polyfill](#modulepreload) in non Chromium browsers for both shimmed and unshimmed preloading scenarios.
16
-
17
- When running in shim mode, module rewriting is applied for all users and custom [resolve](#resolve-hook) and [fetch](#fetch-hook) hooks can be implemented allowing for custom resolution and streaming in-browser transform workflows.
18
-
19
- Because we are still using the native module loader the edge cases work out comprehensively, including:
20
-
21
- * Live bindings in ES modules
22
- * Dynamic import expressions (`import('src/' + varname')`)
23
- * Circular references, with the execption that live bindings are disabled for the first unexecuted circular parent.
24
-
25
- > [Built with](https://github.com/guybedford/es-module-shims/blob/main/chompfile.toml) [Chomp](https://chompbuild.com/)
26
-
27
- ## Usage
28
-
29
- Include ES Module Shims with a `async` attribute on the script, then include an import map and module scripts normally:
30
-
31
- ```html
32
- <script async src="https://ga.jspm.io/npm:es-module-shims@1.6.3/dist/es-module-shims.js"></script>
33
-
34
- <!-- https://generator.jspm.io/#U2NhYGBkDM0rySzJSU1hKEpNTC5xMLTQM9Az0C1K1jMAAKFS5w0gAA -->
35
- <script type="importmap">
36
- {
37
- "imports": {
38
- "react": "https://ga.jspm.io/npm:react@18.0.0-rc.0/index.js"
39
- },
40
- "scopes": {
41
- "https://ga.jspm.io/npm:react@18.0.0-rc.0/": {
42
- "object-assign": "https://ga.jspm.io/npm:object-assign@4.1.1/index.js"
43
- }
44
- }
45
- }
46
- </script>
47
-
48
- <script type="module">
49
- import react from 'react';
50
- console.log(react);
51
- </script>
52
- ```
53
-
54
- ## Polyfill Explainer
55
-
56
- When running the previous example in a browser without import maps support, the browser will output the following console error:
57
-
58
- ```
59
- Uncaught TypeError: Failed to resolve module specifier "react". Relative references must start with either "/", "./", or "../".
60
- at <anonymous>:1:15
61
- ```
62
-
63
- This error is important - it means that the native browser loader didn't execute any of the code at all because this error happens
64
- at link time, and before execution time. And this is what allows the polyfill to be able to reexecute the modules and their dependencies
65
- without risk of duplicate execution.
66
-
67
- The ES Module Shims polyfill will analyze the browser to see if it supports import maps. If it does, it doesn't do anything more,
68
- otherwise it will analyze all module scripts on the page to see if any of them have bare specifier imports that will fail like this.
69
- If one is found, it will then be reexecuted through ES Module Shims using its internal shimming of modules features.
70
-
71
- When the polyfill kicks in another console log message is output(which can be disabled or customized via the [polyfill hook](#polyfill-hook)):
72
-
73
- ```
74
- ^^ Module TypeError above is polyfilled and can be ignored ^^
75
- ```
76
-
77
- ### Polyfill Edge Case: Dynamic Import
78
-
79
- Only static link-time errors are polyfilled, not runtime errors.
80
-
81
- Module feature errors that are not _static errors_ but rather _runtime errors_ will bypass the polyfill detection.
82
-
83
- For example:
84
-
85
- ```html
86
- <script type="module">
87
- import './supported.js';
88
- console.log('Static Ok');
89
- import('react').then(x => {
90
- console.log('Dynamic Ok');
91
- }, err => {
92
- console.log('Dynamic Fail');
93
- });
94
- </script>
95
- ```
96
-
97
- In the above, the native browser loader without import maps support will execute the above module, but fail the dynamic import.
98
-
99
- See the log output in various scenarios:
100
-
101
- * Native with Import Maps: `Static Ok`, `Dynamic Ok`
102
- * Native without Import Maps: `Static Ok`, `Dynamic Fail`
103
- * Native without Import Maps running ES Module Shims: `Static Ok`, `Dynamic Fail`
104
-
105
- ES Module Shims **does not polyfill the dynamic import**. The reason for this is that if it did, it would need to reexecute the entire outer module resulting in `Static Ok`, `Dynamic Fail`, `Static Ok`, `Dynamic Ok`. This _double execution_ would be a change of the normal execution in running code twice that would not be deemed suitable for calling it a polyfill.
106
-
107
- This is why it is advisable to always ensure modules use a bare specifier early to avoid non-execution.
108
-
109
- If a static failure is not possible and dynamic import must be used, one alternative is to use the `importShim` ES Module Shims top-level loader:
110
-
111
- ```html
112
- <script type="module">
113
- import './supported.js';
114
- console.log('Static Ok');
115
- importShim('react').then(x => {
116
- console.log('Ok');
117
- }, err => {
118
- console.log('Fail');
119
- });
120
- </script>
121
- ```
122
-
123
- `importShim` will automatically pass-through to the native dynamic import or polyfill as necessary, just like it does for script tags.
124
-
125
- ### Polyfill Edge Case: Instance Sharing
126
-
127
- When running in polyfill mode, it can be thought of that are effectively two loaders running on the page - the ES Module Shims polyfill loader, and the native loader.
128
-
129
- Note that instances are not shared between these loaders for consistency and performance, since some browsers do not properly share the fetch cache and native loader cache resulting in a double fetch which would be inefficient.
130
-
131
- As a result, if you have two module graphs - one native and one polyfilled, they will not share the same dependency instance, for example:
132
-
133
- ```html
134
- <script type="importmap">
135
- {
136
- "imports": {
137
- "dep": "/dep.js"
138
- }
139
- }
140
- </script>
141
- <script type="module">
142
- import '/dep.js';
143
- </script>
144
- <script type="module">
145
- import 'dep';
146
- </script>
147
- ```
148
-
149
- ```dep
150
- console.log('DEP');
151
- ```
152
-
153
- When polyfilling import maps, ES Module Shims will pick up on the second import failure and reexecute `/dep.js` as a new instance, logging `"DEP"` twice.
154
-
155
- For this reason it is important to always ensure all modules hit the polyfill path, either by having all graphs use import maps at the top-level, or via `importShim` directly.
156
-
157
- If you really need to support instance sharing with the native loader, a useful workaround is to use the [`skip` option](#skip) to list modules which should always be loaded via the native loader:
158
-
159
- ```html
160
- <script type="esms-options">
161
- {
162
- "skip": ["/dep.js"]
163
- }
164
- </script>
165
- ```
166
-
167
- The above would then fully cause dependency module instance to be shared between ES Module Shims and the native loader, with the polyfill then logging `"DEP"` only once.
168
-
169
- #### No Shim Scripts
170
-
171
- If the polyfill is analyzing or applying to a module script that doesn't need to or shouldn't be polyfilled, adding the `"noshim"` attribute to the script tag will ensure that ES Module Shims ignores processing this script entirely:
172
-
173
- ```html
174
- <script type="module" noshim>
175
- // ...
176
- </script>
177
- ```
178
-
179
-
180
- ### Polyfill Features
181
-
182
- If using more modern features like CSS Modules or JSON Modules, these need to be manually enabled via the [`polyfillEnable` init option](#polyfill-enable-option) to raise the native baseline from just checking import maps to also checking that browsers support these features:
183
-
184
- ```html
185
- <script>
186
- window.esmsInitOptions = { polyfillEnable: ['css-modules', 'json-modules'] }
187
- </script>
188
- ```
189
-
190
- To verify when the polyfill is actively engaging as opposed to relying on the native loader, [a `polyfill` hook](#polyfill-hook) is also provided.
191
-
192
- ## Shim Mode
193
-
194
- Shim mode is an alternative to polyfill mode and doesn't rely on native modules erroring - instead it is triggered by the existence of any `<script type="importmap-shim">` or `<script type="module-shim">`, or when explicitly setting the [`shimMode` init option](#shim-mode-option).
195
-
196
- In shim mode, only the above `importmap-shim` and `module-shim` tags will be parsed and executed by ES Module Shims.
197
-
198
- Shim mode also provides some additional features that aren't yet natively supported such as supporting multiple import maps, [external import maps](#external-import-maps) with a `"src"` attribute, [dynamically injecting import maps](#dynamic-import-maps), and [reading current import map state](#reading-current-import-map-state), which can be useful in certain applications.
199
-
200
- ## Benchmarks
201
-
202
- ES Module Shims is designed for production performance. A [comprehensive benchmark suite](bench/README.md) tracks multiple loading scenarios for the project.
203
-
204
- Benchmark summary:
205
-
206
- * [ES Module Shims Chrome Passthrough](bench/README.md#chrome-passthrough-performance) (for [72% of users](https://caniuse.com/import-maps)) results in ~5ms extra initialization time over native for ES Module Shims fetching, execution and initialization, and on a slow connection the additional non-blocking bandwidth cost of its 10KB compressed download as expected.
207
- * [ES Module Shims Polyfilling](bench/README.md#native-v-polyfill-performance) (for the remaining [28% of users](https://caniuse.com/import-maps)) is on average 1.4x - 1.5x slower than native module loading, and up to 1.8x slower on slow networks (most likely due to the browser preloader), both for cached and uncached loads, and this result scales linearly up to 10MB and 20k modules loaded executing on the fastest connection in just over 2 seconds in Firefox.
208
- * [Very large import maps](bench/README.md#large-import-maps-performance) (100s of entries) cost only a few extra milliseconds upfront for the additional loading cost.
209
-
210
- ## Features
211
-
212
- ### Browser Support
213
-
214
- Works in all browsers with [baseline ES module support](https://caniuse.com/#feat=es6-module).
215
-
216
- Browser Compatibility on baseline ES modules support **with** ES Module Shims:
217
-
218
- | ES Modules Features | Chrome (61+) | Firefox (60+) | Safari (10.1+) |
219
- | ----------------------------------------------- | ------------------------------------ | ------------------------------------ | ------------------------------------ |
220
- | [modulepreload](#modulepreload) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
221
- | [Dynamic Import](#dynamic-import) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
222
- | [import.meta.url](#importmetaurl) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
223
- | [Import Maps](#import-maps) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
224
- | [JSON Modules](#json-modules) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
225
- | [CSS Modules](#css-modules) | :heavy_check_mark:<sup>1</sup> | :heavy_check_mark:<sup>1</sup> | :heavy_check_mark:<sup>1</sup> |
226
- | [import.meta.resolve](#resolve) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
227
- | [Module Workers](#module-workers) (via wrapper) | 63+ | :x:<sup>2</sup> | 15+ |
228
- | Top-Level Await (unpolyfilled<sup>3</sup>) | 89+ | 89+ | 15+ |
229
-
230
- * 1: _CSS module support requires a separate [Constructable Stylesheets polyfill](https://github.com/calebdwilliams/construct-style-sheets#readme)._
231
- * 2: _Module worker support cannot yet be implemented in Firefox due to no dynamic import support in web workers._
232
- * 3: _Top-level await support is not currently polyfilled but is possible for ES Module Shims to implement for intermediate browser versions, with the feature request tracking in https://github.com/guybedford/es-module-shims/issues/5. The compatibility gap with native modules is currently < 5% of users so it may not even be necessary._
233
-
234
- Browser compatibility **without** ES Module Shims:
235
-
236
- | ES Modules Features | Chrome | Firefox | Safari |
237
- | ---------------------------------- | ------------------ | ------------------ | ------------------ |
238
- | [modulepreload](#modulepreload) | 66+ | :x: | :x: |
239
- | [Dynamic Import](#dynamic-import) | 63+ | 67+ | 11.1+ |
240
- | [import.meta.url](#importmetaurl) | ~76+ | ~67+ | ~12+ ❕<sup>1</sup> |
241
- | [Import Maps](#import-maps) | 89+ | :x: | :x: |
242
- | [JSON Modules](#json-modules) | 91+ | :x: | :x: |
243
- | [CSS Modules](#css-modules) | 95+ | :x: | :x: |
244
- | [import.meta.resolve](#resolve) | :x: | :x: | :x: |
245
- | [Module Workers](#module-workers) | ~68+ | :x: | :x: |
246
- | Top-Level Await | 89+ | 89+ | 15+ |
247
-
248
- * ❕<sup>1</sup>: On module redirects, Safari returns the request URL in `import.meta.url` instead of the response URL as per the spec.
249
-
250
- ### Import Maps
251
-
252
- > Stability: WhatWG Standard, Single Browser Implementer
253
-
254
- Import maps allow for importing "bare specifiers" in JavaScript modules, which prior to import maps throw in all browsers with native modules support.
255
-
256
- Using this polyfill we can write:
257
-
258
- ```html
259
- <script type="importmap-shim">
260
- {
261
- "imports": {
262
- "test": "/test.js"
263
- },
264
- "scopes": {
265
- "/": {
266
- "test-dep": "/test-dep.js"
267
- }
268
- }
269
- }
270
- </script>
271
- <script type="module-shim">
272
- import test from "test";
273
- console.log(test);
274
- </script>
275
- ```
276
-
277
- All modules are still loaded with the native browser module loader, but with their specifiers rewritten then executed as Blob URLs, so there is a relatively minimal overhead to using a polyfill approach like this.
278
-
279
- #### Multiple Import Maps
280
-
281
- Multiple import maps are not currently supported in any native implementation, Chromium support is currently being tracked in https://bugs.chromium.org/p/chromium/issues/detail?id=927119.
282
-
283
- In polyfill mode, multiple import maps are therefore not supported.
284
-
285
- In shim mode, support for multiple `importmap-shim` scripts follows the [import map extensions](https://github.com/guybedford/import-maps-extensions) proposal.
286
-
287
- #### External Import Maps
288
-
289
- External import maps (using a `"src"` attribute) are not currently supported in any native implementation.
290
-
291
- In polyfill mode, external import maps are therefore not supported.
292
-
293
- In shim mode, external import maps are fully supported.
294
-
295
- #### Dynamic Import Maps
296
-
297
- Support for dynamically injecting import maps with JavaScript via eg:
298
-
299
- ```js
300
- document.body.appendChild(Object.assign(document.createElement('script'), {
301
- type: 'importmap',
302
- innerHTML: JSON.stringify({ imports: { x: './y.js' } }),
303
- }));
304
- ```
305
-
306
- is supported in Chromium, provided it is injected before any module loads and there is no other import map yet loaded (multiple import maps are not supported).
307
-
308
- Both modes in ES Module Shims support dynamic injection using DOM Mutation Observers.
309
-
310
- While in polyfill mode the same restrictions apply that multiple import maps, import maps with a `src` attribute, and import maps loaded after the first module load are not supported, in shim mode all of these behaviours are fully enabled for `"importmap-shim"`.
311
-
312
- #### Reading current import map state
313
-
314
- To make it easy to keep track of import map state, es-module-shims provides a `importShim.getImportMap` utility function, available only in shim mode.
315
-
316
- ```js
317
- const importMap = importShim.getImportMap();
318
-
319
- // importMap will be an object in the same shape as the json in a importmap script
320
- ```
321
-
322
- #### Setting current import map state
323
- To make it easy to set the import map state, es-module-shims provides a `importShim.addImportMap` utility function, available only in shim mode.
324
-
325
- ```js
326
- // importMap will be an object in the same shape as the json in a importmap script
327
- const importMap = { imports: {/*...*/}, scopes: {/*...*/} };
328
-
329
- importShim.addImportMap(importMap);
330
- ```
331
-
332
-
333
- ### Dynamic Import
334
-
335
- > Stability: Stable browser standard
336
-
337
- Dynamic `import(...)` within any modules loaded will be rewritten as `importShim(...)` automatically
338
- providing full support for all es-module-shims features through dynamic import.
339
-
340
- To load code dynamically (say from the browser console), `importShim` can be called similarly:
341
-
342
- ```js
343
- importShim('/path/to/module.js').then(x => console.log(x));
344
- ```
345
-
346
- ### import.meta.url
347
-
348
- > Stability: Stable browser standard
349
-
350
- `import.meta.url` provides the full URL of the current module within the context of the module execution.
351
-
352
- ### modulepreload
353
-
354
- > Stability: WhatWG Standard, Single Browser Implementer
355
-
356
- Preloading of modules can be achieved by including a `<link rel="modulepreload" href="/module.js" />` tag in the HTML or injecting it dynamically.
357
-
358
- This tag also supports the `"integrity"`, `"crossorigin"` and `"referrerpolicy"` attributes as supported on module scripts.
359
-
360
- This tag just initiates a fetch request in the browser and thus works equally as a preload polyfill in both shimmed and unshimmed modes, with integrity validation support.
361
-
362
- Unlike the browser specification, the modulepreload polyfill does not request dependency modules by default, in order to avoid unnecessary
363
- code analysis in the polyfill scenarios. **It is recommended to preload deep imports anyway so that this feature shouldn't be necessary.**
364
-
365
- #### Preload shim
366
-
367
- When in shim mode, `<link rel="modulepreload-shim" href="/module.js" />` must be used to properly cache the preloaded modules.
368
-
369
- ### CSP Support
370
-
371
- By default ES Module Shims provides full support for CSP by using the asm.js ES Module Lexer build. This is absolutely identical in performance to the Wasm version in Firefox and Chrome, while in Safari the asm.js version is actually faster than Wasm making this build preferable.
372
-
373
- The CSP nonce to use for module scripts will be picked up from the first script on the page or via the [`nonce` init option](#nonce).
374
-
375
- A full example of such a CSP workflow is provided below:
376
-
377
- ```html
378
- <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'nonce-n0nce'" />
379
- <script async src="es-module-shims.js"></script>
380
- <script type="importmap" nonce="n0nce">
381
- {
382
- "pkg": "/pkg.js"
383
- }
384
- </script>
385
- <script type="module" nonce="n0nce">
386
- import pkg from 'pkg';
387
- </script>
388
- ```
389
-
390
- #### Wasm Build
391
-
392
- To use the Web Assembly / non-CSP build of ES Module Shims, this is available as a self-contained single file at `es-module-shims/wasm` or `es-module-shims/dist/es-module-shims.wasm.js` in the package folder.
393
-
394
- ### JSON Modules
395
-
396
- > Stability: WhatWG Standard, Single Browser Implementer
397
-
398
- In shim mode, JSON modules are always supported. In polyfill mode, JSON modules require the `polyfillEnable: ['json-modules']` [init option](#polyfill-enable-option).
399
-
400
- JSON Modules are currently supported in Chrome when using them via an import assertion:
401
-
402
- ```html
403
- <script type="module">
404
- import json from 'https://site.com/data.json' assert { type: 'json' };
405
- </script>
406
- ```
407
-
408
- In addition JSON modules need to be served with a valid JSON content type.
409
-
410
- Checks for assertion failures are not currently included.
411
-
412
- ### CSS Modules
413
-
414
- > Stability: WhatWG Standard, Single Browser Implementer
415
-
416
- In shim mode, CSS modules are always supported. In polyfill mode, CSS modules require the `polyfillEnable: ['css-modules']` [init option](#polyfill-enable-option).
417
-
418
- CSS Modules are currently supported in Chrome when using them via an import assertion:
419
-
420
- ```html
421
- <script type="module">
422
- import sheet from 'https://site.com/sheet.css' assert { type: 'css' };
423
- </script>
424
- ```
425
-
426
- To support the polyfill or shim of this feature, the [Constructable Stylesheets polyfill](https://github.com/calebdwilliams/construct-style-sheets#readme) must be separately included in browsers not supporting [Constructable Stylesheets](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/CSSStyleSheet) eg via:
427
-
428
- ```html
429
- <script async src="https://unpkg.com/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.js"></script>
430
- ```
431
-
432
- For more information see the [web.dev article](https://web.dev/css-module-scripts/).
433
-
434
- In addition CSS modules need to be served with a valid CSS content type.
435
-
436
- Checks for assertion failures are not currently included.
437
-
438
- ### Resolve
439
-
440
- > Stability: Draft HTML PR
441
-
442
- `import.meta.resolve` provides a contextual resolver within modules. It is synchronous, changed from being formerly asynchronous due to following the [browser specification PR](https://github.com/whatwg/html/pull/5572).
443
-
444
- The second argument to `import.meta.resolve` permits a custom parent URL scope for the resolution (not currently in the browser spec), which defaults to `import.meta.url`.
445
-
446
- ```js
447
- // resolve a relative path to a module
448
- var resolvedUrl = import.meta.resolve('./relative.js');
449
- // resolve a dependency from a module
450
- var resolvedUrl = import.meta.resolve('dep');
451
- // resolve a path
452
- var resolvedUrlPath = import.meta.resolve('dep/');
453
- // resolve with a custom parent scope
454
- var resolvedUrl = import.meta.resolve('dep', 'https://site.com/another/scope');
455
- ```
456
-
457
- Node.js also implements a similar API, although it's in the process of shifting to a synchronous resolver.
458
-
459
- ### Module Workers
460
-
461
- ES Module Shims can be used in module workers in browsers that provide dynamic import in worker environments, which at the moment are Chrome(80+), Edge(80+) and Safari(15+).
462
-
463
- By default, when there is no DOM present, ES Module Shims will switch into shim mode. An example of ES Module Shims usage through shim mode in web workers is provided below:
464
-
465
- ```js
466
- /**
467
- *
468
- * @param {string} aURL a string representing the URL of the module script the worker will execute.
469
- * @returns {string} The string representing the URL of the script the worker will execute.
470
- */
471
- function getWorkerScriptURL(aURL) {
472
- // baseURL, esModuleShimsURL are considered to be known in advance
473
- // esModuleShimsURL - must point to the non-CSP build of ES Module Shims,
474
- // namely the `es-module-shim.wasm.js` output: es-module-shims/dist/es-module-shims.wasm.js
475
-
476
- return URL.createObjectURL(new Blob(
477
- [
478
- `importScripts('${new URL(esModuleShimsURL, baseURL).href}');
479
- importShim.addImportMap(${JSON.stringify(importShim.getImportMap())});
480
- importShim('${new URL(aURL, baseURL).href}').catch(e => setTimeout(() => { throw e; }))`
481
- ],
482
- { type: 'application/javascript' }))
483
- }
484
-
485
- const worker = new Worker(getWorkerScriptURL('myEsModule.js'));
486
- ```
487
-
488
- > Web workers must use the non-CSP build of ES Module Shims via `es-module-shim.wasm.js` from the `dist/` folder, since the CSP build currently assumes a DOM.
489
-
490
- ## Init Options
491
-
492
- Provide a `esmsInitOptions` on the global scope before `es-module-shims` is loaded to configure various aspects of the module loading process:
493
-
494
- * [shimMode](#shim-mode-option)
495
- * [polyfillEnable](#polyfill-enable-option)
496
- * [enforceIntegrity](#enforce-integrity)
497
- * [nonce](#nonce)
498
- * [noLoadEventRetriggers](#no-load-event-retriggers)
499
- * [skip](#skip)
500
- * [onerror](#error-hook)
501
- * [onpolyfill](#polyfill-hook)
502
- * [resolve](#resolve-hook)
503
- * [fetch](#fetch-hook)
504
- * [revokeBlobURLs](#revoke-blob-urls)
505
- * [mapOverrides](#overriding-import-map-entries)
506
-
507
- ```html
508
- <script>
509
- window.esmsInitOptions = {
510
- // Enable Shim Mode
511
- shimMode: true, // default false
512
- // Enable newer modules features
513
- polyfillEnable: ['css-modules', 'json-modules'], // default empty
514
- // Custom CSP nonce
515
- nonce: 'n0nce', // default is automatic detection
516
- // Don't retrigger load events on module scripts
517
- noLoadEventRetriggers: true, // default false
518
- // Skip source analysis of certain URLs for full native passthrough
519
- skip: /^https:\/\/cdn\.com/, // defaults to null
520
- // Clean up blob URLs after execution
521
- revokeBlobURLs: true, // default false
522
- // Secure mode to not support loading modules without integrity (integrity is always verified though)
523
- enforceIntegrity: true, // default false
524
- // Permit overrides to import maps
525
- mapOverrides: true, // default false
526
-
527
- // -- Hooks --
528
- // Module load error
529
- onerror: (e) => { /*...*/ }, // default noop
530
- // Called when polyfill mode first engages
531
- onpolyfill: () => {}, // default logs to the console
532
- // Hook all module resolutions
533
- resolve: (id, parentUrl, resolve) => resolve(id, parentUrl), // default is spec resolution
534
- // Hook source fetch function
535
- fetch: (url, options) => fetch(url, options), // default is native
536
- // Hook import.meta construction
537
- meta: (meta, url) => void // default is noop
538
- // Hook top-level imports
539
- onimport: (url, options, parentUrl) => void // default is noop
540
- }
541
- </script>
542
- <script async src="es-module-shims.js"></script>
543
- ```
544
-
545
- `<script type="esms-options">` can also be used:
546
-
547
- ```html
548
- <script type="esms-options">
549
- {
550
- "shimMode": true,
551
- "polyfillEnable": ["css-modules", "json-modules"],
552
- "nonce": "n0nce",
553
- "onpolyfill": "polyfill"
554
- }
555
- </script>
556
- ```
557
-
558
- This can be convenient when using a CSP policy. Function strings correspond to global function names.
559
-
560
- ### Shim Mode Option
561
-
562
- [Shim Mode](#shim-mode) can be overridden using the `shimMode` option:
563
-
564
- ```html
565
- <script type="esms-options">
566
- {
567
- "shimMode": true
568
- }
569
- </script>
570
- ```
571
-
572
- For example, if lazy loading `<script type="module-shim">` scripts alongside static native module scripts, shim mode would not be enabled at initialization time.
573
-
574
- DOM `load` events are fired for all `"module-shim"` scripts both for success and failure just like for native module scripts.
575
-
576
- ### Pollyfill Enable Option
577
-
578
- The `polyfillEnable` option allows enabling polyfill features which are newer and would otherwise result in unnecessary polyfilling in modern browsers that haven't yet updated.
579
-
580
- Currently this option supports just `"css-modules"` and `"json-modules"`.
581
-
582
- ```html
583
- <script type="esms-options">
584
- {
585
- "polyfillEnable": ["css-modules", "json-modules"]
586
- }
587
- </script>
588
- ```
589
-
590
- The above is necessary to enable CSS modules and JSON modules.
591
-
592
- #### Baseline Support Analysis Opt-Out
593
-
594
- The reason the `polyfillEnable` option is needed is because ES Module Shims implements a performance optimization where if a browser supports modern modules features to an expected baseline of import maps support, it will skip all polyfill source analysis resulting in full native passthrough performance.
595
-
596
- If the application code then tries to use modern features like CSS modules beyond this baseline it won't support those features. As a result all modules features which are considered newer or beyond the recommended baseline require explicit enabling. This common baseline itself will change to track the common future modules baseline supported by this project for each release cycle.
597
-
598
- This option can also be set to `true` to entirely disable the native passthrough system and ensure all sources are fetched and analyzed through ES Module Shims. This will still avoid duplicate execution since module graphs are still only reexecuted when they use unsupported native features, but there is a small extra cost in doing the analysis.
599
-
600
- ### Enforce Integrity
601
-
602
- When enabled, `enforceIntegrity` will ensure that all modules loaded through ES Module Shims must have integrity defined either on a `<link rel="modulepreload" integrity="...">` or on
603
- a `<link rel="modulepreload-shim" integrity="...">` preload tag in shim mode. Modules without integrity will throw at fetch time.
604
-
605
- For example in the following, only the listed `app.js` and `dep.js` modules will be able to execute with the provided integrity:
606
-
607
- ```html
608
- <script type="esms-options">{ "enforceIntegrity": true }</script>
609
- <link rel="modulepreload-shim" href="/app.js" integrity="sha384-..." />\
610
- <link rel="modulepreload-shim" href="/dep.js" integrity="sha384-..." />
611
- <script type="module-shim">
612
- import '/app.js';
613
- </script>
614
- ```
615
-
616
- Strong execution guarantees are only possible in shim mode since in polyfill mode it is not possible to stop the native loader from executing code without an integrity.
617
-
618
- Future versions of this option may provide support for origin-specific allow lists.
619
-
620
- ### Nonce
621
-
622
- The `nonce` option allows setting a CSP nonce to be used with all script injections for full CSP compatibility supported by the [CSP build](#csp-build) of ES Module Shims.
623
-
624
- Alternatively, add a `blob:` URL policy with the CSP build to get CSP compatibility.
625
-
626
- ```js
627
- <script type="esms-options">
628
- {
629
- "nonce": "n0nce"
630
- }
631
- </script>
632
- ```
633
-
634
- ### No Load Event Retriggers
635
-
636
- Because of the extra processing done by ES Module Shims it is possible for static module scripts to execute after the `DOMContentLoaded` or `readystatechange` events they expect, which can cause missed attachment.
637
-
638
- In order to ensure libraries that rely on these event still behave correctly, ES Module Shims will always double trigger these events that would normally have executed before the document ready state transition to completion, once all the static module scripts in the page have been completely executed through ES module shims.
639
-
640
- In such a case, this double event firing can be disabled with the `noLoadEventRetriggers` option:
641
-
642
- ```js
643
- <script type="esms-options">
644
- {
645
- // do not re-trigger DOM events (onreadystatechange, DOMContentLoaded)
646
- "noLoadEventRetriggers": true
647
- }
648
- </script>
649
- <script async src="es-module-shims.js"></script>
650
- ```
651
-
652
- ### Skip
653
-
654
- When loading modules that you know will only use baseline modules features, it is possible to set a rule to explicitly opt-out modules from being polyfilled to always load and be referenced through the native loader only. This enables instance sharing with the native loader and also improves performance because those modules then do not need to be processed or transformed at all, so that only local application code is handled and not library code.
655
-
656
- The `skip` option supports a string regular expression or array of exact module URLs to check:
657
-
658
- ```js
659
- <script type="esms-options">
660
- {
661
- "skip": "^https?:\/\/(cdn\.skypack\.dev|jspm\.dev)\/"
662
- }
663
- </script>
664
- <script async src="es-module-shims.js"></script>
665
- ```
666
-
667
- When passing an array, relative URLs or paths ending in `/` can be provided:
668
-
669
- ```js
670
- <script type="esms-options">
671
- {
672
- "skip": ["./app.js", "https://jspm.dev/"]
673
- }
674
- </script>
675
- <script async src="es-module-shims.js"></script>
676
- ```
677
-
678
- ### Revoke Blob URLs
679
-
680
- When polyfilling the missing features `es-module-shims` would create in-memory blobs using `URL.createObjectURL()` for each processed module.
681
- In most cases, memory footprint of these blobs is negligible so there is no need to call `URL.revokeObjectURL()`
682
- for them, and we don't do that by default.
683
-
684
- That said, in some scenarios, e.g. when evaluating some continuously changing modules without a page reload, like in a web-based code editor,
685
- you might want to reduce the growth of memory usage by revoking those blob URLs after they were already `import`ed.
686
-
687
- You can do that by enabling the `revokeBlobURLs` init option:
688
-
689
- ```js
690
- <script type="esms-options">
691
- {
692
- "revokeBlobURLs": true
693
- }
694
- </script>
695
- <script type="module" src="es-module-shims.js"></script>
696
- ```
697
-
698
- NOTE: revoking object URLs is not entirely free, while we are trying to be smart about it and make sure it doesn't
699
- cause janks, we recommend enabling this option only if you have done the measurements and identified that you really need it.
700
-
701
- ### Overriding import map entries
702
-
703
- When [dynamically injecting import maps](#dynamic-import-maps), an error will be thrown in both polyfill and shim modes if the new import map would override existing entries with a different value.
704
-
705
- It is possible to disable this behavior in shim mode by setting the `mapOverrides` option:
706
-
707
- ```js
708
- <script type="esms-options">
709
- {
710
- "shimMode": true,
711
- "mapOverrides": true
712
- }
713
- </script>
714
- <script type="importmap-shim">
715
- {
716
- "imports": {
717
- "x": "/x.js"
718
- }
719
- }
720
- </script>
721
- <script>
722
- // No error will be thrown here
723
- document.body.appendChild(Object.assign(document.createElement('script'), {
724
- type: 'importmap',
725
- innerHTML: JSON.stringify({ imports: { x: './y.js' } }),
726
- }));
727
- </script>
728
- ```
729
-
730
- This can be useful for HMR workflows.
731
-
732
- ### Hooks
733
-
734
- #### Polyfill hook
735
-
736
- The polyfill hook is called when running in polyfill mode and the polyfill is kicking in instead of passing through to the native loader.
737
-
738
- This can be a useful way to verify that the native passthrough is working correctly in latest browsers for performance, while also allowing eg the ability to analyze or get metrics reports of how many users are getting the polyfill actively applying to their browser application loads.
739
-
740
- ```js
741
- <script>
742
- window.polyfilling = () => console.log('The polyfill is actively applying');
743
- </script>
744
- <script type="esms-options">
745
- {
746
- "onpolyfill": "polyfilling"
747
- }
748
- </script>
749
- ```
750
-
751
- The default hook will log a message to the console with `console.info` noting that polyfill mode is enabled and that the native error can be ignored.
752
-
753
- Overriding this hook with an empty function will disable the default polyfill log output.
754
-
755
- In the above, running in latest Chromium browsers, nothing will be logged, while running in an older browser that does not support newer features like import maps the console log will be output.
756
-
757
- #### Error hook
758
-
759
- You can provide a function to handle errors during the module loading process by providing an `onerror` option:
760
-
761
- ```js
762
- <script>
763
- window.esmsInitOptions = {
764
- onerror: error => console.log(error) // defaults to `((e) => { throw e; })`
765
- }
766
- </script>
767
- <script async src="es-module-shims.js"></script>
768
- ```
769
-
770
- #### Import Hook
771
-
772
- The import hook is supported for both shim and polyfill modes and provides an async hook which can ensure any necessary work is done before a top-level module import or dynamic `import()` starts further processing.
773
-
774
- ```js
775
- <script>
776
- window.esmsInitOptions = {
777
- onimport: function (url, options, parentUrl) {
778
- console.log(`Top-level import for ${url}`);
779
- }
780
- }
781
- </script>
782
- ```
783
-
784
- #### Resolve Hook
785
-
786
- The resolve hook is supported for both shim and polyfill modes and allows full customization of the resolver, while still having access to the original resolve function.
787
-
788
- Note that in polyfill mode the resolve hook may not be called for all modules when native passthrough is occurring and that it still will not affect
789
- the native passthrough executions.
790
-
791
- If the resolve hook should apply for all modules in the entire module graph, make sure to set `polyfillEnable: true` to [disable the baseline support analysis opt-out](#baseline-support-analysis-opt-out).
792
-
793
- ```js
794
- <script>
795
- window.esmsInitOptions = {
796
- shimMode: true,
797
- resolve: function (id, parentUrl, defaultResolve) {
798
- if (id === 'custom' && parentUrl.startsWith('https://custom.com/'))
799
- return 'https://custom.com/custom.js';
800
-
801
- // Default resolve will handle the typical URL and import map resolution
802
- return defaultResolve(id, parentUrl);
803
- }
804
- }
805
- </script>
806
- ```
807
-
808
- Support for an asynchronous resolve hook has been deprecated as of 1.5.0 and will be removed in the next major.
809
-
810
- Instead async work should be done with the import hook.
811
-
812
- #### Meta Hook
813
-
814
- The meta hook allows customizing the `import.meta` object in each module scope.
815
-
816
- The function takes as arguments the `import.meta` object itself (with `import.meta.url` an `import.meta.resolve` already present), and the URL of the module as its second argument.
817
-
818
- Example:
819
-
820
- ```js
821
- <script>
822
- window.esmsInitOptions = {
823
- shimMode: true,
824
- meta: function (metaObj, url) {
825
- metaObj.custom = `custom value for ${url}`;
826
- }
827
- }
828
- </script>
829
- ```
830
-
831
- Where within the module the following would be supported:
832
-
833
- ```js
834
- import assert from 'assert';
835
- assert.ok(import.meta.custom.startsWith('custom value'));
836
- ```
837
-
838
- #### Fetch Hook
839
-
840
- The fetch hook is supported for shim mode only.
841
-
842
- The ES Module Shims fetch hook can be used to implement transform plugins.
843
-
844
- For example TypeScript support:
845
-
846
- ```js
847
- <script>
848
- window.esmsInitOptions = {
849
- shimMode: true,
850
- fetch: async function (url, options) {
851
- const res = await fetch(url, options);
852
- if (!res.ok)
853
- return res;
854
- if (res.url.endsWith('.ts')) {
855
- const source = await res.body();
856
- const transformed = tsCompile(source);
857
- return new Response(new Blob([transformed], { type: 'application/javascript' }));
858
- }
859
- return res;
860
- } // defaults to `((url, options) => fetch(url, options))`
861
- }
862
- </script>
863
- <script async src="es-module-shims.js"></script>
864
- ```
865
-
866
- Because the dependency analysis applies by ES Module Shims takes care of ensuring all dependencies run through the same fetch hook,
867
- the above is all that is needed to implement custom plugins.
868
-
869
- Streaming support is also provided, for example here is a hook with streaming support for JSON:
870
-
871
- ```js
872
- window.esmsInitOptions = {
873
- shimMode: true,
874
- fetch: async function (url, options) {
875
- const res = await fetch(url, options);
876
- if (!res.ok || !/^application\/json($|;)/.test(res.headers.get('content-type')))
877
- return res;
878
- const reader = res.body.getReader();
879
- const headers = new Headers(res.headers);
880
- headers.set('Content-Type', 'application/javascript');
881
- return new Response(new ReadableStream({
882
- async start (controller) {
883
- let done, value;
884
- controller.enqueue(new TextEncoder().encode('export default '));
885
- while (({ done, value } = await reader.read()) && !done) {
886
- controller.enqueue(value);
887
- }
888
- controller.close();
889
- }
890
- }), { headers });
891
- }
892
- }
893
- ```
894
-
895
- ## Implementation Details
896
-
897
- ### Import Rewriting
898
-
899
- * Sources are fetched, import specifiers are rewritten to reference exact URLs, and then executed as BlobURLs through the whole module graph.
900
- * The [lexer](https://github.com/guybedford/es-module-lexer) handles the full language grammar including nested template strings, comments, regexes and division operator ambiguity based on backtracking.
901
- * When executing a circular reference A -> B -> A, a shell module technique is used to acyclify into the graph A -> B -> A Shell, with A -> A Shell. The shell module exports an update function which is called by the original once after the last import statement, and again after the last statement of the source file.
902
-
903
- ## Inspiration
904
-
905
- Huge thanks to Rich Harris for inspiring this approach with [Shimport](https://github.com/rich-harris/shimport).
906
-
907
- ## License
908
-
909
- MIT
1
+ # ES Module Shims
2
+
3
+ Shims modern ES Modules features like import maps on top of the baseline modules support in browsers supported by [95% of users](https://caniuse.com/#feat=es6-module).
4
+
5
+ When running in polyfill mode, [the 72% of users](https://caniuse.com/import-maps) with import maps entirely bypass the shim code entirely.
6
+
7
+ For the remaining 28% of users, the highly performant (see [benchmarks](#benchmarks)) production and [CSP-compatible](#csp-support) shim kicks in to rewrite module specifiers driven by the [Web Assembly ES Module Lexer](https://github.com/guybedford/es-module-lexer).
8
+
9
+ The following modules features are polyfilled:
10
+
11
+ * [Import Maps](#import-maps) polyfill.
12
+ * Dynamic `import()` shimming when necessary in eg older Firefox versions.
13
+ * `import.meta` and `import.meta.url`.
14
+ * [JSON](#json-modules) and [CSS modules](#css-modules) with import assertions (when enabled).
15
+ * [`<link rel="modulepreload">` polyfill](#modulepreload) in non Chromium browsers for both shimmed and unshimmed preloading scenarios.
16
+
17
+ When running in shim mode, module rewriting is applied for all users and custom [resolve](#resolve-hook) and [fetch](#fetch-hook) hooks can be implemented allowing for custom resolution and streaming in-browser transform workflows.
18
+
19
+ Because we are still using the native module loader the edge cases work out comprehensively, including:
20
+
21
+ * Live bindings in ES modules
22
+ * Dynamic import expressions (`import('src/' + varname')`)
23
+ * Circular references, with the execption that live bindings are disabled for the first unexecuted circular parent.
24
+
25
+ > [Built with](https://github.com/guybedford/es-module-shims/blob/main/chompfile.toml) [Chomp](https://chompbuild.com/)
26
+
27
+ ## Usage
28
+
29
+ Include ES Module Shims with a `async` attribute on the script, then include an import map and module scripts normally:
30
+
31
+ ```html
32
+ <script async src="https://ga.jspm.io/npm:es-module-shims@1.7.0/dist/es-module-shims.js"></script>
33
+
34
+ <!-- https://generator.jspm.io/#U2NhYGBkDM0rySzJSU1hKEpNTC5xMLTQM9Az0C1K1jMAAKFS5w0gAA -->
35
+ <script type="importmap">
36
+ {
37
+ "imports": {
38
+ "react": "https://ga.jspm.io/npm:react@18.0.0-rc.0/index.js"
39
+ },
40
+ "scopes": {
41
+ "https://ga.jspm.io/npm:react@18.0.0-rc.0/": {
42
+ "object-assign": "https://ga.jspm.io/npm:object-assign@4.1.1/index.js"
43
+ }
44
+ }
45
+ }
46
+ </script>
47
+
48
+ <script type="module">
49
+ import react from 'react';
50
+ console.log(react);
51
+ </script>
52
+ ```
53
+
54
+ ## Polyfill Explainer
55
+
56
+ When running the previous example in a browser without import maps support, the browser will output the following console error:
57
+
58
+ ```
59
+ Uncaught TypeError: Failed to resolve module specifier "react". Relative references must start with either "/", "./", or "../".
60
+ at <anonymous>:1:15
61
+ ```
62
+
63
+ This error is important - it means that the native browser loader didn't execute any of the code at all because this error happens
64
+ at link time, and before execution time. And this is what allows the polyfill to be able to reexecute the modules and their dependencies
65
+ without risk of duplicate execution.
66
+
67
+ The ES Module Shims polyfill will analyze the browser to see if it supports import maps. If it does, it doesn't do anything more,
68
+ otherwise it will analyze all module scripts on the page to see if any of them have bare specifier imports that will fail like this.
69
+ If one is found, it will then be reexecuted through ES Module Shims using its internal shimming of modules features.
70
+
71
+ When the polyfill kicks in another console log message is output(which can be disabled or customized via the [polyfill hook](#polyfill-hook)):
72
+
73
+ ```
74
+ ^^ Module TypeError above is polyfilled and can be ignored ^^
75
+ ```
76
+
77
+ ### Polyfill Edge Case: Dynamic Import
78
+
79
+ Only static link-time errors are polyfilled, not runtime errors.
80
+
81
+ Module feature errors that are not _static errors_ but rather _runtime errors_ will bypass the polyfill detection.
82
+
83
+ For example:
84
+
85
+ ```html
86
+ <script type="module">
87
+ import './supported-relative-import.js';
88
+ console.log('Static Ok');
89
+ import('unsupported-import-map').then(x => {
90
+ console.log('Dynamic Ok');
91
+ }, err => {
92
+ console.log('Dynamic Fail');
93
+ });
94
+ </script>
95
+ ```
96
+
97
+ In the above, the native browser loader without import maps support will execute the above module, but fail the dynamic import.
98
+
99
+ See the log output in various scenarios:
100
+
101
+ * Native with Import Maps: `Static Ok`, `Dynamic Ok`
102
+ * Native without Import Maps: `Static Ok`, `Dynamic Fail`
103
+ * Native without Import Maps running ES Module Shims: `Static Ok`, `Dynamic Fail`
104
+
105
+ ES Module Shims **does not polyfill the dynamic import**. The reason for this is that if it did, it would need to reexecute the entire outer module resulting in `Static Ok`, `Dynamic Fail`, `Static Ok`, `Dynamic Ok`. This _double execution_ would be a change of the normal execution in running code twice that would not be deemed suitable for calling it a polyfill.
106
+
107
+ This is why it is advisable to always ensure modules use a bare specifier early to avoid non-execution.
108
+
109
+ If a static failure is not possible and dynamic import must be used, one alternative is to use the `importShim` ES Module Shims top-level loader:
110
+
111
+ ```html
112
+ <script type="module">
113
+ import './supported-relative-import.js';
114
+ console.log('Static Ok');
115
+ importShim('unsupported-import-map').then(x => {
116
+ console.log('Ok');
117
+ }, err => {
118
+ console.log('Fail');
119
+ });
120
+ </script>
121
+ ```
122
+
123
+ `importShim` will automatically pass-through to the native dynamic import or polyfill as necessary, just like it does for script tags.
124
+
125
+ ### Polyfill Edge Case: Instance Sharing
126
+
127
+ When running in polyfill mode, it can be thought of that are effectively two loaders running on the page - the ES Module Shims polyfill loader, and the native loader.
128
+
129
+ Note that instances are not shared between these loaders for consistency and performance, since some browsers do not properly share the fetch cache and native loader cache resulting in a double fetch which would be inefficient.
130
+
131
+ As a result, if you have two module graphs - one native and one polyfilled, they will not share the same dependency instance, for example:
132
+
133
+ ```html
134
+ <script type="importmap">
135
+ {
136
+ "imports": {
137
+ "dep": "/dep.js"
138
+ }
139
+ }
140
+ </script>
141
+ <script type="module">
142
+ import '/dep.js';
143
+ </script>
144
+ <script type="module">
145
+ import 'dep';
146
+ </script>
147
+ ```
148
+
149
+ ```dep
150
+ console.log('DEP');
151
+ ```
152
+
153
+ When polyfilling import maps, ES Module Shims will pick up on the second import failure and reexecute `/dep.js` as a new instance, logging `"DEP"` twice.
154
+
155
+ For this reason it is important to always ensure all modules hit the polyfill path, either by having all graphs use import maps at the top-level, or via `importShim` directly.
156
+
157
+ If you really need to support instance sharing with the native loader, a useful workaround is to use the [`skip` option](#skip) to list modules which should always be loaded via the native loader:
158
+
159
+ ```html
160
+ <script type="esms-options">
161
+ {
162
+ "skip": ["/dep.js"]
163
+ }
164
+ </script>
165
+ ```
166
+
167
+ The above would then fully cause dependency module instance to be shared between ES Module Shims and the native loader, with the polyfill then logging `"DEP"` only once.
168
+
169
+ #### No Shim Scripts
170
+
171
+ If the polyfill is analyzing or applying to a module script that doesn't need to or shouldn't be polyfilled, adding the `"noshim"` attribute to the script tag will ensure that ES Module Shims ignores processing this script entirely:
172
+
173
+ ```html
174
+ <script type="module" noshim>
175
+ // ...
176
+ </script>
177
+ ```
178
+
179
+ ### Polyfill Features
180
+
181
+ If using more modern features like CSS Modules or JSON Modules, these need to be manually enabled via the [`polyfillEnable` init option](#polyfill-enable-option) to raise the native baseline from just checking import maps to also checking that browsers support these features:
182
+
183
+ ```html
184
+ <script>
185
+ window.esmsInitOptions = { polyfillEnable: ['css-modules', 'json-modules'] }
186
+ </script>
187
+ ```
188
+
189
+ To verify when the polyfill is actively engaging as opposed to relying on the native loader, [a `polyfill` hook](#polyfill-hook) is also provided.
190
+
191
+ ## Shim Mode
192
+
193
+ Shim mode is an alternative to polyfill mode and doesn't rely on native modules erroring - instead it is triggered by the existence of any `<script type="importmap-shim">` or `<script type="module-shim">`, or when explicitly setting the [`shimMode` init option](#shim-mode-option).
194
+
195
+ In shim mode, only the above `importmap-shim` and `module-shim` tags will be parsed and executed by ES Module Shims.
196
+
197
+ Shim mode also provides some additional features that aren't yet natively supported such as supporting multiple import maps, [external import maps](#external-import-maps) with a `"src"` attribute, [dynamically injecting import maps](#dynamic-import-maps), and [reading current import map state](#reading-current-import-map-state), which can be useful in certain applications.
198
+
199
+ ## Benchmarks
200
+
201
+ ES Module Shims is designed for production performance. A [comprehensive benchmark suite](bench/README.md) tracks multiple loading scenarios for the project.
202
+
203
+ Benchmark summary:
204
+
205
+ * [ES Module Shims Chrome Passthrough](bench/README.md#chrome-passthrough-performance) (for [72% of users](https://caniuse.com/import-maps)) results in ~5ms extra initialization time over native for ES Module Shims fetching, execution and initialization, and on a slow connection the additional non-blocking bandwidth cost of its 10KB compressed download as expected.
206
+ * [ES Module Shims Polyfilling](bench/README.md#native-v-polyfill-performance) (for the remaining [28% of users](https://caniuse.com/import-maps)) is on average 1.4x - 1.5x slower than native module loading, and up to 1.8x slower on slow networks (most likely due to the browser preloader), both for cached and uncached loads, and this result scales linearly up to 10MB and 20k modules loaded executing on the fastest connection in just over 2 seconds in Firefox.
207
+ * [Very large import maps](bench/README.md#large-import-maps-performance) (100s of entries) cost only a few extra milliseconds upfront for the additional loading cost.
208
+
209
+ ## Features
210
+
211
+ ### Browser Support
212
+
213
+ Works in all browsers with [baseline ES module support](https://caniuse.com/#feat=es6-module).
214
+
215
+ Browser Compatibility on baseline ES modules support **with** ES Module Shims:
216
+
217
+ | ES Modules Features | Chrome (61+) | Firefox (60+) | Safari (10.1+) |
218
+ | ----------------------------------------------- | ------------------------------------ | ------------------------------------ | ------------------------------------ |
219
+ | [modulepreload](#modulepreload) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
220
+ | [Dynamic Import](#dynamic-import) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
221
+ | [import.meta.url](#importmetaurl) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
222
+ | [Import Maps](#import-maps) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
223
+ | [JSON Modules](#json-modules) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
224
+ | [CSS Modules](#css-modules) | :heavy_check_mark:<sup>1</sup> | :heavy_check_mark:<sup>1</sup> | :heavy_check_mark:<sup>1</sup> |
225
+ | [import.meta.resolve](#resolve) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
226
+ | [Module Workers](#module-workers) (via wrapper) | 63+ | :x:<sup>2</sup> | 15+ |
227
+ | Top-Level Await (unpolyfilled<sup>3</sup>) | 89+ | 89+ | 15+ |
228
+
229
+ * 1: _CSS module support requires a separate [Constructable Stylesheets polyfill](https://github.com/calebdwilliams/construct-style-sheets#readme)._
230
+ * 2: _Module worker support cannot yet be implemented in Firefox due to no dynamic import support in web workers._
231
+ * 3: _Top-level await support is not currently polyfilled but is possible for ES Module Shims to implement for intermediate browser versions, with the feature request tracking in https://github.com/guybedford/es-module-shims/issues/5. The compatibility gap with native modules is currently < 5% of users so it may not even be necessary._
232
+
233
+ Browser compatibility **without** ES Module Shims:
234
+
235
+ | ES Modules Features | Chrome | Firefox | Safari |
236
+ | ---------------------------------- | ------------------ | ------------------ | ------------------ |
237
+ | [modulepreload](#modulepreload) | 66+ | :x: | :x: |
238
+ | [Dynamic Import](#dynamic-import) | 63+ | 67+ | 11.1+ |
239
+ | [import.meta.url](#importmetaurl) | ~76+ | ~67+ | ~12+ ❕<sup>1</sup> |
240
+ | [Import Maps](#import-maps) | 89+ | 108+ | :x: |
241
+ | [JSON Modules](#json-modules) | 91+ | :x: | :x: |
242
+ | [CSS Modules](#css-modules) | 95+ | :x: | :x: |
243
+ | [import.meta.resolve](#resolve) | :x: | :x: | :x: |
244
+ | [Module Workers](#module-workers) | ~68+ | :x: | :x: |
245
+ | Top-Level Await | 89+ | 89+ | 15+ |
246
+
247
+ * ❕<sup>1</sup>: On module redirects, Safari returns the request URL in `import.meta.url` instead of the response URL as per the spec.
248
+
249
+ ### Import Maps
250
+
251
+ > Stability: WhatWG Standard, implemented in all browsers although only recently in Firefox and Safari
252
+
253
+ Import maps allow for importing _"bare specifiers"_ in JavaScript modules, which prior to import maps throw in all browsers with native modules support.
254
+
255
+ Using this polyfill we can write:
256
+
257
+ ```html
258
+ <script type="importmap-shim">
259
+ {
260
+ "imports": {
261
+ "test": "/test.js"
262
+ },
263
+ "scopes": {
264
+ "/": {
265
+ "test-dep": "/test-dep.js"
266
+ }
267
+ }
268
+ }
269
+ </script>
270
+ <script type="module-shim">
271
+ import test from "test";
272
+ console.log(test);
273
+ </script>
274
+ ```
275
+
276
+ All modules are still loaded with the native browser module loader, but with their specifiers rewritten then executed as Blob URLs, so there is a relatively minimal overhead to using a polyfill approach like this.
277
+
278
+ #### Multiple Import Maps
279
+
280
+ Multiple import maps are not currently supported in any native implementation, Chromium support is currently being tracked in https://bugs.chromium.org/p/chromium/issues/detail?id=927119.
281
+
282
+ In polyfill mode, multiple import maps are therefore not supported.
283
+
284
+ In shim mode, support for multiple `importmap-shim` scripts follows the [import map extensions](https://github.com/guybedford/import-maps-extensions) proposal.
285
+
286
+ #### External Import Maps
287
+
288
+ External import maps (using a `"src"` attribute) are not currently supported in any native implementation.
289
+
290
+ In polyfill mode, external import maps are therefore not supported.
291
+
292
+ In shim mode, external import maps are fully supported.
293
+
294
+ #### Dynamic Import Maps
295
+
296
+ Support for dynamically injecting import maps with JavaScript via eg:
297
+
298
+ ```js
299
+ document.body.appendChild(Object.assign(document.createElement('script'), {
300
+ type: 'importmap',
301
+ innerHTML: JSON.stringify({ imports: { x: './y.js' } }),
302
+ }));
303
+ ```
304
+
305
+ is supported in Chromium, provided it is injected before any module loads and there is no other import map yet loaded (multiple import maps are not supported).
306
+
307
+ Both modes in ES Module Shims support dynamic injection using DOM Mutation Observers.
308
+
309
+ While in polyfill mode the same restrictions apply that multiple import maps, import maps with a `src` attribute, and import maps loaded after the first module load are not supported, in shim mode all of these behaviours are fully enabled for `"importmap-shim"`.
310
+
311
+ #### Reading current import map state
312
+
313
+ To make it easy to keep track of import map state, es-module-shims provides a `importShim.getImportMap` utility function, available only in shim mode.
314
+
315
+ ```js
316
+ const importMap = importShim.getImportMap();
317
+
318
+ // importMap will be an object in the same shape as the json in a importmap script
319
+ ```
320
+
321
+ #### Setting current import map state
322
+ To make it easy to set the import map state, es-module-shims provides a `importShim.addImportMap` utility function, available only in shim mode.
323
+
324
+ ```js
325
+ // importMap will be an object in the same shape as the json in a importmap script
326
+ const importMap = { imports: {/*...*/}, scopes: {/*...*/} };
327
+
328
+ importShim.addImportMap(importMap);
329
+ ```
330
+
331
+
332
+ ### Dynamic Import
333
+
334
+ > Stability: Stable browser standard
335
+
336
+ Dynamic `import(...)` within any modules loaded will be rewritten as `importShim(...)` automatically
337
+ providing full support for all es-module-shims features through dynamic import.
338
+
339
+ To load code dynamically (say from the browser console), `importShim` can be called similarly:
340
+
341
+ ```js
342
+ importShim('/path/to/module.js').then(x => console.log(x));
343
+ ```
344
+
345
+ ### import.meta.url
346
+
347
+ > Stability: Stable browser standard
348
+
349
+ `import.meta.url` provides the full URL of the current module within the context of the module execution.
350
+
351
+ ### modulepreload
352
+
353
+ > Stability: WhatWG Standard, Single Browser Implementer
354
+
355
+ Preloading of modules can be achieved by including a `<link rel="modulepreload" href="/module.js" />` tag in the HTML or injecting it dynamically.
356
+
357
+ This tag also supports the `"integrity"`, `"crossorigin"` and `"referrerpolicy"` attributes as supported on module scripts.
358
+
359
+ This tag just initiates a fetch request in the browser and thus works equally as a preload polyfill in both shimmed and unshimmed modes, with integrity validation support.
360
+
361
+ In browsers that don't support modulepreload, polyfilled preloading behaviour is provided using an early `fetch()` call with the same request options as the module script, resulting in network-level cache sharing.
362
+
363
+ Unlike the browser specification, the modulepreload polyfill does not request dependency modules by default, in order to avoid unnecessary
364
+ code analysis in the polyfill scenarios, _it is always recommended to preload deep imports so that this feature shouldn't be necessary._
365
+
366
+ #### Preload shim
367
+
368
+ When in shim mode, `<link rel="modulepreload-shim" href="/module.js" />` must be used to properly cache the preloaded modules.
369
+
370
+ ### CSP Support
371
+
372
+ By default ES Module Shims provides full support for CSP by using the asm.js ES Module Lexer build. This is absolutely identical in performance to the Wasm version in Firefox and Chrome (in Safari the asm.js version is actually faster than Wasm).
373
+
374
+ The CSP nonce to use for module scripts will be picked up from the first script on the page or via the [`nonce` init option](#nonce).
375
+
376
+ A full example of such a CSP workflow is provided below:
377
+
378
+ ```html
379
+ <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'nonce-n0nce'" />
380
+ <script async src="es-module-shims.js"></script>
381
+ <script type="importmap" nonce="n0nce">
382
+ {
383
+ "pkg": "/pkg.js"
384
+ }
385
+ </script>
386
+ <script type="module" nonce="n0nce">
387
+ import pkg from 'pkg';
388
+ </script>
389
+ ```
390
+
391
+ #### Wasm Build
392
+
393
+ To use the Web Assembly / non-CSP build of ES Module Shims, this is available as a self-contained single file at `es-module-shims/wasm` or `es-module-shims/dist/es-module-shims.wasm.js` in the package folder.
394
+
395
+ ### JSON Modules
396
+
397
+ > Stability: WhatWG Standard, Single Browser Implementer
398
+
399
+ In shim mode, JSON modules are always supported. In polyfill mode, JSON modules require the `polyfillEnable: ['json-modules']` [init option](#polyfill-enable-option).
400
+
401
+ JSON Modules are currently supported in Chrome when using them via an import assertion:
402
+
403
+ ```html
404
+ <script type="module">
405
+ import json from 'https://site.com/data.json' assert { type: 'json' };
406
+ </script>
407
+ ```
408
+
409
+ In addition JSON modules need to be served with a valid JSON content type.
410
+
411
+ Checks for assertion failures are not currently included.
412
+
413
+ ### CSS Modules
414
+
415
+ > Stability: WhatWG Standard, Single Browser Implementer
416
+
417
+ In shim mode, CSS modules are always supported. In polyfill mode, CSS modules require the `polyfillEnable: ['css-modules']` [init option](#polyfill-enable-option).
418
+
419
+ CSS Modules are currently supported in Chrome when using them via an import assertion:
420
+
421
+ ```html
422
+ <script type="module">
423
+ import sheet from 'https://site.com/sheet.css' assert { type: 'css' };
424
+ </script>
425
+ ```
426
+
427
+ To support the polyfill or shim of this feature, the [Constructable Stylesheets polyfill](https://github.com/calebdwilliams/construct-style-sheets#readme) must be separately included in browsers not supporting [Constructable Stylesheets](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/CSSStyleSheet) eg via:
428
+
429
+ ```html
430
+ <script async src="https://unpkg.com/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.js"></script>
431
+ ```
432
+
433
+ For more information see the [web.dev article](https://web.dev/css-module-scripts/).
434
+
435
+ In addition CSS modules need to be served with a valid CSS content type.
436
+
437
+ Checks for assertion failures are not currently included.
438
+
439
+ ### Resolve
440
+
441
+ > Stability: Draft HTML PR
442
+
443
+ `import.meta.resolve` provides a contextual resolver within modules. It is synchronous, changed from being formerly asynchronous due to following the [browser specification PR](https://github.com/whatwg/html/pull/5572).
444
+
445
+ The second argument to `import.meta.resolve` permits a custom parent URL scope for the resolution (not currently in the browser spec), which defaults to `import.meta.url`.
446
+
447
+ ```js
448
+ // resolve a relative path to a module
449
+ var resolvedUrl = import.meta.resolve('./relative.js');
450
+ // resolve a dependency from a module
451
+ var resolvedUrl = import.meta.resolve('dep');
452
+ // resolve a path
453
+ var resolvedUrlPath = import.meta.resolve('dep/');
454
+ // resolve with a custom parent scope
455
+ var resolvedUrl = import.meta.resolve('dep', 'https://site.com/another/scope');
456
+ ```
457
+
458
+ Node.js also implements a similar API, although it's in the process of shifting to a synchronous resolver.
459
+
460
+ ### Module Workers
461
+
462
+ ES Module Shims can be used in module workers in browsers that provide dynamic import in worker environments, which at the moment are Chrome(80+), Edge(80+) and Safari(15+).
463
+
464
+ By default, when there is no DOM present, ES Module Shims will switch into shim mode. An example of ES Module Shims usage through shim mode in web workers is provided below:
465
+
466
+ ```js
467
+ /**
468
+ *
469
+ * @param {string} aURL a string representing the URL of the module script the worker will execute.
470
+ * @returns {string} The string representing the URL of the script the worker will execute.
471
+ */
472
+ function getWorkerScriptURL(aURL) {
473
+ // baseURL, esModuleShimsURL are considered to be known in advance
474
+ // esModuleShimsURL - must point to the non-CSP build of ES Module Shims,
475
+ // namely the `es-module-shim.wasm.js` output: es-module-shims/dist/es-module-shims.wasm.js
476
+
477
+ return URL.createObjectURL(new Blob(
478
+ [
479
+ `importScripts('${new URL(esModuleShimsURL, baseURL).href}');
480
+ importShim.addImportMap(${JSON.stringify(importShim.getImportMap())});
481
+ importShim('${new URL(aURL, baseURL).href}').catch(e => setTimeout(() => { throw e; }))`
482
+ ],
483
+ { type: 'application/javascript' }))
484
+ }
485
+
486
+ const worker = new Worker(getWorkerScriptURL('myEsModule.js'));
487
+ ```
488
+
489
+ > Web workers must use the non-CSP build of ES Module Shims via `es-module-shim.wasm.js` from the `dist/` folder, since the CSP build currently assumes a DOM.
490
+
491
+ ## Init Options
492
+
493
+ Provide a `esmsInitOptions` on the global scope before `es-module-shims` is loaded to configure various aspects of the module loading process:
494
+
495
+ * [enforceIntegrity](#enforce-integrity)
496
+ * [fetch](#fetch-hook)
497
+ * [mapOverrides](#overriding-import-map-entries)
498
+ * [modulepreload](#modulepreload)
499
+ * [noLoadEventRetriggers](#no-load-event-retriggers)
500
+ * [nonce](#nonce)
501
+ * [onerror](#error-hook)
502
+ * [onpolyfill](#polyfill-hook)
503
+ * [polyfillEnable](#polyfill-enable-option)
504
+ * [resolve](#resolve-hook)
505
+ * [revokeBlobURLs](#revoke-blob-urls)
506
+ * [shimMode](#shim-mode-option)
507
+ * [skip](#skip)
508
+
509
+ ```html
510
+ <script>
511
+ window.esmsInitOptions = {
512
+ // Enable Shim Mode
513
+ shimMode: true, // default false
514
+ // Enable newer modules features
515
+ polyfillEnable: ['css-modules', 'json-modules'], // default empty
516
+ // Custom CSP nonce
517
+ nonce: 'n0nce', // default is automatic detection
518
+ // Don't retrigger load events on module scripts
519
+ noLoadEventRetriggers: true, // default false
520
+ // Skip source analysis of certain URLs for full native passthrough
521
+ skip: /^https:\/\/cdn\.com/, // defaults to null
522
+ // Clean up blob URLs after execution
523
+ revokeBlobURLs: true, // default false
524
+ // Secure mode to not support loading modules without integrity
525
+ // (integrity is always verified even when disabled though)
526
+ enforceIntegrity: true, // default false
527
+ // Permit overrides to import maps
528
+ mapOverrides: true, // default false
529
+
530
+ // -- Hooks --
531
+ // Module load error
532
+ onerror: (e) => { /*...*/ }, // default noop
533
+ // Called when polyfill mode first engages
534
+ onpolyfill: () => {}, // default logs to the console
535
+ // Hook all module resolutions
536
+ resolve: (id, parentUrl, resolve) => resolve(id, parentUrl), // default is spec resolution
537
+ // Hook source fetch function
538
+ fetch: (url, options) => fetch(url, options), // default is native
539
+ // Hook import.meta construction
540
+ meta: (meta, url) => void // default is noop
541
+ // Hook top-level imports
542
+ onimport: (url, options, parentUrl) => void // default is noop
543
+ }
544
+ </script>
545
+ <script async src="es-module-shims.js"></script>
546
+ ```
547
+
548
+ `<script type="esms-options">` can also be used:
549
+
550
+ ```html
551
+ <script type="esms-options">
552
+ {
553
+ "shimMode": true,
554
+ "polyfillEnable": ["css-modules", "json-modules"],
555
+ "nonce": "n0nce",
556
+ "onpolyfill": "polyfill"
557
+ }
558
+ </script>
559
+ ```
560
+
561
+ This can be convenient when using a CSP policy. Function strings correspond to global function names.
562
+
563
+ ### Shim Mode Option
564
+
565
+ [Shim Mode](#shim-mode) can be overridden using the `shimMode` option:
566
+
567
+ ```html
568
+ <script type="esms-options">
569
+ {
570
+ "shimMode": true
571
+ }
572
+ </script>
573
+ ```
574
+
575
+ For example, if lazy loading `<script type="module-shim">` scripts alongside static native module scripts, shim mode would not be enabled at initialization time.
576
+
577
+ DOM `load` events are fired for all `"module-shim"` scripts both for success and failure just like for native module scripts.
578
+
579
+ ### Pollyfill Enable Option
580
+
581
+ The `polyfillEnable` option allows enabling polyfill features which are newer and would otherwise result in unnecessary polyfilling in modern browsers that haven't yet updated.
582
+
583
+ Currently this option supports just `"css-modules"` and `"json-modules"`.
584
+
585
+ ```html
586
+ <script type="esms-options">
587
+ {
588
+ "polyfillEnable": ["css-modules", "json-modules"]
589
+ }
590
+ </script>
591
+ ```
592
+
593
+ The above is necessary to enable CSS modules and JSON modules.
594
+
595
+ #### Baseline Support Analysis Opt-Out
596
+
597
+ The reason the `polyfillEnable` option is needed is because ES Module Shims implements a performance optimization where if a browser supports modern modules features to an expected baseline of import maps support, it will skip all polyfill source analysis resulting in full native passthrough performance.
598
+
599
+ If the application code then tries to use modern features like CSS modules beyond this baseline it won't support those features. As a result all modules features which are considered newer or beyond the recommended baseline require explicit enabling. This common baseline itself will change to track the common future modules baseline supported by this project for each release cycle.
600
+
601
+ This option can also be set to `true` to entirely disable the native passthrough system and ensure all sources are fetched and analyzed through ES Module Shims. This will still avoid duplicate execution since module graphs are still only reexecuted when they use unsupported native features, but there is a small extra cost in doing the analysis.
602
+
603
+ ### Enforce Integrity
604
+
605
+ When enabled, `enforceIntegrity` will ensure that all modules loaded through ES Module Shims must have integrity defined either on a `<link rel="modulepreload" integrity="...">` or on
606
+ a `<link rel="modulepreload-shim" integrity="...">` preload tag in shim mode. Modules without integrity will throw at fetch time.
607
+
608
+ For example in the following, only the listed `app.js` and `dep.js` modules will be able to execute with the provided integrity:
609
+
610
+ ```html
611
+ <script type="esms-options">{ "enforceIntegrity": true }</script>
612
+ <link rel="modulepreload-shim" href="/app.js" integrity="sha384-..." />\
613
+ <link rel="modulepreload-shim" href="/dep.js" integrity="sha384-..." />
614
+ <script type="module-shim">
615
+ import '/app.js';
616
+ </script>
617
+ ```
618
+
619
+ Strong execution guarantees are only possible in shim mode since in polyfill mode it is not possible to stop the native loader from executing code without an integrity.
620
+
621
+ Future versions of this option may provide support for origin-specific allow lists.
622
+
623
+ ### Nonce
624
+
625
+ The `nonce` option allows setting a CSP nonce to be used with all script injections for full CSP compatibility supported by the [CSP build](#csp-build) of ES Module Shims.
626
+
627
+ Alternatively, add a `blob:` URL policy with the CSP build to get CSP compatibility.
628
+
629
+ ```js
630
+ <script type="esms-options">
631
+ {
632
+ "nonce": "n0nce"
633
+ }
634
+ </script>
635
+ ```
636
+
637
+ ### No Load Event Retriggers
638
+
639
+ Because of the extra processing done by ES Module Shims it is possible for static module scripts to execute after the `DOMContentLoaded` or `readystatechange` events they expect, which can cause missed attachment.
640
+
641
+ In order to ensure libraries that rely on these event still behave correctly, ES Module Shims will always double trigger these events that would normally have executed before the document ready state transition to completion, once all the static module scripts in the page have been completely executed through ES module shims.
642
+
643
+ In such a case, this double event firing can be disabled with the `noLoadEventRetriggers` option:
644
+
645
+ ```js
646
+ <script type="esms-options">
647
+ {
648
+ // do not re-trigger DOM events (onreadystatechange, DOMContentLoaded)
649
+ "noLoadEventRetriggers": true
650
+ }
651
+ </script>
652
+ <script async src="es-module-shims.js"></script>
653
+ ```
654
+
655
+ ### Skip
656
+
657
+ When loading modules that you know will only use baseline modules features, it is possible to set a rule to explicitly opt-out modules from being polyfilled to always load and be referenced through the native loader only. This enables instance sharing with the native loader and also improves performance because those modules then do not need to be processed or transformed at all, so that only local application code is handled and not library code.
658
+
659
+ The `skip` option supports a string regular expression or array of exact module URLs to check:
660
+
661
+ ```js
662
+ <script type="esms-options">
663
+ {
664
+ "skip": "^https?:\/\/(cdn\.skypack\.dev|jspm\.dev)\/"
665
+ }
666
+ </script>
667
+ <script async src="es-module-shims.js"></script>
668
+ ```
669
+
670
+ When passing an array, relative URLs or paths ending in `/` can be provided:
671
+
672
+ ```js
673
+ <script type="esms-options">
674
+ {
675
+ "skip": ["./app.js", "https://jspm.dev/"]
676
+ }
677
+ </script>
678
+ <script async src="es-module-shims.js"></script>
679
+ ```
680
+
681
+ ### Revoke Blob URLs
682
+
683
+ When polyfilling the missing features `es-module-shims` would create in-memory blobs using `URL.createObjectURL()` for each processed module.
684
+ In most cases, memory footprint of these blobs is negligible so there is no need to call `URL.revokeObjectURL()`
685
+ for them, and we don't do that by default.
686
+
687
+ That said, in some scenarios, e.g. when evaluating some continuously changing modules without a page reload, like in a web-based code editor,
688
+ you might want to reduce the growth of memory usage by revoking those blob URLs after they were already `import`ed.
689
+
690
+ You can do that by enabling the `revokeBlobURLs` init option:
691
+
692
+ ```js
693
+ <script type="esms-options">
694
+ {
695
+ "revokeBlobURLs": true
696
+ }
697
+ </script>
698
+ <script type="module" src="es-module-shims.js"></script>
699
+ ```
700
+
701
+ NOTE: revoking object URLs is not entirely free, while we are trying to be smart about it and make sure it doesn't
702
+ cause janks, we recommend enabling this option only if you have done the measurements and identified that you really need it.
703
+
704
+ ### Overriding import map entries
705
+
706
+ When [dynamically injecting import maps](#dynamic-import-maps), an error will be thrown in both polyfill and shim modes if the new import map would override existing entries with a different value.
707
+
708
+ It is possible to disable this behavior in shim mode by setting the `mapOverrides` option:
709
+
710
+ ```js
711
+ <script type="esms-options">
712
+ {
713
+ "shimMode": true,
714
+ "mapOverrides": true
715
+ }
716
+ </script>
717
+ <script type="importmap-shim">
718
+ {
719
+ "imports": {
720
+ "x": "/x.js"
721
+ }
722
+ }
723
+ </script>
724
+ <script>
725
+ // No error will be thrown here
726
+ document.body.appendChild(Object.assign(document.createElement('script'), {
727
+ type: 'importmap',
728
+ innerHTML: JSON.stringify({ imports: { x: './y.js' } }),
729
+ }));
730
+ </script>
731
+ ```
732
+
733
+ This can be useful for HMR workflows.
734
+
735
+ ### Hooks
736
+
737
+ #### Polyfill hook
738
+
739
+ The polyfill hook is called when running in polyfill mode and the polyfill is kicking in instead of passing through to the native loader.
740
+
741
+ This can be a useful way to verify that the native passthrough is working correctly in latest browsers for performance, while also allowing eg the ability to analyze or get metrics reports of how many users are getting the polyfill actively applying to their browser application loads.
742
+
743
+ ```js
744
+ <script>
745
+ window.polyfilling = () => console.log('The polyfill is actively applying');
746
+ </script>
747
+ <script type="esms-options">
748
+ {
749
+ "onpolyfill": "polyfilling"
750
+ }
751
+ </script>
752
+ ```
753
+
754
+ The default hook will log a message to the console with `console.info` noting that polyfill mode is enabled and that the native error can be ignored.
755
+
756
+ Overriding this hook with an empty function will disable the default polyfill log output.
757
+
758
+ In the above, running in latest Chromium browsers, nothing will be logged, while running in an older browser that does not support newer features like import maps the console log will be output.
759
+
760
+ #### Error hook
761
+
762
+ You can provide a function to handle errors during the module loading process by providing an `onerror` option:
763
+
764
+ ```js
765
+ <script>
766
+ window.esmsInitOptions = {
767
+ onerror: error => console.log(error) // defaults to `((e) => { throw e; })`
768
+ }
769
+ </script>
770
+ <script async src="es-module-shims.js"></script>
771
+ ```
772
+
773
+ #### Import Hook
774
+
775
+ The import hook is supported for both shim and polyfill modes and provides an async hook which can ensure any necessary work is done before a top-level module import or dynamic `import()` starts further processing.
776
+
777
+ ```js
778
+ <script>
779
+ window.esmsInitOptions = {
780
+ onimport: function (url, options, parentUrl) {
781
+ console.log(`Top-level import for ${url}`);
782
+ }
783
+ }
784
+ </script>
785
+ ```
786
+
787
+ #### Resolve Hook
788
+
789
+ The resolve hook is supported for both shim and polyfill modes and allows full customization of the resolver, while still having access to the original resolve function.
790
+
791
+ Note that in polyfill mode the resolve hook may not be called for all modules when native passthrough is occurring and that it still will not affect
792
+ the native passthrough executions.
793
+
794
+ If the resolve hook should apply for all modules in the entire module graph, make sure to set `polyfillEnable: true` to [disable the baseline support analysis opt-out](#baseline-support-analysis-opt-out).
795
+
796
+ ```js
797
+ <script>
798
+ window.esmsInitOptions = {
799
+ shimMode: true,
800
+ resolve: function (id, parentUrl, defaultResolve) {
801
+ if (id === 'custom' && parentUrl.startsWith('https://custom.com/'))
802
+ return 'https://custom.com/custom.js';
803
+
804
+ // Default resolve will handle the typical URL and import map resolution
805
+ return defaultResolve(id, parentUrl);
806
+ }
807
+ }
808
+ </script>
809
+ ```
810
+
811
+ Support for an asynchronous resolve hook has been deprecated as of 1.5.0 and will be removed in the next major.
812
+
813
+ Instead async work should be done with the import hook.
814
+
815
+ #### Meta Hook
816
+
817
+ The meta hook allows customizing the `import.meta` object in each module scope.
818
+
819
+ The function takes as arguments the `import.meta` object itself (with `import.meta.url` an `import.meta.resolve` already present), and the URL of the module as its second argument.
820
+
821
+ Example:
822
+
823
+ ```js
824
+ <script>
825
+ window.esmsInitOptions = {
826
+ shimMode: true,
827
+ meta: function (metaObj, url) {
828
+ metaObj.custom = `custom value for ${url}`;
829
+ }
830
+ }
831
+ </script>
832
+ ```
833
+
834
+ Where within the module the following would be supported:
835
+
836
+ ```js
837
+ import assert from 'assert';
838
+ assert.ok(import.meta.custom.startsWith('custom value'));
839
+ ```
840
+
841
+ #### Fetch Hook
842
+
843
+ The fetch hook is supported for shim mode only.
844
+
845
+ The ES Module Shims fetch hook can be used to implement transform plugins.
846
+
847
+ For example TypeScript support:
848
+
849
+ ```js
850
+ <script>
851
+ window.esmsInitOptions = {
852
+ shimMode: true,
853
+ fetch: async function (url, options) {
854
+ const res = await fetch(url, options);
855
+ if (!res.ok)
856
+ return res;
857
+ if (res.url.endsWith('.ts')) {
858
+ const source = await res.body();
859
+ const transformed = tsCompile(source);
860
+ return new Response(new Blob([transformed], { type: 'application/javascript' }));
861
+ }
862
+ return res;
863
+ } // defaults to `((url, options) => fetch(url, options))`
864
+ }
865
+ </script>
866
+ <script async src="es-module-shims.js"></script>
867
+ ```
868
+
869
+ Because the dependency analysis applies by ES Module Shims takes care of ensuring all dependencies run through the same fetch hook,
870
+ the above is all that is needed to implement custom plugins.
871
+
872
+ Streaming support is also provided, for example here is a hook with streaming support for JSON:
873
+
874
+ ```js
875
+ window.esmsInitOptions = {
876
+ shimMode: true,
877
+ fetch: async function (url, options) {
878
+ const res = await fetch(url, options);
879
+ if (!res.ok || !/^application\/json($|;)/.test(res.headers.get('content-type')))
880
+ return res;
881
+ const reader = res.body.getReader();
882
+ const headers = new Headers(res.headers);
883
+ headers.set('Content-Type', 'application/javascript');
884
+ return new Response(new ReadableStream({
885
+ async start (controller) {
886
+ let done, value;
887
+ controller.enqueue(new TextEncoder().encode('export default '));
888
+ while (({ done, value } = await reader.read()) && !done) {
889
+ controller.enqueue(value);
890
+ }
891
+ controller.close();
892
+ }
893
+ }), { headers });
894
+ }
895
+ }
896
+ ```
897
+
898
+ ## Implementation Details
899
+
900
+ ### Import Rewriting
901
+
902
+ * Sources are fetched, import specifiers are rewritten to reference exact URLs, and then executed as BlobURLs through the whole module graph.
903
+ * The [lexer](https://github.com/guybedford/es-module-lexer) handles the full language grammar including nested template strings, comments, regexes and division operator ambiguity based on backtracking.
904
+ * When executing a circular reference A -> B -> A, a shell module technique is used to acyclify into the graph A -> B -> A Shell, with A -> A Shell. The shell module exports an update function which is called by the original once after the last import statement, and again after the last statement of the source file.
905
+
906
+ ## Inspiration
907
+
908
+ Huge thanks to Rich Harris for inspiring this approach with [Shimport](https://github.com/rich-harris/shimport).
909
+
910
+ ## License
911
+
912
+ MIT