es-module-shims 1.4.1 → 1.4.5

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,704 +1,741 @@
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 67% of users](https://caniuse.com/import-maps) with import maps entirely bypass the shim code entirely.
6
-
7
- In for the remaining 30% 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
- ## Usage
26
-
27
- Include ES Module Shims with a `async` attribute on the script, then include an import map and module scripts normally:
28
-
29
- ```html
30
- <script async src="https://ga.jspm.io/npm:es-module-shims@1.3.6/dist/es-module-shims.js"></script>
31
-
32
- <!-- https://generator.jspm.io/#U2NhYGBkDM0rySzJSU1hKEpNTC5xMLTQM9Az0C1K1jMAAKFS5w0gAA -->
33
- <script type="importmap">
34
- {
35
- "imports": {
36
- "react": "https://ga.jspm.io/npm:react@18.0.0-rc.0/index.js"
37
- },
38
- "scopes": {
39
- "https://ga.jspm.io/npm:react@18.0.0-rc.0/": {
40
- "object-assign": "https://ga.jspm.io/npm:object-assign@4.1.1/index.js"
41
- }
42
- }
43
- }
44
- </script>
45
-
46
- <script type="module">
47
- import react from 'react';
48
- console.log(react);
49
- </script>
50
- ```
51
-
52
- In browsers without import maps support, a console error will be given:
53
-
54
- ```
55
- Uncaught TypeError: Failed to resolve module specifier "app". Relative references must start with either "/", "./", or "../".
56
- at <anonymous>:1:15
57
- ```
58
-
59
- This execution failure is a feature - it avoids the polyfill causing double execution. The first import being a bare specifier in the pattern above is important to ensure this.
60
-
61
- This is because the polyfill cannot disable the native loader - instead it will only execute modules that would otherwise fail resolving or parsing to avoid duplicate fetches or executions that would cause performance and reliability issues.
62
-
63
- ### Shim Mode
64
-
65
- 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).
66
-
67
- In shim mode, only the above `importmap-shim` and `module-shim` tags will be parsed and executed by ES Module Shims.
68
-
69
- 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.
70
-
71
- ## Benchmarks
72
-
73
- ES Module Shims is designed for production performance. A [comprehensive benchmark suite](bench/README.md) tracks multiple loading scenarios for the project.
74
-
75
- Benchmark summary:
76
-
77
- * [ES Module Shims Chrome Passthrough](bench/README.md#chrome-passthrough-performance) (for [70% 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.
78
- * [ES Module Shims Polyfilling](bench/README.md#native-v-polyfill-performance) (for the remaining [30% 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.
79
- * [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.
80
-
81
- ## Features
82
-
83
- ### Browser Support
84
-
85
- Works in all browsers with [baseline ES module support](https://caniuse.com/#feat=es6-module).
86
-
87
- Browser Compatibility **with ES Module Shims**:
88
-
89
- | ES Modules Features | Chrome (61+) | Firefox (60+) | Safari (10.1+) | Edge (17+) |
90
- | ---------------------------------- | ------------------------------------ | ------------------------------------ | ------------------------------------ | ------------------------------------ |
91
- | Executes Modules in Correct Order | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark:<sup>1</sup> |
92
- | [Dynamic Import](#dynamic-import) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
93
- | [import.meta.url](#importmetaurl) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
94
- | [Module Workers](#module-workers) | :heavy_check_mark: ~68+ | :x:<sup>2</sup> | :x:<sup>2</sup> | :x:<sup>2</sup> |
95
- | [modulepreload](#modulepreload) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
96
- | [Import Maps](#import-maps) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
97
- | [JSON Modules](#json-modules) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
98
- | [CSS Modules](#css-modules) | :heavy_check_mark:<sup>3</sup> | :heavy_check_mark:<sup>3</sup> | :heavy_check_mark:<sup>3</sup> | :heavy_check_mark:<sup>3</sup> |
99
- | [import.meta.resolve](#resolve) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
100
- | [Top-Level Await](#tla) | :heavy_check_mark: 89+ | :heavy_check_mark: 89+ | :x:<sup>4</sup> | :x:<sup>4</sup> |
101
-
102
- * 1: _The Edge parallel execution ordering bug is corrected by ES Module Shims with an execution chain inlining approach._
103
- * 2: _Module worker support cannot be implemented without dynamic import support in web workers._
104
- * 3: _CSS module support requires a separate [Constructable Stylesheets polyfill](https://github.com/calebdwilliams/construct-style-sheets#readme)._
105
- * 4: _Top-level await support is possible for ES Module Shims to implement, with the feature request tracking in https://github.com/guybedford/es-module-shims/issues/5._
106
-
107
- Browser compatibility **without ES module shims**:
108
-
109
- | ES Modules Features | Chrome (61+) | Firefox (60+) | Safari (10.1+) | Edge (17+) |
110
- | ---------------------------------- | ------------------------------------ | ------------------------------------ | ------------------------------------ | ------------------------------------ |
111
- | Executes Modules in Correct Order | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x:<sup>1</sup> |
112
- | [Dynamic Import](#dynamic-import) | :heavy_check_mark: 63+ | :heavy_check_mark: 67+ | :heavy_check_mark: 11.1+ | :x: |
113
- | [import.meta.url](#importmetaurl) | :heavy_check_mark: ~76+ | :heavy_check_mark: ~67+ | :heavy_check_mark: ~12+ ❕<sup>1</sup>| :x: |
114
- | [Module Workers](#module-workers) | :heavy_check_mark: ~68+ | :x: | :x: | :x: |
115
- | [modulepreload](#modulepreload) | :heavy_check_mark: 66+ | :x: | :x: | :x: |
116
- | [Import Maps](#import-maps) | :heavy_check_mark: 89+ | :x: | :x: | :x: |
117
- | [JSON Modules](#json-modules) | :heavy_check_mark: 91+ | :x: | :x: | :x: |
118
- | [CSS Modules](#css-modules) | :heavy_check_mark: 95+ | :x: | :x: | :x: |
119
- | [import.meta.resolve](#resolve) | :x: | :x: | :x: | :x: |
120
- | [Top-Level Await](#tla) | :heavy_check_mark: 89+ | :heavy_check_mark: 89+ | :x: | :x: |
121
-
122
- * 1: _Edge executes parallel dependencies in non-deterministic order. ([ChakraCore bug](https://github.com/microsoft/ChakraCore/issues/6261))._
123
- * ~: _Indicates the exact first version support has not yet been determined (PR's welcome!)._
124
- * ❕<sup>1</sup>: On module redirects, Safari returns the request URL in `import.meta.url` instead of the response URL as per the spec.
125
-
126
- ### Import Maps
127
-
128
- > Stability: WhatWG Standard, Single Browser Implementer
129
-
130
- Import maps allow for importing "bare specifiers" in JavaScript modules, which prior to import maps throw in all browsers with native modules support.
131
-
132
- Using this polyfill we can write:
133
-
134
- ```html
135
- <script type="importmap-shim">
136
- {
137
- "imports": {
138
- "test": "/test.js"
139
- },
140
- "scopes": {
141
- "/": {
142
- "test-dep": "/test-dep.js"
143
- }
144
- }
145
- }
146
- </script>
147
- <script type="module-shim">
148
- import test from "test";
149
- console.log(test);
150
- </script>
151
- ```
152
-
153
- 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.
154
-
155
- #### Multiple Import Maps
156
-
157
- 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.
158
-
159
- In polyfill mode, multiple import maps are therefore not supported.
160
-
161
- In shim mode, support for multiple `importmap-shim` scripts follows the [import map extensions](https://github.com/guybedford/import-maps-extensions) proposal.
162
-
163
- #### External Import Maps
164
-
165
- External import maps (using a `"src"` attribute) are not currently supported in any native implementation.
166
-
167
- In polyfill mode, external import maps are therefore not supported.
168
-
169
- In shim mode, external import maps are fully supported.
170
-
171
- #### Dynamic Import Maps
172
-
173
- Support for dynamically injecting import maps with JavaScript via eg:
174
-
175
- ```js
176
- document.body.appendChild(Object.assign(document.createElement('script'), {
177
- type: 'importmap',
178
- innerHTML: JSON.stringify({ imports: { x: './y.js' } }),
179
- }));
180
- ```
181
-
182
- 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).
183
-
184
- Both modes in ES Module Shims support dynamic injection using DOM Mutation Observers.
185
-
186
- 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"`.
187
-
188
- #### Reading current import map state
189
-
190
- 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.
191
-
192
- ```js
193
- const importMap = importShim.getImportMap()
194
-
195
- // importMap will be an object in the same shape as the json in a importmap script
196
- ```
197
-
198
- ### Dynamic Import
199
-
200
- > Stability: Stable browser standard
201
-
202
- Dynamic `import(...)` within any modules loaded will be rewritten as `importShim(...)` automatically
203
- providing full support for all es-module-shims features through dynamic import.
204
-
205
- To load code dynamically (say from the browser console), `importShim` can be called similarly:
206
-
207
- ```js
208
- importShim('/path/to/module.js').then(x => console.log(x));
209
- ```
210
-
211
- ### import.meta.url
212
-
213
- > Stability: Stable browser standard
214
-
215
- `import.meta.url` provides the full URL of the current module within the context of the module execution.
216
-
217
- ### modulepreload
218
-
219
- > Stability: WhatWG Standard, Single Browser Implementer
220
-
221
- Preloading of modules can be achieved by including a `<link rel="modulepreload" href="/module.js" />` tag in the HTML or injecting it dynamically.
222
-
223
- This tag also supports the `"integrity"`, `"crossorigin"` and `"referrerpolicy"` attributes as supported on module scripts.
224
-
225
- 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.
226
-
227
- Unlike the browser specification, the modulepreload polyfill does not request dependency modules by default, in order to avoid unnecessary
228
- code analysis in the polyfill scenarios. **It is recommended to preload deep imports anyway so that this feature shouldn't be necessary.**
229
-
230
- ### CSP Support
231
-
232
- 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.
233
-
234
- 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).
235
-
236
- A full example of such a CSP workflow is provided below:
237
-
238
- ```html
239
- <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'nonce-n0nce'" />
240
- <script async src="es-module-shims.js"></script>
241
- <script type="importmap" nonce="n0nce">
242
- {
243
- "pkg": "/pkg.js"
244
- }
245
- </script>
246
- <script type="module" nonce="n0nce">
247
- import pkg from 'pkg';
248
- </script>
249
- ```
250
-
251
- #### Wasm Build
252
-
253
- 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.
254
-
255
- ### JSON Modules
256
-
257
- > Stability: WhatWG Standard, Single Browser Implementer
258
-
259
- In shim mode, JSON modules are always supported. In polyfill mode, JSON modules require the `polyfillEnable: ['json-modules']` [init option](#polyfill-enable-option).
260
-
261
- JSON Modules are currently supported in Chrome when using them via an import assertion:
262
-
263
- ```html
264
- <script type="module">
265
- import json from 'https://site.com/data.json' assert { type: 'json' };
266
- </script>
267
- ```
268
-
269
- In addition JSON modules need to be served with a valid JSON content type.
270
-
271
- Checks for assertion failures are not currently included.
272
-
273
- ### CSS Modules
274
-
275
- > Stability: WhatWG Standard, Single Browser Implementer
276
-
277
- In shim mode, CSS modules are always supported. In polyfill mode, CSS modules require the `polyfillEnable: ['css-modules']` [init option](#polyfill-enable-option).
278
-
279
- CSS Modules are currently supported in Chrome when using them via an import assertion:
280
-
281
- ```html
282
- <script type="module">
283
- import sheet from 'https://site.com/sheet.css' assert { type: 'css' };
284
- </script>
285
- ```
286
-
287
- 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:
288
-
289
- ```html
290
- <script async src="https://unpkg.com/construct-style-sheets-polyfill@3.0.0/dist/adoptedStyleSheets.js"></script>
291
- ```
292
-
293
- For more information see the [web.dev article](https://web.dev/css-module-scripts/).
294
-
295
- In addition CSS modules need to be served with a valid CSS content type.
296
-
297
- Checks for assertion failures are not currently included.
298
-
299
- ### Resolve
300
-
301
- > Stability: No current browser standard
302
-
303
- `import.meta.resolve` provides a contextual resolver within modules. It is asynchronous, like the Node.js implementation, to support waiting on any in-flight
304
- import map loads when import maps are [loaded dynamically](#dynamic-import-map-updates).
305
-
306
- The second argument to `import.meta.resolve` permits a custom parent URL scope for the resolution, which defaults to `import.meta.url`.
307
-
308
- ```js
309
- // resolve a relative path to a module
310
- var resolvedUrl = await import.meta.resolve('./relative.js');
311
- // resolve a dependency from a module
312
- var resolvedUrl = await import.meta.resolve('dep');
313
- // resolve a path
314
- var resolvedUrlPath = await import.meta.resolve('dep/');
315
- // resolve with a custom parent scope
316
- var resolvedUrl = await import.meta.resolve('dep', 'https://site.com/another/scope');
317
- ```
318
-
319
- This implementation is as provided experimentally in Node.js - https://nodejs.org/dist/latest-v14.x/docs/api/esm.html#esm_no_require_resolve.
320
-
321
- ## Polyfill Mode Details
322
-
323
- In polyfill mode, feature detections are performed for ES modules features. In browsers with full feature support no further processing is done.
324
-
325
- In browsers with variable feature support, sources are analyzed with module specifiers rewritten using the very fast Wasm / asm.js lexer while sharing the source network fetch cache with the native loader.
326
-
327
- #### Polyfill Features
328
-
329
- The current default native baseline for the ES module shims polyfill mode is browsers supporting import maps.
330
-
331
- 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 to only browsers supporting these features:
332
-
333
- ```html
334
- <script>
335
- window.esmsInitOptions = { polyfillEnable: ['css-modules', 'json-modules'] }
336
- </script>
337
- ```
338
-
339
- To verify when the polyfill is actively engaging as opposed to relying on the native loader, [a `polyfill` hook](#polyfill-hook) is also provided.
340
-
341
- #### Polyfill Edge Case: Dynamic Import
342
-
343
- The guarantee of the polyfill is that any module graph that would have failed will be reexecuted through the shim layer. This leaves any edge case where execution succeeds but not as expected. For example when using dynamic imports:
344
-
345
- ```html
346
- <script type="module">
347
- console.log('Executing');
348
- const dynamic = 'bare-specifier';
349
- import(dynamic).then(x => {
350
- console.log('Ok');
351
- }, err => {
352
- console.log('Fail');
353
- });
354
- </script>
355
- ```
356
-
357
- The native browser loader without import maps support will execute the above module fine, but fail on the lazy dynamic import.
358
-
359
- ES Module Shims will not reexecute the above in browsers without import maps support though because it will see that the execution did complete successfully therefore it will not attempt reexecution and as a result, `"Ok"` is never logged.
360
-
361
- Other examples include dynamically injecting import maps, or using import maps with a `"src"` attribute, which aren't supported in native Chrome.
362
-
363
- This is why it is advisable to always ensure modules use syntax that will fail early to avoid non-execution.
364
-
365
- #### Polyfill Edge Case: Instance Sharing
366
-
367
- 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.
368
-
369
- Note that instances are not shared between these loaders for consistency and performance.
370
-
371
- As a result, if you have two module graphs - one native and one polyfilled, they will not share the same dependency instance, for example:
372
-
373
- ```html
374
- <script type="importmap">
375
- {
376
- "imports": {
377
- "dep": "/dep.js"
378
- }
379
- }
380
- </script>
381
- <script type="module">
382
- import '/dep.js';
383
- </script>
384
- <script type="module">
385
- import 'dep';
386
- </script>
387
- ```
388
-
389
- In the above, on browsers without import maps support, the `/dep.js` instance will be loaded natively by the first module, then the second import will fail.
390
-
391
- ES Module Shims will pick up on the second import and reexecute `/dep.js`. As a result, `/dep.js` will be executed twice on the page.
392
-
393
- 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.
394
-
395
- #### Skip Polyfill
396
-
397
- Adding the `"noshim"` attribute to the script tag will also ensure that ES Module Shims skips processing this script entirely:
398
-
399
- ```html
400
- <script type="module" noshim>
401
- // ...
402
- </script>
403
- ```
404
-
405
- ## Init Options
406
-
407
- Provide a `esmsInitOptions` on the global scope before `es-module-shims` is loaded to configure various aspects of the module loading process:
408
-
409
- * [shimMode](#shim-mode-option)
410
- * [polyfillEnable](#polyfill-enable-option)
411
- * [enforceIntegrity](#enforce-integrity)
412
- * [nonce](#nonce)
413
- * [noLoadEventRetriggers](#no-load-event-retriggers)
414
- * [skip](#skip-processing)
415
- * [onerror](#error-hook)
416
- * [onpolyfill](#polyfill-hook)
417
- * [resolve](#resolve-hook)
418
- * [fetch](#fetch-hook)
419
- * [revokeBlobURLs](#revoke-blob-urls)
420
-
421
- ```html
422
- <script>
423
- window.esmsInitOptions = {
424
- shimMode: true, // default false
425
- polyfillEnable: ['css-modules', 'json-modules'], // default empty
426
- nonce: 'n0nce', // default null
427
- noLoadEventRetriggers: true, // default false
428
- skip: /^https:\/\/cdn\.com/, // defaults to null
429
- onerror: (e) => { /*...*/ }, // default noop
430
- onpolyfill: () => {}, // default logs to the console
431
- resolve: (id, parentUrl, resolve) => resolve(id, parentUrl), // default is spec resolution
432
- fetch: (url, options) => fetch(url, options), // default is native
433
- revokeBlobURLs: true, // default false
434
- enforceIntegrity: true, // default true
435
- }
436
- </script>
437
- <script async src="es-module-shims.js"></script>
438
- ```
439
-
440
- If only setting JSON-compatible options, the `<script type="esms-options">` can be used instead:
441
-
442
- ```html
443
- <script type="esms-options">
444
- {
445
- "shimMode": true,
446
- "polyfillEnable": ["css-modules", "json-modules"],
447
- "nonce": "n0nce",
448
- "onpolyfill": "polyfill"
449
- }
450
- </script>
451
- ```
452
-
453
- This can be convenient when using a CSP policy.
454
-
455
- Function strings correspond to global function names.
456
-
457
- See below for a detailed description of each of these options.
458
-
459
- ### Shim Mode Option
460
-
461
- [Shim Mode](#shim-mode) can be overridden using the `shimMode` option:
462
-
463
- ```html
464
- <script type="esms-options">
465
- {
466
- "shimMode": true
467
- }
468
- </script>
469
- ```
470
-
471
- For example, if lazy loading `<script type="module-shim">` scripts alongside static native module scripts, shim mode would not be enabled at initialization time.
472
-
473
- DOM `load` events are fired for all `"module-shim"` scripts both for success and failure just like for native module scripts.
474
-
475
- ### Pollyfill Enable Option
476
-
477
- 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.
478
-
479
- Currently this option supports just `"css-modules"` and `"json-modules"`.
480
-
481
- ```html
482
- <script type="esms-options">
483
- {
484
- "polyfillEnable": ["css-modules", "json-modules"]
485
- }
486
- </script>
487
- ```
488
-
489
- ### Enforce Integrity
490
-
491
- 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
492
- a `<link rel="modulepreload-shim" integrity="...">` preload tag in shim mode. Modules without integrity will throw at fetch time.
493
-
494
- For example in the following, only the listed `app.js` and `dep.js` modules will be able to execute with the provided integrity:
495
-
496
- ```html
497
- <script type="esms-options">{ "enforceIntegrity": true }</script>
498
- <link rel="modulepreload-shim" href="/app.js" integrity="sha384-..." />\
499
- <link rel="modulepreload-shim" href="/dep.js" integrity="sha384-..." />
500
- <script type="module-shim">
501
- import '/app.js';
502
- </script>
503
- ```
504
-
505
- 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.
506
-
507
- Future versions of this option may provide support for origin-specific allow lists.
508
-
509
- ### Nonce
510
-
511
- 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.
512
-
513
- Alternatively, add a `blob:` URL policy with the CSP build to get CSP compatibility.
514
-
515
- ```js
516
- <script type="esms-options">
517
- {
518
- "nonce": "n0nce"
519
- }
520
- </script>
521
- ```
522
-
523
- ### No Load Event Retriggers
524
-
525
- 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.
526
-
527
- 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.
528
-
529
- In such a case, this double event firing can be disabled with the `noLoadEventRetriggers` option:
530
-
531
- ```js
532
- <script type="esms-options">
533
- {
534
- // do not re-trigger DOM events (onreadystatechange, DOMContentLoaded)
535
- "noLoadEventRetriggers": true
536
- }
537
- </script>
538
- <script async src="es-module-shims.js"></script>
539
- ```
540
-
541
- ### Skip Processing
542
-
543
- 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 rewriting. This 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.
544
-
545
- This can be configured by providing a URL regular expression for the `skip` option:
546
-
547
- ```js
548
- <script type="esms-options">
549
- {
550
- "skip": "/^https?:\/\/(cdn\.skypack\.dev|jspm\.dev)\//`
551
- }
552
- </script>
553
- <script async src="es-module-shims.js"></script>
554
- ```
555
-
556
- #### Polyfill hook
557
-
558
- The polyfill hook is called when running in polyfill mode and the polyfill is kicking in instead of passing through to the native loader.
559
-
560
- 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.
561
-
562
- ```js
563
- <script>
564
- window.polyfilling = () => console.log('The polyfill is actively applying');
565
- </script>
566
- <script type="esms-options">
567
- {
568
- "onpolyfill": "polyfilling"
569
- }
570
- </script>
571
- ```
572
-
573
- 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.
574
-
575
- In the above, running in latest Chromium browsers, nothing will be logged, while running in an older browser that does not support newer features
576
- like import maps the console log will be output.
577
-
578
- #### Error hook
579
-
580
- You can provide a function to handle errors during the module loading process by providing an `onerror` option:
581
-
582
- ```js
583
- <script>
584
- window.esmsInitOptions = {
585
- onerror: error => console.log(error) // defaults to `((e) => { throw e; })`
586
- }
587
- </script>
588
- <script async src="es-module-shims.js"></script>
589
- ```
590
-
591
- #### Resolve Hook
592
-
593
- The resolve hook is supported for shim mode only and allows full customization of the resolver, while still having access to the original resolve function.
594
-
595
- ```js
596
- <script>
597
- window.esmsInitOptions = {
598
- shimMode: true,
599
- resolve: async function (id, parentUrl, defaultResolve) {
600
- if (id === 'custom' && parentUrl.startsWith('https://custom.com/'))
601
- return 'https://custom.com/custom.js';
602
-
603
- // Default resolve will handle the typical URL and import map resolution
604
- return defaultResolve(id, parentUrl);
605
- }
606
- }
607
- </script>
608
- ```
609
-
610
- ### Fetch Hook
611
-
612
- The fetch hook is supported for shim mode only.
613
-
614
- The ES Module Shims fetch hook can be used to implement transform plugins.
615
-
616
- For example:
617
-
618
- ```js
619
- <script>
620
- window.esmsInitOptions = {
621
- shimMode: true,
622
- fetch: async function (url, options) {
623
- const res = await fetch(url, options);
624
- if (!res.ok)
625
- return res;
626
- if (res.url.endsWith('.ts')) {
627
- const source = await res.body();
628
- const transformed = tsCompile(source);
629
- return new Response(new Blob([transformed], { type: 'application/javascript' }));
630
- }
631
- return res;
632
- } // defaults to `((url, options) => fetch(url, options))`
633
- }
634
- </script>
635
- <script async src="es-module-shims.js"></script>
636
- ```
637
-
638
- Because the dependency analysis applies by ES Module Shims takes care of ensuring all dependencies run through the same fetch hook,
639
- the above is all that is needed to implement custom plugins.
640
-
641
- Streaming support is also provided, for example here is a hook with streaming support for JSON:
642
-
643
- ```js
644
- window.esmsInitOptions = {
645
- shimMode: true,
646
- fetch: async function (url, options) {
647
- const res = await fetch(url, options);
648
- if (!res.ok || !/^application\/json($|;)/.test(res.headers.get('content-type')))
649
- return res;
650
- const reader = res.body.getReader();
651
- const headers = new Headers(res.headers);
652
- headers.set('Content-Type', 'application/javascript');
653
- return new Response(new ReadableStream({
654
- async start (controller) {
655
- let done, value;
656
- controller.enqueue(new TextEncoder.encode('export default '));
657
- while (({ done, value } = await reader.read()) && !done) {
658
- controller.enqueue(value);
659
- }
660
- controller.close();
661
- }
662
- }), { headers });
663
- }
664
- }
665
- ```
666
-
667
- ### Revoke Blob URLs
668
-
669
- When polyfilling the missing features `es-module-shims` would create in-memory blobs using `URL.createObjectURL()` for each processed module.
670
- In most cases, memory footprint of these blobs is negligible so there is no need to call `URL.revokeObjectURL()`
671
- for them, and we don't do that by default.
672
-
673
- That said, in some scenarios, e.g. when evaluating some continuously changing modules without a page reload, like in a web-based code editor,
674
- you might want to reduce the growth of memory usage by revoking those blob URLs after they were already `import`ed.
675
-
676
- You can do that by enabling the `revokeBlobURLs` init option:
677
-
678
- ```js
679
- <script type="esms-options">
680
- {
681
- "revokeBlobURLs": true
682
- }
683
- </script>
684
- <script type="module" src="es-module-shims.js"></script>
685
- ```
686
-
687
- NOTE: revoking object URLs is not entirely free, while we are trying to be smart about it and make sure it doesn't
688
- cause janks, we recommend enabling this option only if you have done the measurements and identified that you really need it.
689
-
690
- ## Implementation Details
691
-
692
- ### Import Rewriting
693
-
694
- * Sources are fetched, import specifiers are rewritten to reference exact URLs, and then executed as BlobURLs through the whole module graph.
695
- * 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.
696
- * When executing a circular reference A -> B -> A, a shell module technique is used to "shim" the circular reference into an acyclic graph. As a result, live bindings for the circular parent A are not supported, and instead the bindings are captured immediately after the execution of A.
697
-
698
- ## Inspiration
699
-
700
- Huge thanks to Rich Harris for inspiring this approach with [Shimport](https://github.com/rich-harris/shimport).
701
-
702
- ## License
703
-
704
- 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 67% of users](https://caniuse.com/import-maps) with import maps entirely bypass the shim code entirely.
6
+
7
+ For the remaining 30% 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
+ ## Usage
26
+
27
+ Include ES Module Shims with a `async` attribute on the script, then include an import map and module scripts normally:
28
+
29
+ ```html
30
+ <script async src="https://ga.jspm.io/npm:es-module-shims@1.4.5/dist/es-module-shims.js"></script>
31
+
32
+ <!-- https://generator.jspm.io/#U2NhYGBkDM0rySzJSU1hKEpNTC5xMLTQM9Az0C1K1jMAAKFS5w0gAA -->
33
+ <script type="importmap">
34
+ {
35
+ "imports": {
36
+ "react": "https://ga.jspm.io/npm:react@18.0.0-rc.0/index.js"
37
+ },
38
+ "scopes": {
39
+ "https://ga.jspm.io/npm:react@18.0.0-rc.0/": {
40
+ "object-assign": "https://ga.jspm.io/npm:object-assign@4.1.1/index.js"
41
+ }
42
+ }
43
+ }
44
+ </script>
45
+
46
+ <script type="module">
47
+ import react from 'react';
48
+ console.log(react);
49
+ </script>
50
+ ```
51
+
52
+ In browsers without import maps support, a console error will be given:
53
+
54
+ ```
55
+ Uncaught TypeError: Failed to resolve module specifier "app". Relative references must start with either "/", "./", or "../".
56
+ at <anonymous>:1:15
57
+ ```
58
+
59
+ This execution failure is a feature - it avoids the polyfill causing double execution. The first import being a bare specifier in the pattern above is important to ensure this.
60
+
61
+ This is because the polyfill cannot disable the native loader - instead it will only execute modules that would otherwise fail resolving or parsing to avoid duplicate fetches or executions that would cause performance and reliability issues.
62
+
63
+ ### Shim Mode
64
+
65
+ 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).
66
+
67
+ In shim mode, only the above `importmap-shim` and `module-shim` tags will be parsed and executed by ES Module Shims.
68
+
69
+ 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.
70
+
71
+ ## Benchmarks
72
+
73
+ ES Module Shims is designed for production performance. A [comprehensive benchmark suite](bench/README.md) tracks multiple loading scenarios for the project.
74
+
75
+ Benchmark summary:
76
+
77
+ * [ES Module Shims Chrome Passthrough](bench/README.md#chrome-passthrough-performance) (for [70% 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.
78
+ * [ES Module Shims Polyfilling](bench/README.md#native-v-polyfill-performance) (for the remaining [30% 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.
79
+ * [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.
80
+
81
+ ## Features
82
+
83
+ ### Browser Support
84
+
85
+ Works in all browsers with [baseline ES module support](https://caniuse.com/#feat=es6-module).
86
+
87
+ Browser Compatibility **with ES Module Shims**:
88
+
89
+ | ES Modules Features | Chrome (61+) | Firefox (60+) | Safari (10.1+) | Edge (17+) |
90
+ | ---------------------------------- | ------------------------------------ | ------------------------------------ | ------------------------------------ | ------------------------------------ |
91
+ | Executes Modules in Correct Order | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark:<sup>1</sup> |
92
+ | [Dynamic Import](#dynamic-import) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
93
+ | [import.meta.url](#importmetaurl) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
94
+ | [Module Workers](#module-workers) | :heavy_check_mark: ~68+ | :x:<sup>2</sup> | :x:<sup>2</sup> | :x:<sup>2</sup> |
95
+ | [modulepreload](#modulepreload) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
96
+ | [Import Maps](#import-maps) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
97
+ | [JSON Modules](#json-modules) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
98
+ | [CSS Modules](#css-modules) | :heavy_check_mark:<sup>3</sup> | :heavy_check_mark:<sup>3</sup> | :heavy_check_mark:<sup>3</sup> | :heavy_check_mark:<sup>3</sup> |
99
+ | [import.meta.resolve](#resolve) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
100
+ | [Top-Level Await](#tla) | :heavy_check_mark: 89+ | :heavy_check_mark: 89+ | :x:<sup>4</sup> | :x:<sup>4</sup> |
101
+
102
+ * 1: _The Edge parallel execution ordering bug is corrected by ES Module Shims with an execution chain inlining approach._
103
+ * 2: _Module worker support cannot be implemented without dynamic import support in web workers._
104
+ * 3: _CSS module support requires a separate [Constructable Stylesheets polyfill](https://github.com/calebdwilliams/construct-style-sheets#readme)._
105
+ * 4: _Top-level await support is possible for ES Module Shims to implement, with the feature request tracking in https://github.com/guybedford/es-module-shims/issues/5._
106
+
107
+ Browser compatibility **without ES module shims**:
108
+
109
+ | ES Modules Features | Chrome (61+) | Firefox (60+) | Safari (10.1+) | Edge (17+) |
110
+ | ---------------------------------- | ------------------------------------ | ------------------------------------ | ------------------------------------ | ------------------------------------ |
111
+ | Executes Modules in Correct Order | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x:<sup>1</sup> |
112
+ | [Dynamic Import](#dynamic-import) | :heavy_check_mark: 63+ | :heavy_check_mark: 67+ | :heavy_check_mark: 11.1+ | :x: |
113
+ | [import.meta.url](#importmetaurl) | :heavy_check_mark: ~76+ | :heavy_check_mark: ~67+ | :heavy_check_mark: ~12+ ❕<sup>1</sup>| :x: |
114
+ | [Module Workers](#module-workers) | :heavy_check_mark: ~68+ | :x: | :x: | :x: |
115
+ | [modulepreload](#modulepreload) | :heavy_check_mark: 66+ | :x: | :x: | :x: |
116
+ | [Import Maps](#import-maps) | :heavy_check_mark: 89+ | :x: | :x: | :x: |
117
+ | [JSON Modules](#json-modules) | :heavy_check_mark: 91+ | :x: | :x: | :x: |
118
+ | [CSS Modules](#css-modules) | :heavy_check_mark: 95+ | :x: | :x: | :x: |
119
+ | [import.meta.resolve](#resolve) | :x: | :x: | :x: | :x: |
120
+ | [Top-Level Await](#tla) | :heavy_check_mark: 89+ | :heavy_check_mark: 89+ | :x: | :x: |
121
+
122
+ * 1: _Edge executes parallel dependencies in non-deterministic order. ([ChakraCore bug](https://github.com/microsoft/ChakraCore/issues/6261))._
123
+ * ~: _Indicates the exact first version support has not yet been determined (PR's welcome!)._
124
+ * ❕<sup>1</sup>: On module redirects, Safari returns the request URL in `import.meta.url` instead of the response URL as per the spec.
125
+
126
+ ### Import Maps
127
+
128
+ > Stability: WhatWG Standard, Single Browser Implementer
129
+
130
+ Import maps allow for importing "bare specifiers" in JavaScript modules, which prior to import maps throw in all browsers with native modules support.
131
+
132
+ Using this polyfill we can write:
133
+
134
+ ```html
135
+ <script type="importmap-shim">
136
+ {
137
+ "imports": {
138
+ "test": "/test.js"
139
+ },
140
+ "scopes": {
141
+ "/": {
142
+ "test-dep": "/test-dep.js"
143
+ }
144
+ }
145
+ }
146
+ </script>
147
+ <script type="module-shim">
148
+ import test from "test";
149
+ console.log(test);
150
+ </script>
151
+ ```
152
+
153
+ 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.
154
+
155
+ #### Multiple Import Maps
156
+
157
+ 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.
158
+
159
+ In polyfill mode, multiple import maps are therefore not supported.
160
+
161
+ In shim mode, support for multiple `importmap-shim` scripts follows the [import map extensions](https://github.com/guybedford/import-maps-extensions) proposal.
162
+
163
+ #### External Import Maps
164
+
165
+ External import maps (using a `"src"` attribute) are not currently supported in any native implementation.
166
+
167
+ In polyfill mode, external import maps are therefore not supported.
168
+
169
+ In shim mode, external import maps are fully supported.
170
+
171
+ #### Dynamic Import Maps
172
+
173
+ Support for dynamically injecting import maps with JavaScript via eg:
174
+
175
+ ```js
176
+ document.body.appendChild(Object.assign(document.createElement('script'), {
177
+ type: 'importmap',
178
+ innerHTML: JSON.stringify({ imports: { x: './y.js' } }),
179
+ }));
180
+ ```
181
+
182
+ 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).
183
+
184
+ Both modes in ES Module Shims support dynamic injection using DOM Mutation Observers.
185
+
186
+ 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"`.
187
+
188
+ #### Reading current import map state
189
+
190
+ 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.
191
+
192
+ ```js
193
+ const importMap = importShim.getImportMap()
194
+
195
+ // importMap will be an object in the same shape as the json in a importmap script
196
+ ```
197
+
198
+ ### Dynamic Import
199
+
200
+ > Stability: Stable browser standard
201
+
202
+ Dynamic `import(...)` within any modules loaded will be rewritten as `importShim(...)` automatically
203
+ providing full support for all es-module-shims features through dynamic import.
204
+
205
+ To load code dynamically (say from the browser console), `importShim` can be called similarly:
206
+
207
+ ```js
208
+ importShim('/path/to/module.js').then(x => console.log(x));
209
+ ```
210
+
211
+ ### import.meta.url
212
+
213
+ > Stability: Stable browser standard
214
+
215
+ `import.meta.url` provides the full URL of the current module within the context of the module execution.
216
+
217
+ ### modulepreload
218
+
219
+ > Stability: WhatWG Standard, Single Browser Implementer
220
+
221
+ Preloading of modules can be achieved by including a `<link rel="modulepreload" href="/module.js" />` tag in the HTML or injecting it dynamically.
222
+
223
+ This tag also supports the `"integrity"`, `"crossorigin"` and `"referrerpolicy"` attributes as supported on module scripts.
224
+
225
+ 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.
226
+
227
+ Unlike the browser specification, the modulepreload polyfill does not request dependency modules by default, in order to avoid unnecessary
228
+ code analysis in the polyfill scenarios. **It is recommended to preload deep imports anyway so that this feature shouldn't be necessary.**
229
+
230
+ #### Preload shim
231
+
232
+ When in shim mode, `<link rel="modulepreload-shim" href="/module.js" />` must be used to properly cache the preloaded modules.
233
+
234
+ ### CSP Support
235
+
236
+ 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.
237
+
238
+ 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).
239
+
240
+ A full example of such a CSP workflow is provided below:
241
+
242
+ ```html
243
+ <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'nonce-n0nce'" />
244
+ <script async src="es-module-shims.js"></script>
245
+ <script type="importmap" nonce="n0nce">
246
+ {
247
+ "pkg": "/pkg.js"
248
+ }
249
+ </script>
250
+ <script type="module" nonce="n0nce">
251
+ import pkg from 'pkg';
252
+ </script>
253
+ ```
254
+
255
+ #### Wasm Build
256
+
257
+ 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.
258
+
259
+ ### JSON Modules
260
+
261
+ > Stability: WhatWG Standard, Single Browser Implementer
262
+
263
+ In shim mode, JSON modules are always supported. In polyfill mode, JSON modules require the `polyfillEnable: ['json-modules']` [init option](#polyfill-enable-option).
264
+
265
+ JSON Modules are currently supported in Chrome when using them via an import assertion:
266
+
267
+ ```html
268
+ <script type="module">
269
+ import json from 'https://site.com/data.json' assert { type: 'json' };
270
+ </script>
271
+ ```
272
+
273
+ In addition JSON modules need to be served with a valid JSON content type.
274
+
275
+ Checks for assertion failures are not currently included.
276
+
277
+ ### CSS Modules
278
+
279
+ > Stability: WhatWG Standard, Single Browser Implementer
280
+
281
+ In shim mode, CSS modules are always supported. In polyfill mode, CSS modules require the `polyfillEnable: ['css-modules']` [init option](#polyfill-enable-option).
282
+
283
+ CSS Modules are currently supported in Chrome when using them via an import assertion:
284
+
285
+ ```html
286
+ <script type="module">
287
+ import sheet from 'https://site.com/sheet.css' assert { type: 'css' };
288
+ </script>
289
+ ```
290
+
291
+ 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:
292
+
293
+ ```html
294
+ <script async src="https://unpkg.com/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.js"></script>
295
+ ```
296
+
297
+ For more information see the [web.dev article](https://web.dev/css-module-scripts/).
298
+
299
+ In addition CSS modules need to be served with a valid CSS content type.
300
+
301
+ Checks for assertion failures are not currently included.
302
+
303
+ ### Resolve
304
+
305
+ > Stability: No current browser standard
306
+
307
+ `import.meta.resolve` provides a contextual resolver within modules. It is asynchronous, like the Node.js implementation, to support waiting on any in-flight
308
+ import map loads when import maps are [loaded dynamically](#dynamic-import-map-updates).
309
+
310
+ The second argument to `import.meta.resolve` permits a custom parent URL scope for the resolution, which defaults to `import.meta.url`.
311
+
312
+ ```js
313
+ // resolve a relative path to a module
314
+ var resolvedUrl = await import.meta.resolve('./relative.js');
315
+ // resolve a dependency from a module
316
+ var resolvedUrl = await import.meta.resolve('dep');
317
+ // resolve a path
318
+ var resolvedUrlPath = await import.meta.resolve('dep/');
319
+ // resolve with a custom parent scope
320
+ var resolvedUrl = await import.meta.resolve('dep', 'https://site.com/another/scope');
321
+ ```
322
+
323
+ This implementation is as provided experimentally in Node.js - https://nodejs.org/dist/latest-v14.x/docs/api/esm.html#esm_no_require_resolve.
324
+
325
+ ## Polyfill Mode Details
326
+
327
+ In polyfill mode, feature detections are performed for ES modules features. In browsers with full feature support no further processing is done.
328
+
329
+ In browsers with variable feature support, sources are analyzed with module specifiers rewritten using the very fast Wasm / asm.js lexer while sharing the source network fetch cache with the native loader.
330
+
331
+ #### Polyfill Features
332
+
333
+ The current default native baseline for the ES module shims polyfill mode is browsers supporting import maps.
334
+
335
+ 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 to only browsers supporting these features:
336
+
337
+ ```html
338
+ <script>
339
+ window.esmsInitOptions = { polyfillEnable: ['css-modules', 'json-modules'] }
340
+ </script>
341
+ ```
342
+
343
+ To verify when the polyfill is actively engaging as opposed to relying on the native loader, [a `polyfill` hook](#polyfill-hook) is also provided.
344
+
345
+ #### Polyfill Edge Case: Dynamic Import
346
+
347
+ The guarantee of the polyfill is that any module graph that would have failed will be reexecuted through the shim layer. This leaves any edge case where execution succeeds but not as expected. For example when using dynamic imports:
348
+
349
+ ```html
350
+ <script type="module">
351
+ console.log('Executing');
352
+ const dynamic = 'bare-specifier';
353
+ import(dynamic).then(x => {
354
+ console.log('Ok');
355
+ }, err => {
356
+ console.log('Fail');
357
+ });
358
+ </script>
359
+ ```
360
+
361
+ The native browser loader without import maps support will execute the above module fine, but fail on the lazy dynamic import.
362
+
363
+ ES Module Shims will not reexecute the above in browsers without import maps support though because it will see that the execution did complete successfully therefore it will not attempt reexecution and as a result, `"Ok"` is never logged.
364
+
365
+ Other examples include dynamically injecting import maps, or using import maps with a `"src"` attribute, which aren't supported in native Chrome.
366
+
367
+ This is why it is advisable to always ensure modules use syntax that will fail early to avoid non-execution.
368
+
369
+ #### Polyfill Edge Case: Instance Sharing
370
+
371
+ 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.
372
+
373
+ Note that instances are not shared between these loaders for consistency and performance.
374
+
375
+ As a result, if you have two module graphs - one native and one polyfilled, they will not share the same dependency instance, for example:
376
+
377
+ ```html
378
+ <script type="importmap">
379
+ {
380
+ "imports": {
381
+ "dep": "/dep.js"
382
+ }
383
+ }
384
+ </script>
385
+ <script type="module">
386
+ import '/dep.js';
387
+ </script>
388
+ <script type="module">
389
+ import 'dep';
390
+ </script>
391
+ ```
392
+
393
+ In the above, on browsers without import maps support, the `/dep.js` instance will be loaded natively by the first module, then the second import will fail.
394
+
395
+ ES Module Shims will pick up on the second import and reexecute `/dep.js`. As a result, `/dep.js` will be executed twice on the page.
396
+
397
+ 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.
398
+
399
+ #### Skip Polyfill
400
+
401
+ Adding the `"noshim"` attribute to the script tag will also ensure that ES Module Shims skips processing this script entirely:
402
+
403
+ ```html
404
+ <script type="module" noshim>
405
+ // ...
406
+ </script>
407
+ ```
408
+
409
+ ## Init Options
410
+
411
+ Provide a `esmsInitOptions` on the global scope before `es-module-shims` is loaded to configure various aspects of the module loading process:
412
+
413
+ * [shimMode](#shim-mode-option)
414
+ * [polyfillEnable](#polyfill-enable-option)
415
+ * [enforceIntegrity](#enforce-integrity)
416
+ * [nonce](#nonce)
417
+ * [noLoadEventRetriggers](#no-load-event-retriggers)
418
+ * [skip](#skip-processing)
419
+ * [onerror](#error-hook)
420
+ * [onpolyfill](#polyfill-hook)
421
+ * [resolve](#resolve-hook)
422
+ * [fetch](#fetch-hook)
423
+ * [revokeBlobURLs](#revoke-blob-urls)
424
+ * [mapOverrides](#overriding-import-map-entries)
425
+
426
+ ```html
427
+ <script>
428
+ window.esmsInitOptions = {
429
+ shimMode: true, // default false
430
+ polyfillEnable: ['css-modules', 'json-modules'], // default empty
431
+ nonce: 'n0nce', // default null
432
+ noLoadEventRetriggers: true, // default false
433
+ skip: /^https:\/\/cdn\.com/, // defaults to null
434
+ onerror: (e) => { /*...*/ }, // default noop
435
+ onpolyfill: () => {}, // default logs to the console
436
+ resolve: (id, parentUrl, resolve) => resolve(id, parentUrl), // default is spec resolution
437
+ fetch: (url, options) => fetch(url, options), // default is native
438
+ revokeBlobURLs: true, // default false
439
+ enforceIntegrity: true, // default false
440
+ mapOverrides: true, // default false
441
+ }
442
+ </script>
443
+ <script async src="es-module-shims.js"></script>
444
+ ```
445
+
446
+ If only setting JSON-compatible options, the `<script type="esms-options">` can be used instead:
447
+
448
+ ```html
449
+ <script type="esms-options">
450
+ {
451
+ "shimMode": true,
452
+ "polyfillEnable": ["css-modules", "json-modules"],
453
+ "nonce": "n0nce",
454
+ "onpolyfill": "polyfill"
455
+ }
456
+ </script>
457
+ ```
458
+
459
+ This can be convenient when using a CSP policy.
460
+
461
+ Function strings correspond to global function names.
462
+
463
+ See below for a detailed description of each of these options.
464
+
465
+ ### Shim Mode Option
466
+
467
+ [Shim Mode](#shim-mode) can be overridden using the `shimMode` option:
468
+
469
+ ```html
470
+ <script type="esms-options">
471
+ {
472
+ "shimMode": true
473
+ }
474
+ </script>
475
+ ```
476
+
477
+ For example, if lazy loading `<script type="module-shim">` scripts alongside static native module scripts, shim mode would not be enabled at initialization time.
478
+
479
+ DOM `load` events are fired for all `"module-shim"` scripts both for success and failure just like for native module scripts.
480
+
481
+ ### Pollyfill Enable Option
482
+
483
+ 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.
484
+
485
+ Currently this option supports just `"css-modules"` and `"json-modules"`.
486
+
487
+ ```html
488
+ <script type="esms-options">
489
+ {
490
+ "polyfillEnable": ["css-modules", "json-modules"]
491
+ }
492
+ </script>
493
+ ```
494
+
495
+ ### Enforce Integrity
496
+
497
+ 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
498
+ a `<link rel="modulepreload-shim" integrity="...">` preload tag in shim mode. Modules without integrity will throw at fetch time.
499
+
500
+ For example in the following, only the listed `app.js` and `dep.js` modules will be able to execute with the provided integrity:
501
+
502
+ ```html
503
+ <script type="esms-options">{ "enforceIntegrity": true }</script>
504
+ <link rel="modulepreload-shim" href="/app.js" integrity="sha384-..." />\
505
+ <link rel="modulepreload-shim" href="/dep.js" integrity="sha384-..." />
506
+ <script type="module-shim">
507
+ import '/app.js';
508
+ </script>
509
+ ```
510
+
511
+ 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.
512
+
513
+ Future versions of this option may provide support for origin-specific allow lists.
514
+
515
+ ### Nonce
516
+
517
+ 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.
518
+
519
+ Alternatively, add a `blob:` URL policy with the CSP build to get CSP compatibility.
520
+
521
+ ```js
522
+ <script type="esms-options">
523
+ {
524
+ "nonce": "n0nce"
525
+ }
526
+ </script>
527
+ ```
528
+
529
+ ### No Load Event Retriggers
530
+
531
+ 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.
532
+
533
+ 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.
534
+
535
+ In such a case, this double event firing can be disabled with the `noLoadEventRetriggers` option:
536
+
537
+ ```js
538
+ <script type="esms-options">
539
+ {
540
+ // do not re-trigger DOM events (onreadystatechange, DOMContentLoaded)
541
+ "noLoadEventRetriggers": true
542
+ }
543
+ </script>
544
+ <script async src="es-module-shims.js"></script>
545
+ ```
546
+
547
+ ### Skip Processing
548
+
549
+ 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 rewriting. This 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.
550
+
551
+ This can be configured by providing a URL regular expression for the `skip` option:
552
+
553
+ ```js
554
+ <script type="esms-options">
555
+ {
556
+ "skip": "/^https?:\/\/(cdn\.skypack\.dev|jspm\.dev)\//`
557
+ }
558
+ </script>
559
+ <script async src="es-module-shims.js"></script>
560
+ ```
561
+
562
+ #### Polyfill hook
563
+
564
+ The polyfill hook is called when running in polyfill mode and the polyfill is kicking in instead of passing through to the native loader.
565
+
566
+ 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.
567
+
568
+ ```js
569
+ <script>
570
+ window.polyfilling = () => console.log('The polyfill is actively applying');
571
+ </script>
572
+ <script type="esms-options">
573
+ {
574
+ "onpolyfill": "polyfilling"
575
+ }
576
+ </script>
577
+ ```
578
+
579
+ 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.
580
+
581
+ In the above, running in latest Chromium browsers, nothing will be logged, while running in an older browser that does not support newer features
582
+ like import maps the console log will be output.
583
+
584
+ #### Error hook
585
+
586
+ You can provide a function to handle errors during the module loading process by providing an `onerror` option:
587
+
588
+ ```js
589
+ <script>
590
+ window.esmsInitOptions = {
591
+ onerror: error => console.log(error) // defaults to `((e) => { throw e; })`
592
+ }
593
+ </script>
594
+ <script async src="es-module-shims.js"></script>
595
+ ```
596
+
597
+ #### Resolve Hook
598
+
599
+ The resolve hook is supported for shim mode only and allows full customization of the resolver, while still having access to the original resolve function.
600
+
601
+ ```js
602
+ <script>
603
+ window.esmsInitOptions = {
604
+ shimMode: true,
605
+ resolve: async function (id, parentUrl, defaultResolve) {
606
+ if (id === 'custom' && parentUrl.startsWith('https://custom.com/'))
607
+ return 'https://custom.com/custom.js';
608
+
609
+ // Default resolve will handle the typical URL and import map resolution
610
+ return defaultResolve(id, parentUrl);
611
+ }
612
+ }
613
+ </script>
614
+ ```
615
+
616
+ ### Fetch Hook
617
+
618
+ The fetch hook is supported for shim mode only.
619
+
620
+ The ES Module Shims fetch hook can be used to implement transform plugins.
621
+
622
+ For example:
623
+
624
+ ```js
625
+ <script>
626
+ window.esmsInitOptions = {
627
+ shimMode: true,
628
+ fetch: async function (url, options) {
629
+ const res = await fetch(url, options);
630
+ if (!res.ok)
631
+ return res;
632
+ if (res.url.endsWith('.ts')) {
633
+ const source = await res.body();
634
+ const transformed = tsCompile(source);
635
+ return new Response(new Blob([transformed], { type: 'application/javascript' }));
636
+ }
637
+ return res;
638
+ } // defaults to `((url, options) => fetch(url, options))`
639
+ }
640
+ </script>
641
+ <script async src="es-module-shims.js"></script>
642
+ ```
643
+
644
+ Because the dependency analysis applies by ES Module Shims takes care of ensuring all dependencies run through the same fetch hook,
645
+ the above is all that is needed to implement custom plugins.
646
+
647
+ Streaming support is also provided, for example here is a hook with streaming support for JSON:
648
+
649
+ ```js
650
+ window.esmsInitOptions = {
651
+ shimMode: true,
652
+ fetch: async function (url, options) {
653
+ const res = await fetch(url, options);
654
+ if (!res.ok || !/^application\/json($|;)/.test(res.headers.get('content-type')))
655
+ return res;
656
+ const reader = res.body.getReader();
657
+ const headers = new Headers(res.headers);
658
+ headers.set('Content-Type', 'application/javascript');
659
+ return new Response(new ReadableStream({
660
+ async start (controller) {
661
+ let done, value;
662
+ controller.enqueue(new TextEncoder.encode('export default '));
663
+ while (({ done, value } = await reader.read()) && !done) {
664
+ controller.enqueue(value);
665
+ }
666
+ controller.close();
667
+ }
668
+ }), { headers });
669
+ }
670
+ }
671
+ ```
672
+
673
+ ### Revoke Blob URLs
674
+
675
+ When polyfilling the missing features `es-module-shims` would create in-memory blobs using `URL.createObjectURL()` for each processed module.
676
+ In most cases, memory footprint of these blobs is negligible so there is no need to call `URL.revokeObjectURL()`
677
+ for them, and we don't do that by default.
678
+
679
+ That said, in some scenarios, e.g. when evaluating some continuously changing modules without a page reload, like in a web-based code editor,
680
+ you might want to reduce the growth of memory usage by revoking those blob URLs after they were already `import`ed.
681
+
682
+ You can do that by enabling the `revokeBlobURLs` init option:
683
+
684
+ ```js
685
+ <script type="esms-options">
686
+ {
687
+ "revokeBlobURLs": true
688
+ }
689
+ </script>
690
+ <script type="module" src="es-module-shims.js"></script>
691
+ ```
692
+
693
+ NOTE: revoking object URLs is not entirely free, while we are trying to be smart about it and make sure it doesn't
694
+ cause janks, we recommend enabling this option only if you have done the measurements and identified that you really need it.
695
+
696
+ ### Overriding import map entries
697
+
698
+ 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.
699
+
700
+ It is possible to disable this behavior in shim mode by setting the `mapOverrides` option:
701
+
702
+ ```js
703
+ <script type="esms-options">
704
+ {
705
+ "shimMode": true,
706
+ "mapOverrides": true
707
+ }
708
+ </script>
709
+ <script type="importmap-shim">
710
+ {
711
+ "imports": {
712
+ "x": "/x.js"
713
+ }
714
+ }
715
+ </script>
716
+ <script>
717
+ // No error will be thrown here
718
+ document.body.appendChild(Object.assign(document.createElement('script'), {
719
+ type: 'importmap',
720
+ innerHTML: JSON.stringify({ imports: { x: './y.js' } }),
721
+ }));
722
+ </script>
723
+ ```
724
+
725
+ This can be useful for HMR workflows.
726
+
727
+ ## Implementation Details
728
+
729
+ ### Import Rewriting
730
+
731
+ * Sources are fetched, import specifiers are rewritten to reference exact URLs, and then executed as BlobURLs through the whole module graph.
732
+ * 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.
733
+ * When executing a circular reference A -> B -> A, a shell module technique is used to "shim" the circular reference into an acyclic graph. As a result, live bindings for the circular parent A are not supported, and instead the bindings are captured immediately after the execution of A.
734
+
735
+ ## Inspiration
736
+
737
+ Huge thanks to Rich Harris for inspiring this approach with [Shimport](https://github.com/rich-harris/shimport).
738
+
739
+ ## License
740
+
741
+ MIT