diff --git a/packages/runtime/src/utils/load.ts b/packages/runtime/src/utils/load.ts index 0b3aaaa3d34..1700f6d58ca 100644 --- a/packages/runtime/src/utils/load.ts +++ b/packages/runtime/src/utils/load.ts @@ -1,8 +1,10 @@ import { + composeKeyWithSeparator, loadScript, loadScriptNode, - composeKeyWithSeparator, - isBrowserEnv, + loadScriptReactNative, + isNodeEnv, + isReactNativeEnv, } from '@module-federation/sdk'; import { DEFAULT_REMOTE_TYPE, DEFAULT_SCOPE } from '../constant'; import { FederationHost } from '../core'; @@ -194,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); @@ -225,11 +281,16 @@ export async function getRemoteEntry({ .then((res) => res || undefined); } else { const createScriptHook = origin.loaderHook.lifecycle.createScript; - if (!isBrowserEnv()) { + if (isNodeEnv()) { globalLoading[uniqueKey] = loadEntryNode({ remoteInfo, createScriptHook, }); + } else if (isReactNativeEnv()) { + globalLoading[uniqueKey] = loadEntryReactNative({ + remoteInfo, + createScriptHook, + }); } else { globalLoading[uniqueKey] = loadEntryDom({ remoteInfo, diff --git a/packages/sdk/src/env.ts b/packages/sdk/src/env.ts index 63343e58b46..b1e1f5d0a8d 100644 --- a/packages/sdk/src/env.ts +++ b/packages/sdk/src/env.ts @@ -7,6 +7,20 @@ function isBrowserEnv(): boolean { return typeof window !== 'undefined'; } +function isNodeEnv(): boolean { + return ( + typeof window === 'undefined' && + 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 +36,10 @@ const getProcessEnv = function (): Record { return typeof process !== 'undefined' && process.env ? process.env : {}; }; -export { isBrowserEnv, isDebugMode, getProcessEnv }; +export { + isBrowserEnv, + isNodeEnv, + isReactNativeEnv, + isDebugMode, + getProcessEnv, +}; 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..98d833394d7 --- /dev/null +++ b/packages/sdk/src/react-native.ts @@ -0,0 +1,64 @@ +import { CreateScriptHook } from './types'; + +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?: CreateScriptHook, +) { + 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 function loadScriptReactNative( + url: string, + info: { + attrs?: Record; + createScriptHook?: CreateScriptHook; + }, +) { + 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, + ); + }); +} diff --git a/packages/sdk/src/types/hooks.ts b/packages/sdk/src/types/hooks.ts index 1284d890e00..9149dadf1e0 100644 --- a/packages/sdk/src/types/hooks.ts +++ b/packages/sdk/src/types/hooks.ts @@ -1,5 +1,7 @@ export type CreateScriptHookReturnNode = { url: string } | void; +export type CreateScriptHookReturnReactNative = { url: string } | void; + export type CreateScriptHookReturnDom = | HTMLScriptElement | { script?: HTMLScriptElement; timeout?: number } @@ -7,6 +9,7 @@ export type CreateScriptHookReturnDom = export type CreateScriptHookReturn = | CreateScriptHookReturnNode + | CreateScriptHookReturnReactNative | CreateScriptHookReturnDom; export type CreateScriptHookNode = (