diff --git a/package.json b/package.json index 80e6cf873..2f7c67253 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ }, "dependencies": { "@apollo/client": "^3.2.1", + "@module-federation/runtime": "^2.0.0", "classnames": "^2.2.5", "core-js": "^3.26.1", "final-form": "^4.18.2", @@ -148,4 +149,4 @@ "redux-observable": "^1.2.0", "rxjs": "^6.6.3" } -} +} \ No newline at end of file diff --git a/src/components/EntitlementLoader.js b/src/components/EntitlementLoader.js index 9406b4d47..a359f5c5f 100644 --- a/src/components/EntitlementLoader.js +++ b/src/components/EntitlementLoader.js @@ -1,10 +1,11 @@ import { useEffect, useState, useMemo } from 'react'; import PropTypes from 'prop-types'; +import { getInstance } from '@module-federation/runtime'; import { useStripes } from '../StripesContext'; import { ModulesContext, useModules, modulesInitialState } from '../ModulesContext'; -import loadRemoteComponent from '../loadRemoteComponent'; import { loadEntitlement } from './loadEntitlement'; + /** * preloadModules * Loads each module code and sets up its getModule function. @@ -16,14 +17,15 @@ import { loadEntitlement } from './loadEntitlement'; * @param {array} remotes * @returns {app: [], plugin: [], settings: [], handler: []} */ + export const preloadModules = async (stripes, remotes) => { const modules = { app: [], plugin: [], settings: [], handler: [] }; try { const loaderArray = []; remotes.forEach(remote => { - const { name, location } = remote; - loaderArray.push(loadRemoteComponent(location, name) + const { name } = remote; + loaderArray.push(getInstance().loadRemote(`${name}/MainEntry`) .then((module) => { remote.getModule = () => module.default; }) @@ -172,6 +174,10 @@ const EntitlementLoader = ({ children }) => { const controller = new AbortController(); const signal = controller.signal; if (okapi?.discoveryUrl) { + // ENABLE MOD FED DEBUGGING + localStorage.setItem('FEDERATION_DEBUG', 'true'); + + // fetches the list of registered apps/metadata, // loads icons and translations, then module code, // ultimately stores the result in the modules state to pass down into the modules context. @@ -195,9 +201,15 @@ const EntitlementLoader = ({ children }) => { handleRemoteModuleError(stripes, `Error loading remote module assets (icons, translations, sounds): ${e}`); } + const remotesToRegister = remotes.map(remote => ({ + name: remote.name, entry: remote.location + })); + + getInstance().registerRemotes(remotesToRegister); + try { // load module code - this loads each module only once and up `getModule` so that it can be used sychronously. - cachedModules = await preloadModules(stripes, remotesWithLoadedAssets); + cachedModules = await preloadModules(stripes, remotesWithLoadedAssets, remotesToRegister); } catch (e) { handleRemoteModuleError(stripes, `error loading remote modules: ${e}`); } @@ -205,6 +217,7 @@ const EntitlementLoader = ({ children }) => { setRemoteModules(cachedModules); } }; + fetchRegistry(); } return () => { diff --git a/src/components/EntitlementLoader.test.js b/src/components/EntitlementLoader.test.js index 7e7e2934e..0931cf3a2 100644 --- a/src/components/EntitlementLoader.test.js +++ b/src/components/EntitlementLoader.test.js @@ -4,7 +4,7 @@ import { okapi } from 'stripes-config'; import EntitlementLoader, { preloadModules, loadModuleAssets } from './EntitlementLoader'; import { StripesContext } from '../StripesContext'; import { ModulesContext, useModules, modulesInitialState as mockModuleInitialState } from '../ModulesContext'; -import loadRemoteComponent from '../loadRemoteComponent'; +import { loadRemote, registerRemotes } from '@module-federation/runtime'; import { loadEntitlement } from './loadEntitlement'; jest.mock('stripes-config'); diff --git a/src/components/loadEntitlement.js b/src/components/loadEntitlement.js index 33ef9a139..11fc513b1 100644 --- a/src/components/loadEntitlement.js +++ b/src/components/loadEntitlement.js @@ -4,7 +4,7 @@ import { stripesHubAPI } from '../constants'; export const loadEntitlement = async (discoveryUrl, signal) => { let registry = {}; const discovery = await localforage.getItem(stripesHubAPI.REMOTE_LIST_KEY); - if (discovery) { + if (discovery && discovery.length !== 0) { registry = { discovery }; } else if (discoveryUrl) { try { @@ -18,7 +18,7 @@ export const loadEntitlement = async (discoveryUrl, signal) => { // it's present...) registry.discovery = registryData?.discovery.filter((entry) => entry.name !== stripesHubAPI.HOST_APP_NAME); - await localforage.setItem(stripesHubAPI.REMOTE_LIST_KEY, registry.discovery); + // await localforage.setItem(stripesHubAPI.REMOTE_LIST_KEY, registry.discovery); } catch (e) { if (e.name !== 'AbortError') { console.error('Discovery fetch error:', e); // eslint-disable-line no-console @@ -39,7 +39,7 @@ export const loadEntitlement = async (discoveryUrl, signal) => { remote.origin = url.origin; const segments = url.href.split('/'); segments.pop(); - const hrefWithoutFilename = segments.join('/') + const hrefWithoutFilename = segments.join('/'); remote.assetPath = hrefWithoutFilename; }); return Promise.resolve(registry?.discovery); diff --git a/src/loadRemoteComponent.js b/src/loadRemoteComponent.js deleted file mode 100644 index 6c9c7a053..000000000 --- a/src/loadRemoteComponent.js +++ /dev/null @@ -1,39 +0,0 @@ -// injects a script tag to load a remote module. -// This has to be performed in this way for publicPath of the federated remote -// to be automatically discovered since it works based on document.currentScript.src. -// Once the script is loaded, it executes webpack module federation API -// to initialize sharing and retrieve the exposed module. - -function injectScript(remoteUrl, remoteName) { - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = remoteUrl; - script.onload = async () => { - const container = window[remoteName]; - - // eslint-disable-next-line no-undef - await __webpack_init_sharing__('default'); - - // eslint-disable-next-line no-undef - await container.init(__webpack_share_scopes__.default); - - const factory = await container.get('./MainEntry'); - const Module = await factory(); - resolve(Module); - }; - script.onerror = () => { - reject(new Error(`Failed to load remote script from ${remoteUrl}`)); - }; - document.body.appendChild(script); - }); -} - -export default async function loadRemoteComponent(remoteUrl, remoteName) { - try { - const Module = await injectScript(remoteUrl, remoteName); - return Module; - } catch (error) { - console.error(error); - throw error; - } -} diff --git a/src/loginServices.js b/src/loginServices.js index 120166589..1d73bb9a5 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -317,14 +317,14 @@ export async function loadTranslations(store, locale, defaultTranslations = {}) const res = await fetch(translationUrl.href) .then((response) => { if (response.ok) { - response.json().then((stripesTranslations) => { + return response.json().then((stripesTranslations) => { store.dispatch(setTranslations(Object.assign(stripesTranslations, defaultTranslations))); store.dispatch(setLocale(locale)); }); + } else { + return Promise.reject(new Error(`Could not load translations from ${translationsUrl}`)); } }); - - return res; } /** diff --git a/test/bigtest/tests/LoadRemoteComponent-test.js b/test/bigtest/tests/LoadRemoteComponent-test.js deleted file mode 100644 index 396190418..000000000 --- a/test/bigtest/tests/LoadRemoteComponent-test.js +++ /dev/null @@ -1,51 +0,0 @@ -import { beforeEach, it, afterEach, describe } from 'mocha'; -import { expect } from 'chai'; -import { createServer, Response } from 'miragejs'; - -import loadRemoteComponent from '../../../src/loadRemoteComponent'; - -describe.only('loadRemoteComponent', () => { - let server; - const mockRemoteUrl = '/example/testRemote/remoteEntry.js'; - const mockErrorUrl = 'https://example.com/nonexistent/remoteEntry.js'; - - const mockRemoteName = 'testComponent'; - - beforeEach(async function () { - server = createServer({ environment: 'test' }); - server.get(mockRemoteUrl, () => { - const mockScriptContent = `window['${mockRemoteName}'] = { - init: function() { console.log("Component initialized"); }, - get: function() { return function() { return { default: 'I am a module' }; }} - }; - `; - - // return mockScriptContent; - return mockScriptContent; - }); - - server.get(mockErrorUrl, () => (server.serialize({ ok: false }))); - }); - - afterEach(function () { - server?.shutdown(); - server = null; - delete window[mockRemoteName]; - }); - - it('should inject the script tag with the requested src attribute', async () => { - try { - await loadRemoteComponent(mockRemoteUrl, mockRemoteName); - } catch (error) { - expect(Array.from(document.querySelectorAll('script')).find(scr => scr.src === mockRemoteUrl)).to.not.be.null; - } - }); - - it('should handle errors when loading the remote script', async () => { - try { - await loadRemoteComponent(mockErrorUrl, mockRemoteName); - } catch (error) { - expect(error.message).to.equal(`Failed to load remote script from ${mockErrorUrl}`); - } - }); -});