Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support react-native #2812

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
67 changes: 64 additions & 3 deletions packages/runtime/src/utils/load.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -194,6 +196,60 @@ async function loadEntryNode({
});
}

async function loadEntryReactNative({
jbroma marked this conversation as resolved.
Show resolved Hide resolved
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);
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 21 additions & 1 deletion packages/sdk/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' &&
Expand All @@ -22,4 +36,10 @@ const getProcessEnv = function (): Record<string, string | undefined> {
return typeof process !== 'undefined' && process.env ? process.env : {};
};

export { isBrowserEnv, isDebugMode, getProcessEnv };
export {
isBrowserEnv,
isNodeEnv,
isReactNativeEnv,
isDebugMode,
getProcessEnv,
};
1 change: 1 addition & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export * from './logger';
export * from './env';
export * from './dom';
export * from './node';
export * from './react-native';
export * from './normalizeOptions';
64 changes: 64 additions & 0 deletions packages/sdk/src/react-native.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
};

declare const __webpack_require__: WebpackRequire;

export function createScriptReactNative(
url: string,
cb: (error?: Error) => void,
attrs?: Record<string, any>,
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<string, any>;
createScriptHook?: CreateScriptHook;
},
) {
return new Promise<void>((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,
);
});
}
3 changes: 3 additions & 0 deletions packages/sdk/src/types/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
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 CreateScriptHookNode = (
Expand Down
Loading