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 webpack png; 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); }); }