From 12a67c45ba6e6faa48588858f3b062598ba9a933 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Mon, 5 Aug 2024 11:52:42 +0200 Subject: [PATCH 01/18] feat: add loadEntry loader hook --- packages/runtime/src/core.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/runtime/src/core.ts b/packages/runtime/src/core.ts index 69e5f1ba2e1..e67b6ab0063 100644 --- a/packages/runtime/src/core.ts +++ b/packages/runtime/src/core.ts @@ -102,6 +102,7 @@ export class FederationHost { ], HTMLLinkElement | void >(), + loadEntry: new AsyncHook(), // only work for manifest , so not open to the public yet fetch: new AsyncHook< [string, RequestInit], From 141ab717e6dad1c16a7adaa5af934c6b3619c53a Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Mon, 5 Aug 2024 13:25:26 +0200 Subject: [PATCH 02/18] feat: dom & node runtime plugins --- packages/runtime/src/core.ts | 10 +- packages/runtime/src/module/index.ts | 23 --- packages/runtime/src/plugins/dom/index.ts | 122 +++++++++++++++ packages/runtime/src/plugins/node/index.ts | 65 ++++++++ packages/runtime/src/utils/load.ts | 164 ++------------------- packages/runtime/src/utils/preload.ts | 44 ------ packages/sdk/src/dom.ts | 24 +-- packages/sdk/src/node.ts | 15 +- 8 files changed, 222 insertions(+), 245 deletions(-) create mode 100644 packages/runtime/src/plugins/dom/index.ts create mode 100644 packages/runtime/src/plugins/node/index.ts diff --git a/packages/runtime/src/core.ts b/packages/runtime/src/core.ts index e67b6ab0063..f6e382d0cfb 100644 --- a/packages/runtime/src/core.ts +++ b/packages/runtime/src/core.ts @@ -102,7 +102,15 @@ export class FederationHost { ], HTMLLinkElement | void >(), - loadEntry: new AsyncHook(), + loadEntry: new AsyncHook< + [ + { + remoteInfo: RemoteInfo; + remoteEntryExports?: RemoteEntryExports; + }, + ], + Promise + >(), // only work for manifest , so not open to the public yet fetch: new AsyncHook< [string, RequestInit], diff --git a/packages/runtime/src/module/index.ts b/packages/runtime/src/module/index.ts index 28d22aba40d..61681fde422 100644 --- a/packages/runtime/src/module/index.ts +++ b/packages/runtime/src/module/index.ts @@ -32,29 +32,6 @@ class Module { const remoteEntryExports = await getRemoteEntry({ remoteInfo: this.remoteInfo, remoteEntryExports: this.remoteEntryExports, - createScriptHook: (url: string, attrs: any) => { - const res = this.host.loaderHook.lifecycle.createScript.emit({ - url, - attrs, - }); - - if (!res) return; - - if (typeof document === 'undefined') { - //todo: needs real fix - return res as HTMLScriptElement; - } - - if (res instanceof HTMLScriptElement) { - return res; - } - - if ('script' in res || 'timeout' in res) { - return res; - } - - return; - }, }); assert( remoteEntryExports, diff --git a/packages/runtime/src/plugins/dom/index.ts b/packages/runtime/src/plugins/dom/index.ts new file mode 100644 index 00000000000..1e2b5386817 --- /dev/null +++ b/packages/runtime/src/plugins/dom/index.ts @@ -0,0 +1,122 @@ +import { loadScript } from '@module-federation/sdk'; +import { FederationRuntimePlugin } from '../../type/plugin'; +import { assert } from '../../utils'; +import { RemoteEntryExports } from '../../type'; +import { getRemoteEntryExports } from '../../global'; + +async function loadEsmEntry({ + entry, + remoteEntryExports, +}: { + entry: string; + remoteEntryExports: RemoteEntryExports | undefined; +}): Promise { + return new Promise((resolve, reject) => { + try { + if (!remoteEntryExports) { + // eslint-disable-next-line no-eval + new Function( + 'callbacks', + `import("${entry}").then(callbacks[0]).catch(callbacks[1])`, + )([resolve, reject]); + } else { + resolve(remoteEntryExports); + } + } catch (e) { + reject(e); + } + }); +} + +async function loadSystemJsEntry({ + entry, + remoteEntryExports, +}: { + entry: string; + remoteEntryExports: RemoteEntryExports | undefined; +}): Promise { + return new Promise((resolve, reject) => { + try { + if (!remoteEntryExports) { + // eslint-disable-next-line no-eval + new Function( + 'callbacks', + `System.import("${entry}").then(callbacks[0]).catch(callbacks[1])`, + )([resolve, reject]); + } else { + resolve(remoteEntryExports); + } + } catch (e) { + reject(e); + } + }); +} + +async function loadEntryScript({ + name, + globalName, + entry, +}: { + name: string; + globalName: string; + entry: string; +}): Promise { + const { entryExports: remoteEntryExports } = getRemoteEntryExports( + name, + globalName, + ); + + if (remoteEntryExports) { + return remoteEntryExports; + } + + return loadScript(entry, { attrs: {} }) + .then(() => { + const { remoteEntryKey, entryExports } = getRemoteEntryExports( + name, + globalName, + ); + + assert( + entryExports, + ` + Unable to use the ${name}'s '${entry}' URL with ${remoteEntryKey}'s globalName to get remoteEntry exports. + Possible reasons could be:\n + 1. '${entry}' is not the correct URL, or the remoteEntry resource or name is incorrect.\n + 2. ${remoteEntryKey} cannot be used to get remoteEntry exports in the window object. + `, + ); + + return entryExports; + }) + .catch((e) => { + throw e; + }); +} + +export function domPlugin(): FederationRuntimePlugin { + return { + name: 'dom-plugin', + async loadEntry(args) { + const { remoteInfo, remoteEntryExports } = args; + const { entry, entryGlobalName, name, type } = remoteInfo; + + if (['esm', 'module'].includes(type)) { + return loadEsmEntry({ + entry, + remoteEntryExports, + }); + } else if (type === 'system') { + return loadSystemJsEntry({ + entry, + remoteEntryExports, + }); + } + return loadEntryScript({ + entry, + globalName: entryGlobalName, + name, + }); + }, + }; +} diff --git a/packages/runtime/src/plugins/node/index.ts b/packages/runtime/src/plugins/node/index.ts new file mode 100644 index 00000000000..32b676154aa --- /dev/null +++ b/packages/runtime/src/plugins/node/index.ts @@ -0,0 +1,65 @@ +import { loadScriptNode } from '@module-federation/sdk'; +import { FederationRuntimePlugin } from '../../type/plugin'; +import { getRemoteEntryExports } from '../../global'; +import { RemoteEntryExports } from '../../type'; +import { assert } from '../../utils'; + +export async function loadEntryScript({ + name, + globalName, + entry, +}: { + name: string; + globalName: string; + entry: string; +}): Promise { + const { entryExports: remoteEntryExports } = getRemoteEntryExports( + name, + globalName, + ); + + if (remoteEntryExports) { + return remoteEntryExports; + } + + return loadScriptNode(entry, { + attrs: { name, globalName }, + }) + .then(() => { + const { remoteEntryKey, entryExports } = getRemoteEntryExports( + name, + globalName, + ); + + assert( + entryExports, + ` + Unable to use the ${name}'s '${entry}' URL with ${remoteEntryKey}'s globalName to get remoteEntry exports. + Possible reasons could be:\n + 1. '${entry}' is not the correct URL, or the remoteEntry resource or name is incorrect.\n + 2. ${remoteEntryKey} cannot be used to get remoteEntry exports in the window object. + `, + ); + + return entryExports; + }) + .catch((e) => { + throw e; + }); +} + +export function nodePlugin(): FederationRuntimePlugin { + return { + name: 'node-plugin', + async loadEntry(args) { + const { remoteInfo } = args; + const { entry, entryGlobalName, name } = remoteInfo; + + return loadEntryScript({ + entry, + globalName: entryGlobalName, + name, + }); + }, + }; +} diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts index 938d1b8686c..993a3a04321 100644 --- a/packages/runtime/src/utils/load.ts +++ b/packages/runtime/src/utils/load.ts @@ -1,136 +1,7 @@ -import { - composeKeyWithSeparator, - loadScript, - loadScriptNode, - CreateScriptHookReturn, -} from '@module-federation/sdk'; -import { assert } from '../utils/logger'; -import { getRemoteEntryExports, globalLoading } from '../global'; -import { Remote, RemoteEntryExports, RemoteInfo } from '../type'; +import { composeKeyWithSeparator } from '@module-federation/sdk'; import { DEFAULT_REMOTE_TYPE, DEFAULT_SCOPE } from '../constant'; - -export async function loadEsmEntry({ - entry, - remoteEntryExports, -}: { - entry: string; - remoteEntryExports: RemoteEntryExports | undefined; -}): Promise { - return new Promise((resolve, reject) => { - try { - if (!remoteEntryExports) { - // eslint-disable-next-line no-eval - new Function( - 'callbacks', - `import("${entry}").then(callbacks[0]).catch(callbacks[1])`, - )([resolve, reject]); - } else { - resolve(remoteEntryExports); - } - } catch (e) { - reject(e); - } - }); -} - -export async function loadSystemJsEntry({ - entry, - remoteEntryExports, -}: { - entry: string; - remoteEntryExports: RemoteEntryExports | undefined; -}): Promise { - return new Promise((resolve, reject) => { - try { - if (!remoteEntryExports) { - // eslint-disable-next-line no-eval - new Function( - 'callbacks', - `System.import("${entry}").then(callbacks[0]).catch(callbacks[1])`, - )([resolve, reject]); - } else { - resolve(remoteEntryExports); - } - } catch (e) { - reject(e); - } - }); -} - -export async function loadEntryScript({ - name, - globalName, - entry, - createScriptHook, -}: { - name: string; - globalName: string; - entry: string; - createScriptHook?: ( - url: string, - attrs?: Record | undefined, - ) => CreateScriptHookReturn; -}): Promise { - const { entryExports: remoteEntryExports } = getRemoteEntryExports( - name, - globalName, - ); - - if (remoteEntryExports) { - return remoteEntryExports; - } - - if (typeof document === 'undefined') { - return loadScriptNode(entry, { - attrs: { name, globalName }, - createScriptHook, - }) - .then(() => { - const { remoteEntryKey, entryExports } = getRemoteEntryExports( - name, - globalName, - ); - - assert( - entryExports, - ` - Unable to use the ${name}'s '${entry}' URL with ${remoteEntryKey}'s globalName to get remoteEntry exports. - Possible reasons could be:\n - 1. '${entry}' is not the correct URL, or the remoteEntry resource or name is incorrect.\n - 2. ${remoteEntryKey} cannot be used to get remoteEntry exports in the window object. - `, - ); - - return entryExports; - }) - .catch((e) => { - throw e; - }); - } - - return loadScript(entry, { attrs: {}, createScriptHook }) - .then(() => { - const { remoteEntryKey, entryExports } = getRemoteEntryExports( - name, - globalName, - ); - - assert( - entryExports, - ` - Unable to use the ${name}'s '${entry}' URL with ${remoteEntryKey}'s globalName to get remoteEntry exports. - Possible reasons could be:\n - 1. '${entry}' is not the correct URL, or the remoteEntry resource or name is incorrect.\n - 2. ${remoteEntryKey} cannot be used to get remoteEntry exports in the window object. - `, - ); - - return entryExports; - }) - .catch((e) => { - throw e; - }); -} +import { globalLoading } from '../global'; +import { Remote, RemoteEntryExports, RemoteInfo } from '../type'; export function getRemoteEntryUniqueKey(remoteInfo: RemoteInfo): string { const { entry, name } = remoteInfo; @@ -140,40 +11,25 @@ export function getRemoteEntryUniqueKey(remoteInfo: RemoteInfo): string { export async function getRemoteEntry({ remoteEntryExports, remoteInfo, - createScriptHook, }: { remoteInfo: RemoteInfo; remoteEntryExports?: RemoteEntryExports | undefined; - createScriptHook?: ( - url: string, - attrs?: Record | undefined, - ) => CreateScriptHookReturn; }): Promise { - const { entry, name, type, entryGlobalName } = remoteInfo; const uniqueKey = getRemoteEntryUniqueKey(remoteInfo); if (remoteEntryExports) { return remoteEntryExports; } if (!globalLoading[uniqueKey]) { - if (['esm', 'module'].includes(type)) { - globalLoading[uniqueKey] = loadEsmEntry({ - entry, + // @ts-ignore + const loaderHooks = __webpack_require__.federation.instance.loaderHook; + const hook = () => { + return loaderHooks.lifecycle.loadEntry.emit({ + remoteInfo, remoteEntryExports, }); - } else if (type === 'system') { - globalLoading[uniqueKey] = loadSystemJsEntry({ - entry, - remoteEntryExports, - }); - } else { - globalLoading[uniqueKey] = loadEntryScript({ - name, - globalName: entryGlobalName, - entry, - createScriptHook, - }); - } + }; + globalLoading[uniqueKey] = hook(); } return globalLoading[uniqueKey]; } diff --git a/packages/runtime/src/utils/preload.ts b/packages/runtime/src/utils/preload.ts index 8edeb1baeb8..774d1998005 100644 --- a/packages/runtime/src/utils/preload.ts +++ b/packages/runtime/src/utils/preload.ts @@ -82,55 +82,11 @@ export function preloadAssets( getRemoteEntry({ remoteInfo: moduleInfo, remoteEntryExports: module.remoteEntryExports, - createScriptHook: (url: string, attrs: any) => { - const res = host.loaderHook.lifecycle.createScript.emit({ - url, - attrs, - }); - if (!res) return; - - if (typeof document === 'undefined') { - //todo: needs real fix - return res as HTMLScriptElement; - } - - if (res instanceof HTMLScriptElement) { - return res; - } - - if ('script' in res || 'timeout' in res) { - return res; - } - - return; - }, }); } else { getRemoteEntry({ remoteInfo: moduleInfo, remoteEntryExports: undefined, - createScriptHook: (url: string, attrs: any) => { - const res = host.loaderHook.lifecycle.createScript.emit({ - url, - attrs, - }); - if (!res) return; - - if (typeof document === 'undefined') { - //todo: needs real fix - return res as HTMLScriptElement; - } - - if (res instanceof HTMLScriptElement) { - return res; - } - - if ('script' in res || 'timeout' in res) { - return res; - } - - return; - }, }); } }); diff --git a/packages/sdk/src/dom.ts b/packages/sdk/src/dom.ts index e86bfb8640f..49cd95d7f3c 100644 --- a/packages/sdk/src/dom.ts +++ b/packages/sdk/src/dom.ts @@ -22,20 +22,11 @@ export function isStaticResourcesEqual(url1: string, url2: string): boolean { return relativeUrl1 === relativeUrl2; } -export type CreateScriptHookReturn = - | HTMLScriptElement - | { script?: HTMLScriptElement; timeout?: number } - | void; - export function createScript(info: { url: string; cb?: (value: void | PromiseLike) => void; attrs?: Record; needDeleteScript?: boolean; - createScriptHook?: ( - url: string, - attrs?: Record | undefined, - ) => CreateScriptHookReturn; }): { script: HTMLScriptElement; needAttach: boolean } { // Retrieve the existing script element by its src attribute let script: HTMLScriptElement | null = null; @@ -57,8 +48,12 @@ export function createScript(info: { script = document.createElement('script'); script.type = 'text/javascript'; script.src = info.url; - if (info.createScriptHook) { - const createScriptRes = info.createScriptHook(info.url, info.attrs); + + //@ts-ignore + const loaderHooks = __webpack_require__.federation.instance.loaderHook; + const createScriptHook = loaderHooks.lifecycle.createScript; + if (createScriptHook) { + const createScriptRes = createScriptHook.emit(info.url, info.attrs); if (createScriptRes instanceof HTMLScriptElement) { script = createScriptRes; @@ -205,13 +200,9 @@ export function loadScript( url: string, info: { attrs?: Record; - createScriptHook?: ( - url: string, - attrs?: Record | undefined, - ) => CreateScriptHookReturn; }, ) { - const { attrs = {}, createScriptHook } = info; + const { attrs = {} } = info; return new Promise((resolve, _reject) => { const { script, needAttach } = createScript({ url, @@ -220,7 +211,6 @@ export function loadScript( fetchpriority: 'high', ...attrs, }, - createScriptHook, needDeleteScript: true, }); needAttach && document.head.appendChild(script); diff --git a/packages/sdk/src/node.ts b/packages/sdk/src/node.ts index e60b2b1a776..e45950f9aa2 100644 --- a/packages/sdk/src/node.ts +++ b/packages/sdk/src/node.ts @@ -42,12 +42,17 @@ export function createScriptNode( url: string, cb: (error?: Error, scriptContext?: any) => void, attrs?: Record, - createScriptHook?: (url: string) => any | void, ) { + //@ts-ignore + const loaderHooks = __webpack_require__.federation.instance.loaderHook; + const createScriptHook = loaderHooks.lifecycle.createScript; if (createScriptHook) { - const hookResult = createScriptHook(url); - if (hookResult && typeof hookResult === 'object' && 'url' in hookResult) { - url = hookResult.url; + const createScriptRes = createScriptHook.emit(url); + + if (createScriptRes) { + if (typeof createScriptRes === 'object' && 'url' in createScriptRes) { + url = createScriptRes.url; + } } } @@ -140,7 +145,6 @@ export function loadScriptNode( url: string, info: { attrs?: Record; - createScriptHook?: (url: string) => void; }, ) { return new Promise((resolve, reject) => { @@ -159,7 +163,6 @@ export function loadScriptNode( } }, info.attrs, - info.createScriptHook, ); }); } From 5b441bf3e511de8ea5749bd89b33cebadeaae3af Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Mon, 5 Aug 2024 13:41:01 +0200 Subject: [PATCH 03/18] feat: use env runtime plugins --- packages/runtime/src/core.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/core.ts b/packages/runtime/src/core.ts index f6e382d0cfb..eb790a1700a 100644 --- a/packages/runtime/src/core.ts +++ b/packages/runtime/src/core.ts @@ -22,6 +22,8 @@ import { SyncWaterfallHook, } from './utils/hooks'; import { generatePreloadAssetsPlugin } from './plugins/generate-preload-assets'; +import { domPlugin } from './plugins/dom'; +import { nodePlugin } from './plugins/node'; import { snapshotPlugin } from './plugins/snapshot'; import { isBrowserEnv } from './utils/env'; import { getRemoteInfo } from './utils/load'; @@ -124,7 +126,11 @@ export class FederationHost { const defaultOptions: Options = { id: getBuilderId(), name: userOptions.name, - plugins: [snapshotPlugin(), generatePreloadAssetsPlugin()], + plugins: [ + this.envPlugin(), + snapshotPlugin(), + generatePreloadAssetsPlugin(), + ], remotes: [], shared: {}, inBrowser: isBrowserEnv(), @@ -143,6 +149,13 @@ export class FederationHost { this.options = this.formatOptions(defaultOptions, userOptions); } + private envPlugin() { + if (typeof document === 'undefined') { + return nodePlugin(); + } + return domPlugin(); + } + initOptions(userOptions: UserOptions): Options { this.registerPlugins(userOptions.plugins); const options = this.formatOptions(this.options, userOptions); From 4cd34a955d4e490786037919f8ff4a7d72a067da Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Mon, 5 Aug 2024 13:49:28 +0200 Subject: [PATCH 04/18] refactor: single source of truth for createScriptHook return type --- packages/runtime/src/core.ts | 6 ++++-- packages/runtime/src/utils/preload.ts | 10 ---------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/runtime/src/core.ts b/packages/runtime/src/core.ts index eb790a1700a..ae8866f3fb5 100644 --- a/packages/runtime/src/core.ts +++ b/packages/runtime/src/core.ts @@ -1,4 +1,3 @@ -import type { CreateScriptHookReturn } from '@module-federation/sdk'; import { Options, PreloadRemoteArgs, @@ -94,7 +93,10 @@ export class FederationHost { attrs?: Record; }, ], - CreateScriptHookReturn + | HTMLScriptElement + | { script?: HTMLScriptElement; timeout?: number } + | { url: string } + | void >(), createLink: new SyncHook< [ diff --git a/packages/runtime/src/utils/preload.ts b/packages/runtime/src/utils/preload.ts index 774d1998005..3818da5a4d2 100644 --- a/packages/runtime/src/utils/preload.ts +++ b/packages/runtime/src/utils/preload.ts @@ -170,16 +170,6 @@ export function preloadAssets( fetchpriority: 'high', type: remoteInfo?.type === 'module' ? 'module' : 'text/javascript', }, - createScriptHook: (url: string, attrs: any) => { - const res = host.loaderHook.lifecycle.createScript.emit({ - url, - attrs, - }); - if (res instanceof HTMLScriptElement) { - return res; - } - return; - }, needDeleteScript: true, }); needAttach && document.head.appendChild(scriptEl); From f42148b57bb654ca70403b48f880db5dcdac5269 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Mon, 5 Aug 2024 16:04:17 +0200 Subject: [PATCH 05/18] refactor: typing & renaming --- packages/runtime/src/core.ts | 3 ++- packages/runtime/src/global.ts | 2 +- packages/runtime/src/module/index.ts | 1 + packages/runtime/src/utils/load.ts | 21 +++++++++++---------- packages/runtime/src/utils/preload.ts | 2 ++ 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/runtime/src/core.ts b/packages/runtime/src/core.ts index ae8866f3fb5..1aae160f1cb 100644 --- a/packages/runtime/src/core.ts +++ b/packages/runtime/src/core.ts @@ -109,11 +109,12 @@ export class FederationHost { loadEntry: new AsyncHook< [ { + origin: FederationHost; remoteInfo: RemoteInfo; remoteEntryExports?: RemoteEntryExports; }, ], - Promise + RemoteEntryExports >(), // only work for manifest , so not open to the public yet fetch: new AsyncHook< diff --git a/packages/runtime/src/global.ts b/packages/runtime/src/global.ts index 75939143cd2..c7b044f1f5c 100644 --- a/packages/runtime/src/global.ts +++ b/packages/runtime/src/global.ts @@ -39,7 +39,7 @@ declare global { // eslint-disable-next-line no-var __GLOBAL_LOADING_REMOTE_ENTRY__: Record< string, - undefined | Promise + undefined | Promise >; } diff --git a/packages/runtime/src/module/index.ts b/packages/runtime/src/module/index.ts index 61681fde422..7d34c72f953 100644 --- a/packages/runtime/src/module/index.ts +++ b/packages/runtime/src/module/index.ts @@ -30,6 +30,7 @@ class Module { // Get remoteEntry.js const remoteEntryExports = await getRemoteEntry({ + origin: this.host, remoteInfo: this.remoteInfo, remoteEntryExports: this.remoteEntryExports, }); diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts index 993a3a04321..953734c72ce 100644 --- a/packages/runtime/src/utils/load.ts +++ b/packages/runtime/src/utils/load.ts @@ -2,6 +2,7 @@ import { composeKeyWithSeparator } from '@module-federation/sdk'; import { DEFAULT_REMOTE_TYPE, DEFAULT_SCOPE } from '../constant'; import { globalLoading } from '../global'; import { Remote, RemoteEntryExports, RemoteInfo } from '../type'; +import { FederationHost } from '../core'; export function getRemoteEntryUniqueKey(remoteInfo: RemoteInfo): string { const { entry, name } = remoteInfo; @@ -9,28 +10,28 @@ export function getRemoteEntryUniqueKey(remoteInfo: RemoteInfo): string { } export async function getRemoteEntry({ + origin, remoteEntryExports, remoteInfo, }: { + origin: FederationHost; remoteInfo: RemoteInfo; remoteEntryExports?: RemoteEntryExports | undefined; -}): Promise { +}): Promise { const uniqueKey = getRemoteEntryUniqueKey(remoteInfo); if (remoteEntryExports) { return remoteEntryExports; } if (!globalLoading[uniqueKey]) { - // @ts-ignore - const loaderHooks = __webpack_require__.federation.instance.loaderHook; - const hook = () => { - return loaderHooks.lifecycle.loadEntry.emit({ - remoteInfo, - remoteEntryExports, - }); - }; - globalLoading[uniqueKey] = hook(); + const loadEntryHookRes = origin.loaderHook.lifecycle.loadEntry.emit({ + origin, + remoteInfo, + remoteEntryExports, + }); + globalLoading[uniqueKey] = loadEntryHookRes; } + return globalLoading[uniqueKey]; } diff --git a/packages/runtime/src/utils/preload.ts b/packages/runtime/src/utils/preload.ts index 3818da5a4d2..616fba535af 100644 --- a/packages/runtime/src/utils/preload.ts +++ b/packages/runtime/src/utils/preload.ts @@ -80,11 +80,13 @@ export function preloadAssets( const module = host.moduleCache.get(remoteInfo.name); if (module) { getRemoteEntry({ + origin: host, remoteInfo: moduleInfo, remoteEntryExports: module.remoteEntryExports, }); } else { getRemoteEntry({ + origin: host, remoteInfo: moduleInfo, remoteEntryExports: undefined, }); From 2d69a1408cde08d930f7e68b7152b6ae0a05d5fa Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Mon, 5 Aug 2024 16:10:53 +0200 Subject: [PATCH 06/18] chore: revert SDK changes --- packages/runtime/src/utils/preload.ts | 10 ++++++++++ packages/sdk/src/dom.ts | 24 +++++++++++++++++------- packages/sdk/src/node.ts | 15 ++++++--------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/packages/runtime/src/utils/preload.ts b/packages/runtime/src/utils/preload.ts index 616fba535af..450cd95dfe5 100644 --- a/packages/runtime/src/utils/preload.ts +++ b/packages/runtime/src/utils/preload.ts @@ -172,6 +172,16 @@ export function preloadAssets( fetchpriority: 'high', type: remoteInfo?.type === 'module' ? 'module' : 'text/javascript', }, + createScriptHook: (url: string, attrs: any) => { + const res = host.loaderHook.lifecycle.createScript.emit({ + url, + attrs, + }); + if (res instanceof HTMLScriptElement) { + return res; + } + return; + }, needDeleteScript: true, }); needAttach && document.head.appendChild(scriptEl); diff --git a/packages/sdk/src/dom.ts b/packages/sdk/src/dom.ts index 49cd95d7f3c..e86bfb8640f 100644 --- a/packages/sdk/src/dom.ts +++ b/packages/sdk/src/dom.ts @@ -22,11 +22,20 @@ export function isStaticResourcesEqual(url1: string, url2: string): boolean { return relativeUrl1 === relativeUrl2; } +export type CreateScriptHookReturn = + | HTMLScriptElement + | { script?: HTMLScriptElement; timeout?: number } + | void; + export function createScript(info: { url: string; cb?: (value: void | PromiseLike) => void; attrs?: Record; needDeleteScript?: boolean; + createScriptHook?: ( + url: string, + attrs?: Record | undefined, + ) => CreateScriptHookReturn; }): { script: HTMLScriptElement; needAttach: boolean } { // Retrieve the existing script element by its src attribute let script: HTMLScriptElement | null = null; @@ -48,12 +57,8 @@ export function createScript(info: { script = document.createElement('script'); script.type = 'text/javascript'; script.src = info.url; - - //@ts-ignore - const loaderHooks = __webpack_require__.federation.instance.loaderHook; - const createScriptHook = loaderHooks.lifecycle.createScript; - if (createScriptHook) { - const createScriptRes = createScriptHook.emit(info.url, info.attrs); + if (info.createScriptHook) { + const createScriptRes = info.createScriptHook(info.url, info.attrs); if (createScriptRes instanceof HTMLScriptElement) { script = createScriptRes; @@ -200,9 +205,13 @@ export function loadScript( url: string, info: { attrs?: Record; + createScriptHook?: ( + url: string, + attrs?: Record | undefined, + ) => CreateScriptHookReturn; }, ) { - const { attrs = {} } = info; + const { attrs = {}, createScriptHook } = info; return new Promise((resolve, _reject) => { const { script, needAttach } = createScript({ url, @@ -211,6 +220,7 @@ export function loadScript( fetchpriority: 'high', ...attrs, }, + createScriptHook, needDeleteScript: true, }); needAttach && document.head.appendChild(script); diff --git a/packages/sdk/src/node.ts b/packages/sdk/src/node.ts index e45950f9aa2..e60b2b1a776 100644 --- a/packages/sdk/src/node.ts +++ b/packages/sdk/src/node.ts @@ -42,17 +42,12 @@ export function createScriptNode( url: string, cb: (error?: Error, scriptContext?: any) => void, attrs?: Record, + createScriptHook?: (url: string) => any | void, ) { - //@ts-ignore - const loaderHooks = __webpack_require__.federation.instance.loaderHook; - const createScriptHook = loaderHooks.lifecycle.createScript; if (createScriptHook) { - const createScriptRes = createScriptHook.emit(url); - - if (createScriptRes) { - if (typeof createScriptRes === 'object' && 'url' in createScriptRes) { - url = createScriptRes.url; - } + const hookResult = createScriptHook(url); + if (hookResult && typeof hookResult === 'object' && 'url' in hookResult) { + url = hookResult.url; } } @@ -145,6 +140,7 @@ export function loadScriptNode( url: string, info: { attrs?: Record; + createScriptHook?: (url: string) => void; }, ) { return new Promise((resolve, reject) => { @@ -163,6 +159,7 @@ export function loadScriptNode( } }, info.attrs, + info.createScriptHook, ); }); } From cf8e234070a5e8c0343170b01b961d72cd33f585 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Mon, 5 Aug 2024 17:00:52 +0200 Subject: [PATCH 07/18] refactor: typings --- packages/runtime/src/core.ts | 8 +++---- packages/runtime/src/global.ts | 2 +- packages/runtime/src/plugins/dom/index.ts | 24 ++++++++++++++++++--- packages/runtime/src/plugins/node/index.ts | 19 ++++++++++++++-- packages/runtime/src/utils/load.ts | 12 ++++++----- packages/sdk/src/dom.ts | 24 ++++++++------------- packages/sdk/src/node.ts | 6 ++++-- packages/sdk/src/types/hooks.ts | 25 ++++++++++++++++++++++ packages/sdk/src/types/index.ts | 1 + 9 files changed, 88 insertions(+), 33 deletions(-) create mode 100644 packages/sdk/src/types/hooks.ts diff --git a/packages/runtime/src/core.ts b/packages/runtime/src/core.ts index 1aae160f1cb..08f80b085e4 100644 --- a/packages/runtime/src/core.ts +++ b/packages/runtime/src/core.ts @@ -1,3 +1,4 @@ +import { CreateScriptHookReturn } from '@module-federation/sdk'; import { Options, PreloadRemoteArgs, @@ -93,10 +94,7 @@ export class FederationHost { attrs?: Record; }, ], - | HTMLScriptElement - | { script?: HTMLScriptElement; timeout?: number } - | { url: string } - | void + CreateScriptHookReturn >(), createLink: new SyncHook< [ @@ -114,7 +112,7 @@ export class FederationHost { remoteEntryExports?: RemoteEntryExports; }, ], - RemoteEntryExports + Promise | void >(), // only work for manifest , so not open to the public yet fetch: new AsyncHook< diff --git a/packages/runtime/src/global.ts b/packages/runtime/src/global.ts index c7b044f1f5c..75939143cd2 100644 --- a/packages/runtime/src/global.ts +++ b/packages/runtime/src/global.ts @@ -39,7 +39,7 @@ declare global { // eslint-disable-next-line no-var __GLOBAL_LOADING_REMOTE_ENTRY__: Record< string, - undefined | Promise + undefined | Promise >; } diff --git a/packages/runtime/src/plugins/dom/index.ts b/packages/runtime/src/plugins/dom/index.ts index 1e2b5386817..d4d357a2301 100644 --- a/packages/runtime/src/plugins/dom/index.ts +++ b/packages/runtime/src/plugins/dom/index.ts @@ -1,4 +1,4 @@ -import { loadScript } from '@module-federation/sdk'; +import { CreateScriptHookDom, loadScript } from '@module-federation/sdk'; import { FederationRuntimePlugin } from '../../type/plugin'; import { assert } from '../../utils'; import { RemoteEntryExports } from '../../type'; @@ -56,10 +56,12 @@ async function loadEntryScript({ name, globalName, entry, + createScriptHook, }: { name: string; globalName: string; entry: string; + createScriptHook: CreateScriptHookDom; }): Promise { const { entryExports: remoteEntryExports } = getRemoteEntryExports( name, @@ -70,7 +72,7 @@ async function loadEntryScript({ return remoteEntryExports; } - return loadScript(entry, { attrs: {} }) + return loadScript(entry, { attrs: {}, createScriptHook }) .then(() => { const { remoteEntryKey, entryExports } = getRemoteEntryExports( name, @@ -98,7 +100,7 @@ export function domPlugin(): FederationRuntimePlugin { return { name: 'dom-plugin', async loadEntry(args) { - const { remoteInfo, remoteEntryExports } = args; + const { origin, remoteInfo, remoteEntryExports } = args; const { entry, entryGlobalName, name, type } = remoteInfo; if (['esm', 'module'].includes(type)) { @@ -116,6 +118,22 @@ export function domPlugin(): FederationRuntimePlugin { entry, globalName: entryGlobalName, name, + createScriptHook: (url, attrs) => { + const hook = origin.loaderHook.lifecycle.createScript; + const res = hook.emit({ url, attrs }); + + if (!res) return; + + if (res instanceof HTMLScriptElement) { + return res; + } + + if ('script' in res || 'timeout' in res) { + return res; + } + + return; + }, }); }, }; diff --git a/packages/runtime/src/plugins/node/index.ts b/packages/runtime/src/plugins/node/index.ts index 32b676154aa..243cfd9b9bc 100644 --- a/packages/runtime/src/plugins/node/index.ts +++ b/packages/runtime/src/plugins/node/index.ts @@ -1,4 +1,4 @@ -import { loadScriptNode } from '@module-federation/sdk'; +import { CreateScriptHookNode, loadScriptNode } from '@module-federation/sdk'; import { FederationRuntimePlugin } from '../../type/plugin'; import { getRemoteEntryExports } from '../../global'; import { RemoteEntryExports } from '../../type'; @@ -8,10 +8,12 @@ export async function loadEntryScript({ name, globalName, entry, + createScriptHook, }: { name: string; globalName: string; entry: string; + createScriptHook: CreateScriptHookNode; }): Promise { const { entryExports: remoteEntryExports } = getRemoteEntryExports( name, @@ -24,6 +26,7 @@ export async function loadEntryScript({ return loadScriptNode(entry, { attrs: { name, globalName }, + createScriptHook, }) .then(() => { const { remoteEntryKey, entryExports } = getRemoteEntryExports( @@ -52,13 +55,25 @@ export function nodePlugin(): FederationRuntimePlugin { return { name: 'node-plugin', async loadEntry(args) { - const { remoteInfo } = args; + const { origin, remoteInfo } = args; const { entry, entryGlobalName, name } = remoteInfo; return loadEntryScript({ entry, globalName: entryGlobalName, name, + createScriptHook: (url, attrs) => { + const hook = origin.loaderHook.lifecycle.createScript; + const res = hook.emit({ url, attrs }); + + if (!res) return; + + if ('url' in res) { + return res; + } + + return; + }, }); }, }; diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts index 953734c72ce..b055155a740 100644 --- a/packages/runtime/src/utils/load.ts +++ b/packages/runtime/src/utils/load.ts @@ -24,11 +24,13 @@ export async function getRemoteEntry({ } if (!globalLoading[uniqueKey]) { - const loadEntryHookRes = origin.loaderHook.lifecycle.loadEntry.emit({ - origin, - remoteInfo, - remoteEntryExports, - }); + const loadEntryHookRes = origin.loaderHook.lifecycle.loadEntry + .emit({ + origin, + remoteInfo, + remoteEntryExports, + }) + .then((res) => res || undefined); globalLoading[uniqueKey] = loadEntryHookRes; } diff --git a/packages/sdk/src/dom.ts b/packages/sdk/src/dom.ts index e86bfb8640f..95147624da4 100644 --- a/packages/sdk/src/dom.ts +++ b/packages/sdk/src/dom.ts @@ -1,3 +1,4 @@ +import { CreateScriptHookDom } from './types'; import { warn } from './utils'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function safeWrapper) => any>( @@ -22,20 +23,12 @@ export function isStaticResourcesEqual(url1: string, url2: string): boolean { return relativeUrl1 === relativeUrl2; } -export type CreateScriptHookReturn = - | HTMLScriptElement - | { script?: HTMLScriptElement; timeout?: number } - | void; - export function createScript(info: { url: string; cb?: (value: void | PromiseLike) => void; attrs?: Record; needDeleteScript?: boolean; - createScriptHook?: ( - url: string, - attrs?: Record | undefined, - ) => CreateScriptHookReturn; + createScriptHook?: CreateScriptHookDom; }): { script: HTMLScriptElement; needAttach: boolean } { // Retrieve the existing script element by its src attribute let script: HTMLScriptElement | null = null; @@ -63,8 +56,12 @@ export function createScript(info: { if (createScriptRes instanceof HTMLScriptElement) { script = createScriptRes; } else if (typeof createScriptRes === 'object') { - if (createScriptRes.script) script = createScriptRes.script; - if (createScriptRes.timeout) timeout = createScriptRes.timeout; + if ('script' in createScriptRes && createScriptRes.script) { + script = createScriptRes.script; + } + if ('timeout' in createScriptRes && createScriptRes.timeout) { + timeout = createScriptRes.timeout; + } } } const attrs = info.attrs; @@ -205,10 +202,7 @@ export function loadScript( url: string, info: { attrs?: Record; - createScriptHook?: ( - url: string, - attrs?: Record | undefined, - ) => CreateScriptHookReturn; + createScriptHook?: CreateScriptHookDom; }, ) { const { attrs = {}, createScriptHook } = info; diff --git a/packages/sdk/src/node.ts b/packages/sdk/src/node.ts index e60b2b1a776..2757427d096 100644 --- a/packages/sdk/src/node.ts +++ b/packages/sdk/src/node.ts @@ -1,3 +1,5 @@ +import { CreateScriptHookNode } from './types'; + function importNodeModule(name: string): Promise { if (!name) { throw new Error('import specifier is required'); @@ -42,7 +44,7 @@ export function createScriptNode( url: string, cb: (error?: Error, scriptContext?: any) => void, attrs?: Record, - createScriptHook?: (url: string) => any | void, + createScriptHook?: CreateScriptHookNode, ) { if (createScriptHook) { const hookResult = createScriptHook(url); @@ -140,7 +142,7 @@ export function loadScriptNode( url: string, info: { attrs?: Record; - createScriptHook?: (url: string) => void; + createScriptHook?: CreateScriptHookNode; }, ) { return new Promise((resolve, reject) => { diff --git a/packages/sdk/src/types/hooks.ts b/packages/sdk/src/types/hooks.ts new file mode 100644 index 00000000000..1284d890e00 --- /dev/null +++ b/packages/sdk/src/types/hooks.ts @@ -0,0 +1,25 @@ +export type CreateScriptHookReturnNode = { url: string } | void; + +export type CreateScriptHookReturnDom = + | HTMLScriptElement + | { script?: HTMLScriptElement; timeout?: number } + | void; + +export type CreateScriptHookReturn = + | CreateScriptHookReturnNode + | CreateScriptHookReturnDom; + +export type CreateScriptHookNode = ( + url: string, + attrs?: Record | undefined, +) => CreateScriptHookReturnNode; + +export type CreateScriptHookDom = ( + url: string, + attrs?: Record | undefined, +) => CreateScriptHookReturnDom; + +export type CreateScriptHook = ( + url: string, + attrs?: Record | undefined, +) => CreateScriptHookReturn; diff --git a/packages/sdk/src/types/index.ts b/packages/sdk/src/types/index.ts index ee6c4dd2bf3..e2f9ba10f8e 100644 --- a/packages/sdk/src/types/index.ts +++ b/packages/sdk/src/types/index.ts @@ -3,3 +3,4 @@ export * from './manifest'; export * from './stats'; export * from './snapshot'; export * from './plugins'; +export * from './hooks'; From c77d4f835d8c44d0f1bf824176821a542ab399d7 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 9 Aug 2024 10:35:53 +0200 Subject: [PATCH 08/18] refactor: move loadEntry hook to remote handler --- packages/runtime/src/core.ts | 10 ---------- packages/runtime/src/plugins/dom/index.ts | 2 +- packages/runtime/src/plugins/node/index.ts | 2 +- packages/runtime/src/remote/index.ts | 11 +++++++++++ 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/runtime/src/core.ts b/packages/runtime/src/core.ts index 08f80b085e4..148165cc1a6 100644 --- a/packages/runtime/src/core.ts +++ b/packages/runtime/src/core.ts @@ -104,16 +104,6 @@ export class FederationHost { ], HTMLLinkElement | void >(), - loadEntry: new AsyncHook< - [ - { - origin: FederationHost; - remoteInfo: RemoteInfo; - remoteEntryExports?: RemoteEntryExports; - }, - ], - Promise | void - >(), // only work for manifest , so not open to the public yet fetch: new AsyncHook< [string, RequestInit], diff --git a/packages/runtime/src/plugins/dom/index.ts b/packages/runtime/src/plugins/dom/index.ts index d4d357a2301..3f48ccb2664 100644 --- a/packages/runtime/src/plugins/dom/index.ts +++ b/packages/runtime/src/plugins/dom/index.ts @@ -99,7 +99,7 @@ async function loadEntryScript({ export function domPlugin(): FederationRuntimePlugin { return { name: 'dom-plugin', - async loadEntry(args) { + async getRemoteEntry(args) { const { origin, remoteInfo, remoteEntryExports } = args; const { entry, entryGlobalName, name, type } = remoteInfo; diff --git a/packages/runtime/src/plugins/node/index.ts b/packages/runtime/src/plugins/node/index.ts index 243cfd9b9bc..c84e07ed62a 100644 --- a/packages/runtime/src/plugins/node/index.ts +++ b/packages/runtime/src/plugins/node/index.ts @@ -54,7 +54,7 @@ export async function loadEntryScript({ export function nodePlugin(): FederationRuntimePlugin { return { name: 'node-plugin', - async loadEntry(args) { + async getRemoteEntry(args) { const { origin, remoteInfo } = args; const { entry, entryGlobalName, name } = remoteInfo; diff --git a/packages/runtime/src/remote/index.ts b/packages/runtime/src/remote/index.ts index 6cf80fe79f5..4227fe33446 100644 --- a/packages/runtime/src/remote/index.ts +++ b/packages/runtime/src/remote/index.ts @@ -13,6 +13,7 @@ import { PreloadRemoteArgs, Remote, RemoteInfo, + RemoteEntryExports, } from '../type'; import { FederationHost } from '../core'; import { @@ -131,6 +132,16 @@ export class RemoteHandler { options: Options; origin: FederationHost; }>(), + getRemoteEntry: new AsyncHook< + [ + { + origin: FederationHost; + remoteInfo: RemoteInfo; + remoteEntryExports?: RemoteEntryExports; + }, + ], + Promise + >(), }); constructor(host: FederationHost) { From 6cee8e703639bff34633d88e87351da6d5c667fe Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 9 Aug 2024 10:51:53 +0200 Subject: [PATCH 09/18] refactor: use isBrowserEnv check --- packages/runtime/src/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/src/core.ts b/packages/runtime/src/core.ts index 148165cc1a6..a203ed2165a 100644 --- a/packages/runtime/src/core.ts +++ b/packages/runtime/src/core.ts @@ -141,7 +141,7 @@ export class FederationHost { } private envPlugin() { - if (typeof document === 'undefined') { + if (!isBrowserEnv()) { return nodePlugin(); } return domPlugin(); From 9829ed980368851a30e2b8b433606f09e9b80d20 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 9 Aug 2024 11:56:25 +0200 Subject: [PATCH 10/18] refactor: go back to loadEntry name --- packages/runtime/src/plugins/dom/index.ts | 2 +- packages/runtime/src/plugins/node/index.ts | 2 +- packages/runtime/src/remote/index.ts | 2 +- packages/runtime/src/utils/load.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/runtime/src/plugins/dom/index.ts b/packages/runtime/src/plugins/dom/index.ts index 3f48ccb2664..d4d357a2301 100644 --- a/packages/runtime/src/plugins/dom/index.ts +++ b/packages/runtime/src/plugins/dom/index.ts @@ -99,7 +99,7 @@ async function loadEntryScript({ export function domPlugin(): FederationRuntimePlugin { return { name: 'dom-plugin', - async getRemoteEntry(args) { + async loadEntry(args) { const { origin, remoteInfo, remoteEntryExports } = args; const { entry, entryGlobalName, name, type } = remoteInfo; diff --git a/packages/runtime/src/plugins/node/index.ts b/packages/runtime/src/plugins/node/index.ts index c84e07ed62a..243cfd9b9bc 100644 --- a/packages/runtime/src/plugins/node/index.ts +++ b/packages/runtime/src/plugins/node/index.ts @@ -54,7 +54,7 @@ export async function loadEntryScript({ export function nodePlugin(): FederationRuntimePlugin { return { name: 'node-plugin', - async getRemoteEntry(args) { + async loadEntry(args) { const { origin, remoteInfo } = args; const { entry, entryGlobalName, name } = remoteInfo; diff --git a/packages/runtime/src/remote/index.ts b/packages/runtime/src/remote/index.ts index 4227fe33446..925e42d52bd 100644 --- a/packages/runtime/src/remote/index.ts +++ b/packages/runtime/src/remote/index.ts @@ -132,7 +132,7 @@ export class RemoteHandler { options: Options; origin: FederationHost; }>(), - getRemoteEntry: new AsyncHook< + loadEntry: new AsyncHook< [ { origin: FederationHost; diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts index b055155a740..6c668f72306 100644 --- a/packages/runtime/src/utils/load.ts +++ b/packages/runtime/src/utils/load.ts @@ -24,7 +24,7 @@ export async function getRemoteEntry({ } if (!globalLoading[uniqueKey]) { - const loadEntryHookRes = origin.loaderHook.lifecycle.loadEntry + const loadEntryHookRes = origin.remoteHandler.hooks.lifecycle.loadEntry .emit({ origin, remoteInfo, From 3b4263673355e7a7ddefda9e9dca8d7d161fecb9 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 9 Aug 2024 12:02:26 +0200 Subject: [PATCH 11/18] refactor: pass createScriptHook only --- packages/runtime/src/plugins/dom/index.ts | 5 ++--- packages/runtime/src/plugins/node/index.ts | 5 ++--- packages/runtime/src/remote/index.ts | 2 +- packages/runtime/src/utils/load.ts | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/runtime/src/plugins/dom/index.ts b/packages/runtime/src/plugins/dom/index.ts index d4d357a2301..780e45d090b 100644 --- a/packages/runtime/src/plugins/dom/index.ts +++ b/packages/runtime/src/plugins/dom/index.ts @@ -100,7 +100,7 @@ export function domPlugin(): FederationRuntimePlugin { return { name: 'dom-plugin', async loadEntry(args) { - const { origin, remoteInfo, remoteEntryExports } = args; + const { createScriptHook, remoteInfo, remoteEntryExports } = args; const { entry, entryGlobalName, name, type } = remoteInfo; if (['esm', 'module'].includes(type)) { @@ -119,8 +119,7 @@ export function domPlugin(): FederationRuntimePlugin { globalName: entryGlobalName, name, createScriptHook: (url, attrs) => { - const hook = origin.loaderHook.lifecycle.createScript; - const res = hook.emit({ url, attrs }); + const res = createScriptHook.emit({ url, attrs }); if (!res) return; diff --git a/packages/runtime/src/plugins/node/index.ts b/packages/runtime/src/plugins/node/index.ts index 243cfd9b9bc..8aa6617336c 100644 --- a/packages/runtime/src/plugins/node/index.ts +++ b/packages/runtime/src/plugins/node/index.ts @@ -55,7 +55,7 @@ export function nodePlugin(): FederationRuntimePlugin { return { name: 'node-plugin', async loadEntry(args) { - const { origin, remoteInfo } = args; + const { createScriptHook, remoteInfo } = args; const { entry, entryGlobalName, name } = remoteInfo; return loadEntryScript({ @@ -63,8 +63,7 @@ export function nodePlugin(): FederationRuntimePlugin { globalName: entryGlobalName, name, createScriptHook: (url, attrs) => { - const hook = origin.loaderHook.lifecycle.createScript; - const res = hook.emit({ url, attrs }); + const res = createScriptHook.emit({ url, attrs }); if (!res) return; diff --git a/packages/runtime/src/remote/index.ts b/packages/runtime/src/remote/index.ts index 925e42d52bd..a47f04b2ed3 100644 --- a/packages/runtime/src/remote/index.ts +++ b/packages/runtime/src/remote/index.ts @@ -135,7 +135,7 @@ export class RemoteHandler { loadEntry: new AsyncHook< [ { - origin: FederationHost; + createScriptHook: FederationHost['loaderHook']['lifecycle']['createScript']; remoteInfo: RemoteInfo; remoteEntryExports?: RemoteEntryExports; }, diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts index 6c668f72306..0b54742dbd3 100644 --- a/packages/runtime/src/utils/load.ts +++ b/packages/runtime/src/utils/load.ts @@ -26,7 +26,7 @@ export async function getRemoteEntry({ if (!globalLoading[uniqueKey]) { const loadEntryHookRes = origin.remoteHandler.hooks.lifecycle.loadEntry .emit({ - origin, + createScriptHook: origin.loaderHook.lifecycle.createScript, remoteInfo, remoteEntryExports, }) From 68282b1c65851da85063ba6828b18a905f24dc5b Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 13 Aug 2024 14:25:04 +0200 Subject: [PATCH 12/18] refactor: use inlined fallback instead of default env plugins --- packages/runtime/src/core.ts | 13 +---- packages/runtime/src/plugins/dom/index.ts | 48 ++++++++++++----- packages/runtime/src/plugins/node/index.ts | 25 +++++++-- packages/runtime/src/utils/load.ts | 60 +++++++++++++++++++--- 4 files changed, 112 insertions(+), 34 deletions(-) diff --git a/packages/runtime/src/core.ts b/packages/runtime/src/core.ts index a203ed2165a..2342b5ca016 100644 --- a/packages/runtime/src/core.ts +++ b/packages/runtime/src/core.ts @@ -117,11 +117,7 @@ export class FederationHost { const defaultOptions: Options = { id: getBuilderId(), name: userOptions.name, - plugins: [ - this.envPlugin(), - snapshotPlugin(), - generatePreloadAssetsPlugin(), - ], + plugins: [snapshotPlugin(), generatePreloadAssetsPlugin()], remotes: [], shared: {}, inBrowser: isBrowserEnv(), @@ -140,13 +136,6 @@ export class FederationHost { this.options = this.formatOptions(defaultOptions, userOptions); } - private envPlugin() { - if (!isBrowserEnv()) { - return nodePlugin(); - } - return domPlugin(); - } - initOptions(userOptions: UserOptions): Options { this.registerPlugins(userOptions.plugins); const options = this.formatOptions(this.options, userOptions); diff --git a/packages/runtime/src/plugins/dom/index.ts b/packages/runtime/src/plugins/dom/index.ts index 780e45d090b..90288903c24 100644 --- a/packages/runtime/src/plugins/dom/index.ts +++ b/packages/runtime/src/plugins/dom/index.ts @@ -96,6 +96,37 @@ async function loadEntryScript({ }); } +export async function loadEntryDom({ + entry, + entryGlobalName, + remoteEntryExports, + name, + type, + createScriptHook, +}: { + entry: string; + entryGlobalName: string; + remoteEntryExports: RemoteEntryExports | undefined; + name: string; + type: string; + createScriptHook: CreateScriptHookDom; +}) { + switch (type) { + case 'esm': + case 'module': + return loadEsmEntry({ entry, remoteEntryExports }); + case 'system': + return loadSystemJsEntry({ entry, remoteEntryExports }); + default: + return loadEntryScript({ + entry, + globalName: entryGlobalName, + name, + createScriptHook, + }); + } +} + export function domPlugin(): FederationRuntimePlugin { return { name: 'dom-plugin', @@ -103,21 +134,12 @@ export function domPlugin(): FederationRuntimePlugin { const { createScriptHook, remoteInfo, remoteEntryExports } = args; const { entry, entryGlobalName, name, type } = remoteInfo; - if (['esm', 'module'].includes(type)) { - return loadEsmEntry({ - entry, - remoteEntryExports, - }); - } else if (type === 'system') { - return loadSystemJsEntry({ - entry, - remoteEntryExports, - }); - } - return loadEntryScript({ + return loadEntryDom({ entry, - globalName: entryGlobalName, + entryGlobalName, name, + type, + remoteEntryExports, createScriptHook: (url, attrs) => { const res = createScriptHook.emit({ url, attrs }); diff --git a/packages/runtime/src/plugins/node/index.ts b/packages/runtime/src/plugins/node/index.ts index 8aa6617336c..874207e2760 100644 --- a/packages/runtime/src/plugins/node/index.ts +++ b/packages/runtime/src/plugins/node/index.ts @@ -4,7 +4,7 @@ import { getRemoteEntryExports } from '../../global'; import { RemoteEntryExports } from '../../type'; import { assert } from '../../utils'; -export async function loadEntryScript({ +async function loadEntryScript({ name, globalName, entry, @@ -51,6 +51,25 @@ export async function loadEntryScript({ }); } +export async function loadEntryNode({ + entry, + entryGlobalName, + name, + createScriptHook, +}: { + entry: string; + entryGlobalName: string; + name: string; + createScriptHook: CreateScriptHookNode; +}) { + return loadEntryScript({ + entry, + globalName: entryGlobalName, + name, + createScriptHook, + }); +} + export function nodePlugin(): FederationRuntimePlugin { return { name: 'node-plugin', @@ -58,9 +77,9 @@ export function nodePlugin(): FederationRuntimePlugin { const { createScriptHook, remoteInfo } = args; const { entry, entryGlobalName, name } = remoteInfo; - return loadEntryScript({ + return loadEntryNode({ entry, - globalName: entryGlobalName, + entryGlobalName, name, createScriptHook: (url, attrs) => { const res = createScriptHook.emit({ url, attrs }); diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts index 0b54742dbd3..ee5a8af6e08 100644 --- a/packages/runtime/src/utils/load.ts +++ b/packages/runtime/src/utils/load.ts @@ -1,8 +1,10 @@ -import { composeKeyWithSeparator } from '@module-federation/sdk'; +import { composeKeyWithSeparator, isBrowserEnv } from '@module-federation/sdk'; import { DEFAULT_REMOTE_TYPE, DEFAULT_SCOPE } from '../constant'; import { globalLoading } from '../global'; import { Remote, RemoteEntryExports, RemoteInfo } from '../type'; import { FederationHost } from '../core'; +import { loadEntryNode } from '../plugins/node'; +import { loadEntryDom } from '../plugins/dom'; export function getRemoteEntryUniqueKey(remoteInfo: RemoteInfo): string { const { entry, name } = remoteInfo; @@ -24,14 +26,60 @@ export async function getRemoteEntry({ } if (!globalLoading[uniqueKey]) { - const loadEntryHookRes = origin.remoteHandler.hooks.lifecycle.loadEntry - .emit({ + const loadEntryHookRes = + origin.remoteHandler.hooks.lifecycle.loadEntry.emit({ createScriptHook: origin.loaderHook.lifecycle.createScript, remoteInfo, remoteEntryExports, - }) - .then((res) => res || undefined); - globalLoading[uniqueKey] = loadEntryHookRes; + }); + if (loadEntryHookRes) { + globalLoading[uniqueKey] = loadEntryHookRes.then( + (res) => res || undefined, + ); + } else { + const createScriptHook = origin.loaderHook.lifecycle.createScript; + if (!isBrowserEnv()) { + globalLoading[uniqueKey] = loadEntryNode({ + entry: remoteInfo.entry, + entryGlobalName: remoteInfo.entryGlobalName, + name: remoteInfo.name, + createScriptHook: (url, attrs) => { + const res = createScriptHook.emit({ url, attrs }); + + if (!res) return; + + if ('url' in res) { + return res; + } + + return; + }, + }); + } else { + globalLoading[uniqueKey] = loadEntryDom({ + entry: remoteInfo.entry, + entryGlobalName: remoteInfo.entryGlobalName, + name: remoteInfo.name, + type: remoteInfo.type, + remoteEntryExports, + createScriptHook: (url, attrs) => { + const res = createScriptHook.emit({ url, attrs }); + + if (!res) return; + + if (res instanceof HTMLScriptElement) { + return res; + } + + if ('script' in res || 'timeout' in res) { + return res; + } + + return; + }, + }); + } + } } return globalLoading[uniqueKey]; From ef438af0518afbcd0bd933794ead80ca6e08fe9c Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 13 Aug 2024 14:41:26 +0200 Subject: [PATCH 13/18] refactor: cleanup --- packages/runtime/src/plugins/dom/index.ts | 128 +----------------- packages/runtime/src/plugins/dom/loadEntry.ts | 127 +++++++++++++++++ packages/runtime/src/plugins/node/index.ts | 71 +--------- .../runtime/src/plugins/node/loadEntry.ts | 70 ++++++++++ packages/runtime/src/utils/load.ts | 104 +++++++++----- 5 files changed, 266 insertions(+), 234 deletions(-) create mode 100644 packages/runtime/src/plugins/dom/loadEntry.ts create mode 100644 packages/runtime/src/plugins/node/loadEntry.ts diff --git a/packages/runtime/src/plugins/dom/index.ts b/packages/runtime/src/plugins/dom/index.ts index 90288903c24..62ebb895713 100644 --- a/packages/runtime/src/plugins/dom/index.ts +++ b/packages/runtime/src/plugins/dom/index.ts @@ -1,131 +1,5 @@ -import { CreateScriptHookDom, loadScript } from '@module-federation/sdk'; import { FederationRuntimePlugin } from '../../type/plugin'; -import { assert } from '../../utils'; -import { RemoteEntryExports } from '../../type'; -import { getRemoteEntryExports } from '../../global'; - -async function loadEsmEntry({ - entry, - remoteEntryExports, -}: { - entry: string; - remoteEntryExports: RemoteEntryExports | undefined; -}): Promise { - return new Promise((resolve, reject) => { - try { - if (!remoteEntryExports) { - // eslint-disable-next-line no-eval - new Function( - 'callbacks', - `import("${entry}").then(callbacks[0]).catch(callbacks[1])`, - )([resolve, reject]); - } else { - resolve(remoteEntryExports); - } - } catch (e) { - reject(e); - } - }); -} - -async function loadSystemJsEntry({ - entry, - remoteEntryExports, -}: { - entry: string; - remoteEntryExports: RemoteEntryExports | undefined; -}): Promise { - return new Promise((resolve, reject) => { - try { - if (!remoteEntryExports) { - // eslint-disable-next-line no-eval - new Function( - 'callbacks', - `System.import("${entry}").then(callbacks[0]).catch(callbacks[1])`, - )([resolve, reject]); - } else { - resolve(remoteEntryExports); - } - } catch (e) { - reject(e); - } - }); -} - -async function loadEntryScript({ - name, - globalName, - entry, - createScriptHook, -}: { - name: string; - globalName: string; - entry: string; - createScriptHook: CreateScriptHookDom; -}): Promise { - const { entryExports: remoteEntryExports } = getRemoteEntryExports( - name, - globalName, - ); - - if (remoteEntryExports) { - return remoteEntryExports; - } - - return loadScript(entry, { attrs: {}, createScriptHook }) - .then(() => { - const { remoteEntryKey, entryExports } = getRemoteEntryExports( - name, - globalName, - ); - - assert( - entryExports, - ` - Unable to use the ${name}'s '${entry}' URL with ${remoteEntryKey}'s globalName to get remoteEntry exports. - Possible reasons could be:\n - 1. '${entry}' is not the correct URL, or the remoteEntry resource or name is incorrect.\n - 2. ${remoteEntryKey} cannot be used to get remoteEntry exports in the window object. - `, - ); - - return entryExports; - }) - .catch((e) => { - throw e; - }); -} - -export async function loadEntryDom({ - entry, - entryGlobalName, - remoteEntryExports, - name, - type, - createScriptHook, -}: { - entry: string; - entryGlobalName: string; - remoteEntryExports: RemoteEntryExports | undefined; - name: string; - type: string; - createScriptHook: CreateScriptHookDom; -}) { - switch (type) { - case 'esm': - case 'module': - return loadEsmEntry({ entry, remoteEntryExports }); - case 'system': - return loadSystemJsEntry({ entry, remoteEntryExports }); - default: - return loadEntryScript({ - entry, - globalName: entryGlobalName, - name, - createScriptHook, - }); - } -} +import { loadEntryDom } from './loadEntry'; export function domPlugin(): FederationRuntimePlugin { return { diff --git a/packages/runtime/src/plugins/dom/loadEntry.ts b/packages/runtime/src/plugins/dom/loadEntry.ts new file mode 100644 index 00000000000..0905e6836e6 --- /dev/null +++ b/packages/runtime/src/plugins/dom/loadEntry.ts @@ -0,0 +1,127 @@ +import { CreateScriptHookDom, loadScript } from '@module-federation/sdk'; +import { assert } from '../../utils'; +import { RemoteEntryExports } from '../../type'; +import { getRemoteEntryExports } from '../../global'; + +async function loadEsmEntry({ + entry, + remoteEntryExports, +}: { + entry: string; + remoteEntryExports: RemoteEntryExports | undefined; +}): Promise { + return new Promise((resolve, reject) => { + try { + if (!remoteEntryExports) { + // eslint-disable-next-line no-eval + new Function( + 'callbacks', + `import("${entry}").then(callbacks[0]).catch(callbacks[1])`, + )([resolve, reject]); + } else { + resolve(remoteEntryExports); + } + } catch (e) { + reject(e); + } + }); +} + +async function loadSystemJsEntry({ + entry, + remoteEntryExports, +}: { + entry: string; + remoteEntryExports: RemoteEntryExports | undefined; +}): Promise { + return new Promise((resolve, reject) => { + try { + if (!remoteEntryExports) { + // eslint-disable-next-line no-eval + new Function( + 'callbacks', + `System.import("${entry}").then(callbacks[0]).catch(callbacks[1])`, + )([resolve, reject]); + } else { + resolve(remoteEntryExports); + } + } catch (e) { + reject(e); + } + }); +} + +async function loadEntryScript({ + name, + globalName, + entry, + createScriptHook, +}: { + name: string; + globalName: string; + entry: string; + createScriptHook: CreateScriptHookDom; +}): Promise { + const { entryExports: remoteEntryExports } = getRemoteEntryExports( + name, + globalName, + ); + + if (remoteEntryExports) { + return remoteEntryExports; + } + + return loadScript(entry, { attrs: {}, createScriptHook }) + .then(() => { + const { remoteEntryKey, entryExports } = getRemoteEntryExports( + name, + globalName, + ); + + assert( + entryExports, + ` + Unable to use the ${name}'s '${entry}' URL with ${remoteEntryKey}'s globalName to get remoteEntry exports. + Possible reasons could be:\n + 1. '${entry}' is not the correct URL, or the remoteEntry resource or name is incorrect.\n + 2. ${remoteEntryKey} cannot be used to get remoteEntry exports in the window object. + `, + ); + + return entryExports; + }) + .catch((e) => { + throw e; + }); +} + +export async function loadEntryDom({ + entry, + entryGlobalName, + remoteEntryExports, + name, + type, + createScriptHook, +}: { + entry: string; + entryGlobalName: string; + remoteEntryExports: RemoteEntryExports | undefined; + name: string; + type: string; + createScriptHook: CreateScriptHookDom; +}) { + switch (type) { + case 'esm': + case 'module': + return loadEsmEntry({ entry, remoteEntryExports }); + case 'system': + return loadSystemJsEntry({ entry, remoteEntryExports }); + default: + return loadEntryScript({ + entry, + globalName: entryGlobalName, + name, + createScriptHook, + }); + } +} diff --git a/packages/runtime/src/plugins/node/index.ts b/packages/runtime/src/plugins/node/index.ts index 874207e2760..5ebcd92c621 100644 --- a/packages/runtime/src/plugins/node/index.ts +++ b/packages/runtime/src/plugins/node/index.ts @@ -1,74 +1,5 @@ -import { CreateScriptHookNode, loadScriptNode } from '@module-federation/sdk'; import { FederationRuntimePlugin } from '../../type/plugin'; -import { getRemoteEntryExports } from '../../global'; -import { RemoteEntryExports } from '../../type'; -import { assert } from '../../utils'; - -async function loadEntryScript({ - name, - globalName, - entry, - createScriptHook, -}: { - name: string; - globalName: string; - entry: string; - createScriptHook: CreateScriptHookNode; -}): Promise { - const { entryExports: remoteEntryExports } = getRemoteEntryExports( - name, - globalName, - ); - - if (remoteEntryExports) { - return remoteEntryExports; - } - - return loadScriptNode(entry, { - attrs: { name, globalName }, - createScriptHook, - }) - .then(() => { - const { remoteEntryKey, entryExports } = getRemoteEntryExports( - name, - globalName, - ); - - assert( - entryExports, - ` - Unable to use the ${name}'s '${entry}' URL with ${remoteEntryKey}'s globalName to get remoteEntry exports. - Possible reasons could be:\n - 1. '${entry}' is not the correct URL, or the remoteEntry resource or name is incorrect.\n - 2. ${remoteEntryKey} cannot be used to get remoteEntry exports in the window object. - `, - ); - - return entryExports; - }) - .catch((e) => { - throw e; - }); -} - -export async function loadEntryNode({ - entry, - entryGlobalName, - name, - createScriptHook, -}: { - entry: string; - entryGlobalName: string; - name: string; - createScriptHook: CreateScriptHookNode; -}) { - return loadEntryScript({ - entry, - globalName: entryGlobalName, - name, - createScriptHook, - }); -} +import { loadEntryNode } from './loadEntry'; export function nodePlugin(): FederationRuntimePlugin { return { diff --git a/packages/runtime/src/plugins/node/loadEntry.ts b/packages/runtime/src/plugins/node/loadEntry.ts new file mode 100644 index 00000000000..f53cd19f13a --- /dev/null +++ b/packages/runtime/src/plugins/node/loadEntry.ts @@ -0,0 +1,70 @@ +import { CreateScriptHookNode, loadScriptNode } from '@module-federation/sdk'; +import { getRemoteEntryExports } from '../../global'; +import { RemoteEntryExports } from '../../type'; +import { assert } from '../../utils'; + +async function loadEntryScript({ + name, + globalName, + entry, + createScriptHook, +}: { + name: string; + globalName: string; + entry: string; + createScriptHook: CreateScriptHookNode; +}): Promise { + const { entryExports: remoteEntryExports } = getRemoteEntryExports( + name, + globalName, + ); + + if (remoteEntryExports) { + return remoteEntryExports; + } + + return loadScriptNode(entry, { + attrs: { name, globalName }, + createScriptHook, + }) + .then(() => { + const { remoteEntryKey, entryExports } = getRemoteEntryExports( + name, + globalName, + ); + + assert( + entryExports, + ` + Unable to use the ${name}'s '${entry}' URL with ${remoteEntryKey}'s globalName to get remoteEntry exports. + Possible reasons could be:\n + 1. '${entry}' is not the correct URL, or the remoteEntry resource or name is incorrect.\n + 2. ${remoteEntryKey} cannot be used to get remoteEntry exports in the window object. + `, + ); + + return entryExports; + }) + .catch((e) => { + throw e; + }); +} + +export async function loadEntryNode({ + entry, + entryGlobalName, + name, + createScriptHook, +}: { + entry: string; + entryGlobalName: string; + name: string; + createScriptHook: CreateScriptHookNode; +}) { + return loadEntryScript({ + entry, + globalName: entryGlobalName, + name, + createScriptHook, + }); +} diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts index ee5a8af6e08..ab4d6b4f50c 100644 --- a/packages/runtime/src/utils/load.ts +++ b/packages/runtime/src/utils/load.ts @@ -3,8 +3,66 @@ import { DEFAULT_REMOTE_TYPE, DEFAULT_SCOPE } from '../constant'; import { globalLoading } from '../global'; import { Remote, RemoteEntryExports, RemoteInfo } from '../type'; import { FederationHost } from '../core'; -import { loadEntryNode } from '../plugins/node'; -import { loadEntryDom } from '../plugins/dom'; +import { loadEntryNode } from '../plugins/node/loadEntry'; +import { loadEntryDom } from '../plugins/dom/loadEntry'; + +function loadEntryNodeFallback({ + remoteInfo, + createScriptHook, +}: { + remoteInfo: RemoteInfo; + createScriptHook: FederationHost['loaderHook']['lifecycle']['createScript']; +}) { + return loadEntryNode({ + entry: remoteInfo.entry, + entryGlobalName: remoteInfo.entryGlobalName, + name: remoteInfo.name, + createScriptHook: (url, attrs) => { + const res = createScriptHook.emit({ url, attrs }); + + if (!res) return; + + if ('url' in res) { + return res; + } + + return; + }, + }); +} + +function loadEntryDomFallback({ + remoteInfo, + remoteEntryExports, + createScriptHook, +}: { + remoteInfo: RemoteInfo; + remoteEntryExports?: RemoteEntryExports; + createScriptHook: FederationHost['loaderHook']['lifecycle']['createScript']; +}) { + return loadEntryDom({ + entry: remoteInfo.entry, + entryGlobalName: remoteInfo.entryGlobalName, + name: remoteInfo.name, + type: remoteInfo.type, + remoteEntryExports, + createScriptHook: (url, attrs) => { + const res = createScriptHook.emit({ url, attrs }); + + if (!res) return; + + if (res instanceof HTMLScriptElement) { + return res; + } + + if ('script' in res || 'timeout' in res) { + return res; + } + + return; + }, + }); +} export function getRemoteEntryUniqueKey(remoteInfo: RemoteInfo): string { const { entry, name } = remoteInfo; @@ -32,6 +90,7 @@ export async function getRemoteEntry({ remoteInfo, remoteEntryExports, }); + if (loadEntryHookRes) { globalLoading[uniqueKey] = loadEntryHookRes.then( (res) => res || undefined, @@ -39,44 +98,15 @@ export async function getRemoteEntry({ } else { const createScriptHook = origin.loaderHook.lifecycle.createScript; if (!isBrowserEnv()) { - globalLoading[uniqueKey] = loadEntryNode({ - entry: remoteInfo.entry, - entryGlobalName: remoteInfo.entryGlobalName, - name: remoteInfo.name, - createScriptHook: (url, attrs) => { - const res = createScriptHook.emit({ url, attrs }); - - if (!res) return; - - if ('url' in res) { - return res; - } - - return; - }, + globalLoading[uniqueKey] = loadEntryNodeFallback({ + remoteInfo, + createScriptHook, }); } else { - globalLoading[uniqueKey] = loadEntryDom({ - entry: remoteInfo.entry, - entryGlobalName: remoteInfo.entryGlobalName, - name: remoteInfo.name, - type: remoteInfo.type, + globalLoading[uniqueKey] = loadEntryDomFallback({ + remoteInfo, remoteEntryExports, - createScriptHook: (url, attrs) => { - const res = createScriptHook.emit({ url, attrs }); - - if (!res) return; - - if (res instanceof HTMLScriptElement) { - return res; - } - - if ('script' in res || 'timeout' in res) { - return res; - } - - return; - }, + createScriptHook, }); } } From ad295e1b74f17cf3dd5fe951bfb36cf8a787a052 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 13 Aug 2024 15:06:29 +0200 Subject: [PATCH 14/18] fix: use listeners.size to check for existence of plugins --- packages/runtime/src/utils/load.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts index ab4d6b4f50c..8c8b3cdc188 100644 --- a/packages/runtime/src/utils/load.ts +++ b/packages/runtime/src/utils/load.ts @@ -84,17 +84,15 @@ export async function getRemoteEntry({ } if (!globalLoading[uniqueKey]) { - const loadEntryHookRes = - origin.remoteHandler.hooks.lifecycle.loadEntry.emit({ - createScriptHook: origin.loaderHook.lifecycle.createScript, - remoteInfo, - remoteEntryExports, - }); - - if (loadEntryHookRes) { - globalLoading[uniqueKey] = loadEntryHookRes.then( - (res) => res || undefined, - ); + const loadEntryHook = origin.remoteHandler.hooks.lifecycle.loadEntry; + if (loadEntryHook.listeners.size) { + globalLoading[uniqueKey] = loadEntryHook + .emit({ + createScriptHook: origin.loaderHook.lifecycle.createScript, + remoteInfo, + remoteEntryExports, + }) + .then((res) => res || undefined); } else { const createScriptHook = origin.loaderHook.lifecycle.createScript; if (!isBrowserEnv()) { From ab4bfbd66e0a4264050e3f4a7aeaed1617ebf303 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 13 Aug 2024 15:25:34 +0200 Subject: [PATCH 15/18] chore: cleanup --- packages/runtime/src/core.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/runtime/src/core.ts b/packages/runtime/src/core.ts index 2b9636939e7..ee8b45a5a81 100644 --- a/packages/runtime/src/core.ts +++ b/packages/runtime/src/core.ts @@ -1,4 +1,4 @@ -import { CreateScriptHookReturn } from '@module-federation/sdk'; +import type { CreateScriptHookReturn } from '@module-federation/sdk'; import { Options, PreloadRemoteArgs, @@ -22,8 +22,6 @@ import { SyncWaterfallHook, } from './utils/hooks'; import { generatePreloadAssetsPlugin } from './plugins/generate-preload-assets'; -import { domPlugin } from './plugins/dom'; -import { nodePlugin } from './plugins/node'; import { snapshotPlugin } from './plugins/snapshot'; import { isBrowserEnv } from './utils/env'; import { getRemoteInfo } from './utils/load'; From c626959edb8d34d9c6af216542a6083f47add2aa Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 14 Aug 2024 10:40:08 +0200 Subject: [PATCH 16/18] fix: use create script hook dom return type --- packages/sdk/src/dom.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/dom.ts b/packages/sdk/src/dom.ts index 4e46f62e19b..e701a179b5b 100644 --- a/packages/sdk/src/dom.ts +++ b/packages/sdk/src/dom.ts @@ -1,4 +1,4 @@ -import { CreateScriptHookDom } from './types'; +import { CreateScriptHookDom, CreateScriptHookReturnDom } from './types'; import { warn } from './utils'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function safeWrapper) => any>( @@ -50,7 +50,7 @@ export function createScript(info: { script = document.createElement('script'); script.type = 'text/javascript'; script.src = info.url; - let createScriptRes: CreateScriptHookReturn = undefined; + let createScriptRes: CreateScriptHookReturnDom = undefined; if (info.createScriptHook) { createScriptRes = info.createScriptHook(info.url, info.attrs); From 630b514d8573ab373a805e54f1aa3773847019f6 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 14 Aug 2024 12:41:26 +0200 Subject: [PATCH 17/18] refactor: move utils to load --- packages/runtime/src/plugins/dom/index.ts | 35 ---- packages/runtime/src/plugins/dom/loadEntry.ts | 127 ------------ packages/runtime/src/plugins/node/index.ts | 29 --- .../runtime/src/plugins/node/loadEntry.ts | 70 ------- packages/runtime/src/utils/load.ts | 190 +++++++++++++++--- 5 files changed, 160 insertions(+), 291 deletions(-) delete mode 100644 packages/runtime/src/plugins/dom/index.ts delete mode 100644 packages/runtime/src/plugins/dom/loadEntry.ts delete mode 100644 packages/runtime/src/plugins/node/index.ts delete mode 100644 packages/runtime/src/plugins/node/loadEntry.ts diff --git a/packages/runtime/src/plugins/dom/index.ts b/packages/runtime/src/plugins/dom/index.ts deleted file mode 100644 index 62ebb895713..00000000000 --- a/packages/runtime/src/plugins/dom/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { FederationRuntimePlugin } from '../../type/plugin'; -import { loadEntryDom } from './loadEntry'; - -export function domPlugin(): FederationRuntimePlugin { - return { - name: 'dom-plugin', - async loadEntry(args) { - const { createScriptHook, remoteInfo, remoteEntryExports } = args; - const { entry, entryGlobalName, name, type } = remoteInfo; - - return loadEntryDom({ - entry, - entryGlobalName, - name, - type, - remoteEntryExports, - createScriptHook: (url, attrs) => { - const res = createScriptHook.emit({ url, attrs }); - - if (!res) return; - - if (res instanceof HTMLScriptElement) { - return res; - } - - if ('script' in res || 'timeout' in res) { - return res; - } - - return; - }, - }); - }, - }; -} diff --git a/packages/runtime/src/plugins/dom/loadEntry.ts b/packages/runtime/src/plugins/dom/loadEntry.ts deleted file mode 100644 index 0905e6836e6..00000000000 --- a/packages/runtime/src/plugins/dom/loadEntry.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { CreateScriptHookDom, loadScript } from '@module-federation/sdk'; -import { assert } from '../../utils'; -import { RemoteEntryExports } from '../../type'; -import { getRemoteEntryExports } from '../../global'; - -async function loadEsmEntry({ - entry, - remoteEntryExports, -}: { - entry: string; - remoteEntryExports: RemoteEntryExports | undefined; -}): Promise { - return new Promise((resolve, reject) => { - try { - if (!remoteEntryExports) { - // eslint-disable-next-line no-eval - new Function( - 'callbacks', - `import("${entry}").then(callbacks[0]).catch(callbacks[1])`, - )([resolve, reject]); - } else { - resolve(remoteEntryExports); - } - } catch (e) { - reject(e); - } - }); -} - -async function loadSystemJsEntry({ - entry, - remoteEntryExports, -}: { - entry: string; - remoteEntryExports: RemoteEntryExports | undefined; -}): Promise { - return new Promise((resolve, reject) => { - try { - if (!remoteEntryExports) { - // eslint-disable-next-line no-eval - new Function( - 'callbacks', - `System.import("${entry}").then(callbacks[0]).catch(callbacks[1])`, - )([resolve, reject]); - } else { - resolve(remoteEntryExports); - } - } catch (e) { - reject(e); - } - }); -} - -async function loadEntryScript({ - name, - globalName, - entry, - createScriptHook, -}: { - name: string; - globalName: string; - entry: string; - createScriptHook: CreateScriptHookDom; -}): Promise { - const { entryExports: remoteEntryExports } = getRemoteEntryExports( - name, - globalName, - ); - - if (remoteEntryExports) { - return remoteEntryExports; - } - - return loadScript(entry, { attrs: {}, createScriptHook }) - .then(() => { - const { remoteEntryKey, entryExports } = getRemoteEntryExports( - name, - globalName, - ); - - assert( - entryExports, - ` - Unable to use the ${name}'s '${entry}' URL with ${remoteEntryKey}'s globalName to get remoteEntry exports. - Possible reasons could be:\n - 1. '${entry}' is not the correct URL, or the remoteEntry resource or name is incorrect.\n - 2. ${remoteEntryKey} cannot be used to get remoteEntry exports in the window object. - `, - ); - - return entryExports; - }) - .catch((e) => { - throw e; - }); -} - -export async function loadEntryDom({ - entry, - entryGlobalName, - remoteEntryExports, - name, - type, - createScriptHook, -}: { - entry: string; - entryGlobalName: string; - remoteEntryExports: RemoteEntryExports | undefined; - name: string; - type: string; - createScriptHook: CreateScriptHookDom; -}) { - switch (type) { - case 'esm': - case 'module': - return loadEsmEntry({ entry, remoteEntryExports }); - case 'system': - return loadSystemJsEntry({ entry, remoteEntryExports }); - default: - return loadEntryScript({ - entry, - globalName: entryGlobalName, - name, - createScriptHook, - }); - } -} diff --git a/packages/runtime/src/plugins/node/index.ts b/packages/runtime/src/plugins/node/index.ts deleted file mode 100644 index 5ebcd92c621..00000000000 --- a/packages/runtime/src/plugins/node/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { FederationRuntimePlugin } from '../../type/plugin'; -import { loadEntryNode } from './loadEntry'; - -export function nodePlugin(): FederationRuntimePlugin { - return { - name: 'node-plugin', - async loadEntry(args) { - const { createScriptHook, remoteInfo } = args; - const { entry, entryGlobalName, name } = remoteInfo; - - return loadEntryNode({ - entry, - entryGlobalName, - name, - createScriptHook: (url, attrs) => { - const res = createScriptHook.emit({ url, attrs }); - - if (!res) return; - - if ('url' in res) { - return res; - } - - return; - }, - }); - }, - }; -} diff --git a/packages/runtime/src/plugins/node/loadEntry.ts b/packages/runtime/src/plugins/node/loadEntry.ts deleted file mode 100644 index f53cd19f13a..00000000000 --- a/packages/runtime/src/plugins/node/loadEntry.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { CreateScriptHookNode, loadScriptNode } from '@module-federation/sdk'; -import { getRemoteEntryExports } from '../../global'; -import { RemoteEntryExports } from '../../type'; -import { assert } from '../../utils'; - -async function loadEntryScript({ - name, - globalName, - entry, - createScriptHook, -}: { - name: string; - globalName: string; - entry: string; - createScriptHook: CreateScriptHookNode; -}): Promise { - const { entryExports: remoteEntryExports } = getRemoteEntryExports( - name, - globalName, - ); - - if (remoteEntryExports) { - return remoteEntryExports; - } - - return loadScriptNode(entry, { - attrs: { name, globalName }, - createScriptHook, - }) - .then(() => { - const { remoteEntryKey, entryExports } = getRemoteEntryExports( - name, - globalName, - ); - - assert( - entryExports, - ` - Unable to use the ${name}'s '${entry}' URL with ${remoteEntryKey}'s globalName to get remoteEntry exports. - Possible reasons could be:\n - 1. '${entry}' is not the correct URL, or the remoteEntry resource or name is incorrect.\n - 2. ${remoteEntryKey} cannot be used to get remoteEntry exports in the window object. - `, - ); - - return entryExports; - }) - .catch((e) => { - throw e; - }); -} - -export async function loadEntryNode({ - entry, - entryGlobalName, - name, - createScriptHook, -}: { - entry: string; - entryGlobalName: string; - name: string; - createScriptHook: CreateScriptHookNode; -}) { - return loadEntryScript({ - entry, - globalName: entryGlobalName, - name, - createScriptHook, - }); -} diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts index 8c8b3cdc188..0b3aaaa3d34 100644 --- a/packages/runtime/src/utils/load.ts +++ b/packages/runtime/src/utils/load.ts @@ -1,37 +1,125 @@ -import { composeKeyWithSeparator, isBrowserEnv } from '@module-federation/sdk'; +import { + loadScript, + loadScriptNode, + composeKeyWithSeparator, + isBrowserEnv, +} from '@module-federation/sdk'; import { DEFAULT_REMOTE_TYPE, DEFAULT_SCOPE } from '../constant'; -import { globalLoading } from '../global'; -import { Remote, RemoteEntryExports, RemoteInfo } from '../type'; import { FederationHost } from '../core'; -import { loadEntryNode } from '../plugins/node/loadEntry'; -import { loadEntryDom } from '../plugins/dom/loadEntry'; +import { globalLoading, getRemoteEntryExports } from '../global'; +import { Remote, RemoteEntryExports, RemoteInfo } from '../type'; +import { assert } from '../utils'; -function loadEntryNodeFallback({ - remoteInfo, +async function loadEsmEntry({ + entry, + remoteEntryExports, +}: { + entry: string; + remoteEntryExports: RemoteEntryExports | undefined; +}): Promise { + return new Promise((resolve, reject) => { + try { + if (!remoteEntryExports) { + // eslint-disable-next-line no-eval + new Function( + 'callbacks', + `import("${entry}").then(callbacks[0]).catch(callbacks[1])`, + )([resolve, reject]); + } else { + resolve(remoteEntryExports); + } + } catch (e) { + reject(e); + } + }); +} + +async function loadSystemJsEntry({ + entry, + remoteEntryExports, +}: { + entry: string; + remoteEntryExports: RemoteEntryExports | undefined; +}): Promise { + return new Promise((resolve, reject) => { + try { + if (!remoteEntryExports) { + // eslint-disable-next-line no-eval + new Function( + 'callbacks', + `System.import("${entry}").then(callbacks[0]).catch(callbacks[1])`, + )([resolve, reject]); + } else { + resolve(remoteEntryExports); + } + } catch (e) { + reject(e); + } + }); +} + +async function loadEntryScript({ + name, + globalName, + entry, createScriptHook, }: { - remoteInfo: RemoteInfo; + name: string; + globalName: string; + entry: string; createScriptHook: FederationHost['loaderHook']['lifecycle']['createScript']; -}) { - return loadEntryNode({ - entry: remoteInfo.entry, - entryGlobalName: remoteInfo.entryGlobalName, - name: remoteInfo.name, +}): Promise { + const { entryExports: remoteEntryExports } = getRemoteEntryExports( + name, + globalName, + ); + + if (remoteEntryExports) { + return remoteEntryExports; + } + + return loadScript(entry, { + attrs: {}, createScriptHook: (url, attrs) => { const res = createScriptHook.emit({ url, attrs }); if (!res) return; - if ('url' in res) { + if (res instanceof HTMLScriptElement) { + return res; + } + + if ('script' in res || 'timeout' in res) { return res; } return; }, - }); + }) + .then(() => { + const { remoteEntryKey, entryExports } = getRemoteEntryExports( + name, + globalName, + ); + + assert( + entryExports, + ` + Unable to use the ${name}'s '${entry}' URL with ${remoteEntryKey}'s globalName to get remoteEntry exports. + Possible reasons could be:\n + 1. '${entry}' is not the correct URL, or the remoteEntry resource or name is incorrect.\n + 2. ${remoteEntryKey} cannot be used to get remoteEntry exports in the window object. + `, + ); + + return entryExports; + }) + .catch((e) => { + throw e; + }); } -function loadEntryDomFallback({ +async function loadEntryDom({ remoteInfo, remoteEntryExports, createScriptHook, @@ -40,28 +128,70 @@ function loadEntryDomFallback({ remoteEntryExports?: RemoteEntryExports; createScriptHook: FederationHost['loaderHook']['lifecycle']['createScript']; }) { - return loadEntryDom({ - entry: remoteInfo.entry, - entryGlobalName: remoteInfo.entryGlobalName, - name: remoteInfo.name, - type: remoteInfo.type, - remoteEntryExports, + const { entry, entryGlobalName: globalName, name, type } = remoteInfo; + switch (type) { + case 'esm': + case 'module': + return loadEsmEntry({ entry, remoteEntryExports }); + case 'system': + return loadSystemJsEntry({ entry, remoteEntryExports }); + default: + return loadEntryScript({ entry, globalName, name, createScriptHook }); + } +} + +async function loadEntryNode({ + remoteInfo, + createScriptHook, +}: { + remoteInfo: RemoteInfo; + createScriptHook: FederationHost['loaderHook']['lifecycle']['createScript']; +}) { + const { entry, entryGlobalName: globalName, name } = remoteInfo; + const { entryExports: remoteEntryExports } = getRemoteEntryExports( + name, + globalName, + ); + + if (remoteEntryExports) { + return remoteEntryExports; + } + + return loadScriptNode(entry, { + attrs: { name, globalName }, createScriptHook: (url, attrs) => { const res = createScriptHook.emit({ url, attrs }); if (!res) return; - if (res instanceof HTMLScriptElement) { - return res; - } - - if ('script' in res || 'timeout' in res) { + if ('url' in res) { return res; } return; }, - }); + }) + .then(() => { + const { remoteEntryKey, entryExports } = getRemoteEntryExports( + name, + globalName, + ); + + assert( + entryExports, + ` + Unable to use the ${name}'s '${entry}' URL with ${remoteEntryKey}'s globalName to get remoteEntry exports. + Possible reasons could be:\n + 1. '${entry}' is not the correct URL, or the remoteEntry resource or name is incorrect.\n + 2. ${remoteEntryKey} cannot be used to get remoteEntry exports in the window object. + `, + ); + + return entryExports; + }) + .catch((e) => { + throw e; + }); } export function getRemoteEntryUniqueKey(remoteInfo: RemoteInfo): string { @@ -96,12 +226,12 @@ export async function getRemoteEntry({ } else { const createScriptHook = origin.loaderHook.lifecycle.createScript; if (!isBrowserEnv()) { - globalLoading[uniqueKey] = loadEntryNodeFallback({ + globalLoading[uniqueKey] = loadEntryNode({ remoteInfo, createScriptHook, }); } else { - globalLoading[uniqueKey] = loadEntryDomFallback({ + globalLoading[uniqueKey] = loadEntryDom({ remoteInfo, remoteEntryExports, createScriptHook, From 82403465599a230d1ccb9eeb941736824aa6758b Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 14 Aug 2024 12:51:55 +0200 Subject: [PATCH 18/18] chore: add changeset --- .changeset/modern-suns-arrive.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/modern-suns-arrive.md diff --git a/.changeset/modern-suns-arrive.md b/.changeset/modern-suns-arrive.md new file mode 100644 index 00000000000..17bf199b51b --- /dev/null +++ b/.changeset/modern-suns-arrive.md @@ -0,0 +1,6 @@ +--- +'@module-federation/runtime': minor +'@module-federation/sdk': patch +--- + +feat(runtime): add loadEntry hook