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/core.ts b/packages/runtime-core/src/core.ts
index 6ed436b1bb1..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 },
+ options?: { loadFactory?: boolean; from?: CallFrom; root?: HTMLElement },
): Promise {
return this.remoteHandler.loadRemote(id, options);
}
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..ea73e941aa7 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..7e0e162d869 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;
@@ -99,6 +100,7 @@ export function preloadAssets(
};
cssAssets.forEach((cssUrl) => {
const { link: cssEl, needAttach } = createLink({
+ root,
url: cssUrl,
cb: () => {
// noop
@@ -116,7 +118,7 @@ export function preloadAssets(
},
});
- needAttach && document.head.appendChild(cssEl);
+ needAttach && root.appendChild(cssEl);
});
} else {
const defaultAttrs = {
@@ -125,6 +127,7 @@ export function preloadAssets(
};
cssAssets.forEach((cssUrl) => {
const { link: cssEl, needAttach } = createLink({
+ root,
url: cssUrl,
cb: () => {
// noop
@@ -143,7 +146,7 @@ export function preloadAssets(
needDeleteLink: false,
});
- needAttach && document.head.appendChild(cssEl);
+ needAttach && root.appendChild(cssEl);
});
}
@@ -154,6 +157,7 @@ export function preloadAssets(
};
jsAssetsWithoutEntry.forEach((jsUrl) => {
const { link: linkEl, needAttach } = createLink({
+ root: document.head,
url: jsUrl,
cb: () => {
// noop
diff --git a/packages/runtime/__tests__/load-remote.spec.ts b/packages/runtime/__tests__/load-remote.spec.ts
index b072baccd6d..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/';
@@ -600,7 +608,92 @@ 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('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/globalinfo': {
+ 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: ['sub2/say.sync.css'],
+ async: ['sub2/say.async.css'],
+ },
+ js: {
+ sync: [jsSyncAssetPath],
+ async: [],
+ },
+ },
+ },
+ ],
+ version: '0.0.1',
+ remoteEntry: 'resources/app2/federation-remote-entry.js',
+ },
+ });
+
+ const FederationInstance = new FederationHost({
+ name: '@federation-test/globalinfo',
+ remotes: [
+ {
+ name: '@federation-test/app2',
+ version: '*',
+ },
+ ],
+ });
+
+ const root = document.createElement('div');
+ await FederationInstance.loadRemote<() => string>(
+ '@federation-test/app2/say',
+ { root },
+ );
+ // @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,
+ );
+ const loadedStyles = [...root.querySelectorAll('link')].map(
+ (link) => link.href,
+ );
+ 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();
});
});
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,
});
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');