@bm-fe/react-native-multi-bundle 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INTEGRATION.md +371 -0
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/package.json +86 -0
- package/scripts/build-multi-bundle.js +483 -0
- package/scripts/release-codepush-dev.sh +90 -0
- package/scripts/release-codepush.js +420 -0
- package/scripts/sync-bundles-to-assets.js +252 -0
- package/src/index.ts +40 -0
- package/src/multi-bundle/DevModeConfig.ts +23 -0
- package/src/multi-bundle/HMRClient.ts +157 -0
- package/src/multi-bundle/LocalBundleManager.ts +155 -0
- package/src/multi-bundle/ModuleErrorFallback.tsx +85 -0
- package/src/multi-bundle/ModuleLoaderMock.ts +169 -0
- package/src/multi-bundle/ModuleLoadingPlaceholder.tsx +34 -0
- package/src/multi-bundle/ModuleRegistry.ts +295 -0
- package/src/multi-bundle/README.md +343 -0
- package/src/multi-bundle/config.ts +141 -0
- package/src/multi-bundle/createModuleLoader.tsx +92 -0
- package/src/multi-bundle/createModuleRouteLoader.tsx +31 -0
- package/src/multi-bundle/devUtils.ts +48 -0
- package/src/multi-bundle/init.ts +131 -0
- package/src/multi-bundle/metro-config-helper.js +140 -0
- package/src/multi-bundle/preloadModule.ts +33 -0
- package/src/multi-bundle/routeRegistry.ts +118 -0
- package/src/types/global.d.ts +14 -0
- package/templates/metro.config.js.template +45 -0
- package/templates/multi-bundle.config.json.template +31 -0
- package/templates/native/android/ModuleLoaderModule.kt +227 -0
- package/templates/native/android/ModuleLoaderPackage.kt +26 -0
- package/templates/native/ios/ModuleLoaderModule.h +13 -0
- package/templates/native/ios/ModuleLoaderModule.m +60 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MultiBundleConfig - 多 Bundle 配置系统
|
|
3
|
+
* 提供统一的配置接口,支持代码配置方式
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BundleManifest } from './ModuleRegistry';
|
|
7
|
+
import { Platform } from 'react-native';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 开发服务器配置
|
|
11
|
+
*/
|
|
12
|
+
export interface DevServerConfig {
|
|
13
|
+
host: string;
|
|
14
|
+
port: number;
|
|
15
|
+
protocol?: 'http' | 'https';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Manifest 提供者类型
|
|
20
|
+
* 支持函数(异步获取)或 URL 字符串
|
|
21
|
+
*/
|
|
22
|
+
export type ManifestProvider = (() => Promise<BundleManifest>) | string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* ModuleLoader 接口
|
|
26
|
+
*/
|
|
27
|
+
export interface ModuleLoader {
|
|
28
|
+
loadBusinessBundle(moduleId: string, bundlePath: string): Promise<{ success: boolean; errorMessage?: string }>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 多 Bundle 配置接口
|
|
33
|
+
*/
|
|
34
|
+
export interface MultiBundleConfig {
|
|
35
|
+
/**
|
|
36
|
+
* 模块路径规则(glob 模式)
|
|
37
|
+
* 用于 Metro 配置中识别模块 bundle
|
|
38
|
+
* 例如: ['src/modules/**']
|
|
39
|
+
*/
|
|
40
|
+
modulePaths?: string[];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 共享依赖路径列表(glob 模式)
|
|
44
|
+
* 这些路径下的代码会被打包到主 bundle,模块 bundle 可以共享使用
|
|
45
|
+
* 例如: ['src/multi-bundle/**', 'src/navigation/**']
|
|
46
|
+
*/
|
|
47
|
+
sharedDependencies?: string[];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Manifest 提供者
|
|
51
|
+
* - 函数:自定义获取逻辑
|
|
52
|
+
* - 字符串:manifest URL(开发环境从 HTTP 服务器获取)
|
|
53
|
+
*/
|
|
54
|
+
manifestProvider?: ManifestProvider;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 自定义 ModuleLoader(可选)
|
|
58
|
+
* 如果不提供,将使用默认的 ModuleLoader(开发环境用 Mock,生产环境用 Native)
|
|
59
|
+
*/
|
|
60
|
+
moduleLoader?: ModuleLoader;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 预加载模块列表
|
|
64
|
+
* 应用启动时自动预加载这些模块
|
|
65
|
+
*/
|
|
66
|
+
preloadModules?: string[];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 开发服务器配置
|
|
70
|
+
* 用于开发环境从 HTTP 服务器获取 manifest 和 bundle
|
|
71
|
+
*/
|
|
72
|
+
devServer?: DevServerConfig;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 错误处理回调
|
|
76
|
+
*/
|
|
77
|
+
onError?: (error: Error) => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 默认配置
|
|
82
|
+
*/
|
|
83
|
+
const DEFAULT_CONFIG: Required<Omit<MultiBundleConfig, 'moduleLoader' | 'manifestProvider' | 'onError'>> & {
|
|
84
|
+
moduleLoader?: ModuleLoader;
|
|
85
|
+
manifestProvider?: ManifestProvider;
|
|
86
|
+
onError?: (error: Error) => void;
|
|
87
|
+
} = {
|
|
88
|
+
modulePaths: ['src/modules/**'],
|
|
89
|
+
sharedDependencies: ['src/multi-bundle/**', 'src/navigation/**'],
|
|
90
|
+
preloadModules: [],
|
|
91
|
+
devServer: {
|
|
92
|
+
host: Platform.OS === 'android' ? '10.0.2.2' : 'localhost',
|
|
93
|
+
port: 8081,
|
|
94
|
+
protocol: 'http',
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 合并配置
|
|
100
|
+
* 将用户配置与默认配置合并
|
|
101
|
+
*/
|
|
102
|
+
export function mergeConfig(
|
|
103
|
+
userConfig: MultiBundleConfig
|
|
104
|
+
): Required<Omit<MultiBundleConfig, 'moduleLoader' | 'manifestProvider' | 'onError'>> & {
|
|
105
|
+
moduleLoader?: ModuleLoader;
|
|
106
|
+
manifestProvider?: ManifestProvider;
|
|
107
|
+
onError?: (error: Error) => void;
|
|
108
|
+
} {
|
|
109
|
+
return {
|
|
110
|
+
modulePaths: userConfig.modulePaths ?? DEFAULT_CONFIG.modulePaths,
|
|
111
|
+
sharedDependencies: userConfig.sharedDependencies ?? DEFAULT_CONFIG.sharedDependencies,
|
|
112
|
+
preloadModules: userConfig.preloadModules ?? DEFAULT_CONFIG.preloadModules,
|
|
113
|
+
devServer: {
|
|
114
|
+
...DEFAULT_CONFIG.devServer,
|
|
115
|
+
...userConfig.devServer,
|
|
116
|
+
},
|
|
117
|
+
moduleLoader: userConfig.moduleLoader,
|
|
118
|
+
manifestProvider: userConfig.manifestProvider,
|
|
119
|
+
onError: userConfig.onError,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 全局配置实例(用于 Metro 配置等场景)
|
|
125
|
+
*/
|
|
126
|
+
let globalConfig: ReturnType<typeof mergeConfig> | null = null;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 设置全局配置
|
|
130
|
+
*/
|
|
131
|
+
export function setGlobalConfig(config: ReturnType<typeof mergeConfig>) {
|
|
132
|
+
globalConfig = config;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 获取全局配置
|
|
137
|
+
*/
|
|
138
|
+
export function getGlobalConfig(): ReturnType<typeof mergeConfig> | null {
|
|
139
|
+
return globalConfig;
|
|
140
|
+
}
|
|
141
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createModuleLoader - 创建模块加载器函数
|
|
3
|
+
* 用于 React Navigation 的 getComponent API,实现真正的懒加载
|
|
4
|
+
* 组件只在导航到该路由时才被创建和加载
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useEffect, useState } from 'react';
|
|
8
|
+
import { ModuleRegistry } from './ModuleRegistry';
|
|
9
|
+
import { ModuleLoadingPlaceholder } from './ModuleLoadingPlaceholder';
|
|
10
|
+
import { ModuleErrorFallback } from './ModuleErrorFallback';
|
|
11
|
+
|
|
12
|
+
type ModuleState = 'idle' | 'loading' | 'success' | 'error';
|
|
13
|
+
|
|
14
|
+
interface CreateModuleLoaderOptions {
|
|
15
|
+
onError?: (error: Error) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 创建模块加载器函数
|
|
20
|
+
* @param moduleId 模块 ID
|
|
21
|
+
* @param pickComponent 从模块 exports 中提取组件的函数
|
|
22
|
+
* @param options 可选配置
|
|
23
|
+
* @returns 返回一个函数,该函数返回 React 组件(用于 getComponent)
|
|
24
|
+
*/
|
|
25
|
+
export function createModuleLoader(
|
|
26
|
+
moduleId: string,
|
|
27
|
+
pickComponent: (exports: any) => React.ComponentType<any>,
|
|
28
|
+
options: CreateModuleLoaderOptions = {}
|
|
29
|
+
): () => React.ComponentType<any> {
|
|
30
|
+
return function getModuleComponent() {
|
|
31
|
+
function ModuleWrappedScreen(props: any) {
|
|
32
|
+
const [state, setState] = useState<ModuleState>('idle');
|
|
33
|
+
const [InnerComponent, setInnerComponent] =
|
|
34
|
+
useState<React.ComponentType<any> | null>(null);
|
|
35
|
+
const [error, setError] = useState<Error | null>(null);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
let isMounted = true;
|
|
39
|
+
|
|
40
|
+
async function load() {
|
|
41
|
+
try {
|
|
42
|
+
setState('loading');
|
|
43
|
+
|
|
44
|
+
await ModuleRegistry.loadModule(moduleId);
|
|
45
|
+
if (!isMounted) return;
|
|
46
|
+
|
|
47
|
+
const exports = ModuleRegistry.getModuleExports(moduleId);
|
|
48
|
+
const Comp = pickComponent(exports);
|
|
49
|
+
|
|
50
|
+
setInnerComponent(() => Comp);
|
|
51
|
+
setState('success');
|
|
52
|
+
} catch (e) {
|
|
53
|
+
if (!isMounted) return;
|
|
54
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
55
|
+
setError(err);
|
|
56
|
+
setState('error');
|
|
57
|
+
options.onError?.(err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
load();
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
isMounted = false;
|
|
65
|
+
};
|
|
66
|
+
}, [moduleId]);
|
|
67
|
+
|
|
68
|
+
if (state === 'error') {
|
|
69
|
+
return (
|
|
70
|
+
<ModuleErrorFallback
|
|
71
|
+
moduleId={moduleId}
|
|
72
|
+
error={error}
|
|
73
|
+
onRetry={() => {
|
|
74
|
+
setState('idle');
|
|
75
|
+
setError(null);
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (state === 'loading' || !InnerComponent) {
|
|
82
|
+
return <ModuleLoadingPlaceholder moduleId={moduleId} />;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return <InnerComponent {...props} />;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
(ModuleWrappedScreen as any).displayName = `ModuleLoader(${moduleId})`;
|
|
89
|
+
|
|
90
|
+
return ModuleWrappedScreen;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createModuleRouteLoader - 创建路由模块加载器函数
|
|
3
|
+
* 简化路由定义的辅助函数,自动从模块 exports.routes 中提取组件
|
|
4
|
+
* 用于 React Navigation 的 getComponent API
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { createModuleLoader } from './createModuleLoader';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 创建路由模块加载器函数
|
|
12
|
+
* @param moduleId 模块 ID
|
|
13
|
+
* @param routeKey 路由 key(对应模块 exports.routes[routeKey])
|
|
14
|
+
* @returns 返回一个函数,该函数返回 React 组件(用于 getComponent)
|
|
15
|
+
*/
|
|
16
|
+
export function createModuleRouteLoader(
|
|
17
|
+
moduleId: string,
|
|
18
|
+
routeKey: string
|
|
19
|
+
): () => React.ComponentType<any> {
|
|
20
|
+
return createModuleLoader(
|
|
21
|
+
moduleId,
|
|
22
|
+
(exports: any) => {
|
|
23
|
+
if (!exports || !exports.routes || !exports.routes[routeKey]) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Route "${routeKey}" not found in module "${moduleId}"`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return exports.routes[routeKey];
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* devUtils - 开发工具函数
|
|
3
|
+
* 提供开发模式下的调试和工具函数
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ModuleRegistry } from './ModuleRegistry';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 获取所有模块的状态信息
|
|
10
|
+
*/
|
|
11
|
+
export function getAllModuleStates() {
|
|
12
|
+
// 这里需要访问 ModuleRegistry 的内部状态
|
|
13
|
+
// 为了不暴露内部实现,我们可以通过 ModuleRegistry 添加一个调试方法
|
|
14
|
+
// 当前简化实现
|
|
15
|
+
return {
|
|
16
|
+
// 可以通过 ModuleRegistry 暴露的方法获取
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 开发模式下的日志
|
|
22
|
+
*/
|
|
23
|
+
export function logModuleLoad(moduleId: string, event: string, data?: any) {
|
|
24
|
+
if (__DEV__) {
|
|
25
|
+
console.log(`[ModuleRegistry:${moduleId}] ${event}`, data || '');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 重置模块状态(仅开发环境)
|
|
31
|
+
*/
|
|
32
|
+
export function resetModule(moduleId: string) {
|
|
33
|
+
if (__DEV__) {
|
|
34
|
+
console.warn(`[devUtils] resetModule is not implemented yet for: ${moduleId}`);
|
|
35
|
+
// 实际实现需要 ModuleRegistry 暴露 reset 方法
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 重新加载模块(仅开发环境)
|
|
41
|
+
*/
|
|
42
|
+
export function reloadModule(moduleId: string) {
|
|
43
|
+
if (__DEV__) {
|
|
44
|
+
console.warn(`[devUtils] reloadModule is not implemented yet for: ${moduleId}`);
|
|
45
|
+
// 实际实现需要 ModuleRegistry 暴露 reload 方法
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* initMultiBundle - 多 Bundle 统一初始化入口
|
|
3
|
+
* 封装所有初始化逻辑,业务代码只需调用一次即可完成所有初始化
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ModuleRegistry, BundleManifest } from './ModuleRegistry';
|
|
7
|
+
import { preloadModule } from './preloadModule';
|
|
8
|
+
import { mergeConfig, setGlobalConfig, MultiBundleConfig } from './config';
|
|
9
|
+
import { LocalBundleManager } from './LocalBundleManager';
|
|
10
|
+
import { HMRClient } from './HMRClient';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 初始化结果
|
|
14
|
+
*/
|
|
15
|
+
export interface InitResult {
|
|
16
|
+
success: boolean;
|
|
17
|
+
error?: Error;
|
|
18
|
+
manifest?: BundleManifest;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 从 manifestProvider 获取 manifest
|
|
23
|
+
*/
|
|
24
|
+
async function getManifestFromProvider(
|
|
25
|
+
provider: MultiBundleConfig['manifestProvider']
|
|
26
|
+
): Promise<BundleManifest> {
|
|
27
|
+
if (!provider) {
|
|
28
|
+
// 如果没有提供 provider,使用默认的 LocalBundleManager
|
|
29
|
+
return await LocalBundleManager.getCurrentBundleManifest();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof provider === 'string') {
|
|
33
|
+
// URL 模式:从 HTTP 服务器获取
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(provider);
|
|
36
|
+
if (response.ok) {
|
|
37
|
+
return await response.json();
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`Failed to fetch manifest from ${provider}: ${response.statusText}`);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
42
|
+
console.warn(`[initMultiBundle] Failed to fetch manifest from URL: ${err.message}`);
|
|
43
|
+
// 降级到默认方式
|
|
44
|
+
return await LocalBundleManager.getCurrentBundleManifest();
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
// 函数模式:调用自定义函数
|
|
48
|
+
return await provider();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 初始化多 Bundle 系统
|
|
54
|
+
*
|
|
55
|
+
* @param config 配置对象
|
|
56
|
+
* @returns 初始化结果
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const result = await initMultiBundle({
|
|
61
|
+
* modulePaths: ['src/modules/**'],
|
|
62
|
+
* sharedDependencies: ['src/multi-bundle/**', 'src/navigation/**'],
|
|
63
|
+
* preloadModules: ['settings'],
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export async function initMultiBundle(config: MultiBundleConfig = {}): Promise<InitResult> {
|
|
68
|
+
try {
|
|
69
|
+
// 1. 合并配置
|
|
70
|
+
const mergedConfig = mergeConfig(config);
|
|
71
|
+
|
|
72
|
+
// 2. 设置全局配置(供 Metro 配置等使用)
|
|
73
|
+
setGlobalConfig(mergedConfig);
|
|
74
|
+
|
|
75
|
+
// 2.5 初始化 HMR Client (开发环境)
|
|
76
|
+
if (__DEV__ && mergedConfig.devServer) {
|
|
77
|
+
HMRClient.getInstance().setup(
|
|
78
|
+
mergedConfig.devServer.host,
|
|
79
|
+
mergedConfig.devServer.port
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 3. 获取 manifest
|
|
84
|
+
let manifest: BundleManifest;
|
|
85
|
+
try {
|
|
86
|
+
manifest = await getManifestFromProvider(mergedConfig.manifestProvider);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
89
|
+
console.error('[initMultiBundle] Failed to get manifest:', err);
|
|
90
|
+
config.onError?.(err);
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
error: err,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 4. 设置自定义 ModuleLoader(如果提供)
|
|
98
|
+
if (mergedConfig.moduleLoader) {
|
|
99
|
+
ModuleRegistry.setModuleLoader(mergedConfig.moduleLoader);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 5. 初始化 ModuleRegistry
|
|
103
|
+
ModuleRegistry.init({ manifest });
|
|
104
|
+
|
|
105
|
+
// 6. 预加载模块
|
|
106
|
+
if (mergedConfig.preloadModules && mergedConfig.preloadModules.length > 0) {
|
|
107
|
+
const preloadPromises = mergedConfig.preloadModules.map((moduleId) =>
|
|
108
|
+
preloadModule(moduleId).catch((error) => {
|
|
109
|
+
// 预加载失败不影响初始化,只记录日志
|
|
110
|
+
console.warn(`[initMultiBundle] Preload module ${moduleId} failed:`, error);
|
|
111
|
+
config.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
await Promise.allSettled(preloadPromises);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
manifest,
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
123
|
+
console.error('[initMultiBundle] Initialization failed:', err);
|
|
124
|
+
config.onError?.(err);
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
error: err,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metro 配置辅助模块
|
|
3
|
+
* 用于在 Node.js 环境中读取多 Bundle 配置
|
|
4
|
+
* 支持从环境变量或默认值读取配置
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 从环境变量读取配置
|
|
12
|
+
*/
|
|
13
|
+
function getConfigFromEnv() {
|
|
14
|
+
const modulePaths = process.env.RN_MULTI_BUNDLE_MODULE_PATHS
|
|
15
|
+
? JSON.parse(process.env.RN_MULTI_BUNDLE_MODULE_PATHS)
|
|
16
|
+
: ['src/modules/**'];
|
|
17
|
+
|
|
18
|
+
const sharedDependencies = process.env.RN_MULTI_BUNDLE_SHARED_DEPS
|
|
19
|
+
? JSON.parse(process.env.RN_MULTI_BUNDLE_SHARED_DEPS)
|
|
20
|
+
: ['src/multi-bundle/**', 'src/navigation/**'];
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
modulePaths,
|
|
24
|
+
sharedDependencies,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 检查路径是否匹配 glob 模式
|
|
30
|
+
* 简化版 glob 匹配(支持 ** 和 *)
|
|
31
|
+
*/
|
|
32
|
+
function matchGlob(filePath, pattern) {
|
|
33
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
34
|
+
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
35
|
+
|
|
36
|
+
// 将 glob 模式转换为正则表达式
|
|
37
|
+
const regexPattern = normalizedPattern
|
|
38
|
+
.replace(/\*\*/g, '___DOUBLE_STAR___')
|
|
39
|
+
.replace(/\*/g, '[^/]*')
|
|
40
|
+
.replace(/___DOUBLE_STAR___/g, '.*');
|
|
41
|
+
|
|
42
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
43
|
+
return regex.test(normalizedPath);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 检查文件路径是否匹配模块路径规则
|
|
48
|
+
*/
|
|
49
|
+
function isModulePath(filePath, modulePaths) {
|
|
50
|
+
return modulePaths.some((pattern) => matchGlob(filePath, pattern));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 检查文件路径是否是共享依赖
|
|
55
|
+
*/
|
|
56
|
+
function isSharedDependencyPath(filePath, sharedDependencies) {
|
|
57
|
+
// 先检查 node_modules 中的共享依赖
|
|
58
|
+
if (filePath.includes('/node_modules/')) {
|
|
59
|
+
const sharedNodeModules = [
|
|
60
|
+
'react',
|
|
61
|
+
'react/jsx-runtime',
|
|
62
|
+
'react-native',
|
|
63
|
+
'@react-navigation',
|
|
64
|
+
];
|
|
65
|
+
return sharedNodeModules.some((dep) =>
|
|
66
|
+
filePath.includes(`/node_modules/${dep}`)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 再检查本地共享依赖路径
|
|
71
|
+
return sharedDependencies.some((pattern) => matchGlob(filePath, pattern));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 从模块路径提取模块名(支持配置化的路径规则)
|
|
76
|
+
*/
|
|
77
|
+
function extractModuleNameFromPath(modulePath, modulePaths) {
|
|
78
|
+
const normalizedPath = modulePath.replace(/\\/g, '/');
|
|
79
|
+
|
|
80
|
+
// 尝试从配置的模块路径中提取模块名
|
|
81
|
+
for (const pattern of modulePaths) {
|
|
82
|
+
// 将 glob 模式转换为匹配模式
|
|
83
|
+
// 例如: src/modules/** -> src/modules/([^/]+)
|
|
84
|
+
const matchPattern = pattern
|
|
85
|
+
.replace(/\*\*/g, '.*')
|
|
86
|
+
.replace(/\*/g, '[^/]*')
|
|
87
|
+
.replace(/\(\[/g, '(')
|
|
88
|
+
.replace(/\]\*\)/g, '+)');
|
|
89
|
+
|
|
90
|
+
// 尝试提取模块名
|
|
91
|
+
// 对于 src/modules/**,我们尝试匹配 src/modules/([^/]+)/
|
|
92
|
+
const regex = new RegExp(`src/modules/([^/]+)/`, 'i');
|
|
93
|
+
const match = normalizedPath.match(regex);
|
|
94
|
+
if (match) {
|
|
95
|
+
return match[1].toLowerCase();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 降级到默认规则(向后兼容)
|
|
100
|
+
const defaultMatch = normalizedPath.match(/src\/modules\/([^\/]+)\//i);
|
|
101
|
+
if (defaultMatch) {
|
|
102
|
+
return defaultMatch[1].toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 从模块入口路径提取模块名(支持配置化的路径规则)
|
|
110
|
+
*/
|
|
111
|
+
function extractModuleNameFromEntry(modulePath, modulePaths) {
|
|
112
|
+
const normalizedPath = modulePath.replace(/\\/g, '/');
|
|
113
|
+
|
|
114
|
+
// 尝试从配置的模块路径中提取模块名
|
|
115
|
+
for (const pattern of modulePaths) {
|
|
116
|
+
// 对于入口文件,通常格式为 src/modules/ModuleName/index.ts
|
|
117
|
+
const regex = new RegExp(`src/modules/([^/]+)/index\\.(ts|tsx|js|jsx)$`, 'i');
|
|
118
|
+
const match = normalizedPath.match(regex);
|
|
119
|
+
if (match) {
|
|
120
|
+
return match[1].toLowerCase();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 降级到默认规则(向后兼容)
|
|
125
|
+
const defaultMatch = normalizedPath.match(/src\/modules\/([^\/]+)\/index\.(ts|tsx|js|jsx)$/i);
|
|
126
|
+
if (defaultMatch) {
|
|
127
|
+
return defaultMatch[1].toLowerCase();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
getConfigFromEnv,
|
|
135
|
+
isModulePath,
|
|
136
|
+
isSharedDependencyPath,
|
|
137
|
+
extractModuleNameFromPath,
|
|
138
|
+
extractModuleNameFromEntry,
|
|
139
|
+
};
|
|
140
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preloadModule - 模块预加载函数
|
|
3
|
+
* 静默预加载模块,失败不打断主流程
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ModuleRegistry } from './ModuleRegistry';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 预加载模块
|
|
10
|
+
* @param moduleId 模块 ID
|
|
11
|
+
*/
|
|
12
|
+
export async function preloadModule(moduleId: string): Promise<void> {
|
|
13
|
+
const state = ModuleRegistry.getModuleState(moduleId);
|
|
14
|
+
|
|
15
|
+
// 已加载就不干活
|
|
16
|
+
if (state === 'loaded') {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// loading 中也不重复发起
|
|
21
|
+
if (state === 'loading') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await ModuleRegistry.loadModule(moduleId);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
// 预加载失败对用户"无感知",但记录日志 / 埋点
|
|
29
|
+
console.warn(`[preloadModule] failed: ${moduleId}`, error);
|
|
30
|
+
// 可以在这里上报监控
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|