diff --git a/CHANGELOG.md b/CHANGELOG.md index f1beeaae..53793bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ See [https://github.com/ice-lab/icestark/releases](https://github.com/ice-lab/icestark/releases) for what has changed in each version of icestark. +## 2.8.0 + +- [fix] When icestark unloads, avoid re-triggering 'unloadMicroApp'. ([#521](https://github.com/ice-lab/icestark/issues/521)) + ## 2.7.5 - [fix] avoid to set undefined url when state change. @@ -93,6 +97,7 @@ See [https://github.com/ice-lab/icestark/releases](https://github.com/ice-lab/ic ## 2.2.1 - [fix] css assets are unable to load when remove `umd` from sub-application. + ## 2.2.0 - [feat] no need to use `umd` anymore. Migrate to `loadScriptMode` or use `setLibraryName` in sub-application. ([#240](https://github.com/ice-lab/icestark/issues/240)) diff --git a/package.json b/package.json index f22cbb24..df583dfc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "icestark-monorepo", - "version": "2.7.3", + "version": "2.8.0", "private": true, "description": "Icestark is a JavaScript library for multiple projects, Ice workbench solution.", "scripts": { diff --git a/packages/icestark/__tests__/AppRouter.spec.tsx b/packages/icestark/__tests__/AppRouter.spec.tsx index 43b731ce..954c8757 100644 --- a/packages/icestark/__tests__/AppRouter.spec.tsx +++ b/packages/icestark/__tests__/AppRouter.spec.tsx @@ -6,6 +6,7 @@ import { getCache, setCache } from '../src/util/cache'; import * as React from 'react'; import { render } from '@testing-library/react'; import { AppRouter, AppRoute } from '../src/index'; +import { getMicroApps } from '../src/apps'; const delay = (milliscond: number) => new Promise(resolve => setTimeout(resolve, milliscond)); @@ -145,4 +146,60 @@ describe('AppRouter', () => { unmount(); }) -}) \ No newline at end of file + + test('app-cached', async () => { + (fetch as FetchMock).mockResponseOnce(umdSourceWithSetLibrary.toString()); + const { container, unmount } = render( + + + + ); + window.history.pushState({}, 'test', '/seller'); + + await delay(1000); + expect(container.innerHTML).toContain('商家平台') + + window.history.pushState({}, 'test', '/waiter'); + await delay(1000); + + const app = getMicroApps().find(app => app.name === 'seller'); + expect(app.status).toEqual('UNMOUNTED'); + expect(!!app.mount).toBeTruthy(); + expect(!!app.unmount).toBeTruthy(); + + unmount(); + }) + + test('app-className-custom', async () => { + (fetch as FetchMock).mockResponseOnce(umdSourceWithSetLibrary.toString()); + const { container, unmount } = render( + + + + ); + + window.history.pushState({}, 'test', '/seller'); + await delay(1000); + + expect(container.querySelector('.ice-app-router-custom')).toBeTruthy(); + + unmount(); + }) +}) diff --git a/packages/icestark/__tests__/app.spec.tsx b/packages/icestark/__tests__/app.spec.tsx index 6a073436..089cdbfc 100644 --- a/packages/icestark/__tests__/app.spec.tsx +++ b/packages/icestark/__tests__/app.spec.tsx @@ -117,5 +117,38 @@ describe('app start', () => { const errorApp = await createMicroApp('app-error'); expect(errorApp).toBe(null); - }) -}); \ No newline at end of file + }); + + test('window.microApps should be equal to insider microApps', async () => { + let status = ''; + registerMicroApps([ + { + name: 'app7', + activePath: '/testapp', + url: ['//icestark.com/index.js'], + mount: () => { + status = MOUNTED; + }, + unmount: () => { + status = UNMOUNTED; + }, + } as AppConfig, + ]); + window.history.pushState({}, 'test', '/testapp'); + const findMicroAppByName = (microApps, name) => microApps.filter(d => d.name === name)[0]; + await mountMicroApp('app7'); + expect(status).toBe(MOUNTED); + expect(findMicroAppByName(getMicroApps(), 'app7').status).toEqual(findMicroAppByName((window as any).microApps, 'app7').status); + expect(findMicroAppByName(getMicroApps(), 'app7').status).toEqual(status); + + await unmountMicroApp('app7'); + expect(status).toBe(UNMOUNTED); + expect(findMicroAppByName(getMicroApps(), 'app7').status).toEqual(findMicroAppByName((window as any).microApps, 'app7').status); + expect(findMicroAppByName(getMicroApps(), 'app7').status).toEqual(status); + + await createMicroApp('app7'); + expect(status).toBe(MOUNTED); + expect(findMicroAppByName(getMicroApps(), 'app7').status).toEqual(findMicroAppByName((window as any).microApps, 'app7').status); + expect(findMicroAppByName(getMicroApps(), 'app7').status).toEqual(status); + }); +}); diff --git a/packages/icestark/__tests__/appLifeCycle.spec.tsx b/packages/icestark/__tests__/appLifeCycle.spec.tsx deleted file mode 100644 index c8c5233d..00000000 --- a/packages/icestark/__tests__/appLifeCycle.spec.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import '@testing-library/jest-dom/extend-expect'; - -import { AppLifeCycleEnum, cacheApp, callAppEnter, callAppLeave, isCached, deleteCache } from '../src/util/appLifeCycle'; -import { setCache } from '../src/util/cache'; - -describe('appLifeCycle', () => { - test('callAppEnter', () => { - const appEnterMockFn = jest.fn(); - - setCache(AppLifeCycleEnum.AppEnter, appEnterMockFn); - - callAppEnter(); - expect(appEnterMockFn).toBeCalledTimes(1); - - setCache(AppLifeCycleEnum.AppEnter, null); - callAppEnter(); - expect(appEnterMockFn).toBeCalledTimes(1); - }); - - test('callAppLeave', () => { - const appLeaveMockFn = jest.fn(); - - setCache(AppLifeCycleEnum.AppLeave, appLeaveMockFn); - - callAppLeave(); - expect(appLeaveMockFn).toBeCalledTimes(1); - - setCache(AppLifeCycleEnum.AppLeave, null); - callAppLeave(); - expect(appLeaveMockFn).toBeCalledTimes(1); - }); - - test('cache app', () => { - const appEnterMockFn = jest.fn(); - const appLeaveMockFn = jest.fn(); - const appKey = 'appKey'; - setCache(AppLifeCycleEnum.AppEnter, appEnterMockFn); - setCache(AppLifeCycleEnum.AppLeave, appLeaveMockFn); - cacheApp(appKey); - expect(isCached(appKey)).toBe(true); - deleteCache(appKey); - expect(isCached(appKey)).toBe(false); - }) -}); diff --git a/packages/icestark/__tests__/handleAssets.spec.tsx b/packages/icestark/__tests__/handleAssets.spec.tsx index def6962f..2d46669b 100644 --- a/packages/icestark/__tests__/handleAssets.spec.tsx +++ b/packages/icestark/__tests__/handleAssets.spec.tsx @@ -649,12 +649,13 @@ describe('appendCSS', () => { const div = document.createElement('div'); appendCSS( - div, { type: AssetTypeEnum.EXTERNAL, content: '/test.css' - }, - 'icestark-css-0' + },{ + root: div, + id: 'icestark-css-0' + } ) .then(() => { expect(div.innerHTML).toContain('id="icestark-css-0"'); @@ -671,10 +672,13 @@ describe('appendCSS', () => { test('appendCSS -> style', () => { const div = document.createElement('div'); const style = '.test { color: #fff;}'; - appendCSS(div, { + appendCSS({ type: AssetTypeEnum.INLINE, content: style, - }, 'icestark-css-0') + }, { + root: div, + id: 'icestark-css-0' + }) .then(() => { expect(div.innerHTML).toContain('id="icestark-css-0"'); expect(div.innerHTML).toContain(style); @@ -694,12 +698,14 @@ describe('appendCSS', () => { const div = document.createElement('div'); appendCSS( - div, { type: AssetTypeEnum.EXTERNAL, content: '/test.css' }, - 'icestark-css-0' + { + root: div, + id: 'icestark-css-0' + } ) .then(() => { expect(div.innerHTML).toContain('id="icestark-css-0"'); diff --git a/packages/icestark/package.json b/packages/icestark/package.json index e87d4e28..f358f1af 100644 --- a/packages/icestark/package.json +++ b/packages/icestark/package.json @@ -1,6 +1,6 @@ { "name": "@ice/stark", - "version": "2.7.5", + "version": "2.8.0", "description": "Icestark is a JavaScript library for multiple projects, Ice workbench solution.", "scripts": { "build": "rm -rf lib && tsc", diff --git a/packages/icestark/src/AppRoute.tsx b/packages/icestark/src/AppRoute.tsx index 84558289..db50d6d8 100644 --- a/packages/icestark/src/AppRoute.tsx +++ b/packages/icestark/src/AppRoute.tsx @@ -1,9 +1,13 @@ import * as React from 'react'; import renderComponent from './util/renderComponent'; import { AppHistory } from './appHistory'; -import { unloadMicroApp, BaseConfig, createMicroApp } from './apps'; +import { unloadMicroApp, unmountMicroApp, BaseConfig, createMicroApp } from './apps'; import { converArray2String } from './util/helpers'; -import { callCapturedEventListeners, resetCapturedEventListeners } from './util/capturedListeners'; +import { started } from './start'; +import { + callCapturedEventListeners, + resetCapturedEventListeners, +} from './util/capturedListeners'; import isEqual from 'lodash.isequal'; import type { PathData } from './util/checkActive'; @@ -136,6 +140,7 @@ export default class AppRoute extends React.Component { resetCapturedEventListeners(); + const { onAppEnter } = this.props; // Trigger app enter @@ -151,15 +156,15 @@ export default class AppRoute extends React.Component { - const { name, onAppLeave } = this.props; + const { name, onAppLeave, cached } = this.props; // Trigger app leave if (typeof onAppLeave === 'function') { onAppLeave(genCompatibleAppConfig(this.props)); } - if (!this.validateRender()) { - unloadMicroApp(name); + if (!this.validateRender() && started) { + cached ? unmountMicroApp(name) : unloadMicroApp(name); } }; diff --git a/packages/icestark/src/AppRouter.tsx b/packages/icestark/src/AppRouter.tsx index 04c3d497..2c674131 100644 --- a/packages/icestark/src/AppRouter.tsx +++ b/packages/icestark/src/AppRouter.tsx @@ -35,6 +35,7 @@ export interface AppRouterProps { basename?: string; fetch?: Fetch; prefetch?: Prefetch; + className?: string; } interface AppRouterState { @@ -195,6 +196,7 @@ export default class AppRouter extends React.Component +
{appLoading === this.appKey ? renderComponent(LoadingComponent, {}) : null} {React.cloneElement(element, { key: this.appKey, diff --git a/packages/icestark/src/apps.ts b/packages/icestark/src/apps.ts index d39f153c..b61ab435 100644 --- a/packages/icestark/src/apps.ts +++ b/packages/icestark/src/apps.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import Sandbox, { SandboxConstructor, SandboxProps } from '@ice/sandbox'; import isEmpty from 'lodash.isempty'; import { NOT_LOADED, NOT_MOUNTED, LOADING_ASSETS, UNMOUNTED, LOAD_ERROR, MOUNTED } from './util/constant'; @@ -167,6 +168,8 @@ export function updateAppConfig(appName: string, config) { } return microApp; }); + // Update global variable of microApps. + (window as any).microApps = microApps; } /** @@ -179,7 +182,8 @@ export async function loadAppModule(appConfig: AppConfig) { let lifecycle: ModuleLifeCycle = {}; onLoadingApp(appConfig); - const { url, container, entry, entryContent, name, scriptAttributes = [], loadScriptMode, appSandbox } = appConfig; + + const { url, container, entry, entryContent, name, scriptAttributes = [], loadScriptMode, appSandbox, cached } = appConfig; const appAssets = url ? getUrlAssets(url) : await getEntryAssets({ root: container, entry, @@ -189,6 +193,8 @@ export async function loadAppModule(appConfig: AppConfig) { fetch, }); + const cacheId = cached ? name : undefined; + updateAppConfig(appConfig.name, { appAssets }); const cacheCss = shouldCacheCss(loadScriptMode); @@ -201,6 +207,7 @@ export async function loadAppModule(appConfig: AppConfig) { ], { cacheCss, fetch, + cacheId, }); lifecycle = await loadScriptByImport(appAssets.jsList); // Not to handle script element temporarily. @@ -209,6 +216,7 @@ export async function loadAppModule(appConfig: AppConfig) { await loadAndAppendCssAssets(appAssets.cssList, { cacheCss, fetch, + cacheId, }); lifecycle = await loadScriptByFetch(appAssets.jsList, appSandbox, fetch); break; @@ -217,13 +225,14 @@ export async function loadAppModule(appConfig: AppConfig) { loadAndAppendCssAssets(appAssets.cssList, { cacheCss, fetch, + cacheId, }), - loadAndAppendJsAssets(appAssets, { scriptAttributes }), + loadAndAppendJsAssets(appAssets, { scriptAttributes, cacheId }), ]); lifecycle = - getLifecyleByLibrary() || - getLifecyleByRegister() || - {}; + getLifecyleByLibrary() || + getLifecyleByRegister() || + {}; } if (isEmpty(lifecycle)) { @@ -446,14 +455,29 @@ export async function unmountMicroApp(appName: string) { } } -// unload micro app, load app bundles when create micro app +/** + * uninstall micro app thoroughly + * @param appName + */ export async function unloadMicroApp(appName: string) { const appConfig = getAppConfig(appName); if (appConfig) { + if (appConfig.cached) { + log.warn( + formatErrMessage( + ErrorCode.CACHED_APP_USE_UNLOAD, + isDev && 'Use unmountMicroApp instead of unloadMicroApp to uninstall app {0}. This will not break the whole app but invalidate caching', + appName, + ), + ); + } + unmountMicroApp(appName); + delete appConfig.mount; delete appConfig.unmount; delete appConfig.appAssets; + updateAppConfig(appName, { status: NOT_LOADED }); } else { log.error( diff --git a/packages/icestark/src/start.ts b/packages/icestark/src/start.ts index b45abbc0..e621d981 100644 --- a/packages/icestark/src/start.ts +++ b/packages/icestark/src/start.ts @@ -197,5 +197,5 @@ function unload() { clearMicroApps(); } -export { unload, globalConfiguration }; +export { unload, globalConfiguration, started }; export default start; diff --git a/packages/icestark/src/util/appLifeCycle.ts b/packages/icestark/src/util/appLifeCycle.ts deleted file mode 100644 index f651efb1..00000000 --- a/packages/icestark/src/util/appLifeCycle.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { setCache, getCache } from './cache'; -import { resetCapturedEventListeners } from './capturedListeners'; - -export enum AppLifeCycleEnum { - AppEnter = 'appEnter', - AppLeave = 'appLeave', -} - -export function cacheApp(cacheKey: string) { - [AppLifeCycleEnum.AppEnter, AppLifeCycleEnum.AppLeave].forEach((lifeCycle) => { - const lifeCycleCacheKey = `cache_${cacheKey}_${lifeCycle}`; - if (getCache(lifeCycle)) { - setCache(lifeCycleCacheKey, getCache(lifeCycle)); - } else if (getCache(lifeCycleCacheKey)) { - // set cache to current lifeCycle - setCache(lifeCycle, getCache(lifeCycleCacheKey)); - } - }); -} - -export function deleteCache(cacheKey: string) { - [AppLifeCycleEnum.AppEnter, AppLifeCycleEnum.AppLeave].forEach((lifeCycle) => { - setCache(`cache_${cacheKey}_${lifeCycle}`, null); - }); -} - -export function isCached(cacheKey: string) { - return !!getCache(`cache_${cacheKey}_${AppLifeCycleEnum.AppEnter}`); -} - -export function callAppEnter() { - const appEnterKey = AppLifeCycleEnum.AppEnter; - const registerAppEnterCallback = getCache(appEnterKey); - - if (registerAppEnterCallback) { - registerAppEnterCallback(); - setCache(appEnterKey, null); - } -} - -export function callAppLeave() { - // resetCapturedEventListeners when app change, remove react-router/vue-router listeners - resetCapturedEventListeners(); - - const appLeaveKey = AppLifeCycleEnum.AppLeave; - const registerAppLeaveCallback = getCache(appLeaveKey); - - if (registerAppLeaveCallback) { - registerAppLeaveCallback(); - setCache(appLeaveKey, null); - } -} diff --git a/packages/icestark/src/util/capturedListeners.ts b/packages/icestark/src/util/capturedListeners.ts index 47aab6e6..c212b0ce 100644 --- a/packages/icestark/src/util/capturedListeners.ts +++ b/packages/icestark/src/util/capturedListeners.ts @@ -84,3 +84,4 @@ export function resetCapturedEventListeners() { capturedEventListeners[CapturedEventNameEnum.POPSTATE] = []; capturedEventListeners[CapturedEventNameEnum.HASHCHANGE] = []; } + diff --git a/packages/icestark/src/util/constant.ts b/packages/icestark/src/util/constant.ts index 27613d0e..13ee6e0f 100644 --- a/packages/icestark/src/util/constant.ts +++ b/packages/icestark/src/util/constant.ts @@ -23,3 +23,8 @@ export const NOT_MOUNTED = 'NOT_MOUNTED'; export const MOUNTED = 'MOUNTED'; export const UNMOUNTED = 'UNMOUNTED'; + +export enum AppLifeCycleEnum { + AppEnter = 'appEnter', + AppLeave = 'appLeave', +} diff --git a/packages/icestark/src/util/error.ts b/packages/icestark/src/util/error.ts index c8b19c9a..6526d8d9 100644 --- a/packages/icestark/src/util/error.ts +++ b/packages/icestark/src/util/error.ts @@ -6,6 +6,7 @@ export enum ErrorCode { 'JS_LOAD_ERROR' = 5, 'CSS_LOAD_ERROR' = 6, 'ACTIVE_PATH_ITEM_CAN_NOT_BE_EMPTY' = 7, + 'CACHED_APP_USE_UNLOAD' = 8, } export function normalizeMsg(msg: string, args: string[]) { diff --git a/packages/icestark/src/util/getLifecycle.ts b/packages/icestark/src/util/getLifecycle.ts index fe578212..bc2b9850 100644 --- a/packages/icestark/src/util/getLifecycle.ts +++ b/packages/icestark/src/util/getLifecycle.ts @@ -1,5 +1,5 @@ import { getCache, setCache } from './cache'; -import { AppLifeCycleEnum } from './appLifeCycle'; +import { AppLifeCycleEnum } from './constant'; import type { ModuleLifeCycle } from '../apps'; export function getLifecyleByLibrary() { diff --git a/packages/icestark/src/util/handleAssets.ts b/packages/icestark/src/util/handleAssets.ts index 41c9ec59..cc31e92c 100644 --- a/packages/icestark/src/util/handleAssets.ts +++ b/packages/icestark/src/util/handleAssets.ts @@ -75,11 +75,16 @@ function isAssetExist(element: HTMLScriptElement | HTMLLinkElement, type: 'scrip /** * Create link/style element and append to root */ -export function appendCSS( - root: HTMLElement | ShadowRoot, - asset: Asset | HTMLElement, - id: string, -): Promise { +export function appendCSS(asset: Asset | HTMLElement, + { + root, + id, + cacheId, + }: { + root?: HTMLElement | ShadowRoot; + id?: string; + cacheId?: string; + }): Promise { return new Promise(async (resolve, reject) => { if (!root) reject(new Error('no root element for css asset')); @@ -95,6 +100,7 @@ export function appendCSS( const styleElement: HTMLStyleElement = document.createElement('style'); styleElement.id = id; styleElement.setAttribute(PREFIX, DYNAMIC); + cacheId && styleElement.setAttribute('data-cache', cacheId); styleElement.innerHTML = content; root.appendChild(styleElement); resolve(); @@ -112,6 +118,7 @@ export function appendCSS( styleElement.innerHTML = await cachedStyleContent[content]; styleElement.id = id; styleElement.setAttribute(PREFIX, DYNAMIC); + cacheId && styleElement.setAttribute('data-cache', cacheId); root.appendChild(styleElement); useExternalLink = false; resolve(); @@ -126,6 +133,7 @@ export function appendCSS( element.id = id; element.rel = 'stylesheet'; element.href = content; + cacheId && element.setAttribute('data-cache', cacheId); element.addEventListener( 'error', @@ -156,19 +164,21 @@ function setAttributeForScriptNode(element: HTMLScriptElement, { id, src, scriptAttributes, + cacheId, }: { module: boolean; id: string; src: string; scriptAttributes: ScriptAttributes; + cacheId?: string; }) { /* * stamped by icestark for recycle when needed. */ element.setAttribute(PREFIX, DYNAMIC); + cacheId && element.setAttribute('data-cache', cacheId); element.id = id; - element.type = module ? 'module' : 'text/javascript'; element.src = src; @@ -225,10 +235,12 @@ export function appendExternalScript(asset: string | Asset, id, root = document.getElementsByTagName('head')[0], scriptAttributes = [], + cacheId, }: { id: string; root?: HTMLElement | ShadowRoot; scriptAttributes?: ScriptAttributes; + cacheId?: string; }): Promise { return new Promise((resolve, reject) => { const { type, content, module } = (asset as Asset); @@ -239,6 +251,7 @@ export function appendExternalScript(asset: string | Asset, element.innerHTML = content; element.id = id; element.setAttribute(PREFIX, DYNAMIC); + cacheId && element.setAttribute('data-cache', cacheId); module && (element.type = 'module'); root.appendChild(element); @@ -254,6 +267,7 @@ export function appendExternalScript(asset: string | Asset, id, src: content || (asset as string), scriptAttributes, + cacheId, }); if (isAssetExist(element, 'script')) { @@ -653,21 +667,8 @@ export function emptyAssets( export function checkCacheKey(node: HTMLElement | HTMLLinkElement | HTMLStyleElement | HTMLScriptElement, cacheKey: string|boolean) { return (typeof cacheKey === 'boolean' && cacheKey) - || !node.getAttribute('cache') - || node.getAttribute('cache') === cacheKey; -} - -/** - * cache all assets loaded by current sub-application - */ -export function cacheAssets(cacheKey: string): void { - const assetsList = getAssetsNode(); - assetsList.forEach((assetsNode) => { - // set cache key if asset attributes without prefix=static and cache - if (assetsNode.getAttribute(PREFIX) !== STATIC && !assetsNode.getAttribute('cache')) { - assetsNode.setAttribute('cache', cacheKey); - } - }); + || !node.getAttribute('data-cache') + || node.getAttribute('data-cache') === cacheKey; } /** @@ -679,9 +680,11 @@ export function cacheAssets(cacheKey: string): void { export async function loadAndAppendCssAssets(cssList: Array, { cacheCss = false, fetch = defaultFetch, + cacheId, }: { cacheCss?: boolean; fetch?: Fetch; + cacheId?: string; }) { const cssRoot: HTMLElement = document.getElementsByTagName('head')[0]; @@ -704,18 +707,28 @@ export async function loadAndAppendCssAssets(cssList: Array // And supposed to be remove from 3.x if (!useLinks) { return await Promise.all([ - ...cssContents.map((content, index) => appendCSS( - cssRoot, - { content, type: AssetTypeEnum.INLINE }, `${PREFIX}-css-${index}`, - )), - ...cssList.filter((css) => isElement(css)).map((asset, index) => appendCSS(cssRoot, asset, `${PREFIX}-css-${index}`)), + ...cssContents.map((content, index) => appendCSS({ content, type: AssetTypeEnum.INLINE }, + { + root: cssRoot, + id: `${PREFIX}-css-${index}`, + cacheId, + })), + ...cssList.filter((css) => isElement(css)).map((asset, index) => appendCSS(asset, { + root: cssRoot, + id: `${PREFIX}-css-${index}`, + cacheId, + })), ]); } } // load css content - return await Promise.all( - cssList.map((asset, index) => appendCSS(cssRoot, asset, `${PREFIX}-css-${index}`)), + await Promise.all( + cssList.map((asset, index) => appendCSS(asset, { + root: cssRoot, + id: `${PREFIX}-css-${index}`, + cacheId, + })), ); } @@ -731,8 +744,10 @@ export function loadAndAppendJsAssets( assets: Assets, { scriptAttributes = [], + cacheId, }: { scriptAttributes?: ScriptAttributes; + cacheId?: string; }, ) { const jsRoot: HTMLElement = document.getElementsByTagName('head')[0]; @@ -748,6 +763,7 @@ export function loadAndAppendJsAssets( root: jsRoot, scriptAttributes, id: `${PREFIX}-js-${index}`, + cacheId, })); }, Promise.resolve()); } @@ -757,6 +773,7 @@ export function loadAndAppendJsAssets( root: jsRoot, scriptAttributes, id: `${PREFIX}-js-${index}`, + cacheId, })), ); } diff --git a/packages/sandbox/CHANGELOG.md b/packages/sandbox/CHANGELOG.md index 0526bbe6..6fc17359 100644 --- a/packages/sandbox/CHANGELOG.md +++ b/packages/sandbox/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.1.5 + +- [fix] exposes `injection` through constructor. + ## 1.1.4 - [fix] simply copy callable funtions's extra properties. diff --git a/packages/sandbox/__tests__/index.spec.ts b/packages/sandbox/__tests__/index.spec.ts index fea1ee99..06d6c6a0 100644 --- a/packages/sandbox/__tests__/index.spec.ts +++ b/packages/sandbox/__tests__/index.spec.ts @@ -151,3 +151,31 @@ describe('callable functions in sandbox', () => { expect(error).toBe(null); }); }); + +describe('inject value to sandbox', () => { + const testObject = { a: 1 }; + const testProxy = new Proxy(testObject, { + get(target, p, receiver) { + if(p === 'a') { + return 2; + } + return Reflect.get(target, p); + } + }) + // in order to pass value outof sandbox to validate, set multiMode false + const sandbox = new Sandbox({multiMode: false, injection: {testObject: testProxy}}); + test('inject proxy object in sandbox', () => { + // @ts-ignore + window.testObject = testObject; + // @ts-ignore + window.result = undefined; + sandbox.execScriptInSandbox( + ` + window.result = window.testObject.a; + `, + ); + + // @ts-ignore + expect(window.result).toBe(2); + }) +}) diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json index 9433e009..2fcf8084 100644 --- a/packages/sandbox/package.json +++ b/packages/sandbox/package.json @@ -1,6 +1,6 @@ { "name": "@ice/sandbox", - "version": "1.1.4", + "version": "1.1.5", "description": "sandbox for execute scripts", "main": "lib/index.js", "scripts": { diff --git a/packages/sandbox/src/index.ts b/packages/sandbox/src/index.ts index b91cf98e..34a5fe88 100644 --- a/packages/sandbox/src/index.ts +++ b/packages/sandbox/src/index.ts @@ -1,5 +1,6 @@ export interface SandboxProps { multiMode?: boolean; + injection?: Record; } export interface SandboxConstructor { @@ -33,6 +34,8 @@ export default class Sandbox { private multiMode = false; + private injection = {}; + private eventListeners = {}; private timeoutIds: number[] = []; @@ -46,7 +49,7 @@ export default class Sandbox { public sandboxDisabled: boolean; constructor(props: SandboxProps = {}) { - const { multiMode } = props; + const { multiMode, injection } = props; if (!window.Proxy) { console.warn('proxy sandbox is not support by current browser'); this.sandboxDisabled = true; @@ -54,8 +57,13 @@ export default class Sandbox { // enable multiMode in case of create mulit sandbox in same time this.multiMode = multiMode; this.sandbox = null; + this.injection = injection; } + /** + * create proxy sandbox + * @param injection @deprecated will be deprecated in the future + */ createProxySandbox(injection?: object) { const { propertyAdded, originalValues, multiMode } = this; const proxyWindow = Object.create(null) as Window; @@ -65,6 +73,9 @@ export default class Sandbox { const originalSetInterval = window.setInterval; const originalSetTimeout = window.setTimeout; + // `this` in Proxy traps will retarget to the trap. + const _self = this; + // hijack addEventListener proxyWindow.addEventListener = (eventName, fn, ...rest) => { this.eventListeners[eventName] = (this.eventListeners[eventName] || []); @@ -135,7 +146,8 @@ export default class Sandbox { } // search from injection - const injectionValue = injection && injection[p]; + const injectionValue = _self.injection?.[p] ?? injection?.[p]; + if (injectionValue) { return injectionValue; } @@ -159,6 +171,7 @@ export default class Sandbox { // Axios, Moment, and other callable functions may have additional properties. // Simply copy them into boundValue. + // eslint-disable-next-line guard-for-in for (const key in value) { boundValue[key] = value[key]; }