diff --git a/.changeset/perfect-camels-type.md b/.changeset/perfect-camels-type.md
new file mode 100644
index 00000000000..7e355ed496a
--- /dev/null
+++ b/.changeset/perfect-camels-type.md
@@ -0,0 +1,5 @@
+---
+'@module-federation/devtools': patch
+---
+
+fix: devtools Adjustments hmr changes the behavior of shared to make changes more stable
diff --git a/.changeset/wet-geckos-tan.md b/.changeset/wet-geckos-tan.md
new file mode 100644
index 00000000000..5ea57522956
--- /dev/null
+++ b/.changeset/wet-geckos-tan.md
@@ -0,0 +1,7 @@
+---
+'runtime-remote1': patch
+'@module-federation/runtime': patch
+'@module-federation/sdk': patch
+---
+
+fix: In load remote, link preload is not used to preload resources, preventing resource reloading
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 14936587db6..ac8453ceafa 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -44,7 +44,7 @@ jobs:
run: npx nx affected -t lint --parallel=7 --exclude='*,!tag:type:pkg'
- name: Run Affected Test
- run: npx nx affected -t test --parallel=3 --exclude='*,!tag:type:pkg'
+ run: npx nx affected -t test --parallel=3 --exclude='*,!tag:type:pkg' --skip-cache
# - name: E2E Test for Next.js Dev
# run: |
diff --git a/apps/runtime-demo/3006-runtime-remote/src/components/WebpackPng.tsx b/apps/runtime-demo/3006-runtime-remote/src/components/WebpackPng.tsx
index 0ca7cff02b1..f371359bfcf 100644
--- a/apps/runtime-demo/3006-runtime-remote/src/components/WebpackPng.tsx
+++ b/apps/runtime-demo/3006-runtime-remote/src/components/WebpackPng.tsx
@@ -1,4 +1,5 @@
import png from '../../public/webpack.png';
+import './a.css';
export default function WebpackPng() {
return
;
diff --git a/apps/runtime-demo/3006-runtime-remote/src/components/a.css b/apps/runtime-demo/3006-runtime-remote/src/components/a.css
new file mode 100644
index 00000000000..8b760222b62
--- /dev/null
+++ b/apps/runtime-demo/3006-runtime-remote/src/components/a.css
@@ -0,0 +1,3 @@
+span {
+ color: red;
+}
diff --git a/packages/chrome-devtools/src/utils/chrome/fast-refresh.ts b/packages/chrome-devtools/src/utils/chrome/fast-refresh.ts
index 41222b0bfbe..ea7462f4ba1 100644
--- a/packages/chrome-devtools/src/utils/chrome/fast-refresh.ts
+++ b/packages/chrome-devtools/src/utils/chrome/fast-refresh.ts
@@ -11,8 +11,8 @@ import { __FEDERATION_DEVTOOLS__ } from '../../template';
const fastRefreshPlugin = (): FederationRuntimePlugin => {
return {
name: 'mf-fast-refresh-plugin',
- // @ts-expect-error
- beforeInit({ origin, userOptions, options, shareInfo }) {
+ beforeInit({ userOptions, ...args }) {
+ const shareInfo = userOptions.shared;
let enableFastRefresh: boolean;
let devtoolsMessage;
@@ -26,7 +26,7 @@ const fastRefreshPlugin = (): FederationRuntimePlugin => {
}
}
- if (isObject(shareInfo)) {
+ if (shareInfo && isObject(shareInfo)) {
let orderResolve: (value?: unknown) => void;
const orderPromise = new Promise((resolve) => {
orderResolve = resolve;
@@ -83,16 +83,13 @@ const fastRefreshPlugin = (): FederationRuntimePlugin => {
});
return {
- origin,
userOptions,
- options,
- shareInfo,
+ ...args,
};
} else {
return {
- origin,
userOptions,
- options,
+ ...args,
};
}
},
diff --git a/packages/runtime/__tests__/__snapshots__/preload-remote.spec.ts.snap b/packages/runtime/__tests__/__snapshots__/preload-remote.spec.ts.snap
index a55bd9b418d..8dd55dc15a5 100644
--- a/packages/runtime/__tests__/__snapshots__/preload-remote.spec.ts.snap
+++ b/packages/runtime/__tests__/__snapshots__/preload-remote.spec.ts.snap
@@ -4,28 +4,28 @@ exports[`preload-remote inBrowser > 1 preload with default config 1`] = `
{
"links": [
{
- "href": "http://localhost:1111/resources/preload/preload-resource/button.sync.js",
+ "href": "http://localhost:1111/resources/preload/preload-resource/button.sync.css",
"rel": "preload",
- "type": "script",
+ "type": "style",
},
{
- "href": "http://localhost:1111/resources/preload/preload-resource/sub1-button/button.sync.js",
+ "href": "http://localhost:1111/resources/preload/preload-resource/button.sync.js",
"rel": "preload",
"type": "script",
},
{
- "href": "http://localhost:1111/resources/preload/preload-resource/button.sync.css",
+ "href": "http://localhost:1111/resources/preload/preload-resource/sub1-button/button.sync.js",
"rel": "preload",
- "type": "style",
+ "type": "script",
},
],
"scripts": [
{
- "crossorigin": "",
+ "crossorigin": "anonymous",
"src": "http://localhost:1111/resources/preload/preload-resource/federation-remote-entry.js",
},
{
- "crossorigin": "",
+ "crossorigin": "anonymous",
"src": "http://localhost:1111/resources/preload/preload-resource/sub1-button/federation-remote-entry.js",
},
],
@@ -35,6 +35,16 @@ exports[`preload-remote inBrowser > 1 preload with default config 1`] = `
exports[`preload-remote inBrowser > 2 preload with all config 1`] = `
{
"links": [
+ {
+ "href": "http://localhost:1111/resources/preload/preload-resource/sub2/button.async.css",
+ "rel": "preload",
+ "type": "style",
+ },
+ {
+ "href": "http://localhost:1111/resources/preload/preload-resource/sub2/button.sync.css",
+ "rel": "preload",
+ "type": "style",
+ },
{
"href": "http://localhost:1111/resources/preload/preload-resource/sub2/button.async.js",
"rel": "preload",
@@ -65,24 +75,14 @@ exports[`preload-remote inBrowser > 2 preload with all config 1`] = `
"rel": "preload",
"type": "script",
},
- {
- "href": "http://localhost:1111/resources/preload/preload-resource/sub2/button.async.css",
- "rel": "preload",
- "type": "style",
- },
- {
- "href": "http://localhost:1111/resources/preload/preload-resource/sub2/button.sync.css",
- "rel": "preload",
- "type": "style",
- },
],
"scripts": [
{
- "crossorigin": "",
+ "crossorigin": "anonymous",
"src": "http://localhost:1111/resources/preload/preload-resource/sub2/federation-remote-entry.js",
},
{
- "crossorigin": "",
+ "crossorigin": "anonymous",
"src": "http://localhost:1111/resources/preload/preload-resource/sub2-button/federation-remote-entry.js",
},
],
@@ -100,7 +100,7 @@ exports[`preload-remote inBrowser > 3 preload with expose config 1`] = `
],
"scripts": [
{
- "crossorigin": "",
+ "crossorigin": "anonymous",
"src": "http://localhost:1111/resources/preload/preload-resource/sub3/federation-remote-entry.js",
},
],
@@ -118,7 +118,7 @@ exports[`preload-remote inBrowser > 3 preload with expose config 2`] = `
],
"scripts": [
{
- "crossorigin": "",
+ "crossorigin": "anonymous",
"src": "http://localhost:1111/resources/preload/preload-resource/sub3/federation-remote-entry.js",
},
],
diff --git a/packages/runtime/src/plugins/snapshot/index.ts b/packages/runtime/src/plugins/snapshot/index.ts
index df42e563a9b..a0e6ab22195 100644
--- a/packages/runtime/src/plugins/snapshot/index.ts
+++ b/packages/runtime/src/plugins/snapshot/index.ts
@@ -67,7 +67,7 @@ export function snapshotPlugin(): FederationRuntimePlugin {
);
if (assets) {
- preloadAssets(remoteInfo, origin, assets);
+ preloadAssets(remoteInfo, origin, assets, false);
}
return {
diff --git a/packages/runtime/src/utils/preload.ts b/packages/runtime/src/utils/preload.ts
index c9edd96b551..9acba137c76 100644
--- a/packages/runtime/src/utils/preload.ts
+++ b/packages/runtime/src/utils/preload.ts
@@ -1,4 +1,4 @@
-import { createLink } from '@module-federation/sdk';
+import { createLink, createScript } from '@module-federation/sdk';
import {
PreloadAssets,
PreloadConfig,
@@ -69,6 +69,8 @@ export function preloadAssets(
remoteInfo: RemoteInfo,
host: FederationHost,
assets: PreloadAssets,
+ // It is used to distinguish preload from load remote parallel loading
+ useLinkPreload: boolean = true,
): void {
const { cssAssets, jsAssetsWithoutEntry, entryAssets } = assets;
@@ -131,52 +133,96 @@ export function preloadAssets(
}
});
- const fragment = document.createDocumentFragment();
- cssAssets.forEach((cssUrl) => {
- const { link: cssEl, needAttach } = createLink(
- cssUrl,
- () => {},
- {
- rel: 'preload',
- as: 'style',
- },
- (url: string) => {
- const res = host.loaderHook.lifecycle.createLink.emit({
- url,
- });
- if (res instanceof HTMLLinkElement) {
- return res;
- }
- return;
- },
- );
-
- needAttach && fragment.appendChild(cssEl);
- });
+ if (useLinkPreload) {
+ cssAssets.forEach((cssUrl) => {
+ const { link: cssEl, needAttach } = createLink({
+ url: cssUrl,
+ cb: () => {},
+ attrs: {
+ rel: 'preload',
+ as: 'style',
+ crossorigin: 'anonymous',
+ },
+ createLinkHook: (url: string) => {
+ const res = host.loaderHook.lifecycle.createLink.emit({
+ url,
+ });
+ if (res instanceof HTMLLinkElement) {
+ return res;
+ }
+ return;
+ },
+ });
- jsAssetsWithoutEntry.forEach((jsUrl) => {
- const { link: linkEl, needAttach } = createLink(
- jsUrl,
- () => {
- // noop
- },
- {
- rel: 'preload',
- as: 'script',
- },
- (url: string) => {
- const res = host.loaderHook.lifecycle.createLink.emit({
- url,
- });
- if (res instanceof HTMLLinkElement) {
- return res;
- }
- return;
- },
- );
- needAttach && document.head.appendChild(linkEl);
- });
+ needAttach && document.head.appendChild(cssEl);
+ });
+ } else {
+ cssAssets.forEach((cssUrl) => {
+ const { link: cssEl, needAttach } = createLink({
+ url: cssUrl,
+ cb: () => {},
+ attrs: {
+ rel: 'stylesheet',
+ type: 'text/css',
+ },
+ createLinkHook: (url: string) => {
+ const res = host.loaderHook.lifecycle.createLink.emit({
+ url,
+ });
+ if (res instanceof HTMLLinkElement) {
+ return res;
+ }
+ return;
+ },
+ });
+
+ needAttach && document.head.appendChild(cssEl);
+ });
+ }
- document.head.appendChild(fragment);
+ if (useLinkPreload) {
+ jsAssetsWithoutEntry.forEach((jsUrl) => {
+ const { link: linkEl, needAttach } = createLink({
+ url: jsUrl,
+ cb: () => {},
+ attrs: {
+ rel: 'preload',
+ as: 'script',
+ crossorigin: 'anonymous',
+ },
+ createLinkHook: (url: string) => {
+ const res = host.loaderHook.lifecycle.createLink.emit({
+ url,
+ });
+ if (res instanceof HTMLLinkElement) {
+ return res;
+ }
+ return;
+ },
+ });
+ needAttach && document.head.appendChild(linkEl);
+ });
+ } else {
+ jsAssetsWithoutEntry.forEach((jsUrl) => {
+ const { script: scriptEl, needAttach } = createScript({
+ url: jsUrl,
+ cb: () => {},
+ attrs: {
+ crossorigin: 'anonymous',
+ fetchpriority: 'high',
+ },
+ createScriptHook: (url: string) => {
+ const res = host.loaderHook.lifecycle.createScript.emit({
+ url,
+ });
+ if (res instanceof HTMLScriptElement) {
+ return res;
+ }
+ return;
+ },
+ });
+ needAttach && document.head.appendChild(scriptEl);
+ });
+ }
}
}
diff --git a/packages/sdk/__tests__/dom.spec.ts b/packages/sdk/__tests__/dom.spec.ts
index 0b4a69e42a1..3f219ad9175 100644
--- a/packages/sdk/__tests__/dom.spec.ts
+++ b/packages/sdk/__tests__/dom.spec.ts
@@ -8,7 +8,7 @@ describe('createScript', () => {
it('should create a new script element if one does not exist', () => {
const url = 'https://example.com/script.js';
const cb = jest.fn();
- const { script, needAttach } = createScript(url, cb);
+ const { script, needAttach } = createScript({ url, cb });
expect(script.tagName).toBe('SCRIPT');
expect(script.src).toBe(url);
@@ -19,7 +19,7 @@ describe('createScript', () => {
const url = 'https://example.com/script.js';
const cb = jest.fn();
document.body.innerHTML = ``;
- const { script, needAttach } = createScript(url, cb);
+ const { script, needAttach } = createScript({ url, cb });
expect(script.tagName).toBe('SCRIPT');
expect(script.src).toBe(url);
@@ -30,7 +30,7 @@ describe('createScript', () => {
const url = 'https://example.com/script.js';
const cb = jest.fn();
const attrs = { async: true, 'data-test': 'test' };
- const { script } = createScript(url, cb, attrs);
+ const { script } = createScript({ url, cb, attrs });
expect(script.async).toBe(true);
expect(script.getAttribute('data-test')).toBe('test');
@@ -39,7 +39,7 @@ describe('createScript', () => {
it('should call the callback when the script loads', () => {
const url = 'https://example.com/script.js';
const cb = jest.fn();
- const { script, needAttach } = createScript(url, cb);
+ const { script, needAttach } = createScript({ url, cb });
if (needAttach) {
document.body.appendChild(script);
@@ -52,7 +52,12 @@ describe('createScript', () => {
it('should call the callback when the script times out', () => {
const url = 'https://example.com/script.js';
const cb = jest.fn();
- createScript(url, cb, {}, () => ({ timeout: 100 }));
+ createScript({
+ url,
+ cb,
+ attrs: {},
+ createScriptHook: () => ({ timeout: 100 }),
+ });
setTimeout(() => {
expect(cb).toHaveBeenCalled();
@@ -71,7 +76,7 @@ describe('createScript', () => {
it('should use the default timeout of 20000ms if no timeout is specified', () => {
const url = 'https://example.com/script.js';
const cb = jest.fn();
- const { script } = createScript(url, cb);
+ const { script } = createScript({ url, cb });
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 20000);
});
@@ -80,7 +85,12 @@ describe('createScript', () => {
const url = 'https://example.com/script.js';
const cb = jest.fn();
const customTimeout = 5000;
- createScript(url, cb, {}, () => ({ timeout: customTimeout }));
+ createScript({
+ url,
+ cb,
+ attrs: {},
+ createScriptHook: () => ({ timeout: customTimeout }),
+ });
expect(setTimeout).toHaveBeenCalledWith(
expect.any(Function),
@@ -91,7 +101,7 @@ describe('createScript', () => {
it('should clear the timeout when the script loads successfully', () => {
const url = 'https://example.com/script.js';
const cb = jest.fn();
- const { script, needAttach } = createScript(url, cb);
+ const { script, needAttach } = createScript({ url, cb });
if (needAttach) {
document.body.appendChild(script);
@@ -104,7 +114,7 @@ describe('createScript', () => {
it('should clear the timeout when the script fails to load', () => {
const url = 'https://example.com/script.js';
const cb = jest.fn();
- const { script, needAttach } = createScript(url, cb);
+ const { script, needAttach } = createScript({ url, cb });
if (needAttach) {
document.body.appendChild(script);
@@ -124,7 +134,11 @@ describe('createLink', () => {
it('should create a new link element if one does not exist', () => {
const url = 'https://example.com/script.js';
const cb = jest.fn();
- const { link, needAttach } = createLink(url, cb, { as: 'script' });
+ const { link, needAttach } = createLink({
+ url,
+ cb,
+ attrs: { as: 'script' },
+ });
expect(link.tagName).toBe('LINK');
expect(link.href).toBe(url);
@@ -136,9 +150,13 @@ describe('createLink', () => {
const url = 'https://example.com/script.js';
const cb = jest.fn();
document.head.innerHTML = ``;
- const { link, needAttach } = createLink(url, cb, {
- rel: 'preload',
- as: 'script',
+ const { link, needAttach } = createLink({
+ url,
+ cb,
+ attrs: {
+ rel: 'preload',
+ as: 'script',
+ },
});
expect(link.tagName).toBe('LINK');
@@ -150,7 +168,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 });
expect(link.rel).toBe('preload');
expect(link.getAttribute('as')).toBe('script');
@@ -160,7 +178,11 @@ describe('createLink', () => {
it('should call the callback when the link loads', () => {
const url = 'https://example.com/script.js';
const cb = jest.fn();
- const { link, needAttach } = createLink(url, cb, { as: 'script' });
+ const { link, needAttach } = createLink({
+ url,
+ cb,
+ attrs: { as: 'script' },
+ });
if (needAttach) {
document.head.appendChild(link);
@@ -173,7 +195,11 @@ describe('createLink', () => {
it('should call the callback when the link fails to load', () => {
const url = 'https://example.com/script.js';
const cb = jest.fn();
- const { link, needAttach } = createLink(url, cb, { as: 'script' });
+ const { link, needAttach } = createLink({
+ url,
+ cb,
+ attrs: { as: 'script' },
+ });
if (needAttach) {
document.head.appendChild(link);
@@ -190,7 +216,12 @@ describe('createLink', () => {
customLink.href = url;
customLink.rel = 'preload';
customLink.setAttribute('as', 'script');
- const { link } = createLink(url, cb, {}, () => customLink);
+ const { link } = createLink({
+ url,
+ cb,
+ attrs: {},
+ createLinkHook: () => customLink,
+ });
expect(link).toBe(customLink);
});
diff --git a/packages/sdk/src/dom.ts b/packages/sdk/src/dom.ts
index 37dbeb6aaa9..026ce92ac52 100644
--- a/packages/sdk/src/dom.ts
+++ b/packages/sdk/src/dom.ts
@@ -27,12 +27,13 @@ export type CreateScriptHookReturn =
| { script?: HTMLScriptElement; timeout?: number }
| void;
-export function createScript(
- url: string,
- cb: (value: void | PromiseLike) => void,
- attrs?: Record,
- createScriptHook?: (url: string) => CreateScriptHookReturn,
-): { script: HTMLScriptElement; needAttach: boolean } {
+export function createScript(info: {
+ url: string;
+ cb?: (value: void | PromiseLike) => void;
+ attrs?: Record;
+ needDeleteScript?: boolean;
+ createScriptHook?: (url: string) => CreateScriptHookReturn;
+}): { script: HTMLScriptElement; needAttach: boolean } {
// Retrieve the existing script element by its src attribute
let script: HTMLScriptElement | null = null;
let needAttach = true;
@@ -42,7 +43,7 @@ export function createScript(
for (let i = 0; i < scripts.length; i++) {
const s = scripts[i];
const scriptSrc = s.getAttribute('src');
- if (scriptSrc && isStaticResourcesEqual(scriptSrc, url)) {
+ if (scriptSrc && isStaticResourcesEqual(scriptSrc, info.url)) {
script = s;
needAttach = false;
break;
@@ -52,9 +53,9 @@ export function createScript(
if (!script) {
script = document.createElement('script');
script.type = 'text/javascript';
- script.src = url;
- if (createScriptHook) {
- const createScriptRes = createScriptHook(url);
+ script.src = info.url;
+ if (info.createScriptHook) {
+ const createScriptRes = info.createScriptHook(info.url);
if (createScriptRes instanceof HTMLScriptElement) {
script = createScriptRes;
@@ -65,6 +66,7 @@ export function createScript(
}
}
+ const attrs = info.attrs;
if (attrs) {
Object.keys(attrs).forEach((name) => {
if (script) {
@@ -88,34 +90,39 @@ export function createScript(
script.onerror = null;
script.onload = null;
safeWrapper(() => {
- script?.parentNode && script.parentNode.removeChild(script);
+ if (info.needDeleteScript) {
+ script?.parentNode && script.parentNode.removeChild(script);
+ }
});
if (prev) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const res = (prev as any)(event);
- cb();
+ info?.cb?.();
return res;
}
}
- cb();
+ info?.cb?.();
};
script.onerror = onScriptComplete.bind(null, script.onerror);
script.onload = onScriptComplete.bind(null, script.onload);
timeoutId = setTimeout(() => {
- onScriptComplete(null, new Error(`Remote script "${url}" time-outed.`));
+ onScriptComplete(
+ null,
+ new Error(`Remote script "${info.url}" time-outed.`),
+ );
}, timeout);
return { script, needAttach };
}
-export function createLink(
- url: string,
- cb: (value: void | PromiseLike) => void,
- attrs: Record = {},
- createLinkHook?: (url: string) => HTMLLinkElement | void,
-) {
+export function createLink(info: {
+ url: string;
+ cb: (value: void | PromiseLike) => void;
+ attrs: Record;
+ createLinkHook?: (url: string) => HTMLLinkElement | void;
+}) {
//
// Retrieve the existing script element by its src attribute
@@ -128,8 +135,8 @@ export function createLink(
const linkRef = l.getAttribute('ref');
if (
linkHref &&
- isStaticResourcesEqual(linkHref, url) &&
- linkRef === attrs['ref']
+ isStaticResourcesEqual(linkHref, info.url) &&
+ linkRef === info.attrs['ref']
) {
link = l;
needAttach = false;
@@ -139,17 +146,17 @@ export function createLink(
if (!link) {
link = document.createElement('link');
- link.setAttribute('href', url);
- link.setAttribute('crossorigin', 'anonymous');
+ link.setAttribute('href', info.url);
- if (createLinkHook) {
- const createLinkRes = createLinkHook(url);
+ if (info.createLinkHook) {
+ const createLinkRes = info.createLinkHook(info.url);
if (createLinkRes instanceof HTMLLinkElement) {
link = createLinkRes;
}
}
}
+ const attrs = info.attrs;
if (attrs) {
Object.keys(attrs).forEach((name) => {
if (link) {
@@ -173,11 +180,11 @@ export function createLink(
if (prev) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const res = (prev as any)(event);
- cb();
+ info.cb();
return res;
}
}
- cb();
+ info.cb();
};
link.onerror = onLinkComplete.bind(null, link.onerror);
@@ -193,14 +200,19 @@ export function loadScript(
createScriptHook?: (url: string) => CreateScriptHookReturn;
},
) {
- const { attrs, createScriptHook } = info;
+ const { attrs = {}, createScriptHook } = info;
return new Promise((resolve, _reject) => {
- const { script, needAttach } = createScript(
+ const { script, needAttach } = createScript({
url,
- resolve,
- attrs,
+ cb: resolve,
+ attrs: {
+ crossorigin: 'anonymous',
+ fetchpriority: 'high',
+ ...attrs,
+ },
createScriptHook,
- );
- needAttach && document.getElementsByTagName('head')[0].appendChild(script);
+ needDeleteScript: true,
+ });
+ needAttach && document.head.appendChild(script);
});
}