From 4d7798d119f47c01d6e121bbb09d7d6265f21505 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sun, 12 Jan 2025 22:18:32 -0800 Subject: [PATCH 1/3] deprecate: dynamic import, import.meta, edge polyfills --- README.md | 10 +-- src/dynamic-import.js | 53 ------------- src/env.js | 4 +- src/es-module-shims.js | 168 ++++++++++++++++++++-------------------- src/features.js | 34 ++++---- test/shim.js | 14 ++-- test/test-csp.html | 1 + test/test-polyfill.html | 1 + 8 files changed, 114 insertions(+), 171 deletions(-) delete mode 100644 src/dynamic-import.js diff --git a/README.md b/README.md index 7426b952..353593c7 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Polyfills import maps and other ES Modules features on top of the baseline native ESM support in browsers. -With import maps now supported by all major browsers, ES Module Shims entirely bypasses processing for the [91% of users](https://caniuse.com/import-maps) with native import maps support. +With import maps now supported by all major browsers, ES Module Shims entirely bypasses processing for the [94% of users](https://caniuse.com/import-maps) with native import maps support. -For the remaining 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). +For the remaining ~4% 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). The following modules features are polyfilled: @@ -210,8 +210,8 @@ ES Module Shims is designed for production performance. A [comprehensive benchma Benchmark summary: -* [ES Module Shims Chrome Passthrough](bench/README.md#chrome-passthrough-performance) (for [72% of users](https://caniuse.com/import-maps)) results in ~5ms extra initialization time over native for ES Module Shims fetching, execution and initialization, and on a slow connection the additional non-blocking bandwidth cost of its 10KB compressed download as expected. -* [ES Module Shims Polyfilling](bench/README.md#native-v-polyfill-performance) (for the remaining [28% of users](https://caniuse.com/import-maps)) is on average 1.4x - 1.5x slower than native module loading, and up to 1.8x slower on slow networks (most likely due to the browser preloader), both for cached and uncached loads, and this result scales linearly up to 10MB and 20k modules loaded executing on the fastest connection in just over 2 seconds in Firefox. +* [ES Module Shims Chrome Passthrough](bench/README.md#chrome-passthrough-performance) (for [94% 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. +* [ES Module Shims Polyfilling](bench/README.md#native-v-polyfill-performance) (for the remaining [3% 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. * [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. ## Features @@ -222,7 +222,7 @@ Works in all browsers with [baseline ES module support](https://caniuse.com/#fea Browser Compatibility on baseline ES modules support **with** ES Module Shims: -| ES Modules Features | Chrome (71+) | Firefox (60+) | Safari (10.1+) | +| ES Modules Features | Chrome (63+) | Firefox (67+) | Safari (11.1+) | | ----------------------------------------------- | ------------------------------------ | ------------------------------------ | ------------------------------------ | | [modulepreload](#modulepreload) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | [Import Maps](#import-maps) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | diff --git a/src/dynamic-import.js b/src/dynamic-import.js deleted file mode 100644 index 817bae25..00000000 --- a/src/dynamic-import.js +++ /dev/null @@ -1,53 +0,0 @@ -import { createBlob, baseUrl, nonce, hasDocument } from './env.js'; - -export let dynamicImport = !hasDocument && (0, eval)('u=>import(u)'); - -export let supportsDynamicImport; - -export const dynamicImportCheck = - hasDocument && - new Promise(resolve => { - const s = Object.assign(document.createElement('script'), { - src: createBlob('self._d=u=>import(u)'), - ep: true - }); - s.setAttribute('nonce', nonce); - s.addEventListener('load', () => { - if (!(supportsDynamicImport = !!(dynamicImport = self._d))) { - let err; - window.addEventListener('error', _err => (err = _err)); - dynamicImport = (url, opts) => - new Promise((resolve, reject) => { - const s = Object.assign(document.createElement('script'), { - type: 'module', - src: createBlob(`import*as m from'${url}';self._esmsi=m`) - }); - err = undefined; - s.ep = true; - if (nonce) s.setAttribute('nonce', nonce); - // Safari is unique in supporting module script error events - s.addEventListener('error', cb); - s.addEventListener('load', cb); - function cb(_err) { - document.head.removeChild(s); - if (self._esmsi) { - resolve(self._esmsi, baseUrl); - self._esmsi = undefined; - } else { - reject( - (!(_err instanceof Event) && _err) || - (err && err.error) || - new Error(`Error loading ${(opts && opts.errUrl) || url} (${s.src}).`) - ); - err = undefined; - } - } - document.head.appendChild(s); - }); - } - document.head.removeChild(s); - delete self._d; - resolve(); - }); - document.head.appendChild(s); - }); diff --git a/src/env.js b/src/env.js index ea1f9a68..9ed63fbb 100644 --- a/src/env.js +++ b/src/env.js @@ -3,6 +3,8 @@ export const hasDocument = typeof document !== 'undefined'; export const noop = () => {}; +export const dynamicImport = (u, errUrl) => import(u); + const optionsScript = hasDocument ? document.querySelector('script[type=esms-options]') : undefined; export const esmsInitOptions = optionsScript ? JSON.parse(optionsScript.innerHTML) : {}; @@ -50,8 +52,6 @@ export const onpolyfill = console.log(`%c^^ Module error above is polyfilled and can be ignored ^^`, 'font-weight:900;color:#391'); }; -export const edge = !navigator.userAgentData && !!navigator.userAgent.match(/Edge\/\d+\.\d+/); - export const baseUrl = hasDocument ? document.baseURI diff --git a/src/es-module-shims.js b/src/es-module-shims.js index 8a517381..5bb834a8 100755 --- a/src/es-module-shims.js +++ b/src/es-module-shims.js @@ -1,8 +1,8 @@ import { resolveAndComposeImportMap, resolveUrl, resolveImportMap, resolveIfNotPlainOrUrl, asURL } from './resolve.js'; import { baseUrl as pageBaseUrl, + dynamicImport, createBlob, - edge, throwError, shimMode, resolveHook, @@ -22,9 +22,7 @@ import { esmsInitOptions, hasDocument } from './env.js'; -import { dynamicImport, supportsDynamicImport } from './dynamic-import.js'; import { - supportsImportMeta, supportsImportMaps, supportsCssType, supportsJsonType, @@ -35,7 +33,7 @@ import { } from './features.js'; import * as lexer from '../node_modules/es-module-lexer/dist/lexer.asm.js'; -async function _resolve(id, parentUrl) { +function _resolve(id, parentUrl = pageBaseUrl) { const urlResolved = resolveIfNotPlainOrUrl(id, parentUrl) || asURL(id); let composedFallback = false; const firstResolved = firstImportMap && resolveImportMap(firstImportMap, urlResolved || id, parentUrl); @@ -63,44 +61,58 @@ async function _resolve(id, parentUrl) { const resolve = resolveHook ? - (id, parentUrl) => { + (id, parentUrl = pageBaseUrl) => { const result = resolveHook(id, parentUrl, defaultResolve); return result ? { r: result, n: true, N: true } : _resolve(id, parentUrl); } : _resolve; -// supports: -// import('mod'); -// import('mod', { opts }); -// import('mod', { opts }, parentUrl); -// import('mod', parentUrl); -async function importHandler(id, ...args) { - // parentUrl if present will be the last argument - let parentUrl = args[args.length - 1]; - if (typeof parentUrl !== 'string') parentUrl = pageBaseUrl; - // needed for shim check - await initPromise; - if (importHook) await importHook(id, typeof args[1] !== 'string' ? args[1] : {}, parentUrl); +async function importHandler(id, opts, parentUrl = pageBaseUrl, sourcePhase) { + await initPromise; // needed for shim check + if (self.ESMS_DEBUG) + console.info( + `es-module-shims: importShim${sourcePhase ? '.source' : ''}("${id}"${opts ? ', ' + JSON.stringify(opts) : ''})` + ); + if (importHook) await importHook(id, opts, parentUrl); if (shimMode || !baselinePassthrough) { if (hasDocument) processScriptsAndPreloads(); legacyAcceptingImportMaps = false; } await importMapPromise; - return (await resolve(id, parentUrl)).r; + return resolve(id, parentUrl).r; } -// import() -async function importShim(...args) { - if (self.ESMS_DEBUG) console.info(`es-module-shims: importShim("${args[0]}")`); - return topLevelLoad(await importHandler(...args), { credentials: 'same-origin' }); +// supports: +// import('mod'); +// import('mod', { opts }); +// import('mod', { opts }, parentUrl); +// import('mod', parentUrl); +async function importShim(specifier, opts, parentUrl) { + if (typeof opts === 'string') { + parentUrl = opts; + opts = undefined; + } + const url = await importHandler(specifier, opts, parentUrl); + // if we dynamically import CSS or JSON, and these are supported, use a wrapper module to + // support getting those imports from the native loader, because `import(specifier, options)` + // is not supported in older browsers to use syntactically. + const type = typeof opts === 'object' && typeof opts.with === 'object' && opts.with.type; + if ((supportsCssType && type === 'css') || (supportsJsonType && type === 'json')) { + return dynamicImport(createBlob(`export{default}from"${url}"with{type:"${type}"`), url); + } + return topLevelLoad(url, { credentials: 'same-origin' }); } // import.source() +// (opts not currently supported as no use cases yet) if (sourcePhaseEnabled) - importShim.source = async function importShimSource(...args) { - const url = await importHandler(...args); + importShim.source = async function importShimSource(specifier, opts, parentUrl) { + if (typeof opts === 'string') { + parentUrl = opts; + opts = undefined; + } + const url = await importHandler(specifier, opts, parentUrl, true); const load = getOrCreateLoad(url, { credentials: 'same-origin' }, null, null); - lastLoad = undefined; if (firstPolyfillLoad && !shimMode && load.n && nativelyLoaded) { onpolyfill(); firstPolyfillLoad = false; @@ -122,17 +134,11 @@ function throwUnresolved(id, parentUrl) { throw Error(`Unable to resolve specifier '${id}'${fromParent(parentUrl)}`); } -const resolveSync = (id, parentUrl = pageBaseUrl) => { - parentUrl = `${parentUrl}`; - const result = resolveHook && resolveHook(id, parentUrl, defaultResolve); - return result && !result.then ? result : defaultResolve(id, parentUrl); -}; - function metaResolve(id, parentUrl = this.url) { - return resolveSync(id, parentUrl); + return resolve(id, `${parentUrl}`).r; } -importShim.resolve = resolveSync; +importShim.resolve = (id, parentUrl) => resolve(id, parentUrl).r; importShim.getImportMap = () => JSON.parse(JSON.stringify(composedImportMap)); importShim.addImportMap = importMapIn => { if (!shimMode) throw new Error('Unsupported in polyfill mode.'); @@ -164,8 +170,6 @@ let baselinePassthrough; const initPromise = featureDetectionPromise.then(() => { baselinePassthrough = esmsInitOptions.polyfillEnable !== true && - supportsDynamicImport && - supportsImportMeta && supportsImportMaps && (!jsonModulesEnabled || supportsJsonType) && (!cssModulesEnabled || supportsCssType) && @@ -229,7 +233,7 @@ const initPromise = featureDetectionPromise.then(() => { }); function attachMutationObserver() { - new MutationObserver(mutations => { + const observer = new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type !== 'childList') continue; for (const node of mutation.addedNodes) { @@ -245,7 +249,9 @@ function attachMutationObserver() { } } } - }).observe(document, { childList: true, subtree: true }); + }); + observer.observe(document.head, { childList: true }); + observer.observe(document.body, { childList: true }); processScriptsAndPreloads(); } @@ -260,33 +266,35 @@ async function topLevelLoad(url, fetchOpts, source, nativelyLoaded, lastStaticLo if (importHook) await importHook(url, typeof fetchOpts !== 'string' ? fetchOpts : {}, ''); // early analysis opt-out - no need to even fetch if we have feature support if (!shimMode && baselinePassthrough) { - if (self.ESMS_DEBUG) console.info(`es-module-shims: early load exit as we have baseline modules support ${url}`); + if (self.ESMS_DEBUG) console.info(`es-module-shims: early exit for ${url} due to baseline modules support`); // for polyfill case, only dynamic import needs a return value here, and dynamic import will never pass nativelyLoaded if (nativelyLoaded) return null; await lastStaticLoadPromise; - return dynamicImport(source ? createBlob(source) : url, { - errUrl: url || source - }); + return dynamicImport(source ? createBlob(source) : url, url || source); } const load = getOrCreateLoad(url, fetchOpts, null, source); linkLoad(load, fetchOpts); const seen = {}; await loadAll(load, seen); - lastLoad = undefined; resolveDeps(load, seen); await lastStaticLoadPromise; + if (!shimMode && !load.n && nativelyLoaded) { + if (self.ESMS_DEBUG) + console.info( + `es-module-shims: early exit after graph analysis of ${url} - graph ran natively without needing polyfill` + ); + return; + } if (source && !shimMode && !load.n) { - if (nativelyLoaded) return; - if (revokeBlobURLs) revokeObjectURLs(Object.keys(seen)); - return await dynamicImport(createBlob(source), { errUrl: source }); + return await dynamicImport(createBlob(source), source); } if (firstPolyfillLoad && !shimMode && load.n && nativelyLoaded) { onpolyfill(); firstPolyfillLoad = false; } - const module = await dynamicImport(!shimMode && !load.n && nativelyLoaded ? load.u : load.b, { errUrl: load.u }); + const module = await (!shimMode && !load.n ? import(load.u) : dynamicImport(load.b, load.u)); // if the top-level load is a shell, run its update function - if (load.s) (await dynamicImport(load.s)).u$_(module); + if (load.s) (await dynamicImport(load.s, load.u)).u$_(module); if (revokeBlobURLs) revokeObjectURLs(Object.keys(seen)); // when tla is supported, this should return the tla promise as an actual handle // so readystate can still correspond to the sync subgraph exec completions @@ -303,7 +311,7 @@ function revokeObjectURLs(registryKeys) { if (batchStartIndex > keysLength) return; for (const key of registryKeys.slice(batchStartIndex, batchStartIndex + 100)) { const load = registry[key]; - if (load) URL.revokeObjectURL(load.b); + if (load && load.b) URL.revokeObjectURL(load.b); } batch++; schedule(cleanup); @@ -314,7 +322,6 @@ function urlJsString(url) { return `'${url.replace(/'/g, "\\'")}'`; } -let lastLoad; function resolveDeps(load, seen) { if (load.b || !seen[load.u]) return; seen[load.u] = 0; @@ -329,7 +336,7 @@ function resolveDeps(load, seen) { // use native loader whenever possible (n = needs shim) via executable subgraph passthrough // so long as the module doesn't use dynamic import or unsupported URL mappings (N = should shim) if (!shimMode && !load.n && !load.N) { - load.b = lastLoad = load.u; + load.b = load.u; load.S = undefined; return; } @@ -341,8 +348,7 @@ function resolveDeps(load, seen) { // "execution" 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 = ''; // once all deps have loaded we can inline the dependency resolution blobs // and define this blob @@ -466,7 +472,7 @@ function resolveDeps(load, seen) { if (sourceURLCommentStart === -1) resolvedSource += sourceURLCommentPrefix + load.r; - load.b = lastLoad = createBlob(resolvedSource); + load.b = createBlob(resolvedSource); load.S = undefined; } @@ -521,7 +527,7 @@ async function fetchModule(url, fetchOpts, parent) { ); const r = res.url; const contentType = res.headers.get('content-type'); - if (jsContentType.test(contentType)) return { r, s: await res.text(), sp: null, t: 'js' }; + if (jsContentType.test(contentType)) return { r, s: await res.text(), t: 'js' }; else if (wasmContentType.test(contentType)) { const module = await (sourceCache[r] || (sourceCache[r] = WebAssembly.compileStreaming(res))); sourceCache[r] = module; @@ -539,8 +545,7 @@ async function fetchModule(url, fetchOpts, parent) { s += `export const ${expt.name} = instance.exports['${expt.name}'];\n`; } return { r, s, t: 'wasm' }; - } else if (jsonContentType.test(contentType)) - return { r, s: `export default ${await res.text()}`, sp: null, t: 'json' }; + } else if (jsonContentType.test(contentType)) return { r, s: `export default ${await res.text()}`, t: 'json' }; else if (cssContentType.test(contentType)) { return { r, @@ -550,7 +555,6 @@ async function fetchModule(url, fetchOpts, parent) { (_match, quotes = '', relUrl1, relUrl2) => `url(${quotes}${resolveUrl(relUrl1 || relUrl2, url)}${quotes})` ) )});export default s;`, - ss: null, t: 'css' }; } else @@ -637,36 +641,30 @@ function linkLoad(load, fetchOpts) { if (load.L) return; load.L = load.f.then(async () => { let childFetchOpts = fetchOpts; - load.d = ( - await Promise.all( - load.a[0].map(async ({ n, d, t }) => { - const sourcePhase = t >= 4; - if (sourcePhase && !sourcePhaseEnabled) throw featErr('source-phase'); - if ( - (d >= 0 && !supportsDynamicImport) || - (d === -2 && !supportsImportMeta) || - (sourcePhase && !supportsSourcePhase) - ) - load.n = true; - if (d !== -1 || !n) return; - const resolved = await resolve(n, load.r || load.u); - if (resolved.n) load.n = true; - if (d >= 0 || resolved.N) load.N = true; - if (d !== -1) return; - if (skip && skip(resolved.r) && !sourcePhase) return { l: { b: resolved.r }, s: false }; - if (childFetchOpts.integrity) childFetchOpts = Object.assign({}, childFetchOpts, { integrity: undefined }); - const child = { l: getOrCreateLoad(resolved.r, childFetchOpts, load.r, null), s: sourcePhase }; - if (!child.s) linkLoad(child.l, fetchOpts); - // load, sourcePhase - return child; - }) - ) - ).filter(l => l); + load.d = load.a[0] + .map(({ n, d, t }) => { + const sourcePhase = t >= 4; + if (sourcePhase) { + if (!sourcePhaseEnabled) throw featErr('source-phase'); + if (!supportsSourcePhase) load.n = true; + } + if (d !== -1 || !n) return; + const resolved = resolve(n, load.r || load.u); + if (resolved.n) load.n = true; + if (d >= 0 || resolved.N) load.N = true; + if (d !== -1) return; + if (skip && skip(resolved.r) && !sourcePhase) return { l: { b: resolved.r }, s: false }; + if (childFetchOpts.integrity) childFetchOpts = Object.assign({}, childFetchOpts, { integrity: undefined }); + const child = { l: getOrCreateLoad(resolved.r, childFetchOpts, load.r, null), s: sourcePhase }; + if (!child.s) linkLoad(child.l, fetchOpts); + // load, sourcePhase + return child; + }) + .filter(l => l); }); } function processScriptsAndPreloads() { - if (self.ESMS_DEBUG) console.info(`es-module-shims: processing scripts`); for (const link of document.querySelectorAll(shimMode ? 'link[rel=modulepreload-shim]' : 'link[rel=modulepreload]')) { if (!link.ep) processPreload(link); } @@ -736,6 +734,7 @@ const epCheck = (script, ready) => function processImportMap(script, ready = readyStateCompleteCnt > 0) { if (epCheck(script, ready)) return; + if (self.ESMS_DEBUG) console.info(`es-module-shims: reading import map`); // we dont currently support external import maps in polyfill mode to match native if (script.src) { if (!shimMode) return; @@ -750,7 +749,6 @@ function processImportMap(script, ready = readyStateCompleteCnt > 0) { ); }) .catch(e => { - console.log(e); if (e instanceof SyntaxError) e = new Error(`Unable to parse import map ${e.message} in: ${script.src || script.innerHTML}`); throwError(e); @@ -769,6 +767,7 @@ function processImportMap(script, ready = readyStateCompleteCnt > 0) { function processScript(script, ready = readyStateCompleteCnt > 0) { if (epCheck(script, ready)) return; + if (self.ESMS_DEBUG) console.info(`es-module-shims: checking script ${script.src || ''}`); // does this load block readystate complete const isBlockingReadyScript = script.getAttribute('async') === null && readyStateCompleteCnt > 0; // does this load block DOMContentLoaded @@ -777,7 +776,6 @@ function processScript(script, ready = readyStateCompleteCnt > 0) { if (isLoadScript) loadCnt++; if (isBlockingReadyScript) readyStateCompleteCnt++; if (isDomContentLoadedScript) domContentLoadedCnt++; - if (self.ESMS_DEBUG) console.info(`es-module-shims: loading ${script.src || ''}`); const loadPromise = topLevelLoad( script.src || pageBaseUrl, getFetchOpts(script), diff --git a/src/features.js b/src/features.js index ff2c2f97..dda73afa 100644 --- a/src/features.js +++ b/src/features.js @@ -1,4 +1,3 @@ -import { dynamicImport, supportsDynamicImport, dynamicImportCheck } from './dynamic-import.js'; import { createBlob, noop, @@ -17,38 +16,36 @@ export let supportsCssType = false; const supports = hasDocument && HTMLScriptElement.supports; export let supportsImportMaps = supports && supports.name === 'supports' && supports('importmap'); -export let supportsImportMeta = supportsDynamicImport; export let supportsWasmModules = false; export let supportsSourcePhase = false; export let supportsMultipleImportMaps = false; const wasmBytes = [0, 97, 115, 109, 1, 0, 0, 0]; -export let featureDetectionPromise = Promise.resolve(dynamicImportCheck).then(() => { - if (!supportsDynamicImport) return; +export let featureDetectionPromise = (async function () { if (!hasDocument) return Promise.all([ - supportsImportMaps || dynamicImport(createBlob('import.meta')).then(() => (supportsImportMeta = true), noop), cssModulesEnabled && - dynamicImport(createBlob(`import"${createBlob('', 'text/css')}"with{type:"css"}`)).then( + import(createBlob(`import"${createBlob('', 'text/css')}"with{type:"css"}`)).then( () => (supportsCssType = true), noop ), jsonModulesEnabled && - dynamicImport(createBlob(`import"${createBlob('{}', 'text/json')}"with{type:"json"}`)).then( + import(createBlob(`import"${createBlob('{}', 'text/json')}"with{type:"json"}`)).then( () => (supportsJsonType = true), noop ), wasmModulesEnabled && - dynamicImport(createBlob(`import"${createBlob(new Uint8Array(wasmBytes), 'application/wasm')}"`)).then( + import(createBlob(`import"${createBlob(new Uint8Array(wasmBytes), 'application/wasm')}"`)).then( () => (supportsWasmModules = true), noop ), wasmModulesEnabled && sourcePhaseEnabled && - dynamicImport( - createBlob(`import source x from"${createBlob(new Uint8Array(wasmBytes), 'application/wasm')}"`) - ).then(() => (supportsSourcePhase = true), noop) + import(createBlob(`import source x from"${createBlob(new Uint8Array(wasmBytes), 'application/wasm')}"`)).then( + () => (supportsSourcePhase = true), + noop + ) ]); return new Promise(resolve => { @@ -61,7 +58,6 @@ export let featureDetectionPromise = Promise.resolve(dynamicImportCheck).then(() [ , supportsImportMaps, - supportsImportMeta, supportsMultipleImportMaps, supportsCssType, supportsJsonType, @@ -75,15 +71,15 @@ export let featureDetectionPromise = Promise.resolve(dynamicImportCheck).then(() window.addEventListener('message', cb, false); const importMapTest = `