Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support modulepreload polyfill #141

Merged
merged 2 commits into from
Jun 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This includes support for:
* Dynamic `import()` shimming when necessary in eg older Firefox versions.
* `import.meta` and `import.meta.url`.
* JSON modules with import assertions.
* `<link rel="modulepreload">` polyfill in non Chromium browsers for both shimmed and unshimmed preloading scenarios.

In addition a custom [fetch hook](#fetch-hook) can be implemented allowing for streaming in-browser transform workflows to support custom module types.

Expand Down Expand Up @@ -91,10 +92,13 @@ Works in all browsers with [baseline ES module support](https://caniuse.com/#fea
| Executes Modules in Correct Order | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark:<sup>1</sup> |
| [Dynamic Import](#dynamic-import) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| [import.meta.url](#importmetaurl) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| [import.meta.resolve](#resolve) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| [Module Workers](#module-workers) | :heavy_check_mark: ~68+ | :x:<sup>2</sup> | :x:<sup>2</sup> | :x:<sup>2</sup> |
| [Module Workers](#module-workers) | :heavy_check_mark: ~68+ | :x:<sup>2</sup> |
:x:<sup>2</sup> | :x:<sup>2</sup> |
| [modulepreload](#modulepreload) | :heavy_check_mark: | :heavy_check_mark: |
:heavy_check_mark: | :heavy_check_mark: |
| [Import Maps](#import-maps) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| [JSON Modules](#json-modules) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| [import.meta.resolve](#resolve) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |

* 1: _The Edge parallel execution ordering bug is corrected by ES Module Shims with an execution chain inlining approach._
* 2: _Module worker support cannot be implemented without dynamic import support in web workers._
Expand All @@ -106,10 +110,11 @@ Works in all browsers with [baseline ES module support](https://caniuse.com/#fea
| Executes Modules in Correct Order | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x:<sup>1</sup> |
| [Dynamic Import](#dynamic-import) | :heavy_check_mark: 63+ | :heavy_check_mark: 67+ | :heavy_check_mark: 11.1+ | :x: |
| [import.meta.url](#importmetaurl) | :heavy_check_mark: ~76+ | :heavy_check_mark: ~67+ | :heavy_check_mark: ~12+ ❕<sup>1</sup>| :x: |
| [import.meta.resolve](#resolve) | :x: | :x: | :x: | :x: |
| [Module Workers](#module-workers) | :heavy_check_mark: ~68+ | :x: | :x: | :x: |
| [modulepreload](#modulepreload) | :heavy_check_mark: 66+ | :x: | :x: | :x: |
| [Import Maps](#import-maps) | :heavy_check_mark: 89+ | :x: | :x: | :x: |
| [JSON Modules](#json-modules) | :heavy_check_mark: 91+ | :x: | :x: | :x: |
| [import.meta.resolve](#resolve) | :x: | :x: | :x: | :x: |

* 1: _Edge executes parallel dependencies in non-deterministic order. ([ChakraCore bug](https://github.com/microsoft/ChakraCore/issues/6261))._
* ~: _Indicates the exact first version support has not yet been determined (PR's welcome!)._
Expand Down Expand Up @@ -163,6 +168,19 @@ importShim('/path/to/module.js').then(x => console.log(x));

`import.meta.url` provides the full URL of the current module within the context of the module execution.

### modulepreload

> Stability: WhatWG Standard, Single Browser Implementer

Preloading of modules can be achieved by including a `<link rel="modulepreload" href="/module.js" />` tag in the HTML or injecting it dynamically.

This tag also supports the `"integrity"`, `"crossorigin"` and `"referrerpolicy"` attributes as supported on module scripts.

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.

Unlike the browser specification, the modulepreload polyfill does not request dependency modules by default, in order to avoid unnecessary
code analysis in the polyfill scenarios. **It is recommended to preload deep imports anyway so that this feature shouldn't be necessary.**

### JSON Modules

> Stability: WhatWG Standard, Single Browser Implementer
Expand Down
52 changes: 44 additions & 8 deletions src/es-module-shims.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async function loadAll (load, seen) {

let waitingForImportMapsInterval;
let firstTopLevelProcess = true;
async function topLevelLoad (url, source, nativelyLoaded) {
async function topLevelLoad (url, fetchOpts, source, nativelyLoaded) {
// no need to even fetch if we have feature support
await featureDetectionPromise;
if (waitingForImportMapsInterval > 0) {
Expand All @@ -51,7 +51,7 @@ async function topLevelLoad (url, source, nativelyLoaded) {
return source && nativelyLoaded ? null : dynamicImport(source ? createBlob(source) : url);
}
await init;
const load = getOrCreateLoad(url, source);
const load = getOrCreateLoad(url, fetchOpts, source);
const seen = {};
await loadAll(load, seen);
lastLoad = undefined;
Expand Down Expand Up @@ -91,7 +91,7 @@ async function importShim (id, parentUrl = pageBaseUrl, _assertion) {
await featureDetectionPromise;
// Make sure all the "in-flight" import maps are loaded and applied.
await importMapPromise;
return topLevelLoad(resolve(id, parentUrl).r || throwUnresolved(id, parentUrl));
return topLevelLoad(resolve(id, parentUrl).r || throwUnresolved(id, parentUrl), { credentials: 'same-origin' });
}

self.importShim = importShim;
Expand All @@ -110,7 +110,7 @@ self._esmsm = meta;
const esmsInitOptions = self.esmsInitOptions || {};
delete self.esmsInitOptions;
let shimMode = typeof esmsInitOptions.shimMode === 'boolean' ? esmsInitOptions.shimMode : !!esmsInitOptions.fetch || !!document.querySelector('script[type="module-shim"],script[type="importmap-shim"]');
const fetchHook = esmsInitOptions.fetch || (url => fetch(url));
const fetchHook = esmsInitOptions.fetch || ((url, opts) => fetch(url, opts));
const skip = esmsInitOptions.skip || /^https?:\/\/(cdn\.skypack\.dev|jspm\.dev)\//;
const onerror = esmsInitOptions.onerror || ((e) => { throw e; });
const shouldRevokeBlobURLs = esmsInitOptions.revokeBlobURLs;
Expand Down Expand Up @@ -217,7 +217,9 @@ const jsonContentType = /^application\/json(;|$)/;
const cssContentType = /^text\/css(;|$)/;
const wasmContentType = /^application\/wasm(;|$)/;

function getOrCreateLoad (url, source) {
const fetchOptsMap = new Map();

function getOrCreateLoad (url, fetchOpts, source) {
let load = registry[url];
if (load)
return load;
Expand Down Expand Up @@ -247,7 +249,8 @@ function getOrCreateLoad (url, source) {

load.f = (async () => {
if (!source) {
const res = await fetchHook(url, { credentials: 'same-origin' });
// preload fetch options override fetch options (race)
const res = await fetchHook(url, fetchOptsMap.get(url) || fetchOpts);
if (!res.ok)
throw new Error(`${res.status} ${res.statusText} ${res.url}`);
load.r = res.url;
Expand Down Expand Up @@ -275,6 +278,7 @@ function getOrCreateLoad (url, source) {
})();

load.L = load.f.then(async () => {
let childFetchOpts = fetchOpts;
load.d = await Promise.all(load.a[0].map(({ n, d, a }) => {
if (d >= 0 && !supportsDynamicImport ||
d === 2 && (!supportsImportMeta || source.slice(end, end + 8) === '.resolve') ||
Expand All @@ -288,7 +292,9 @@ function getOrCreateLoad (url, source) {
if (!r)
throwUnresolved(n, load.r || load.u);
if (skip.test(r)) return { b: r };
return getOrCreateLoad(r).f;
if (childFetchOpts.integrity)
childFetchOpts = Object.assign({}, childFetchOpts, { integrity: undefined });
return getOrCreateLoad(r, childFetchOpts).f;
}).filter(l => l));
});

Expand All @@ -309,6 +315,8 @@ async function processScripts () {
clearTimeout(waitingForImportMapsInterval);
waitingForImportMapsInterval = 0;
}
for (const link of document.querySelectorAll('link[rel="modulepreload"]'))
processPreload(link);
for (const script of document.querySelectorAll('script[type="module-shim"],script[type="importmap-shim"],script[type="module"],script[type="importmap"]'))
await processScript(script);
}
Expand All @@ -319,10 +327,27 @@ new MutationObserver(mutations => {
for (const node of mutation.addedNodes) {
if (node.tagName === 'SCRIPT' && node.type)
processScript(node, !firstTopLevelProcess);
else if (node.tagName === 'LINK' && node.rel)
processPreload(node);
}
}
}).observe(document, { childList: true, subtree: true });

function getFetchOpts (script) {
const fetchOpts = {};
if (script.integrity)
fetchOpts.integrity = script.integrity;
if (script.referrerpolicy)
fetchOpts.referrerPolicy = script.referrerpolicy;
if (script.crossorigin === 'use-credentials')
fetchOpts.credentials = 'include';
else if (script.crossorigin === 'anonymous')
fetchOpts.credentials = 'omit';
else
fetchOpts.credentials = 'same-origin';
return fetchOpts;
}

async function processScript (script, dynamic) {
if (script.ep) // ep marker = script processed
return;
Expand All @@ -336,7 +361,7 @@ async function processScript (script, dynamic) {
return;
script.ep = true;
if (type === 'module') {
await topLevelLoad(script.src || `${pageBaseUrl}?${id++}`, !script.src && script.innerHTML, !shim).catch(onerror);
await topLevelLoad(script.src || `${pageBaseUrl}?${id++}`, getFetchOpts(script), !script.src && script.innerHTML, !shim).catch(onerror);
}
else if (type === 'importmap') {
importMapPromise = importMapPromise.then(async () => {
Expand All @@ -347,6 +372,17 @@ async function processScript (script, dynamic) {
}
}

function processPreload (link) {
if (link.ep) // ep marker = processed
return;
link.ep = true;
// prepopulate the load record
const fetchOpts = getFetchOpts(link);
// save preloaded fetch options for later load
fetchOptsMap.set(link.href, fetchOpts);
fetch(link.href, fetchOpts);
}

function resolve (id, parentUrl) {
const urlResolved = resolveIfNotPlainOrUrl(id, parentUrl);
const resolved = resolveImportMap(importMap, urlResolved || id, parentUrl);
Expand Down
1 change: 1 addition & 0 deletions test/test.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!doctype html>
<link rel="stylesheet" type="text/css" href="../node_modules/mocha/mocha.css"/>
<script src="../node_modules/mocha/mocha.js"></script>
<link rel="modulepreload" integrity="sha256-EwxIX0ecy1M0ZPP9fYvSRqDFxVMbDixhZl7rE+l+mjA=" href="/test/fixtures/es-modules/bare-dynamic-import.js" />
<script type="importmap-shim">
{
"imports": {
Expand Down