diff --git a/index.d.ts b/index.d.ts index 4b0e713..d740ae7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -8,7 +8,7 @@ interface IImportResult { assetPublicPath: string; - execScripts(sandbox?: object): Promise; + execScripts(sandbox?: object, strictGlobal?: boolean): Promise; getExternalScripts(): Promise; @@ -20,9 +20,11 @@ export type ImportEntryOpts = { getPublicPath?: (rawPublicPath: string) => string; getTemplate?: (tpl: string) => string; } -export type ExecScriptsOpts = { - fetch?: Function; + +type ExecScriptsOpts = Pick & { + strictGlobal?: boolean; } + export type Entry = string | { styles?: string[], scripts?: string[], html?: string }; export function execScripts(entry: string | null, scripts: string[], proxy: Window, opts?: ExecScriptsOpts): Promise; diff --git a/src/index.js b/src/index.js index 539dc05..db320fe 100644 --- a/src/index.js +++ b/src/index.js @@ -40,15 +40,20 @@ function getEmbedHTML(template, styles, opts = {}) { }); } +function getExecutableScript(scriptText, proxy) { + window.proxy = proxy; + return `;(function(window, self){;${scriptText}\n}).bind(window.proxy)(window.proxy, window.proxy);`; +} + // for prefetch export function getExternalStyleSheets(styles, fetch = defaultFetch) { return Promise.all(styles.map(styleLink => { - if (styleLink.startsWith('<')) { - // if it is inline style - return getInlineCode(styleLink); - } else { - // external styles - return styleCache[styleLink] || + if (styleLink.startsWith('<')) { + // if it is inline style + return getInlineCode(styleLink); + } else { + // external styles + return styleCache[styleLink] || (styleCache[styleLink] = fetch(styleLink).then(response => response.text())); } @@ -96,12 +101,11 @@ const supportsUserTiming = typeof performance.clearMeasures === 'function'; export function execScripts(entry, scripts, proxy = window, opts = {}) { - const { fetch = defaultFetch } = opts; + const { fetch = defaultFetch, strictGlobal = false } = opts; return getExternalScripts(scripts, fetch) .then(scriptsText => { - window.proxy = proxy; const geval = eval; function exec(scriptSrc, inlineScript, resolve) { @@ -114,17 +118,17 @@ export function execScripts(entry, scripts, proxy = window, opts = {}) { } if (scriptSrc === entry) { - noteGlobalProps(); + noteGlobalProps(strictGlobal ? proxy : window); try { // bind window.proxy to change `this` reference in script - geval(`;(function(window){;${inlineScript}\n}).bind(window.proxy)(window.proxy);`); + geval(getExecutableScript(inlineScript, proxy)); } catch (e) { console.error(`error occurs while executing the entry ${scriptSrc}`); throw e; } - const exports = proxy[getGlobalProp()] || {}; + const exports = proxy[getGlobalProp(strictGlobal ? proxy : window)] || {}; resolve(exports); } else { @@ -132,14 +136,15 @@ export function execScripts(entry, scripts, proxy = window, opts = {}) { if (typeof inlineScript === 'string') { try { // bind window.proxy to change `this` reference in script - geval(`;(function(window){;${inlineScript}\n}).bind(window.proxy)(window.proxy);`); + geval(getExecutableScript(inlineScript, proxy)); } catch (e) { console.error(`error occurs while executing ${scriptSrc}`); throw e; } } else { + // external script marked with async inlineScript.async && inlineScript?.content - .then(downloadedScriptText => geval(`;(function(window){;${downloadedScriptText}\n}).bind(window.proxy)(window.proxy);`)) + .then(downloadedScriptText => geval(getExecutableScript(downloadedScriptText, proxy))) .catch(e => { console.error(`error occurs while executing async script ${scriptSrc?.src}`); throw e; @@ -200,11 +205,11 @@ export default function importHTML(url, opts = {}) { assetPublicPath, getExternalScripts: () => getExternalScripts(scripts, fetch), getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch), - execScripts: proxy => { + execScripts: (proxy, strictGlobal) => { if (!scripts.length) { return Promise.resolve(); } - return execScripts(entry, scripts, proxy, { fetch }); + return execScripts(entry, scripts, proxy, { fetch, strictGlobal }); }, })); })); @@ -233,11 +238,11 @@ export function importEntry(entry, opts = {}) { assetPublicPath: getPublicPath('/'), getExternalScripts: () => getExternalScripts(scripts, fetch), getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch), - execScripts: proxy => { + execScripts: (proxy, strictGlobal) => { if (!scripts.length) { return Promise.resolve(); } - return execScripts(scripts[scripts.length - 1], scripts, proxy, { fetch }); + return execScripts(scripts[scripts.length - 1], scripts, proxy, { fetch, strictGlobal }); }, })); diff --git a/src/utils.js b/src/utils.js index e6d1b55..f127408 100644 --- a/src/utils.js +++ b/src/utils.js @@ -9,12 +9,16 @@ const isIE = navigator.userAgent.indexOf('Trident') !== -1; // safari unpredictably lists some new globals first or second in object order let firstGlobalProp, secondGlobalProp, lastGlobalProp; -export function getGlobalProp() { + +export function getGlobalProp(global) { let cnt = 0; let lastProp; let hasIframe = false; - for (let p in global) { + // use Object.keys to make it trigger the trap if global is proxy + const props = Object.keys(global); + for (let i = 0; i < props.length; i++) { + const p = props[i]; // do not check frames cause it could be removed during import if ( !global.hasOwnProperty(p) || @@ -37,15 +41,18 @@ export function getGlobalProp() { cnt++; lastProp = p; } + if (lastProp !== lastGlobalProp) return lastProp; } -export function noteGlobalProps() { - // alternatively Object.keys(global).pop() - // but this may be faster (pending benchmarks) +export function noteGlobalProps(global) { firstGlobalProp = secondGlobalProp = undefined; - for (let p in global) { + + // use Object.keys to make it trigger the trap if global is proxy + const props = Object.keys(global); + for (let i = 0; i < props.length; i++) { + const p = props[i]; // do not check frames cause it could be removed during import if ( !global.hasOwnProperty(p) || @@ -59,6 +66,7 @@ export function noteGlobalProps() { secondGlobalProp = p; lastGlobalProp = p; } + return lastGlobalProp; }