UNPKG

@c8y/bootstrap

Version:

Bootstrap layer

212 lines 8.67 kB
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