From 8f1948874340f260f568eb29d7e15d096f425786 Mon Sep 17 00:00:00 2001 From: iammerrick Date: Wed, 30 Apr 2025 12:24:58 -0600 Subject: [PATCH 1/6] feat(runtime): provide a root --- .../3005-runtime-host/src/App.tsx | 5 ++++ .../3005-runtime-host/src/Remote3.tsx | 28 +++++++++++++++++++ .../src/plugins/snapshot/index.ts | 4 +-- packages/runtime-core/src/remote/index.ts | 10 +++++-- packages/runtime-core/src/utils/preload.ts | 9 +++--- 5 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 apps/runtime-demo/3005-runtime-host/src/Remote3.tsx diff --git a/apps/runtime-demo/3005-runtime-host/src/App.tsx b/apps/runtime-demo/3005-runtime-host/src/App.tsx index 2cf2933f74e..9d320c8cd2f 100644 --- a/apps/runtime-demo/3005-runtime-host/src/App.tsx +++ b/apps/runtime-demo/3005-runtime-host/src/App.tsx @@ -4,6 +4,7 @@ import { Link, Routes, Route, BrowserRouter } from 'react-router-dom'; import Root from './Root'; import Remote1 from './Remote1'; import Remote2 from './Remote2'; +import Remote3 from './Remote3'; const App = () => ( @@ -18,11 +19,15 @@ const App = () => (
  • remote2
  • +
  • + remote3 +
  • } /> } /> } /> + } />
    ); diff --git a/apps/runtime-demo/3005-runtime-host/src/Remote3.tsx b/apps/runtime-demo/3005-runtime-host/src/Remote3.tsx new file mode 100644 index 00000000000..2818f654496 --- /dev/null +++ b/apps/runtime-demo/3005-runtime-host/src/Remote3.tsx @@ -0,0 +1,28 @@ +import React, { Suspense, lazy } from 'react'; +import { createRoot } from 'react-dom/client'; +import { loadRemote } from '@module-federation/enhanced/runtime'; + +class CustomElement extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + async connectedCallback() { + if (!this.shadowRoot) return; + + const module = await loadRemote('dynamic-remote/ButtonOldAnt', { + //@ts-ignore + root: this.shadowRoot, + }); + //@ts-ignore + createRoot(this.shadowRoot).render(React.createElement(module.default)); + } +} + +customElements.define('custom-element', CustomElement); + +function DynamicRemoteButton() { + return React.createElement('custom-element'); +} + +export default DynamicRemoteButton; diff --git a/packages/runtime-core/src/plugins/snapshot/index.ts b/packages/runtime-core/src/plugins/snapshot/index.ts index 990dd62e284..908bbec5816 100644 --- a/packages/runtime-core/src/plugins/snapshot/index.ts +++ b/packages/runtime-core/src/plugins/snapshot/index.ts @@ -41,7 +41,7 @@ export function snapshotPlugin(): FederationRuntimePlugin { return { name: 'snapshot-plugin', async afterResolve(args) { - const { remote, pkgNameOrAlias, expose, origin, remoteInfo } = args; + const { remote, pkgNameOrAlias, expose, origin, remoteInfo, root } = args; if (!isRemoteInfoWithEntry(remote) || !isPureRemoteEntry(remote)) { const { remoteSnapshot, globalSnapshot } = @@ -73,7 +73,7 @@ export function snapshotPlugin(): FederationRuntimePlugin { ); if (assets) { - preloadAssets(remoteInfo, origin, assets, false); + preloadAssets(remoteInfo, origin, assets, false, root); } return { diff --git a/packages/runtime-core/src/remote/index.ts b/packages/runtime-core/src/remote/index.ts index f51935c4440..a67268aeec7 100644 --- a/packages/runtime-core/src/remote/index.ts +++ b/packages/runtime-core/src/remote/index.ts @@ -57,6 +57,7 @@ export interface LoadRemoteMatch { origin: FederationHost; remoteInfo: RemoteInfo; remoteSnapshot?: ModuleInfo; + root?: HTMLElement; } export class RemoteHandler { @@ -197,7 +198,7 @@ export class RemoteHandler { // eslint-disable-next-line @typescript-eslint/member-ordering async loadRemote( id: string, - options?: { loadFactory?: boolean; from: CallFrom }, + options?: { loadFactory?: boolean; from: CallFrom; root?: HTMLElement }, ): Promise { const { host } = this; try { @@ -214,6 +215,7 @@ export class RemoteHandler { const { module, moduleOptions, remoteMatchInfo } = await this.getRemoteModuleAndOptions({ id, + root: options?.root, }); const { pkgNameOrAlias, @@ -314,7 +316,10 @@ export class RemoteHandler { }); } - async getRemoteModuleAndOptions(options: { id: string }): Promise<{ + async getRemoteModuleAndOptions(options: { + id: string; + root?: HTMLElement; + }): Promise<{ module: Module; moduleOptions: ModuleOptions; remoteMatchInfo: LoadRemoteMatch; @@ -371,6 +376,7 @@ export class RemoteHandler { options: host.options, origin: host, remoteInfo, + root: options.root, }); const { remote, expose } = matchInfo; diff --git a/packages/runtime-core/src/utils/preload.ts b/packages/runtime-core/src/utils/preload.ts index ed99ee6c3bf..43983022731 100644 --- a/packages/runtime-core/src/utils/preload.ts +++ b/packages/runtime-core/src/utils/preload.ts @@ -70,6 +70,7 @@ export function preloadAssets( assets: PreloadAssets, // It is used to distinguish preload from load remote parallel loading useLinkPreload = true, + root: HTMLElement = document.head, ): void { const { cssAssets, jsAssetsWithoutEntry, entryAssets } = assets; @@ -116,7 +117,7 @@ export function preloadAssets( }, }); - needAttach && document.head.appendChild(cssEl); + needAttach && root.appendChild(cssEl); }); } else { const defaultAttrs = { @@ -143,7 +144,7 @@ export function preloadAssets( needDeleteLink: false, }); - needAttach && document.head.appendChild(cssEl); + needAttach && root.appendChild(cssEl); }); } @@ -170,7 +171,7 @@ export function preloadAssets( return; }, }); - needAttach && document.head.appendChild(linkEl); + needAttach && root.appendChild(linkEl); }); } else { const defaultAttrs = { @@ -196,7 +197,7 @@ export function preloadAssets( }, needDeleteScript: true, }); - needAttach && document.head.appendChild(scriptEl); + needAttach && root.appendChild(scriptEl); }); } } From ce411107f76fc687c626604af3dca65913b8444f Mon Sep 17 00:00:00 2001 From: iammerrick Date: Wed, 30 Apr 2025 12:38:32 -0600 Subject: [PATCH 2/6] fix(runtime): use root to ensure needsattach is computed correctly --- packages/runtime-core/src/utils/preload.ts | 7 +++++-- packages/sdk/src/dom.ts | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/runtime-core/src/utils/preload.ts b/packages/runtime-core/src/utils/preload.ts index 43983022731..7e0e162d869 100644 --- a/packages/runtime-core/src/utils/preload.ts +++ b/packages/runtime-core/src/utils/preload.ts @@ -100,6 +100,7 @@ export function preloadAssets( }; cssAssets.forEach((cssUrl) => { const { link: cssEl, needAttach } = createLink({ + root, url: cssUrl, cb: () => { // noop @@ -126,6 +127,7 @@ export function preloadAssets( }; cssAssets.forEach((cssUrl) => { const { link: cssEl, needAttach } = createLink({ + root, url: cssUrl, cb: () => { // noop @@ -155,6 +157,7 @@ export function preloadAssets( }; jsAssetsWithoutEntry.forEach((jsUrl) => { const { link: linkEl, needAttach } = createLink({ + root: document.head, url: jsUrl, cb: () => { // noop @@ -171,7 +174,7 @@ export function preloadAssets( return; }, }); - needAttach && root.appendChild(linkEl); + needAttach && document.head.appendChild(linkEl); }); } else { const defaultAttrs = { @@ -197,7 +200,7 @@ export function preloadAssets( }, needDeleteScript: true, }); - needAttach && root.appendChild(scriptEl); + needAttach && document.head.appendChild(scriptEl); }); } } diff --git a/packages/sdk/src/dom.ts b/packages/sdk/src/dom.ts index 5c478b13068..38d9d7af885 100644 --- a/packages/sdk/src/dom.ts +++ b/packages/sdk/src/dom.ts @@ -136,6 +136,7 @@ export function createScript(info: { } export function createLink(info: { + root: HTMLElement; url: string; cb?: (value: void | PromiseLike) => void; onErrorCallback?: (error: Error) => void; @@ -151,7 +152,7 @@ export function createLink(info: { // Retrieve the existing script element by its src attribute let link: HTMLLinkElement | null = null; let needAttach = true; - const links = document.getElementsByTagName('link'); + const links = info.root.querySelectorAll('link'); for (let i = 0; i < links.length; i++) { const l = links[i]; const linkHref = l.getAttribute('href'); From ee1a2ac23cb10fae3283dd855eebb4076a716c8f Mon Sep 17 00:00:00 2001 From: iammerrick Date: Wed, 30 Apr 2025 12:43:44 -0600 Subject: [PATCH 3/6] fix: spec to tolerate custom root --- packages/sdk/__tests__/dom.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/sdk/__tests__/dom.spec.ts b/packages/sdk/__tests__/dom.spec.ts index 729991f75e8..5d732fb91b2 100644 --- a/packages/sdk/__tests__/dom.spec.ts +++ b/packages/sdk/__tests__/dom.spec.ts @@ -164,6 +164,7 @@ describe('createLink', () => { const url = 'https://example.com/script.js'; const cb = jest.fn(); const { link, needAttach } = createLink({ + root: document.head, url, cb, attrs: { as: 'script' }, @@ -181,6 +182,7 @@ describe('createLink', () => { document.head.innerHTML = ``; const { link, needAttach } = createLink({ url, + root: document.head, cb, attrs: { rel: 'preload', @@ -197,7 +199,7 @@ describe('createLink', () => { const url = 'https://example.com/script.js'; const cb = jest.fn(); const attrs = { rel: 'preload', as: 'script', 'data-test': 'test' }; - const { link } = createLink({ url, cb, attrs }); + const { link } = createLink({ url, cb, attrs, root: document.head }); expect(link.rel).toBe('preload'); expect(link.getAttribute('as')).toBe('script'); @@ -215,6 +217,7 @@ describe('createLink', () => { }; const { link } = createLink({ url, + root: document.head, cb, attrs, createLinkHook: (url) => { @@ -237,6 +240,7 @@ describe('createLink', () => { const cb = jest.fn(); const { link, needAttach } = createLink({ url, + root: document.head, cb, attrs: { as: 'script' }, }); @@ -255,6 +259,7 @@ describe('createLink', () => { const onErrorCallback = jest.fn(); const { link, needAttach } = createLink({ url, + root: document.head, cb, onErrorCallback, attrs: { as: 'script' }, @@ -277,6 +282,7 @@ describe('createLink', () => { const { link } = createLink({ url, cb, + root: document.head, attrs: {}, createLinkHook: () => customLink, }); From 46b008ac4f2dc48f42230d8b3fbab993ab9d83fc Mon Sep 17 00:00:00 2001 From: iammerrick Date: Wed, 30 Apr 2025 13:28:26 -0600 Subject: [PATCH 4/6] fix: types --- packages/runtime-core/src/core.ts | 2 +- .../runtime/__tests__/load-remote.spec.ts | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/src/core.ts b/packages/runtime-core/src/core.ts index 6ed436b1bb1..8afb5e69612 100644 --- a/packages/runtime-core/src/core.ts +++ b/packages/runtime-core/src/core.ts @@ -247,7 +247,7 @@ export class FederationHost { // eslint-disable-next-line @typescript-eslint/member-ordering async loadRemote( id: string, - options?: { loadFactory?: boolean; from: CallFrom }, + options?: { loadFactory?: boolean; from: CallFrom; root?: HTMLElement }, ): Promise { return this.remoteHandler.loadRemote(id, options); } diff --git a/packages/runtime/__tests__/load-remote.spec.ts b/packages/runtime/__tests__/load-remote.spec.ts index b072baccd6d..d0fe6c7ef2d 100644 --- a/packages/runtime/__tests__/load-remote.spec.ts +++ b/packages/runtime/__tests__/load-remote.spec.ts @@ -603,4 +603,94 @@ describe('loadRemote', () => { expect(loadedSrcs.includes(`${remotePublicPath}${jsSyncAssetPath}`)); reset(); }); + + it('renders css in shadowRoot when providing a different root', async () => { + const shadowRoot = document.createElement('div'); + const cssSyncPath = 'sub2/say.sync.css'; + const cssAsyncPath = 'sub2/say.async.css'; + const remotePublicPath = 'http://localhost:1111/'; + const reset = addGlobalSnapshot({ + '@federation-test/shadow-css': { + globalName: '', + buildVersion: '', + publicPath: '', + remoteTypes: '', + shared: [], + remoteEntry: '', + remoteEntryType: 'global', + modules: [], + version: '0.0.1', + remotesInfo: { + '@federation-test/app2': { + matchedVersion: '0.0.1', + }, + }, + }, + '@federation-test/app2:0.0.1': { + globalName: '', + publicPath: remotePublicPath, + remoteTypes: '', + shared: [], + buildVersion: 'custom', + remotesInfo: {}, + remoteEntryType: 'global', + modules: [ + { + moduleName: 'say', + assets: { + css: { + sync: [cssSyncPath], + async: [cssAsyncPath], + }, + js: { + sync: ['resources/load-remote/app2/say.sync.js'], + async: [], + }, + }, + }, + ], + version: '0.0.1', + remoteEntry: 'resources/app2/federation-remote-entry.js', + }, + }); + + const FederationInstance = new FederationHost({ + name: '@federation-test/shadow-css', + remotes: [ + { + name: '@federation-test/app2', + version: '*', + }, + ], + }); + + await FederationInstance.loadRemote<() => string>( + '@federation-test/app2/say', + { root: shadowRoot }, + ); + + // Verify CSS links were appended to the shadowRoot + const cssLinks = shadowRoot.querySelectorAll('link'); + console.log(document.head.querySelectorAll('link')); + expect(cssLinks.length).toBeGreaterThan(0); // Should have CSS links + + // Check that the CSS links have the correct href attributes + const hrefs = Array.from(cssLinks).map((link) => link.getAttribute('href')); + + // At least one of the CSS files should be loaded in the shadowRoot + const hasCssInShadowRoot = hrefs.some( + (href) => + href === `${remotePublicPath}${cssSyncPath}` || + href === `${remotePublicPath}${cssAsyncPath}`, + ); + expect(hasCssInShadowRoot).toBe(true); + + // Verify the links have the correct rel attribute for stylesheets + const stylesheetLinks = Array.from(cssLinks).filter( + (link) => link.getAttribute('rel') === 'stylesheet', + ); + expect(stylesheetLinks.length).toBeGreaterThan(0); + + reset(); + }); }); From b1c2710e5bbc1347d05fbbcf33d27ffc33d5c6e0 Mon Sep 17 00:00:00 2001 From: iammerrick Date: Wed, 30 Apr 2025 13:54:33 -0600 Subject: [PATCH 5/6] fix: add some failing tests --- packages/runtime-core/src/core.ts | 2 +- packages/runtime-core/src/remote/index.ts | 2 +- .../runtime/__tests__/load-remote.spec.ts | 55 +++++++++---------- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/packages/runtime-core/src/core.ts b/packages/runtime-core/src/core.ts index 8afb5e69612..34bab3f128b 100644 --- a/packages/runtime-core/src/core.ts +++ b/packages/runtime-core/src/core.ts @@ -247,7 +247,7 @@ export class FederationHost { // eslint-disable-next-line @typescript-eslint/member-ordering async loadRemote( id: string, - options?: { loadFactory?: boolean; from: CallFrom; root?: HTMLElement }, + options?: { loadFactory?: boolean; from?: CallFrom; root?: HTMLElement }, ): Promise { return this.remoteHandler.loadRemote(id, options); } diff --git a/packages/runtime-core/src/remote/index.ts b/packages/runtime-core/src/remote/index.ts index a67268aeec7..ea73e941aa7 100644 --- a/packages/runtime-core/src/remote/index.ts +++ b/packages/runtime-core/src/remote/index.ts @@ -198,7 +198,7 @@ export class RemoteHandler { // eslint-disable-next-line @typescript-eslint/member-ordering async loadRemote( id: string, - options?: { loadFactory?: boolean; from: CallFrom; root?: HTMLElement }, + options?: { loadFactory?: boolean; from?: CallFrom; root?: HTMLElement }, ): Promise { const { host } = this; try { diff --git a/packages/runtime/__tests__/load-remote.spec.ts b/packages/runtime/__tests__/load-remote.spec.ts index d0fe6c7ef2d..5544dcdd8fc 100644 --- a/packages/runtime/__tests__/load-remote.spec.ts +++ b/packages/runtime/__tests__/load-remote.spec.ts @@ -600,17 +600,20 @@ describe('loadRemote', () => { const loadedSrcs = [...document.querySelectorAll('script')].map( (i) => (i as any).fakeSrc, ); + const loadedStyles = [...document.querySelectorAll('link')].map( + (link) => link.href, + ); expect(loadedSrcs.includes(`${remotePublicPath}${jsSyncAssetPath}`)); + expect(loadedStyles.includes(`${remotePublicPath}sub2/say.sync.css`)); + reset(); }); - it('renders css in shadowRoot when providing a different root', async () => { - const shadowRoot = document.createElement('div'); - const cssSyncPath = 'sub2/say.sync.css'; - const cssAsyncPath = 'sub2/say.async.css'; + it('loads remote synchronously in a custom root', async () => { + const jsSyncAssetPath = 'resources/load-remote/app2/say.sync.js'; const remotePublicPath = 'http://localhost:1111/'; const reset = addGlobalSnapshot({ - '@federation-test/shadow-css': { + '@federation-test/globalinfo': { globalName: '', buildVersion: '', publicPath: '', @@ -639,11 +642,11 @@ describe('loadRemote', () => { moduleName: 'say', assets: { css: { - sync: [cssSyncPath], - async: [cssAsyncPath], + sync: ['sub2/say.sync.css'], + async: ['sub2/say.async.css'], }, js: { - sync: ['resources/load-remote/app2/say.sync.js'], + sync: [jsSyncAssetPath], async: [], }, }, @@ -655,7 +658,7 @@ describe('loadRemote', () => { }); const FederationInstance = new FederationHost({ - name: '@federation-test/shadow-css', + name: '@federation-test/globalinfo', remotes: [ { name: '@federation-test/app2', @@ -664,32 +667,24 @@ describe('loadRemote', () => { ], }); + const root = document.createElement('div'); await FederationInstance.loadRemote<() => string>( '@federation-test/app2/say', - { root: shadowRoot }, + { root }, ); - - // Verify CSS links were appended to the shadowRoot - const cssLinks = shadowRoot.querySelectorAll('link'); - console.log(document.head.querySelectorAll('link')); - expect(cssLinks.length).toBeGreaterThan(0); // Should have CSS links - - // Check that the CSS links have the correct href attributes - const hrefs = Array.from(cssLinks).map((link) => link.getAttribute('href')); - - // At least one of the CSS files should be loaded in the shadowRoot - const hasCssInShadowRoot = hrefs.some( - (href) => - href === `${remotePublicPath}${cssSyncPath}` || - href === `${remotePublicPath}${cssAsyncPath}`, + // @ts-ignore fakeSrc is local mock attr, which value is the same as src + const loadedSrcs = [...document.querySelectorAll('script')].map( + (i) => (i as any).fakeSrc, ); - expect(hasCssInShadowRoot).toBe(true); - - // Verify the links have the correct rel attribute for stylesheets - const stylesheetLinks = Array.from(cssLinks).filter( - (link) => link.getAttribute('rel') === 'stylesheet', + const loadedStyles = [...root.querySelectorAll('link')].map( + (link) => link.href, ); - expect(stylesheetLinks.length).toBeGreaterThan(0); + const documentStyles = [...document.head.querySelectorAll('link')].map( + (link) => link.href, + ); + expect(loadedSrcs.includes(`${remotePublicPath}${jsSyncAssetPath}`)); + expect(loadedStyles.includes(`${remotePublicPath}sub2/say.sync.css`)); + expect(documentStyles).toEqual([]); reset(); }); From 7d26e1325c4520534ee976d131cc6a8e9391c8b5 Mon Sep 17 00:00:00 2001 From: iammerrick Date: Wed, 30 Apr 2025 13:59:18 -0600 Subject: [PATCH 6/6] fix: reset dom between tests --- packages/runtime/__tests__/load-remote.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/runtime/__tests__/load-remote.spec.ts b/packages/runtime/__tests__/load-remote.spec.ts index 5544dcdd8fc..a20c950986c 100644 --- a/packages/runtime/__tests__/load-remote.spec.ts +++ b/packages/runtime/__tests__/load-remote.spec.ts @@ -535,6 +535,14 @@ describe('lazy loadRemote and add remote into snapshot', () => { }); describe('loadRemote', () => { + beforeEach(() => { + document.querySelectorAll('script').forEach((script) => { + script.remove(); + }); + document.querySelectorAll('link').forEach((link) => { + link.remove(); + }); + }); it('loads remote synchronously', async () => { const jsSyncAssetPath = 'resources/load-remote/app2/say.sync.js'; const remotePublicPath = 'http://localhost:1111/';