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

Enable es-module-shims usage in web workers #300

Merged
merged 5 commits into from
Jun 14, 2022
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
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,22 @@ While in polyfill mode the same restrictions apply that multiple import maps, im
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.

```js
const importMap = importShim.getImportMap()
const importMap = importShim.getImportMap();

// importMap will be an object in the same shape as the json in a importmap script
```

#### Setting current import map state
To make it easy to set the import map state, es-module-shims provides a `importShim.addImportMap` utility function, available only in shim mode.

```js
// importMap will be an object in the same shape as the json in a importmap script
const importMap = { imports: {/*...*/}, scopes: {/*...*/} };

importShim.addImportMap(importMap);
```


### Dynamic Import

> Stability: Stable browser standard
Expand Down Expand Up @@ -423,6 +434,34 @@ var resolvedUrl = import.meta.resolve('dep', 'https://site.com/another/scope');

Node.js also implements a similar API, although it's in the process of shifting to a synchronous resolver.

### Module Workers
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+), Safari(15+).

An example of ES Module Shims usage in web workers is provided below:
```js
/**
*
* @param {string} aURL a string representing the URL of the module script the worker will execute.
* @returns {string} The string representing the URL of the script the worker will execute.
*/
function getWorkerScriptURL(aURL) {
// baseURL, esModuleShimsURL are considered to be known in advance
// esModuleShimsURL - must point to the non-CSP build of ES Module Shims,
// namely the `es-module-shim.wasm.js` output: es-module-shims/dist/es-module-shims.wasm.js

return URL.createObjectURL(new Blob(
[
`importScripts('${new URL(esModuleShimsURL, baseURL).href}');
importShim.addImportMap(${JSON.stringify(importShim.getImportMap())});
importShim('${new URL(aURL, baseURL).href}').catch(e => setTimeout(() => { throw e; }))`
],
{ type: 'application/javascript' }))
}

const worker = new Worker(getWorkerScriptURL('myEsModule.js'));
```
> For now, in web workers must be used the non-CSP build of ES Module Shims, namely the `es-module-shim.wasm.js` output: es-module-shims/dist/es-module-shims.wasm.js.

## Init Options

Provide a `esmsInitOptions` on the global scope before `es-module-shims` is loaded to configure various aspects of the module loading process:
Expand Down
4 changes: 2 additions & 2 deletions src/dynamic-import.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createBlob, baseUrl, nonce } from './env.js';
import { createBlob, baseUrl, nonce, hasDocument } from './env.js';

export let supportsDynamicImportCheck = false;

Expand All @@ -9,7 +9,7 @@ try {
}
catch (e) {}

if (!supportsDynamicImportCheck) {
if (hasDocument && !supportsDynamicImportCheck) {
let err;
window.addEventListener('error', _err => err = _err);
dynamicImport = (url, { errUrl = url }) => {
Expand Down
16 changes: 11 additions & 5 deletions src/env.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
export const hasWindow = typeof window !== 'undefined';
export const hasDocument = typeof document !== 'undefined';

export const noop = () => {};

const optionsScript = document.querySelector('script[type=esms-options]');
const optionsScript = hasDocument ? document.querySelector('script[type=esms-options]') : undefined;

export const esmsInitOptions = optionsScript ? JSON.parse(optionsScript.innerHTML) : {};
Object.assign(esmsInitOptions, self.esmsInitOptions || {});

export let shimMode = !!esmsInitOptions.shimMode;
export let shimMode = hasDocument ? !!esmsInitOptions.shimMode : true;

export const importHook = globalHook(shimMode && esmsInitOptions.onimport);
export const resolveHook = globalHook(shimMode && esmsInitOptions.resolve);
Expand All @@ -19,7 +21,7 @@ export let nonce = esmsInitOptions.nonce;

export const mapOverrides = esmsInitOptions.mapOverrides;

if (!nonce) {
if (!nonce && hasDocument) {
const nonceElement = document.querySelector('script[nonce]');
if (nonceElement)
nonce = nonceElement.nonce || nonceElement.getAttribute('nonce');
Expand All @@ -46,15 +48,19 @@ export function setShimMode () {

export const edge = !navigator.userAgentData && !!navigator.userAgent.match(/Edge\/\d+\.\d+/);

export const baseUrl = document.baseURI;
export const baseUrl = hasDocument
? document.baseURI
: `${location.protocol}//${location.host}${location.pathname.includes('/')
? location.pathname.slice(0, location.pathname.lastIndexOf('/') + 1)
: location.pathname}`;

export function createBlob (source, type = 'text/javascript') {
return URL.createObjectURL(new Blob([source], { type }));
}

const eoop = err => setTimeout(() => { throw err });

export const throwError = err => { (window.reportError || window.safari && console.error || eoop)(err), void onerror(err) };
export const throwError = err => { (self.reportError || hasWindow && window.safari && console.error || eoop)(err), void onerror(err) };

export function fromParent (parent) {
return parent ? ` imported from ${parent}` : '';
Expand Down
102 changes: 57 additions & 45 deletions src/es-module-shims.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
onpolyfill,
enforceIntegrity,
fromParent,
esmsInitOptions
esmsInitOptions,
hasDocument
} from './env.js';
import { dynamicImport } from './dynamic-import-csp.js';
import {
Expand Down Expand Up @@ -67,7 +68,9 @@ async function importShim (id, ...args) {
await initPromise;
if (importHook) await importHook(id, typeof args[1] !== 'string' ? args[1] : {}, parentUrl);
if (acceptingImportMaps || shimMode || !baselinePassthrough) {
processImportMaps();
if (hasDocument)
processImportMaps();

if (!shimMode)
acceptingImportMaps = false;
}
Expand Down Expand Up @@ -97,6 +100,10 @@ function metaResolve (id, parentUrl = this.url) {

importShim.resolve = resolveSync;
importShim.getImportMap = () => JSON.parse(JSON.stringify(importMap));
importShim.addImportMap = importMapIn => {
if (!shimMode) throw new Error('Unsupported in polyfill mode.');
importMap = resolveAndComposeImportMap(importMapIn, pageBaseUrl, importMap);
}

const registry = importShim._r = {};

Expand Down Expand Up @@ -135,44 +142,47 @@ const initPromise = featureDetectionPromise.then(() => {
}
}
baselinePassthrough = esmsInitOptions.polyfillEnable !== true && supportsDynamicImport && supportsImportMeta && supportsImportMaps && (!jsonModulesEnabled || supportsJsonAssertions) && (!cssModulesEnabled || supportsCssAssertions) && !importMapSrcOrLazy && !self.ESMS_DEBUG;
if (!supportsImportMaps) {
const supports = HTMLScriptElement.supports || (type => type === 'classic' || type === 'module');
HTMLScriptElement.supports = type => type === 'importmap' || supports(type);
}
if (shimMode || !baselinePassthrough) {
new MutationObserver(mutations => {
for (const mutation of mutations) {
if (mutation.type !== 'childList') continue;
for (const node of mutation.addedNodes) {
if (node.tagName === 'SCRIPT') {
if (node.type === (shimMode ? 'module-shim' : 'module'))
processScript(node);
if (node.type === (shimMode ? 'importmap-shim' : 'importmap'))
processImportMap(node);
if (hasDocument) {
if (!supportsImportMaps) {
const supports = HTMLScriptElement.supports || (type => type === 'classic' || type === 'module');
HTMLScriptElement.supports = type => type === 'importmap' || supports(type);
}

if (shimMode || !baselinePassthrough) {
new MutationObserver(mutations => {
for (const mutation of mutations) {
if (mutation.type !== 'childList') continue;
for (const node of mutation.addedNodes) {
if (node.tagName === 'SCRIPT') {
if (node.type === (shimMode ? 'module-shim' : 'module'))
processScript(node);
if (node.type === (shimMode ? 'importmap-shim' : 'importmap'))
processImportMap(node);
}
else if (node.tagName === 'LINK' && node.rel === (shimMode ? 'modulepreload-shim' : 'modulepreload'))
processPreload(node);
}
else if (node.tagName === 'LINK' && node.rel === (shimMode ? 'modulepreload-shim' : 'modulepreload'))
processPreload(node);
}
}).observe(document, {childList: true, subtree: true});
processImportMaps();
processScriptsAndPreloads();
if (document.readyState === 'complete') {
readyStateCompleteCheck();
}
}).observe(document, { childList: true, subtree: true });
processImportMaps();
processScriptsAndPreloads();
if (document.readyState === 'complete') {
readyStateCompleteCheck();
}
else {
async function readyListener () {
await initPromise;
processImportMaps();
if (document.readyState === 'complete') {
readyStateCompleteCheck();
document.removeEventListener('readystatechange', readyListener);
else {
async function readyListener() {
await initPromise;
processImportMaps();
if (document.readyState === 'complete') {
readyStateCompleteCheck();
document.removeEventListener('readystatechange', readyListener);
}
}
document.addEventListener('readystatechange', readyListener);
}
document.addEventListener('readystatechange', readyListener);
}
return lexer.init;
}
return lexer.init;
});
let importMapPromise = initPromise;
let firstPolyfillLoad = true;
Expand Down Expand Up @@ -252,7 +262,7 @@ function resolveDeps (load, seen) {
const source = load.S;

// edge doesnt execute sibling in order, so we fix this up by ensuring all previous executions are explicit dependencies
let resolvedSource = edge && lastLoad ? `import '${lastLoad}';` : '';
let resolvedSource = edge && lastLoad ? `import '${lastLoad}';` : '';

if (!imports.length) {
resolvedSource += source;
Expand Down Expand Up @@ -293,7 +303,7 @@ function resolveDeps (load, seen) {
resolvedSource += `/*${source.slice(start - 1, statementEnd)}*/${urlJsString(blobUrl)}`;

// circular shell execution
if (!cycleShell && depLoad.s) {
if (!cycleShell && depLoad.s) {
resolvedSource += `;import*as m$_${depIndex} from'${depLoad.b}';import{u$_ as u$_${depIndex}}from'${depLoad.s}';u$_${depIndex}(m$_${depIndex})`;
depLoad.s = undefined;
}
Expand Down Expand Up @@ -380,8 +390,8 @@ async function fetchModule (url, fetchOpts, parent) {
return { r: res.url, s: `export default ${await res.text()}`, t: 'json' };
else if (cssContentType.test(contentType)) {
return { r: res.url, s: `var s=new CSSStyleSheet();s.replaceSync(${
JSON.stringify((await res.text()).replace(cssUrlRegEx, (_match, quotes = '', relUrl1, relUrl2) => `url(${quotes}${resolveUrl(relUrl1 || relUrl2, url)}${quotes})`))
});export default s;`, t: 'css' };
JSON.stringify((await res.text()).replace(cssUrlRegEx, (_match, quotes = '', relUrl1, relUrl2) => `url(${quotes}${resolveUrl(relUrl1 || relUrl2, url)}${quotes})`))
});export default s;`, t: 'css' };
}
else
throw Error(`Unsupported Content-Type "${contentType}" loading ${url}${fromParent(parent)}. Modules must be served with a valid MIME type like application/javascript.`);
Expand Down Expand Up @@ -502,14 +512,16 @@ function domContentLoadedCheck () {
document.dispatchEvent(new Event('DOMContentLoaded'));
}
// this should always trigger because we assume es-module-shims is itself a domcontentloaded requirement
document.addEventListener('DOMContentLoaded', async () => {
await initPromise;
domContentLoadedCheck();
if (shimMode || !baselinePassthrough) {
processImportMaps();
processScriptsAndPreloads();
}
});
if (hasDocument) {
document.addEventListener('DOMContentLoaded', async () => {
await initPromise;
domContentLoadedCheck();
if (shimMode || !baselinePassthrough) {
processImportMaps();
processScriptsAndPreloads();
}
});
}

let readyStateCompleteCnt = 1;
function readyStateCompleteCheck () {
Expand Down
8 changes: 4 additions & 4 deletions src/features.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { dynamicImport, supportsDynamicImportCheck } from './dynamic-import-csp.js';
import { createBlob, noop, nonce, cssModulesEnabled, jsonModulesEnabled } from './env.js';
import { createBlob, noop, nonce, cssModulesEnabled, jsonModulesEnabled, hasDocument } from './env.js';

// support browsers without dynamic import support (eg Firefox 6x)
export let supportsJsonAssertions = false;
export let supportsCssAssertions = false;

export let supportsImportMaps = HTMLScriptElement.supports ? HTMLScriptElement.supports('importmap') : false;
export let supportsImportMaps = hasDocument && HTMLScriptElement.supports ? HTMLScriptElement.supports('importmap') : false;
export let supportsImportMeta = supportsImportMaps;
export let supportsDynamicImport = false;

Expand All @@ -18,7 +18,7 @@ export const featureDetectionPromise = Promise.resolve(supportsImportMaps || sup
supportsImportMaps || dynamicImport(createBlob('import.meta')).then(() => supportsImportMeta = true, noop),
cssModulesEnabled && dynamicImport(createBlob('import"data:text/css,{}"assert{type:"css"}')).then(() => supportsCssAssertions = true, noop),
jsonModulesEnabled && dynamicImport(createBlob('import"data:text/json,{}"assert{type:"json"}')).then(() => supportsJsonAssertions = true, noop),
supportsImportMaps || new Promise(resolve => {
supportsImportMaps || (hasDocument && new Promise(resolve => {
self._$s = v => {
document.head.removeChild(iframe);
if (v) supportsImportMaps = true;
Expand All @@ -33,6 +33,6 @@ export const featureDetectionPromise = Promise.resolve(supportsImportMaps || sup
// setting src to a blob URL results in a navigation event in webviews
// setting srcdoc is not supported in React native webviews on iOS
iframe.contentWindow.document.write(`<script type=importmap nonce="${nonce}">{"imports":{"x":"data:text/javascript,"}}<${''}/script><script nonce="${nonce}">import('x').then(()=>1,()=>0).then(v=>parent._$s(v))<${''}/script>`);
})
}))
]);
});