@c8y/bootstrap
Version:
Bootstrap layer
212 lines • 8.67 kB
JavaScript
import { reduce, forEach, get, union, camelCase, cloneDeep, isEmpty } from 'lodash';
const loadedRemotesContextPathCache = new Map();
const remoteScriptPromisesMap = new Map();
let urlRemotesCache = null;
/**
* Takes a list of remotes and turns it into an object containing union of corresponding remotes.
* @param remotes List of the remotes.
* @returns Returns object with merged remotes.
*
* **Example**
* ```typescript
* const remotesA:ApplicationRemotePlugins = { contextPathA: ['moduleA', 'moduleB'] };
* const remotesB:ApplicationRemotePlugins = { contextPathA: ['moduleA'], contextPathB: ['moduleZ'] };
* const mergedRemotes:ApplicationRemotePlugins = mergeRemotes([remotesA, remotesB]);
* // Result
* {
* contextPathA: ['moduleA', 'moduleB'],
* contextPathB: ['moduleZ']
* }
*
* ```
*/
export function mergeRemotes(remotes) {
return reduce(remotes, (allRemotes, mfRemote) => {
forEach(mfRemote, (remoteModules, remoteContextPath) => {
const currentRemotes = get(allRemotes, remoteContextPath, []);
allRemotes[remoteContextPath] = union(currentRemotes, remoteModules);
});
return allRemotes;
}, {});
}
export function removeRemotes(remotesToRemoveFrom, remotesToRemove) {
const keysToRemove = Object.keys(remotesToRemove || {});
if (!keysToRemove.length) {
return remotesToRemoveFrom;
}
const currentKeys = Object.keys(remotesToRemoveFrom);
const keysPresentInBoth = currentKeys.filter(key => keysToRemove.includes(key));
if (!keysPresentInBoth.length) {
return remotesToRemoveFrom;
}
remotesToRemoveFrom = cloneDeep(remotesToRemoveFrom);
for (const remoteContextPath of keysPresentInBoth) {
const remoteModulesToBeRemoved = remotesToRemove[remoteContextPath];
if (!Array.isArray(remoteModulesToBeRemoved) || !remoteModulesToBeRemoved?.length) {
continue;
}
let currentModules = remotesToRemoveFrom[remoteContextPath];
if (!Array.isArray(currentModules) || !currentModules?.length) {
delete remotesToRemoveFrom[remoteContextPath];
continue;
}
currentModules = currentModules.filter(module => !remoteModulesToBeRemoved.includes(module));
if (currentModules.length) {
remotesToRemoveFrom[remoteContextPath] = currentModules;
}
else {
delete remotesToRemoveFrom[remoteContextPath];
}
}
return remotesToRemoveFrom;
}
/**
* Loads a list of remotes so that a particular application can use them.
* The request is made to the following address: /apps/<contextPath>/remoteEntry.js
* @param remotes List of remotes to be loaded.
* @returns Returns the list of loaded modules from remotes.
*/
export async function loadRemotes(remotes, options) {
if (!remotes) {
return [];
}
const dateMillis = new Date().getTime();
const promises = [];
for (const pluginId in remotes) {
if (remotes.hasOwnProperty(pluginId)) {
const moduleNames = remotes[pluginId];
const url =
// use relative path for self scoped plugins, in case the app is not hosted like usually (e.g. codex (cumulocity.com/codex/))
pluginId === options?.contextPath
? `./remoteEntry.js?nocache=${dateMillis}`
: `/apps/${pluginId}/remoteEntry.js?nocache=${dateMillis}`;
for (const moduleName of moduleNames) {
promises.push(loadRemoteModule(url, pluginId, moduleName).catch(ex => {
console.warn(`Could not load remote module '%s' from url:`, moduleName, url);
throw ex;
}));
}
}
}
const remoteModulesSettled = await Promise.allSettled(promises);
const remoteModulesSuccessful = remoteModulesSettled
.filter(result => result.status === 'fulfilled')
.map(result => result.value);
return remoteModulesSuccessful;
}
export function clearURLRemotesCache() {
urlRemotesCache = null;
}
/**
* Retrieves the remotes list from the URL.
* @returns Returns the list of remotes.
*/
export function loadUrlRemotes() {
if (!urlRemotesCache) {
const params = new URLSearchParams(window.location.search);
const remotes = params.get('remotes');
if (remotes) {
try {
urlRemotesCache = JSON.parse(decodeURIComponent(remotes));
}
catch (error) {
console.warn(`Failed to parse remotes: ${error}`);
}
}
}
return urlRemotesCache;
}
async function loadRemoteModule(remoteEntryUrl, remoteContextPath, exposedModule) {
let remoteEntryPromise = remoteScriptPromisesMap.get(remoteEntryUrl);
if (!remoteEntryPromise) {
remoteEntryPromise = loadRemoteEntry(remoteEntryUrl);
remoteScriptPromisesMap.set(remoteEntryUrl, remoteEntryPromise);
}
await remoteEntryPromise;
let contextPath = remoteContextPath;
if (contextPath.includes('@')) {
contextPath = remoteContextPath.split('@')[0];
}
const exposedModuleDetails = await lookupExposedModule(camelCase(contextPath), exposedModule);
return { ...exposedModuleDetails, fullContextPath: remoteContextPath };
}
function loadRemoteEntry(remoteEntryUrl) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = remoteEntryUrl;
script.onerror = reject;
script.onload = () => {
resolve(); // window is the global namespace
};
document.body.append(script);
});
}
async function lookupExposedModule(remoteName, exposedModule) {
// Initializes the share scope. This fills it with known provided modules from this build and all remotes
try {
await __webpack_init_sharing__('default');
}
catch (ex) {
console.error(`Module %s could not be loaded. Module Federation is not enabled in this application.`, exposedModule, ex);
}
let container = window[remoteName];
/**
* MTM-60850: In case of e.g. the cockpit app being cloned to a different context path
* the self scoped plugins of cockpit will be loaded from the new context path.
* But the remoteEntry.js will still register the remotes with the original context path,
* as this is hardcoded during compile time.
*
* We therefore add a fallback to the original context path in case there is no container for the remoteName
*/
if (!container) {
const fallbackRemoteName = camelCase(__ORIGINAL_CONTEXT_PATH__);
console.warn(`Attribute "%s" not defined on window object while trying to load "%s". Using "%s" as fallback.`, remoteName, exposedModule, fallbackRemoteName);
container = window[fallbackRemoteName];
}
// Initialize the container, it may provide shared modules
let factory;
try {
await container.init(__webpack_share_scopes__.default);
factory = (await container.get(exposedModule))();
}
catch (ex) {
console.error(`Module %s could not be loaded.`, exposedModule, ex);
}
return { name: exposedModule, factory };
}
// from bootstrap component:
export function bootstrapGetRemotes(config, options) {
if (options.noPlugins) {
return undefined;
}
if (options.forceUrlRemotes) {
return loadUrlRemotes();
}
let selfRemotes;
// We need to import the self imports, as this app is not configured at all
if (options?.exports) {
const selfModuleNames = options.exports
.filter(plugin => plugin.scope === 'self')
.map(plugin => plugin.module || plugin.name);
selfRemotes = { [options.contextPath]: selfModuleNames };
}
// Merge plugins config remotes and remotes passed in the URL.
// Remotes in the URL are used in the development process.
let remotes = mergeRemotes([
selfRemotes,
// if the config remotes are not present we fall back to the application remotes
config?.remotes || options?.remotes || {},
// url remotes should always be loaded
loadUrlRemotes()
]);
// remove remotes that are on the exclude list
remotes = removeRemotes(remotes, config?.excludedRemotes);
// Block the possibility of loading plugins multiple times.
Object.keys(remotes || {}).forEach(contextPath => {
if (loadedRemotesContextPathCache.get(contextPath)) {
delete remotes[contextPath];
}
});
return isEmpty(remotes) ? undefined : remotes;
}
//# sourceMappingURL=plugins.js.map