@bm-fe/react-native-multi-bundle 1.0.0-beta.0 → 1.0.0-beta.10

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.
@@ -13,17 +13,41 @@
13
13
  * - --env: 'development' | 'staging' | 'production' (默认: 'production')
14
14
  *
15
15
  * 环境变量:
16
- * - PROJECT_ROOT: 项目根目录(可选,默认从脚本位置推断)
16
+ * - PROJECT_ROOT: 项目根目录(可选,默认从当前工作目录向上查找包含 multi-bundle.config.json 的目录)
17
17
  */
18
18
 
19
19
  const fs = require('fs');
20
20
  const path = require('path');
21
21
  const { execSync } = require('child_process');
22
22
 
23
- // 支持从任意项目目录运行
24
- // 如果作为 npm 包使用,PROJECT_ROOT 应该是调用脚本的项目根目录
25
- // 如果直接运行,PROJECT_ROOT 是脚本所在目录的父目录
26
- const PROJECT_ROOT = process.env.PROJECT_ROOT || path.join(__dirname, '..');
23
+ /**
24
+ * 查找项目根目录
25
+ * 从当前工作目录向上查找,直到找到包含 multi-bundle.config.json 的目录
26
+ * 如果找不到,则使用当前工作目录(通常打包命令在项目根目录执行)
27
+ */
28
+ function findProjectRoot() {
29
+ // 如果通过环境变量指定,直接使用
30
+ if (process.env.PROJECT_ROOT) {
31
+ return process.env.PROJECT_ROOT;
32
+ }
33
+
34
+ // 从当前工作目录开始向上查找
35
+ let currentDir = process.cwd();
36
+ const root = path.parse(currentDir).root;
37
+
38
+ while (currentDir !== root) {
39
+ const configFile = path.join(currentDir, 'multi-bundle.config.json');
40
+ if (fs.existsSync(configFile)) {
41
+ return currentDir;
42
+ }
43
+ currentDir = path.dirname(currentDir);
44
+ }
45
+
46
+ // 如果找不到配置文件,使用当前工作目录(通常打包命令在项目根目录执行)
47
+ return process.cwd();
48
+ }
49
+
50
+ const PROJECT_ROOT = findProjectRoot();
27
51
  const CONFIG_FILE = path.join(PROJECT_ROOT, 'multi-bundle.config.json');
28
52
  const OUTPUT_DIR = path.join(PROJECT_ROOT, 'build/bundles');
29
53
 
@@ -176,6 +200,7 @@ function removePreludeFromBundle(bundlePath) {
176
200
  function ensureOutputDir(platform) {
177
201
  const platformDir = path.join(OUTPUT_DIR, platform);
178
202
  const modulesDir = path.join(platformDir, 'modules');
203
+ const assetsDir = path.join(platformDir, 'assets');
179
204
 
180
205
  if (!fs.existsSync(platformDir)) {
181
206
  fs.mkdirSync(platformDir, { recursive: true });
@@ -183,6 +208,9 @@ function ensureOutputDir(platform) {
183
208
  if (!fs.existsSync(modulesDir)) {
184
209
  fs.mkdirSync(modulesDir, { recursive: true });
185
210
  }
211
+ if (!fs.existsSync(assetsDir)) {
212
+ fs.mkdirSync(assetsDir, { recursive: true });
213
+ }
186
214
 
187
215
  return platformDir;
188
216
  }
@@ -209,6 +237,16 @@ function buildBundle(entryFile, outputFile, platform, env = 'development', progr
209
237
  // ⭐ 关键:判断是否是模块 bundle(通过检查 entry 路径)
210
238
  const isModuleBundle = entry.includes('src/modules/');
211
239
 
240
+ // 计算 assets 输出目录(统一放在平台目录下的 assets 文件夹)
241
+ const platformDir = path.dirname(outputFile);
242
+ const assetsDest = path.join(platformDir, 'assets');
243
+ const assetsDestRelative = path.relative(PROJECT_ROOT, assetsDest);
244
+
245
+ // 确保 assets 目录存在
246
+ if (!fs.existsSync(assetsDest)) {
247
+ fs.mkdirSync(assetsDest, { recursive: true });
248
+ }
249
+
212
250
  const commandParts = [
213
251
  // 主 bundle 和模块 bundle 都设置 RN_MULTI_BUNDLE_BUILD,但使用不同的 RN_BUILD_TYPE
214
252
  'RN_MULTI_BUNDLE_BUILD=true',
@@ -218,6 +256,7 @@ function buildBundle(entryFile, outputFile, platform, env = 'development', progr
218
256
  `--bundle-output ${out}`,
219
257
  `--platform ${platform}`,
220
258
  `--dev ${dev}`,
259
+ `--assets-dest ${assetsDestRelative}`,
221
260
  '--reset-cache',
222
261
  ];
223
262
 
@@ -4,8 +4,8 @@
4
4
  * 同步 Bundle 到 Assets 目录脚本
5
5
  * 将构建好的 bundle 文件同步到 Android/iOS 的 assets 目录
6
6
  *
7
- * Android: android/app/src/main/assets/bundles/
8
- * iOS: ios/DemoProject/RNModules/Bundles/
7
+ * Android: android/app/src/main/assets/
8
+ * iOS: ios/DemoProject/Bundles/
9
9
  */
10
10
 
11
11
  const fs = require('fs');
@@ -14,7 +14,7 @@ const path = require('path');
14
14
  const PROJECT_ROOT = path.join(__dirname, '..');
15
15
  const BUILD_DIR = path.join(PROJECT_ROOT, 'build/bundles');
16
16
  const ANDROID_ASSETS_DIR = path.join(PROJECT_ROOT, 'android/app/src/main/assets');
17
- const IOS_ASSETS_DIR = path.join(PROJECT_ROOT, 'ios/DemoProject/RNModules/Bundles');
17
+ const IOS_ASSETS_DIR = path.join(PROJECT_ROOT, 'ios/DemoProject/Bundles');
18
18
 
19
19
  /**
20
20
  * ANSI 颜色工具函数
@@ -56,14 +56,37 @@ function convertNativeManifestToBundleManifest(nativeManifest: any): BundleManif
56
56
 
57
57
  /**
58
58
  * 获取当前激活包的 manifest
59
- * - 开发环境:从开发服务器获取 manifest(HTTP 模式)
60
- * - 生产环境:从 Native CodePush 模块获取 manifest(直接返回 JSON 对象)
59
+ * - 优先从 Native 模块获取(支持 CodePush 等热更新场景)
60
+ * - 如果 Native 返回 null,在开发环境下降级到开发服务器获取
61
+ * - 生产环境下如果 Native 返回 null,抛出异常
61
62
  */
62
63
  async function getCurrentBundleManifest(): Promise<BundleManifest> {
63
64
  // 生产环境:从 Native 模块获取 manifest
64
- if (!__DEV__) {
65
- try {
66
- const nativeManifest = await NativeModules.CodePush.getCurrentBundleManifestContent();
65
+ // if (!__DEV__) {
66
+ // try {
67
+ // const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
68
+ // if (nativeManifest) {
69
+ // return convertNativeManifestToBundleManifest(nativeManifest);
70
+ // } else {
71
+ // // Native 模块返回 null 或 undefined
72
+ // throw new Error(
73
+ // '[LocalBundleManager] Native module returned null manifest. ' +
74
+ // 'Please ensure bundle-manifest.json exists in the app bundle.'
75
+ // );
76
+ // }
77
+ // } catch (error) {
78
+ // console.error(
79
+ // `[LocalBundleManager] Failed to get manifest from Native: ${error}`
80
+ // );
81
+ // // 生产环境无法获取 manifest 时抛出异常
82
+ // throw new Error(
83
+ // `[LocalBundleManager] Failed to get manifest from Native module: ${error}`
84
+ // );
85
+ // }
86
+ // }
87
+ console.log("ReactNativeJS","nativeManifest getCurrentBundleManifestContent")
88
+ const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
89
+ console.log("ReactNativeJS","nativeManifest "+nativeManifest)
67
90
  if (nativeManifest) {
68
91
  return convertNativeManifestToBundleManifest(nativeManifest);
69
92
  } else {
@@ -73,16 +96,6 @@ async function getCurrentBundleManifest(): Promise<BundleManifest> {
73
96
  'Please ensure bundle-manifest.json exists in the app bundle.'
74
97
  );
75
98
  }
76
- } catch (error) {
77
- console.error(
78
- `[LocalBundleManager] Failed to get manifest from Native: ${error}`
79
- );
80
- // 生产环境无法获取 manifest 时抛出异常
81
- throw new Error(
82
- `[LocalBundleManager] Failed to get manifest from Native module: ${error}`
83
- );
84
- }
85
- }
86
99
 
87
100
  // 开发环境:从开发服务器获取 manifest
88
101
  const config = getGlobalConfig();
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { Platform } from 'react-native';
11
11
  import { HMRClient } from './HMRClient';
12
+ import { getGlobalConfig } from './config';
12
13
 
13
14
  interface LoadResult {
14
15
  success: boolean;
@@ -22,29 +23,117 @@ const loadedBundles = new Set<string>();
22
23
  // 正在加载中的 bundle(并发复用)
23
24
  const inflightBundles = new Map<string, Promise<LoadResult>>();
24
25
 
26
+ // 缓存 multi-bundle.config.json(开发环境)
27
+ let cachedMultiBundleConfig: any = null;
28
+ let configLoadPromise: Promise<any> | null = null;
29
+
30
+ /**
31
+ * 从开发服务器获取 multi-bundle.config.json
32
+ */
33
+ async function getMultiBundleConfig(): Promise<any> {
34
+ // 如果已缓存,直接返回
35
+ if (cachedMultiBundleConfig) {
36
+ return cachedMultiBundleConfig;
37
+ }
38
+
39
+ // 如果正在加载,等待加载完成
40
+ if (configLoadPromise) {
41
+ return configLoadPromise;
42
+ }
43
+
44
+ // 开始加载配置
45
+ configLoadPromise = (async () => {
46
+ try {
47
+ const config = getGlobalConfig();
48
+ const devServer = config?.devServer;
49
+ const host = devServer?.host || (Platform.OS === 'android' ? '10.0.2.2' : 'localhost');
50
+ const port = devServer?.port || 8081;
51
+ const protocol = devServer?.protocol || 'http';
52
+
53
+ const configUrl = `${protocol}://${host}:${port}/multi-bundle.config.json`;
54
+ const response = await fetch(configUrl);
55
+
56
+ if (response.ok) {
57
+ const configData = await response.json();
58
+ cachedMultiBundleConfig = configData;
59
+ return configData;
60
+ } else {
61
+ console.warn(
62
+ `[ModuleLoaderMock] Failed to fetch multi-bundle.config.json: HTTP ${response.status}`
63
+ );
64
+ return null;
65
+ }
66
+ } catch (error) {
67
+ console.warn(
68
+ `[ModuleLoaderMock] Failed to fetch multi-bundle.config.json: ${error}`
69
+ );
70
+ return null;
71
+ } finally {
72
+ configLoadPromise = null;
73
+ }
74
+ })();
75
+
76
+ return configLoadPromise;
77
+ }
78
+
25
79
  /**
26
- * 根据 bundleId 构建 HTTP URL(开发环境)
80
+ * 根据 bundlePath 构建 HTTP URL(开发环境 HTTP 模式)
27
81
  *
28
- * 在开发环境中,我们通过 Metro bundler HTTP 服务器加载 bundle
29
- * 根据 bundleId 直接构建对应的 HTTP URL
82
+ * 优先从 multi-bundle.config.jsonentry 字段获取模块路径
83
+ * 如果无法获取,则使用 bundlePath 进行简单推断
84
+ *
85
+ * @param bundleId 模块 ID
86
+ * @param bundlePath bundle 文件路径(从 manifest 中获取,开发环境可能不使用)
30
87
  */
31
- function buildHttpUrlFromBundleId(bundleId: string): string {
32
- const host = Platform.OS === 'android' ? '10.0.2.2' : 'localhost';
88
+ async function buildHttpUrlFromBundlePath(bundleId: string, bundlePath: string): Promise<string> {
89
+ const config = getGlobalConfig();
90
+ const devServer = config?.devServer;
91
+ const host = devServer?.host || (Platform.OS === 'android' ? '10.0.2.2' : 'localhost');
92
+ const port = devServer?.port || 8081;
93
+ const protocol = devServer?.protocol || 'http';
33
94
 
34
- // 简单的模块 ID 到目录名的映射
35
- // 假设模块 ID 为小写,目录名为首字母大写
36
- // 例如: home -> Home
37
- const moduleName = bundleId.charAt(0).toUpperCase() + bundleId.slice(1);
95
+ // 优先从 multi-bundle.config.json 的 entry 获取模块路径
96
+ let sourcePath: string | null = null;
97
+
98
+ try {
99
+ const multiBundleConfig = await getMultiBundleConfig();
100
+ if (multiBundleConfig?.modules) {
101
+ const moduleConfig = multiBundleConfig.modules.find((m: any) => m.id === bundleId);
102
+ if (moduleConfig?.entry) {
103
+ sourcePath = moduleConfig.entry;
104
+ }
105
+ }
106
+ } catch (error) {
107
+ console.warn(
108
+ `[ModuleLoaderMock] Failed to get entry from multi-bundle.config.json for module ${bundleId}: ${error}`
109
+ );
110
+ }
111
+
112
+ // 如果无法从配置文件获取,使用 bundlePath 进行简单推断
113
+ if (!sourcePath) {
114
+ // 如果 bundlePath 已经是源码路径,直接使用
115
+ if (bundlePath.startsWith('src/') || bundlePath.startsWith('./src/')) {
116
+ sourcePath = bundlePath;
117
+ } else {
118
+ // 生产环境路径(如 bundles/home/index.bundle),提取模块名
119
+ const match = bundlePath.match(/(?:bundles\/|modules\/)(?:[^/]+\/)?([^/]+)\//);
120
+ const moduleName = match?.[1] || bundleId.charAt(0).toUpperCase() + bundleId.slice(1);
121
+ sourcePath = `src/modules/${moduleName}/index.bundle`;
122
+ }
123
+ }
124
+
125
+ // 确保路径以 .bundle 结尾
126
+ if (!sourcePath.endsWith('.bundle')) {
127
+ sourcePath = sourcePath.replace(/\.(ts|tsx|js|jsx)$/, '.bundle') || sourcePath + '/index.bundle';
128
+ }
38
129
 
39
130
  // 构建 Metro 请求 URL
40
- // 请求源码入口文件,Metro 会自动打包
41
- // 注意:使用 .bundle 后缀,Metro 会识别并构建对应的 .ts/.tsx 文件
42
- return `http://${host}:8081/src/modules/${moduleName}/index.bundle` +
131
+ return `${protocol}://${host}:${port}/${sourcePath}` +
43
132
  `?platform=${Platform.OS}` +
44
133
  `&dev=true` +
45
134
  `&minify=false` +
46
- `&modulesOnly=true` + // ✅ 只生成 __d(...),不带 runtime
47
- `&runModule=true`; // ✅ 不自动执行入口
135
+ `&modulesOnly=true` +
136
+ `&runModule=true`;
48
137
  }
49
138
 
50
139
  /**
@@ -74,8 +163,8 @@ async function loadBusinessBundle(bundleId: string, bundlePath: string): Promise
74
163
  };
75
164
  }
76
165
 
77
- // 开发环境:根据 bundleId 构建 HTTP URL
78
- const httpUrl = buildHttpUrlFromBundleId(bundleId);
166
+ // 开发环境:根据 bundlePath 动态构建 HTTP URL(优先从 multi-bundle.config.json 获取 entry)
167
+ const httpUrl = await buildHttpUrlFromBundlePath(bundleId, bundlePath);
79
168
 
80
169
  // 已经加载过的 bundle,不再重复执行
81
170
  if (loadedBundles.has(httpUrl)) {
@@ -166,4 +255,3 @@ export const ModuleLoader = {
166
255
  };
167
256
 
168
257
  export type { LoadResult };
169
-
@@ -137,8 +137,6 @@ function registerModule(moduleId: string, exportsObj: any): void {
137
137
  return;
138
138
  }
139
139
 
140
- map[moduleId] = 1;
141
-
142
140
  moduleExports[moduleId] = exportsObj;
143
141
  moduleState[moduleId] = 'loaded';
144
142
 
@@ -222,7 +220,7 @@ async function loadModule(moduleId: string): Promise<void> {
222
220
  loader = NativeModules.ModuleLoader;
223
221
  } else {
224
222
  throw new Error(
225
- 'ModuleLoader not available: Native CodePush module not found in production'
223
+ 'ModuleLoader not available: Native ModuleLoader module not found in production'
226
224
  );
227
225
  }
228
226
  }
@@ -341,3 +341,5 @@ function preloadModule(moduleId: string): Promise<void>
341
341
  - [使用指南](../docs/使用指南.md)
342
342
  - [多 Bundle 架构设计](../docs/React%20Native%20多%20bundle%20技术方案.md)
343
343
 
344
+
345
+
@@ -18,6 +18,10 @@ export interface InitResult {
18
18
  manifest?: BundleManifest;
19
19
  }
20
20
 
21
+ // 初始化状态标记
22
+ let isInitialized = false;
23
+ let initPromise: Promise<InitResult> | null = null;
24
+
21
25
  /**
22
26
  * 从 manifestProvider 获取 manifest
23
27
  */
@@ -53,6 +57,7 @@ async function getManifestFromProvider(
53
57
  * 初始化多 Bundle 系统
54
58
  *
55
59
  * @param config 配置对象
60
+ * @param forceReinit 是否强制重新初始化(默认 false,已初始化时直接返回成功结果)
56
61
  * @returns 初始化结果
57
62
  *
58
63
  * @example
@@ -64,68 +69,106 @@ async function getManifestFromProvider(
64
69
  * });
65
70
  * ```
66
71
  */
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
- }
72
+ export async function initMultiBundle(
73
+ config: MultiBundleConfig = {},
74
+ forceReinit: boolean = false
75
+ ): Promise<InitResult> {
76
+ // 如果已经初始化且不强制重新初始化,直接返回成功
77
+ if (isInitialized && !forceReinit) {
78
+ return {
79
+ success: true,
80
+ manifest: undefined, // 不返回 manifest,因为已经初始化过了
81
+ };
82
+ }
83
+
84
+ // 如果正在初始化,等待正在进行的初始化完成
85
+ if (initPromise && !forceReinit) {
86
+ return initPromise;
87
+ }
82
88
 
83
- // 3. 获取 manifest
84
- let manifest: BundleManifest;
89
+ // 开始新的初始化
90
+ initPromise = (async () => {
85
91
  try {
86
- manifest = await getManifestFromProvider(mergedConfig.manifestProvider);
92
+ // 1. 合并配置
93
+ const mergedConfig = mergeConfig(config);
94
+
95
+ // 2. 设置全局配置(供 Metro 配置等使用)
96
+ setGlobalConfig(mergedConfig);
97
+
98
+ // 2.5 初始化 HMR Client (开发环境)
99
+ if (__DEV__ && mergedConfig.devServer) {
100
+ HMRClient.getInstance().setup(
101
+ mergedConfig.devServer.host,
102
+ mergedConfig.devServer.port
103
+ );
104
+ }
105
+
106
+ // 3. 获取 manifest
107
+ let manifest: BundleManifest;
108
+ try {
109
+ manifest = await getManifestFromProvider(mergedConfig.manifestProvider);
110
+ } catch (error) {
111
+ const err = error instanceof Error ? error : new Error(String(error));
112
+ console.error('[initMultiBundle] Failed to get manifest:', err);
113
+ config.onError?.(err);
114
+ return {
115
+ success: false,
116
+ error: err,
117
+ };
118
+ }
119
+
120
+ // 4. 设置自定义 ModuleLoader(如果提供)
121
+ if (mergedConfig.moduleLoader) {
122
+ ModuleRegistry.setModuleLoader(mergedConfig.moduleLoader);
123
+ }
124
+
125
+ // 5. 初始化 ModuleRegistry
126
+ ModuleRegistry.init({ manifest });
127
+
128
+ // 6. 预加载模块
129
+ if (mergedConfig.preloadModules && mergedConfig.preloadModules.length > 0) {
130
+ const preloadPromises = mergedConfig.preloadModules.map((moduleId) =>
131
+ preloadModule(moduleId).catch((error) => {
132
+ // 预加载失败不影响初始化,只记录日志
133
+ console.warn(`[initMultiBundle] Preload module ${moduleId} failed:`, error);
134
+ config.onError?.(error instanceof Error ? error : new Error(String(error)));
135
+ })
136
+ );
137
+ await Promise.allSettled(preloadPromises);
138
+ }
139
+
140
+ isInitialized = true;
141
+ return {
142
+ success: true,
143
+ manifest,
144
+ };
87
145
  } catch (error) {
88
146
  const err = error instanceof Error ? error : new Error(String(error));
89
- console.error('[initMultiBundle] Failed to get manifest:', err);
147
+ console.error('[initMultiBundle] Initialization failed:', err);
90
148
  config.onError?.(err);
149
+ // 初始化失败时,允许下次重试
150
+ isInitialized = false;
151
+ initPromise = null;
91
152
  return {
92
153
  success: false,
93
154
  error: err,
94
155
  };
156
+ } finally {
157
+ // 如果强制重新初始化,清除 promise(允许下次调用)
158
+ if (forceReinit) {
159
+ initPromise = null;
160
+ }
95
161
  }
162
+ })();
96
163
 
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
- }
164
+ return initPromise;
165
+ }
116
166
 
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
- }
167
+ /**
168
+ * 重置初始化状态(用于测试或特殊场景)
169
+ */
170
+ export function resetInitState(): void {
171
+ isInitialized = false;
172
+ initPromise = null;
130
173
  }
131
174