diff --git a/ilc/client/BundleLoader.js b/ilc/client/BundleLoader.js index 1d5450fb..32497b6f 100644 --- a/ilc/client/BundleLoader.js +++ b/ilc/client/BundleLoader.js @@ -107,7 +107,7 @@ export class BundleLoader { return app; }; - #getAppSpaCallbacks = (appBundle, props = {}, { sdkFactory }) => { + #getAppSpaCallbacks = (appBundle, props = {}, { sdkFactory, cacheEnabled }) => { // We do this to make sure that mainSpa function will be called only once if (this.#cache.has(appBundle)) { return this.#cache.get(appBundle); @@ -127,4 +127,15 @@ export class BundleLoader { return appBundle; } }; + + /** + * + * @param {String} appName application name + */ + unloadApp(appName) { + const moduleId = this.#moduleLoader.resolve(appName); + const appBundle = this.#moduleLoader.get(moduleId); + this.#cache.delete(appBundle); + this.#moduleLoader.delete(moduleId); + } } diff --git a/ilc/client/BundleLoader.spec.js b/ilc/client/BundleLoader.spec.js index 8220813f..27266a54 100644 --- a/ilc/client/BundleLoader.spec.js +++ b/ilc/client/BundleLoader.spec.js @@ -17,6 +17,9 @@ const fnCallbacks = { describe('BundleLoader', () => { const SystemJs = { import: sinon.stub(), + resolve: sinon.stub(), + get: sinon.stub(), + delete: sinon.stub(), }; let registry; const configRoot = getIlcConfigRoot(); @@ -118,6 +121,33 @@ describe('BundleLoader', () => { sinon.assert.calledOnce(mainSpa); sinon.assert.calledWith(mainSpa, registry.apps[appName].props); }); + it('loads app and returns callbacks from mainSpa and calls without cache', async () => { + const loader = new BundleLoader(configRoot, SystemJs, sdkFactoryBuilder); + const appName = '@portal/primary'; + + const mainSpa = sinon.stub().returns(fnCallbacks); + + const appBundle = { mainSpa }; + + SystemJs.import.resolves(appBundle); + SystemJs.resolve.returns('bundle.js'); + SystemJs.get.withArgs('bundle.js').returns(appBundle); + SystemJs.delete.withArgs('bundle.js').returns({}); + + const callbacks = await loader.loadApp(appName); + expect(callbacks).to.equal(fnCallbacks); + + loader.unloadApp(appName); + + const callbacks2 = await loader.loadApp(appName, { cachedEnabled: false }); + expect(callbacks2).to.equal(fnCallbacks); + + sinon.assert.calledWith(SystemJs.import, appName); + sinon.assert.calledTwice(SystemJs.import); + + sinon.assert.calledTwice(mainSpa); + sinon.assert.calledWith(mainSpa, registry.apps[appName].props); + }); it('loads app and returns callbacks from mainSpa exported as default and calls it once', async () => { const loader = new BundleLoader(configRoot, SystemJs, sdkFactoryBuilder); diff --git a/ilc/client/Client.js b/ilc/client/Client.js index 2c680b36..6ec4caea 100644 --- a/ilc/client/Client.js +++ b/ilc/client/Client.js @@ -29,7 +29,7 @@ import I18n from './i18n'; import GuardManager from './GuardManager'; import ParcelApi from './ParcelApi'; import { BundleLoader } from './BundleLoader'; -import registerSpaApps from './registerSpaApps'; +import { registerApplications } from './registerSpaApps'; import { TransitionManager } from './TransitionManager/TransitionManager'; import IlcEvents from './constants/ilcEvents'; import singleSpaEvents from './constants/singleSpaEvents'; @@ -273,7 +273,7 @@ export class Client { // TODO: window.ILC.importLibrary - calls bootstrap function with props (if supported), and returns exposed API // TODO: window.ILC.importParcelFromLibrary - same as importParcelFromApp, but for libs - registerSpaApps( + registerApplications( this.#configRoot, this.#router, this.#errorHandlerFor.bind(this), @@ -361,6 +361,7 @@ export class Client { Object.assign(window.ILC, { loadApp: this.#bundleLoader.loadApp.bind(this.#bundleLoader), + unloadApp: this.#bundleLoader.unloadApp.bind(this.#bundleLoader), navigate: this.#router.navigateToUrl.bind(this.#router), onIntlChange: this.#addIntlChangeHandler.bind(this), onRouteChange: this.#addRouteChangeHandlerWithDispatch.bind(this), @@ -392,4 +393,14 @@ export class Client { start() { singleSpa.start({ urlRerouteOnly: true }); } + + /** + * + * @param {String} appId application id + */ + async unloadApp(appId) { + const { appName } = appIdToNameAndSlot(appId); + this.#bundleLoader.unloadApp(appName); + await singleSpa.unloadApplication(appId); + } } diff --git a/ilc/client/registerSpaApps.js b/ilc/client/registerSpaApps.js index 2a1acbe8..6ef09b0b 100644 --- a/ilc/client/registerSpaApps.js +++ b/ilc/client/registerSpaApps.js @@ -6,7 +6,7 @@ import WrapApp from './WrapApp'; import AsyncBootUp from './AsyncBootUp'; import ilcEvents from './constants/ilcEvents'; -const getCustomProps = (slot, router, appErrorHandlerFactory, sdkFactoryBuilder) => { +function getCustomProps(slot, router, appErrorHandlerFactory, sdkFactoryBuilder) { const appName = slot.getApplicationName(); const appId = slot.getApplicationId(); const slotName = slot.getSlotName(); @@ -26,9 +26,9 @@ const getCustomProps = (slot, router, appErrorHandlerFactory, sdkFactoryBuilder) }; return customProps; -}; +} -export default function ( +export function registerApplications( ilcConfigRoot, router, appErrorHandlerFactory, @@ -92,61 +92,60 @@ export default function ( } }; - singleSpa.registerApplication( - appId, - async () => { - if (!slot.isValid()) { - throw new Error(`Can not find application - ${appName}`); - } + const loadingFn = async () => { + if (!slot.isValid()) { + throw new Error(`Can not find application - ${appName}`); + } - const appConf = ilcConfigRoot.getConfigForAppByName(appName); - - let wrapperConf = null; - if (appConf.wrappedWith) { - wrapperConf = { - ...ilcConfigRoot.getConfigForAppByName(appConf.wrappedWith), - appId: makeAppId(appConf.wrappedWith, slotName), - ...{ - wrappedAppConf: appConf, - }, - }; - } + const appConf = ilcConfigRoot.getConfigForAppByName(appName); + + let wrapperConf = null; + if (appConf.wrappedWith) { + wrapperConf = { + ...ilcConfigRoot.getConfigForAppByName(appConf.wrappedWith), + appId: makeAppId(appConf.wrappedWith, slotName), + ...{ + wrappedAppConf: appConf, + }, + }; + } - // Speculative preload of the JS bundle. We don't do it for CSS here as we already did it with preload links - bundleLoader.preloadApp(appName); + // Speculative preload of the JS bundle. We don't do it for CSS here as we already did it with preload links + bundleLoader.preloadApp(appName); - const overrides = await asyncBootUp.waitForSlot(slotName); - // App wrapper was rendered at SSR instead of app - if (wrapperConf !== null && overrides.wrapperPropsOverride === null) { - wrapperConf.cssBundle = overrides.cssBundle ? overrides.cssBundle : wrapperConf.cssBundle; - } else { - appConf.cssBundle = overrides.cssBundle ? overrides.cssBundle : appConf.cssBundle; - } + const overrides = await asyncBootUp.waitForSlot(slotName); + // App wrapper was rendered at SSR instead of app + if (wrapperConf !== null && overrides.wrapperPropsOverride === null) { + wrapperConf.cssBundle = overrides.cssBundle ? overrides.cssBundle : wrapperConf.cssBundle; + } else { + appConf.cssBundle = overrides.cssBundle ? overrides.cssBundle : appConf.cssBundle; + } + + const waitTill = [bundleLoader.loadAppWithCss(appName)]; + if (wrapperConf !== null) { + waitTill.push(bundleLoader.loadAppWithCss(appConf.wrappedWith)); + } - const waitTill = [bundleLoader.loadAppWithCss(appName)]; + lifecycleMethods = await Promise.all(waitTill).then(([spaCallbacks, wrapperSpaCallbacks]) => { if (wrapperConf !== null) { - waitTill.push(bundleLoader.loadAppWithCss(appConf.wrappedWith)); + const wrapper = new WrapApp(wrapperConf, overrides.wrapperPropsOverride, transitionManager); + + spaCallbacks = wrapper.wrapWith(spaCallbacks, wrapperSpaCallbacks); } - lifecycleMethods = await Promise.all(waitTill).then(([spaCallbacks, wrapperSpaCallbacks]) => { - if (wrapperConf !== null) { - const wrapper = new WrapApp(wrapperConf, overrides.wrapperPropsOverride, transitionManager); - - spaCallbacks = wrapper.wrapWith(spaCallbacks, wrapperSpaCallbacks); - } - - return prependSpaCallbacks(spaCallbacks, [ - { type: 'unmount', callback: onUnmount }, - { type: 'mount', callback: onMount }, - ]); - }); - - return lifecycleMethods; - }, - (location) => { - return router.isAppWithinSlotActive(appName, slotName); - }, - customProps, - ); + return prependSpaCallbacks(spaCallbacks, [ + { type: 'unmount', callback: onUnmount }, + { type: 'mount', callback: onMount }, + ]); + }); + + return lifecycleMethods; + }; + + const activityFn = (location) => { + return router.isAppWithinSlotActive(appName, slotName); + }; + + singleSpa.registerApplication(appId, loadingFn, activityFn, customProps); }); } diff --git a/ilc/common/utils.js b/ilc/common/utils.js index c76b59fb..bfd6753e 100644 --- a/ilc/common/utils.js +++ b/ilc/common/utils.js @@ -34,8 +34,6 @@ const decodeHtmlEntities = (value) => .replace(/>/g, '>') .replace(/"/g, '"'); -const fakeBaseInCasesWhereUrlIsRelative = 'http://hack'; - const removeQueryParams = (url) => { const index = url.indexOf('?'); if (index !== -1) {