From 765fa3c0d46acf26d24b54640de89b8eb402bd7b Mon Sep 17 00:00:00 2001 From: Costin Geana Date: Sat, 11 Jun 2022 06:28:38 +0000 Subject: [PATCH 1/5] The purpose of this commit is to enable the usage of es-module-shims in web workers. However, web workers cannot access objects like document or window. Therefore, I had to do the following changes: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add some simple pieces of code that check whether document/window exists before doing something with document/window object; - add a function that computed the baseUrl. In fact, this function was already defined in es-module-shims version 0.4.7. - I also added a new function setImportMap which can be used to set the import map from outside (obviously). This function is useful when you start a web worker which uses es-module-shims. It’s important to mention that in web workers, I use the es-module-shims.wasm.js output. Why? Because of the dynamicImportScript function; I’m not sure if there is a solution for the CSP version in web workers. --- src/dynamic-import.js | 4 +- src/env.js | 28 ++++++++++--- src/es-module-shims.js | 89 +++++++++++++++++++++++------------------- src/features.js | 8 ++-- 4 files changed, 78 insertions(+), 51 deletions(-) diff --git a/src/dynamic-import.js b/src/dynamic-import.js index 1261321c..73541519 100644 --- a/src/dynamic-import.js +++ b/src/dynamic-import.js @@ -1,4 +1,4 @@ -import { createBlob, baseUrl, nonce } from './env.js'; +import { createBlob, baseUrl, nonce, hasDocument } from './env.js'; export let supportsDynamicImportCheck = false; @@ -9,7 +9,7 @@ try { } catch (e) {} -if (!supportsDynamicImportCheck) { +if (hasDocument && !supportsDynamicImportCheck) { let err; window.addEventListener('error', _err => err = _err); dynamicImport = (url, { errUrl = url }) => { diff --git a/src/env.js b/src/env.js index 2915c327..7e2d15b4 100644 --- a/src/env.js +++ b/src/env.js @@ -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); @@ -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'); @@ -46,7 +48,23 @@ export function setShimMode () { export const edge = !navigator.userAgentData && !!navigator.userAgent.match(/Edge\/\d+\.\d+/); -export const baseUrl = document.baseURI; +function getBaseURL() { + let baseUrl; + + if (hasDocument) { + baseUrl = document.baseURI; + } + + if (!baseUrl && typeof location !== 'undefined') { + baseUrl = location.href.split('#')[0].split('?')[0]; + const lastSepIndex = baseUrl.lastIndexOf('/'); + if (lastSepIndex !== -1) + baseUrl = baseUrl.slice(0, lastSepIndex + 1); + } + + return baseUrl; +} +export const baseUrl = getBaseURL(); export function createBlob (source, type = 'text/javascript') { return URL.createObjectURL(new Blob([source], { type })); @@ -54,7 +72,7 @@ export function createBlob (source, type = 'text/javascript') { 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}` : ''; diff --git a/src/es-module-shims.js b/src/es-module-shims.js index 94972f8e..d48573ad 100755 --- a/src/es-module-shims.js +++ b/src/es-module-shims.js @@ -24,7 +24,8 @@ import { onpolyfill, enforceIntegrity, fromParent, - esmsInitOptions + esmsInitOptions, + hasDocument } from './env.js'; import { dynamicImport } from './dynamic-import-csp.js'; import { @@ -97,6 +98,7 @@ function metaResolve (id, parentUrl = this.url) { importShim.resolve = resolveSync; importShim.getImportMap = () => JSON.parse(JSON.stringify(importMap)); +importShim.setImportMap = (importMapIn) => Object.assign(importMap, JSON.parse(JSON.stringify(importMapIn))); const registry = importShim._r = {}; @@ -135,41 +137,42 @@ const initPromise = featureDetectionPromise.then(() => { } } baselinePassthrough = esmsInitOptions.polyfillEnable !== true && supportsDynamicImport && supportsImportMeta && supportsImportMaps && (!jsonModulesEnabled || supportsJsonAssertions) && (!cssModulesEnabled || supportsCssAssertions) && !importMapSrcOrLazy && !self.ESMS_DEBUG; - if (!supportsImportMaps) { + if (hasDocument && !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) { + 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(); - } - else { - async function readyListener () { - await initPromise; - processImportMaps(); - if (document.readyState === 'complete') { - readyStateCompleteCheck(); - document.removeEventListener('readystatechange', readyListener); + }).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); + } } + + document.addEventListener('readystatechange', readyListener); } - document.addEventListener('readystatechange', readyListener); } return lexer.init; } @@ -252,7 +255,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; @@ -293,7 +296,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; } @@ -380,8 +383,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.`); @@ -468,6 +471,8 @@ function getOrCreateLoad (url, fetchOpts, parent, source) { } function processScriptsAndPreloads () { + if (!hasDocument) return; + for (const script of document.querySelectorAll(shimMode ? 'script[type=module-shim]' : 'script[type=module]')) processScript(script); for (const link of document.querySelectorAll(shimMode ? 'link[rel=modulepreload-shim]' : 'link[rel=modulepreload]')) @@ -475,6 +480,8 @@ function processScriptsAndPreloads () { } function processImportMaps () { + if (!hasDocument) return; + for (const script of document.querySelectorAll(shimMode ? 'script[type="importmap-shim"]' : 'script[type="importmap"]')) processImportMap(script); } @@ -502,14 +509,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 () { diff --git a/src/features.js b/src/features.js index 2aa778ef..7eb43b6e 100644 --- a/src/features.js +++ b/src/features.js @@ -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; @@ -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; @@ -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(`