From 8af455ecaedc7d7aec0ad3e6e4ad7dc98694c42c Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 30 Jul 2024 11:41:55 +0200 Subject: [PATCH 1/9] feat: add node and react native env checks --- packages/sdk/src/env.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/env.ts b/packages/sdk/src/env.ts index 63343e58b46..4fcb87e20c4 100644 --- a/packages/sdk/src/env.ts +++ b/packages/sdk/src/env.ts @@ -7,6 +7,18 @@ function isBrowserEnv(): boolean { return typeof window !== 'undefined'; } +function isNodeEnv(): boolean { + return ( + typeof process !== 'undefined' && process?.versions?.node !== 'undefined' + ); +} + +function isReactNativeEnv(): boolean { + return ( + typeof navigator !== 'undefined' && navigator?.product === 'ReactNative' + ); +} + function isDebugMode(): boolean { if ( typeof process !== 'undefined' && @@ -22,4 +34,10 @@ const getProcessEnv = function (): Record { return typeof process !== 'undefined' && process.env ? process.env : {}; }; -export { isBrowserEnv, isDebugMode, getProcessEnv }; +export { + isBrowserEnv, + isNodeEnv, + isReactNativeEnv, + isDebugMode, + getProcessEnv, +}; From 6a761ae8f8c095755114ca5649cc0844aefef396 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 30 Jul 2024 14:30:39 +0200 Subject: [PATCH 2/9] feat: initial support for react native --- packages/runtime/src/utils/load.ts | 40 ++++++------------ packages/sdk/src/index.ts | 1 + packages/sdk/src/react-native.ts | 65 ++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 27 deletions(-) create mode 100644 packages/sdk/src/react-native.ts diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts index 938d1b8686c..2fd01c2c9a4 100644 --- a/packages/runtime/src/utils/load.ts +++ b/packages/runtime/src/utils/load.ts @@ -3,6 +3,8 @@ import { loadScript, loadScriptNode, CreateScriptHookReturn, + isNodeEnv, + isReactNativeEnv, } from '@module-federation/sdk'; import { assert } from '../utils/logger'; import { getRemoteEntryExports, globalLoading } from '../global'; @@ -80,35 +82,19 @@ export async function loadEntryScript({ 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; - }); + let loadScriptCallback, attrs; + if (isNodeEnv()) { + loadScriptCallback = loadScriptNode; + attrs = { name, globalName }; + } else if (isReactNativeEnv()) { + loadScriptCallback = loadScriptNode; + attrs = { name, globalName }; + } else { + loadScriptCallback = loadScript; + attrs = {}; } - return loadScript(entry, { attrs: {}, createScriptHook }) + return loadScriptCallback(entry, { attrs, createScriptHook }) .then(() => { const { remoteEntryKey, entryExports } = getRemoteEntryExports( name, diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 43c33934a9c..ca43ee9a911 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -11,4 +11,5 @@ export * from './logger'; export * from './env'; export * from './dom'; export * from './node'; +export * from './react-native'; export * from './normalizeOptions'; diff --git a/packages/sdk/src/react-native.ts b/packages/sdk/src/react-native.ts new file mode 100644 index 00000000000..4f27b528d2f --- /dev/null +++ b/packages/sdk/src/react-native.ts @@ -0,0 +1,65 @@ +type WebpackRequire = { + l: ( + url: string, + done: (event?: { type: 'load' | string; target?: { src: string } }) => void, + key?: string, + chunkId?: string, + ) => Record; +}; + +declare const __webpack_require__: WebpackRequire; + +export function createScriptReactNative( + url: string, + cb: (error?: Error) => void, + attrs?: Record, + createScriptHook?: ( + url: string, + attrs?: Record | undefined, + ) => { url: string } | void, +) { + if (createScriptHook) { + const hookResult = createScriptHook(url, attrs); + if (hookResult && typeof hookResult === 'object' && 'url' in hookResult) { + url = hookResult.url; + } + } + + if (!attrs || !attrs['globalName']) { + cb(new Error('createScriptReactNative: globalName is required')); + return; + } + + __webpack_require__.l( + url, + (e) => { + cb(e ? new Error(`Script execution failed`) : undefined); + }, + attrs['globalName'], + ); +} + +export async function loadScriptReactNative( + url: string, + info: { + attrs?: Record; + createScriptHook?: (url: string) => void; + }, +) { + return new Promise((resolve, reject) => { + createScriptReactNative( + url, + (error) => { + if (error) { + reject(error); + } else { + const remoteEntryKey = info?.attrs?.['globalName']; + const entryExports = (globalThis as any)[remoteEntryKey]; + resolve(entryExports); + } + }, + info.attrs, + info.createScriptHook, + ); + }); +} From a08e85bbd9de3d3447ac6bd2d2592f17ac6648a3 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 30 Jul 2024 14:47:10 +0200 Subject: [PATCH 3/9] fix: typing for CreateScriptHook --- packages/runtime/src/module/index.ts | 2 +- packages/runtime/src/utils/load.ts | 12 +++--------- packages/runtime/src/utils/preload.ts | 6 +++--- packages/sdk/src/dom.ts | 22 ++++++++-------------- packages/sdk/src/node.ts | 6 ++++-- packages/sdk/src/react-native.ts | 9 ++++----- packages/sdk/src/types/hooks.ts | 18 ++++++++++++++++++ packages/sdk/src/types/index.ts | 1 + 8 files changed, 42 insertions(+), 34 deletions(-) create mode 100644 packages/sdk/src/types/hooks.ts diff --git a/packages/runtime/src/module/index.ts b/packages/runtime/src/module/index.ts index 28d22aba40d..1f452b24788 100644 --- a/packages/runtime/src/module/index.ts +++ b/packages/runtime/src/module/index.ts @@ -32,7 +32,7 @@ class Module { const remoteEntryExports = await getRemoteEntry({ remoteInfo: this.remoteInfo, remoteEntryExports: this.remoteEntryExports, - createScriptHook: (url: string, attrs: any) => { + createScriptHook: (url, attrs) => { const res = this.host.loaderHook.lifecycle.createScript.emit({ url, attrs, diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts index 2fd01c2c9a4..1219b10b4df 100644 --- a/packages/runtime/src/utils/load.ts +++ b/packages/runtime/src/utils/load.ts @@ -1,8 +1,8 @@ import { + CreateScriptHook, composeKeyWithSeparator, loadScript, loadScriptNode, - CreateScriptHookReturn, isNodeEnv, isReactNativeEnv, } from '@module-federation/sdk'; @@ -68,10 +68,7 @@ export async function loadEntryScript({ name: string; globalName: string; entry: string; - createScriptHook?: ( - url: string, - attrs?: Record | undefined, - ) => CreateScriptHookReturn; + createScriptHook?: CreateScriptHook; }): Promise { const { entryExports: remoteEntryExports } = getRemoteEntryExports( name, @@ -130,10 +127,7 @@ export async function getRemoteEntry({ }: { remoteInfo: RemoteInfo; remoteEntryExports?: RemoteEntryExports | undefined; - createScriptHook?: ( - url: string, - attrs?: Record | undefined, - ) => CreateScriptHookReturn; + createScriptHook?: CreateScriptHook; }): Promise { const { entry, name, type, entryGlobalName } = remoteInfo; const uniqueKey = getRemoteEntryUniqueKey(remoteInfo); diff --git a/packages/runtime/src/utils/preload.ts b/packages/runtime/src/utils/preload.ts index 8edeb1baeb8..3d47e4202f6 100644 --- a/packages/runtime/src/utils/preload.ts +++ b/packages/runtime/src/utils/preload.ts @@ -82,7 +82,7 @@ export function preloadAssets( getRemoteEntry({ remoteInfo: moduleInfo, remoteEntryExports: module.remoteEntryExports, - createScriptHook: (url: string, attrs: any) => { + createScriptHook: (url, attrs) => { const res = host.loaderHook.lifecycle.createScript.emit({ url, attrs, @@ -109,7 +109,7 @@ export function preloadAssets( getRemoteEntry({ remoteInfo: moduleInfo, remoteEntryExports: undefined, - createScriptHook: (url: string, attrs: any) => { + createScriptHook: (url, attrs) => { const res = host.loaderHook.lifecycle.createScript.emit({ url, attrs, @@ -214,7 +214,7 @@ export function preloadAssets( fetchpriority: 'high', type: remoteInfo?.type === 'module' ? 'module' : 'text/javascript', }, - createScriptHook: (url: string, attrs: any) => { + createScriptHook: (url, attrs) => { const res = host.loaderHook.lifecycle.createScript.emit({ url, attrs, diff --git a/packages/sdk/src/dom.ts b/packages/sdk/src/dom.ts index e86bfb8640f..42c37a67068 100644 --- a/packages/sdk/src/dom.ts +++ b/packages/sdk/src/dom.ts @@ -1,3 +1,4 @@ +import { CreateScriptHook } 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?: CreateScriptHook; }): { script: HTMLScriptElement; needAttach: boolean } { // Retrieve the existing script element by its src attribute let script: HTMLScriptElement | null = null; @@ -62,7 +55,11 @@ export function createScript(info: { if (createScriptRes instanceof HTMLScriptElement) { script = createScriptRes; - } else if (typeof createScriptRes === 'object') { + } else if ( + typeof createScriptRes === 'object' && + 'script' in createScriptRes && + 'timeout' in createScriptRes + ) { if (createScriptRes.script) script = createScriptRes.script; if (createScriptRes.timeout) timeout = createScriptRes.timeout; } @@ -205,10 +202,7 @@ export function loadScript( url: string, info: { attrs?: Record; - createScriptHook?: ( - url: string, - attrs?: Record | undefined, - ) => CreateScriptHookReturn; + createScriptHook?: CreateScriptHook; }, ) { const { attrs = {}, createScriptHook } = info; diff --git a/packages/sdk/src/node.ts b/packages/sdk/src/node.ts index e60b2b1a776..862e3073b2a 100644 --- a/packages/sdk/src/node.ts +++ b/packages/sdk/src/node.ts @@ -1,3 +1,5 @@ +import { CreateScriptHook } 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?: CreateScriptHook, ) { if (createScriptHook) { const hookResult = createScriptHook(url); @@ -140,7 +142,7 @@ export function loadScriptNode( url: string, info: { attrs?: Record; - createScriptHook?: (url: string) => void; + createScriptHook?: CreateScriptHook; }, ) { return new Promise((resolve, reject) => { diff --git a/packages/sdk/src/react-native.ts b/packages/sdk/src/react-native.ts index 4f27b528d2f..8ab009c578f 100644 --- a/packages/sdk/src/react-native.ts +++ b/packages/sdk/src/react-native.ts @@ -1,3 +1,5 @@ +import { CreateScriptHook } from './types'; + type WebpackRequire = { l: ( url: string, @@ -13,10 +15,7 @@ export function createScriptReactNative( url: string, cb: (error?: Error) => void, attrs?: Record, - createScriptHook?: ( - url: string, - attrs?: Record | undefined, - ) => { url: string } | void, + createScriptHook?: CreateScriptHook, ) { if (createScriptHook) { const hookResult = createScriptHook(url, attrs); @@ -43,7 +42,7 @@ export async function loadScriptReactNative( url: string, info: { attrs?: Record; - createScriptHook?: (url: string) => void; + createScriptHook?: CreateScriptHook; }, ) { 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..35ea2e5d8ac --- /dev/null +++ b/packages/sdk/src/types/hooks.ts @@ -0,0 +1,18 @@ +export type CreateScriptHookReturnNode = { url: string } | void; + +export type CreateScriptHookReturnReactNative = { url: string } | void; + +export type CreateScriptHookReturnDom = + | HTMLScriptElement + | { script?: HTMLScriptElement; timeout?: number } + | void; + +export type CreateScriptHookReturn = + | CreateScriptHookReturnNode + | CreateScriptHookReturnReactNative + | 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 ff4f3ab2255712a61aa6b0390074985ac50f880c Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 30 Jul 2024 17:09:33 +0200 Subject: [PATCH 4/9] fix: use loadReactNative in runtime --- packages/runtime/src/utils/load.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts index 1219b10b4df..5b0af7130e4 100644 --- a/packages/runtime/src/utils/load.ts +++ b/packages/runtime/src/utils/load.ts @@ -3,6 +3,7 @@ import { composeKeyWithSeparator, loadScript, loadScriptNode, + loadScriptReactNative, isNodeEnv, isReactNativeEnv, } from '@module-federation/sdk'; @@ -84,7 +85,7 @@ export async function loadEntryScript({ loadScriptCallback = loadScriptNode; attrs = { name, globalName }; } else if (isReactNativeEnv()) { - loadScriptCallback = loadScriptNode; + loadScriptCallback = loadScriptReactNative; attrs = { name, globalName }; } else { loadScriptCallback = loadScript; From db8237d01599162c69cf05c41ccddc5772b956e6 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 30 Jul 2024 17:26:59 +0200 Subject: [PATCH 5/9] fix: remove async from loadScriptReactNative to match other signatures --- packages/sdk/src/react-native.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/react-native.ts b/packages/sdk/src/react-native.ts index 8ab009c578f..98d833394d7 100644 --- a/packages/sdk/src/react-native.ts +++ b/packages/sdk/src/react-native.ts @@ -38,7 +38,7 @@ export function createScriptReactNative( ); } -export async function loadScriptReactNative( +export function loadScriptReactNative( url: string, info: { attrs?: Record; From ee81c18f8ec063b29337881fc1874e4ce5fcc560 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Tue, 30 Jul 2024 17:40:16 +0200 Subject: [PATCH 6/9] fix: node env check --- packages/sdk/src/env.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/env.ts b/packages/sdk/src/env.ts index 4fcb87e20c4..4afc3a47778 100644 --- a/packages/sdk/src/env.ts +++ b/packages/sdk/src/env.ts @@ -9,7 +9,7 @@ function isBrowserEnv(): boolean { function isNodeEnv(): boolean { return ( - typeof process !== 'undefined' && process?.versions?.node !== 'undefined' + typeof process !== 'undefined' && process?.versions?.node !== undefined ); } From f58c46db44c77b29ff81e41a329f2450e9e04021 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Thu, 1 Aug 2024 11:25:21 +0200 Subject: [PATCH 7/9] fix: sdk tests --- packages/sdk/src/dom.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/sdk/src/dom.ts b/packages/sdk/src/dom.ts index 42c37a67068..d2c581b32c8 100644 --- a/packages/sdk/src/dom.ts +++ b/packages/sdk/src/dom.ts @@ -55,13 +55,13 @@ export function createScript(info: { if (createScriptRes instanceof HTMLScriptElement) { script = createScriptRes; - } else if ( - typeof createScriptRes === 'object' && - 'script' in createScriptRes && - 'timeout' in createScriptRes - ) { - if (createScriptRes.script) script = createScriptRes.script; - if (createScriptRes.timeout) timeout = createScriptRes.timeout; + } else if (typeof createScriptRes === 'object') { + if ('script' in createScriptRes && createScriptRes.script) { + script = createScriptRes.script; + } + if ('timeout' in createScriptRes && createScriptRes.timeout) { + timeout = createScriptRes.timeout; + } } } const attrs = info.attrs; From 8a06249b2526a3411c95fc6b0c596d57210252a5 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Thu, 1 Aug 2024 12:25:34 +0200 Subject: [PATCH 8/9] fix: make node env check more strict --- packages/sdk/src/env.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/env.ts b/packages/sdk/src/env.ts index 4afc3a47778..b1e1f5d0a8d 100644 --- a/packages/sdk/src/env.ts +++ b/packages/sdk/src/env.ts @@ -9,7 +9,9 @@ function isBrowserEnv(): boolean { function isNodeEnv(): boolean { return ( - typeof process !== 'undefined' && process?.versions?.node !== undefined + typeof window === 'undefined' && + typeof process !== 'undefined' && + process?.versions?.node !== undefined ); } From 5e669361d2d8e150201cef79ba9008aae4f9d317 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Thu, 15 Aug 2024 09:58:37 +0200 Subject: [PATCH 9/9] fix: issues after merge --- packages/runtime/src/utils/load.ts | 59 ++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts index 9b23052cd53..1700f6d58ca 100644 --- a/packages/runtime/src/utils/load.ts +++ b/packages/runtime/src/utils/load.ts @@ -5,9 +5,6 @@ import { loadScriptReactNative, isNodeEnv, isReactNativeEnv, - loadScript, - loadScriptNode, - composeKeyWithSeparator, } from '@module-federation/sdk'; import { DEFAULT_REMOTE_TYPE, DEFAULT_SCOPE } from '../constant'; import { FederationHost } from '../core'; @@ -199,6 +196,60 @@ async function loadEntryNode({ }); } +async function loadEntryReactNative({ + 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 loadScriptReactNative(entry, { + attrs: { name, globalName }, + createScriptHook: (url, attrs) => { + const res = createScriptHook.emit({ url, attrs }); + + if (!res) return; + + 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 { const { entry, name } = remoteInfo; return composeKeyWithSeparator(name, entry); @@ -236,7 +287,7 @@ export async function getRemoteEntry({ createScriptHook, }); } else if (isReactNativeEnv()) { - globalLoading[uniqueKey] = loadScriptReactNative({ + globalLoading[uniqueKey] = loadEntryReactNative({ remoteInfo, createScriptHook, });