Skip to content

Commit

Permalink
feat(ilc client): unload app method
Browse files Browse the repository at this point in the history
  • Loading branch information
stas-nc committed Sep 6, 2024
1 parent c3ad96a commit a2a8957
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 57 deletions.
13 changes: 12 additions & 1 deletion ilc/client/BundleLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
}
30 changes: 30 additions & 0 deletions ilc/client/BundleLoader.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 13 additions & 2 deletions ilc/client/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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);
}
}
103 changes: 51 additions & 52 deletions ilc/client/registerSpaApps.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -26,9 +26,9 @@ const getCustomProps = (slot, router, appErrorHandlerFactory, sdkFactoryBuilder)
};

return customProps;
};
}

export default function (
export function registerApplications(
ilcConfigRoot,
router,
appErrorHandlerFactory,
Expand Down Expand Up @@ -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);
});
}
2 changes: 0 additions & 2 deletions ilc/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit a2a8957

Please sign in to comment.