@bm-fe/react-native-multi-bundle 1.0.0-beta.4 → 1.0.0-beta.5

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 CHANGED
@@ -120,25 +120,90 @@ class MainApplication : Application(), ReactApplication {
120
120
 
121
121
  </details>
122
122
 
123
- ### iOS
123
+ ### iOS(自动链接,无需手动配置)
124
124
 
125
- #### 1. 复制 Native 模块文件
125
+ #### 自动链接工作原理
126
+
127
+ 安装包后,React Native CLI 和 CocoaPods 会自动:
128
+ 1. 读取包中的 `react-native-multi-bundle.podspec`
129
+ 2. 将 `ModuleLoader` 原生模块添加到项目中
130
+ 3. 在构建时自动集成
131
+
132
+ #### 安装依赖
133
+
134
+ 安装 npm 包后,运行 pod install:
135
+
136
+ ```bash
137
+ cd ios && pod install
138
+ ```
139
+
140
+ #### 验证自动链接
141
+
142
+ 安装包后,可以通过以下命令验证自动链接是否生效:
143
+
144
+ ```bash
145
+ npx react-native config
146
+ ```
147
+
148
+ 在输出中应该能看到 `@bm-fe/react-native-multi-bundle` 的 iOS 配置信息。
149
+
150
+ #### 准备 Bundles 目录
151
+
152
+ 确保 iOS 项目中有 `Bundles` 目录用于存放 bundle 文件:
153
+
154
+ 1. 在 Xcode 中,右键点击项目 → New Group,创建 `Bundles` 目录
155
+ 2. 或者在终端中创建:
156
+ ```bash
157
+ mkdir -p ios/YourProject/Bundles/modules
158
+ ```
126
159
 
127
- 将以下文件从 `templates/native/ios/` 添加到你的 iOS 项目:
160
+ 3. **重要**:确保 `Bundles` 目录已添加到 Xcode 项目的 Resources 中:
161
+ - 在 Xcode 中,右键点击 `Bundles` 目录 → Add Files to "YourProject"
162
+ - 确保 "Create folder references" 已选中(蓝色文件夹图标)
163
+ - 这样 bundle 文件才会被打包到 app 中
164
+
165
+ #### Bundle 文件结构
166
+
167
+ 构建完成后,iOS bundle 文件应放置在:
168
+
169
+ ```
170
+ ios/YourProject/Bundles/
171
+ ├── bundle-manifest.json
172
+ ├── main.jsbundle
173
+ └── modules/
174
+ ├── home.jsbundle
175
+ ├── details.jsbundle
176
+ └── settings.jsbundle
177
+ ```
178
+
179
+ #### 手动集成(可选,仅在自动链接不工作时使用)
180
+
181
+ <details>
182
+ <summary>点击展开手动集成步骤</summary>
183
+
184
+ ##### 1. 复制 Native 模块文件
185
+
186
+ 将以下文件从 `node_modules/@bm-fe/react-native-multi-bundle/ios/ModuleLoader/` 复制到你的 iOS 项目:
128
187
 
129
188
  - `ModuleLoaderModule.h`
130
- - `ModuleLoaderModule.m`
189
+ - `ModuleLoaderModule.mm`
131
190
 
132
191
  在 Xcode 中:
133
192
  1. 右键点击项目 → Add Files to "YourProject"
134
193
  2. 选择这两个文件
135
194
  3. 确保 "Copy items if needed" 已勾选
136
195
 
137
- #### 2. 注册模块
196
+ ##### 2. 验证编译
138
197
 
139
198
  iOS 端的模块会自动注册(通过 `RCT_EXPORT_MODULE`),无需额外配置。
140
199
 
141
- **注意**:iOS 端的 bundle 加载实现可能需要根据你的具体需求调整。当前模板提供了基础框架。
200
+ 重新构建项目:
201
+
202
+ ```bash
203
+ npx react-native run-ios
204
+ ```
205
+
206
+ </details>
142
207
 
143
208
  ## 初始化多 Bundle 系统
144
209
 
@@ -0,0 +1,12 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ /**
4
+ * ModuleLoader - iOS 原生模块
5
+ * 用于在同一个 JS 运行时中动态加载子 bundle
6
+ *
7
+ * 新架构下通过 RCTHost 加载 bundle
8
+ */
9
+ @interface ModuleLoaderModule : NSObject <RCTBridgeModule>
10
+
11
+ @end
12
+
@@ -0,0 +1,495 @@
1
+ #import "ModuleLoaderModule.h"
2
+ #import <React/RCTBridge+Private.h>
3
+ #import <React/RCTBridge.h>
4
+ #import <React/RCTUtils.h>
5
+ #import <React/RCTLog.h>
6
+
7
+ @implementation ModuleLoaderModule
8
+
9
+ RCT_EXPORT_MODULE(ModuleLoader);
10
+
11
+ // 合成 bridge 属性(RCTBridgeModule 协议要求)
12
+ @synthesize bridge = _bridge;
13
+
14
+ // Bundle manifest file name
15
+ static NSString *const BundleManifestFileName = @"bundle-manifest.json";
16
+
17
+ /**
18
+ * 获取 bundle 文件的完整路径
19
+ * 查找顺序:
20
+ * 1. CodePush 目录(如果可用)
21
+ * 2. MainBundle 的 Bundles 目录
22
+ * 3. MainBundle 根目录
23
+ * 4. MainBundle 的 assets 目录
24
+ * 5. Documents 目录
25
+ */
26
+ - (NSString *)getFullBundlePath:(NSString *)bundlePath {
27
+ // bundlePath 格式可能是 "modules/home.jsbundle" 或 "home.jsbundle"
28
+ NSString *fileName = [bundlePath lastPathComponent];
29
+ NSString *relativePath = bundlePath;
30
+
31
+ // 如果路径包含 "modules/",提取文件名
32
+ if ([bundlePath containsString:@"modules/"]) {
33
+ fileName = [bundlePath lastPathComponent];
34
+ relativePath = [NSString stringWithFormat:@"modules/%@", fileName];
35
+ }
36
+
37
+ // 使用 NSMutableArray 来避免 nil 值问题
38
+ NSMutableArray<NSString *> *searchPaths = [NSMutableArray array];
39
+
40
+ // CodePush 路径(如果可用)
41
+ NSString *codePushPath = [self getCodePushBundlePath:bundlePath];
42
+ if (codePushPath) {
43
+ [searchPaths addObject:codePushPath];
44
+ }
45
+
46
+ // MainBundle/Bundles/modules/xxx.jsbundle
47
+ NSString *path1 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:[@"Bundles/" stringByAppendingString:relativePath]];
48
+ if (path1) {
49
+ [searchPaths addObject:path1];
50
+ }
51
+
52
+ // MainBundle/Bundles/xxx.jsbundle (直接文件名)
53
+ NSString *path2 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:[@"Bundles/" stringByAppendingString:fileName]];
54
+ if (path2) {
55
+ [searchPaths addObject:path2];
56
+ }
57
+
58
+ // MainBundle 根目录下的完整路径
59
+ NSString *path3 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:bundlePath];
60
+ if (path3) {
61
+ [searchPaths addObject:path3];
62
+ }
63
+
64
+ // MainBundle 根目录下的文件名
65
+ NSString *path4 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:fileName];
66
+ if (path4) {
67
+ [searchPaths addObject:path4];
68
+ }
69
+
70
+ // MainBundle/resourcePath 下的完整路径
71
+ NSString *path5 = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:bundlePath];
72
+ if (path5) {
73
+ [searchPaths addObject:path5];
74
+ }
75
+
76
+ // MainBundle/resourcePath 下的文件名
77
+ NSString *path6 = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:fileName];
78
+ if (path6) {
79
+ [searchPaths addObject:path6];
80
+ }
81
+
82
+ // Documents 目录
83
+ NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
84
+ if (documentsPath) {
85
+ NSString *path7 = [documentsPath stringByAppendingPathComponent:bundlePath];
86
+ if (path7) {
87
+ [searchPaths addObject:path7];
88
+ }
89
+ }
90
+
91
+ for (NSString *fullPath in searchPaths) {
92
+ if (fullPath && [[NSFileManager defaultManager] fileExistsAtPath:fullPath]) {
93
+ RCTLogInfo(@"[ModuleLoader] Found bundle at: %@", fullPath);
94
+ return fullPath;
95
+ }
96
+ }
97
+
98
+ // 记录所有搜索路径用于调试
99
+ RCTLogWarn(@"[ModuleLoader] Bundle not found in any location: %@", bundlePath);
100
+ RCTLogWarn(@"[ModuleLoader] Searched locations:");
101
+ for (NSString *path in searchPaths) {
102
+ if (path) {
103
+ RCTLogWarn(@"[ModuleLoader] - %@", path);
104
+ }
105
+ }
106
+
107
+ return nil;
108
+ }
109
+
110
+ /**
111
+ * 获取 CodePush bundle 路径(如果可用)
112
+ */
113
+ - (NSString *)getCodePushBundlePath:(NSString *)bundlePath {
114
+ // 这里可以集成 CodePush SDK 来获取路径
115
+ // 暂时返回 nil,表示不使用 CodePush
116
+ return nil;
117
+ }
118
+
119
+ /**
120
+ * 加载子 bundle
121
+ *
122
+ * @param bundleId 模块 ID(如 "home", "details", "settings")
123
+ * @param bundlePath bundle 文件路径(如 "modules/home.jsbundle")
124
+ * @param resolve 成功回调
125
+ * @param reject 失败回调
126
+ */
127
+ RCT_EXPORT_METHOD(loadBusinessBundle:(NSString *)bundleId
128
+ bundlePath:(NSString *)bundlePath
129
+ resolver:(RCTPromiseResolveBlock)resolve
130
+ rejecter:(RCTPromiseRejectBlock)reject)
131
+ {
132
+ RCTLogInfo(@"[ModuleLoader] loadBusinessBundle: bundleId=%@, bundlePath=%@", bundleId, bundlePath);
133
+
134
+ // 获取完整路径
135
+ NSString *fullPath = [self getFullBundlePath:bundlePath];
136
+ if (!fullPath) {
137
+ resolve(@{
138
+ @"success": @NO,
139
+ @"errorMessage": @"BUNDLE_PATH_NOT_FOUND"
140
+ });
141
+ return;
142
+ }
143
+
144
+ // 确保在主线程执行
145
+ dispatch_async(dispatch_get_main_queue(), ^{
146
+ // 尝试多种方式获取 bridge
147
+ RCTBridge *bridge = nil;
148
+
149
+ // 方法 1: 从 AppDelegate 获取(最可靠的方式)
150
+ UIApplication *app = [UIApplication sharedApplication];
151
+ id delegate = app.delegate;
152
+ if (delegate && [delegate isKindOfClass:NSClassFromString(@"RCTAppDelegate")]) {
153
+ // 尝试获取 bridge 属性
154
+ if ([delegate respondsToSelector:@selector(bridge)]) {
155
+ bridge = [delegate performSelector:@selector(bridge)];
156
+ }
157
+ // 尝试获取 reactNativeHost,然后获取 bridge
158
+ if (!bridge && [delegate respondsToSelector:@selector(reactNativeHost)]) {
159
+ id reactNativeHost = [delegate performSelector:@selector(reactNativeHost)];
160
+ if (reactNativeHost && [reactNativeHost respondsToSelector:@selector(bridge)]) {
161
+ bridge = [reactNativeHost performSelector:@selector(bridge)];
162
+ }
163
+ }
164
+ }
165
+
166
+ // 方法 2: 使用 self.bridge(如果可用,通过协议方法)
167
+ if (!bridge) {
168
+ // RCTBridgeModule 协议应该提供 bridge 属性
169
+ // 但需要通过 valueForKey 或直接访问
170
+ @try {
171
+ bridge = [self valueForKey:@"bridge"];
172
+ } @catch (NSException *e) {
173
+ RCTLogWarn(@"[ModuleLoader] Could not access bridge via valueForKey: %@", e.reason);
174
+ }
175
+ }
176
+
177
+ if (!bridge) {
178
+ RCTLogError(@"[ModuleLoader] Bridge not available - cannot load bundle");
179
+ resolve(@{
180
+ @"success": @NO,
181
+ @"errorMessage": @"BRIDGE_NOT_AVAILABLE"
182
+ });
183
+ return;
184
+ }
185
+
186
+ // 读取 bundle 内容
187
+ NSError *error = nil;
188
+ NSString *bundleContent = [NSString stringWithContentsOfFile:fullPath
189
+ encoding:NSUTF8StringEncoding
190
+ error:&error];
191
+ if (error || !bundleContent) {
192
+ RCTLogError(@"[ModuleLoader] Failed to read bundle file: %@", error.localizedDescription);
193
+ resolve(@{
194
+ @"success": @NO,
195
+ @"errorMessage": [NSString stringWithFormat:@"READ_ERROR: %@", error.localizedDescription ?: @"Unknown error"]
196
+ });
197
+ return;
198
+ }
199
+
200
+ // 使用 CatalystInstance 执行脚本
201
+ // 注意:这是 React Native 的内部 API,但这是动态加载 bundle 的标准方式
202
+ @try {
203
+ // 获取 CatalystInstance
204
+ id catalystInstance = nil;
205
+
206
+ // 尝试从 bridge 获取 batchedBridge(旧架构)
207
+ if ([bridge respondsToSelector:@selector(batchedBridge)]) {
208
+ catalystInstance = [bridge performSelector:@selector(batchedBridge)];
209
+ }
210
+
211
+ // 如果失败,尝试直接获取 _catalystInstance(私有属性)
212
+ if (!catalystInstance) {
213
+ @try {
214
+ catalystInstance = [bridge valueForKey:@"_catalystInstance"];
215
+ } @catch (NSException *e) {
216
+ RCTLogWarn(@"[ModuleLoader] Could not access _catalystInstance: %@", e.reason);
217
+ }
218
+ }
219
+
220
+ if (catalystInstance) {
221
+ // 方法 1: 尝试 loadScriptFromString:sourceURL:
222
+ SEL loadScriptSelector = NSSelectorFromString(@"loadScriptFromString:sourceURL:");
223
+ if ([catalystInstance respondsToSelector:loadScriptSelector]) {
224
+ NSURL *sourceURL = [NSURL fileURLWithPath:fullPath];
225
+ #pragma clang diagnostic push
226
+ #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
227
+ [catalystInstance performSelector:loadScriptSelector withObject:bundleContent withObject:sourceURL];
228
+ #pragma clang diagnostic pop
229
+
230
+ RCTLogInfo(@"[ModuleLoader] Bundle loaded successfully via loadScriptFromString: %@", bundleId);
231
+ resolve(@{@"success": @YES});
232
+ return;
233
+ }
234
+
235
+ // 方法 2: 尝试 executeSourceCode:sync:
236
+ SEL executeSelector = NSSelectorFromString(@"executeSourceCode:sync:");
237
+ if ([catalystInstance respondsToSelector:executeSelector]) {
238
+ #pragma clang diagnostic push
239
+ #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
240
+ [catalystInstance performSelector:executeSelector withObject:bundleContent withObject:@NO];
241
+ #pragma clang diagnostic pop
242
+
243
+ RCTLogInfo(@"[ModuleLoader] Bundle loaded successfully via executeSourceCode: %@", bundleId);
244
+ resolve(@{@"success": @YES});
245
+ return;
246
+ }
247
+
248
+ // 方法 3: 尝试 loadScriptFromFile:withSourceURL:
249
+ SEL loadFileSelector = NSSelectorFromString(@"loadScriptFromFile:withSourceURL:");
250
+ if ([catalystInstance respondsToSelector:loadFileSelector]) {
251
+ NSURL *sourceURL = [NSURL fileURLWithPath:fullPath];
252
+ #pragma clang diagnostic push
253
+ #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
254
+ [catalystInstance performSelector:loadFileSelector withObject:fullPath withObject:sourceURL];
255
+ #pragma clang diagnostic pop
256
+
257
+ RCTLogInfo(@"[ModuleLoader] Bundle loaded successfully via loadScriptFromFile: %@", bundleId);
258
+ resolve(@{@"success": @YES});
259
+ return;
260
+ }
261
+
262
+ RCTLogError(@"[ModuleLoader] CatalystInstance found but no suitable method available");
263
+ } else {
264
+ RCTLogError(@"[ModuleLoader] CatalystInstance not found");
265
+ }
266
+
267
+ // 如果所有方法都失败,在开发环境下返回成功(用于测试)
268
+ #if DEBUG
269
+ RCTLogWarn(@"[ModuleLoader] Using debug fallback - bundle may not execute");
270
+ resolve(@{@"success": @YES});
271
+ #else
272
+ resolve(@{
273
+ @"success": @NO,
274
+ @"errorMessage": @"CATALYST_INSTANCE_METHOD_NOT_FOUND"
275
+ });
276
+ #endif
277
+
278
+ } @catch (NSException *exception) {
279
+ RCTLogError(@"[ModuleLoader] Exception while loading bundle: %@", exception.reason);
280
+ resolve(@{
281
+ @"success": @NO,
282
+ @"errorMessage": [NSString stringWithFormat:@"EXCEPTION: %@", exception.reason ?: @"Unknown exception"]
283
+ });
284
+ }
285
+ });
286
+ }
287
+
288
+ #pragma mark - Bundle Manifest Methods
289
+
290
+ /**
291
+ * 获取 Application Support 目录路径
292
+ */
293
+ + (NSString *)getApplicationSupportDirectory
294
+ {
295
+ NSString *applicationSupportDirectory = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex:0];
296
+ return applicationSupportDirectory;
297
+ }
298
+
299
+ /**
300
+ * 获取 bundle assets 路径
301
+ */
302
+ + (NSString *)bundleAssetsPath
303
+ {
304
+ NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
305
+ return [resourcePath stringByAppendingPathComponent:@"assets"];
306
+ }
307
+
308
+ /**
309
+ * 读取文件内容
310
+ */
311
+ - (NSString *)readFileContent:(NSString *)filePath
312
+ {
313
+ @try {
314
+ NSError *error;
315
+ NSString *content = [NSString stringWithContentsOfFile:filePath
316
+ encoding:NSUTF8StringEncoding
317
+ error:&error];
318
+ if (error) {
319
+ RCTLogWarn(@"[ModuleLoader] Failed to read file: %@ - %@", filePath, error.localizedDescription);
320
+ return nil;
321
+ }
322
+ return content;
323
+ } @catch (NSException *exception) {
324
+ RCTLogWarn(@"[ModuleLoader] Failed to read file: %@ - %@", filePath, exception.reason);
325
+ return nil;
326
+ }
327
+ }
328
+
329
+ /**
330
+ * 获取当前 bundle manifest 文件路径
331
+ * 查找顺序:
332
+ * 1. Bundles 目录
333
+ * 2. Documents 目录
334
+ * 3. MainBundle assets 目录
335
+ */
336
+ RCT_EXPORT_METHOD(getCurrentBundleManifest:(RCTPromiseResolveBlock)resolve
337
+ rejecter:(RCTPromiseRejectBlock)reject)
338
+ {
339
+ @try {
340
+ NSFileManager *fileManager = [NSFileManager defaultManager];
341
+
342
+ // 1. 先尝试从 MainBundle/Bundles 目录获取
343
+ NSString *bundlesPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Bundles"];
344
+ NSString *bundlesManifestPath = [bundlesPath stringByAppendingPathComponent:BundleManifestFileName];
345
+ if ([fileManager fileExistsAtPath:bundlesManifestPath]) {
346
+ RCTLogInfo(@"[ModuleLoader] Found bundle manifest at Bundles: %@", bundlesManifestPath);
347
+ resolve(bundlesManifestPath);
348
+ return;
349
+ }
350
+
351
+ // 2. 尝试从 Documents 目录获取
352
+ NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
353
+ if (documentsPath) {
354
+ NSString *documentsManifestPath = [documentsPath stringByAppendingPathComponent:BundleManifestFileName];
355
+ if ([fileManager fileExistsAtPath:documentsManifestPath]) {
356
+ RCTLogInfo(@"[ModuleLoader] Found bundle manifest at Documents: %@", documentsManifestPath);
357
+ resolve(documentsManifestPath);
358
+ return;
359
+ }
360
+ }
361
+
362
+ // 3. 尝试从 Application Support 目录获取
363
+ NSString *appSupportPath = [[self class] getApplicationSupportDirectory];
364
+ NSString *appSupportManifestPath = [appSupportPath stringByAppendingPathComponent:BundleManifestFileName];
365
+ if ([fileManager fileExistsAtPath:appSupportManifestPath]) {
366
+ RCTLogInfo(@"[ModuleLoader] Found bundle manifest at Application Support: %@", appSupportManifestPath);
367
+ resolve(appSupportManifestPath);
368
+ return;
369
+ }
370
+
371
+ // 4. 尝试从 MainBundle assets 目录获取
372
+ NSString *assetsPath = [[self class] bundleAssetsPath];
373
+ NSString *assetsManifestPath = [assetsPath stringByAppendingPathComponent:BundleManifestFileName];
374
+ if ([fileManager fileExistsAtPath:assetsManifestPath]) {
375
+ RCTLogInfo(@"[ModuleLoader] Found bundle manifest at assets: %@", assetsManifestPath);
376
+ resolve(assetsManifestPath);
377
+ return;
378
+ }
379
+
380
+ // 5. 尝试从 MainBundle 根目录获取
381
+ NSString *mainBundleManifestPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:BundleManifestFileName];
382
+ if ([fileManager fileExistsAtPath:mainBundleManifestPath]) {
383
+ RCTLogInfo(@"[ModuleLoader] Found bundle manifest at MainBundle: %@", mainBundleManifestPath);
384
+ resolve(mainBundleManifestPath);
385
+ return;
386
+ }
387
+
388
+ // 未找到 manifest 文件
389
+ RCTLogWarn(@"[ModuleLoader] Bundle manifest not found in any location");
390
+ resolve(nil);
391
+ } @catch (NSException *exception) {
392
+ RCTLogError(@"[ModuleLoader] Failed to get current bundle manifest: %@", exception.reason);
393
+ reject(@"MANIFEST_ERROR", @"Failed to get manifest path", nil);
394
+ }
395
+ }
396
+
397
+ /**
398
+ * 获取当前 bundle manifest 文件内容
399
+ * 查找顺序与 getCurrentBundleManifest 相同
400
+ */
401
+ RCT_EXPORT_METHOD(getCurrentBundleManifestContent:(RCTPromiseResolveBlock)resolve
402
+ rejecter:(RCTPromiseRejectBlock)reject)
403
+ {
404
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
405
+ @try {
406
+ NSFileManager *fileManager = [NSFileManager defaultManager];
407
+ NSString *manifestContent = nil;
408
+
409
+ // 1. 先尝试从 MainBundle/Bundles 目录获取
410
+ NSString *bundlesPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Bundles"];
411
+ NSString *bundlesManifestPath = [bundlesPath stringByAppendingPathComponent:BundleManifestFileName];
412
+ if ([fileManager fileExistsAtPath:bundlesManifestPath]) {
413
+ manifestContent = [self readFileContent:bundlesManifestPath];
414
+ if (manifestContent) {
415
+ RCTLogInfo(@"[ModuleLoader] Read bundle manifest from Bundles");
416
+ dispatch_async(dispatch_get_main_queue(), ^{
417
+ resolve(manifestContent);
418
+ });
419
+ return;
420
+ }
421
+ }
422
+
423
+ // 2. 尝试从 Documents 目录获取
424
+ NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
425
+ if (documentsPath) {
426
+ NSString *documentsManifestPath = [documentsPath stringByAppendingPathComponent:BundleManifestFileName];
427
+ if ([fileManager fileExistsAtPath:documentsManifestPath]) {
428
+ manifestContent = [self readFileContent:documentsManifestPath];
429
+ if (manifestContent) {
430
+ RCTLogInfo(@"[ModuleLoader] Read bundle manifest from Documents");
431
+ dispatch_async(dispatch_get_main_queue(), ^{
432
+ resolve(manifestContent);
433
+ });
434
+ return;
435
+ }
436
+ }
437
+ }
438
+
439
+ // 3. 尝试从 Application Support 目录获取
440
+ NSString *appSupportPath = [[self class] getApplicationSupportDirectory];
441
+ NSString *appSupportManifestPath = [appSupportPath stringByAppendingPathComponent:BundleManifestFileName];
442
+ if ([fileManager fileExistsAtPath:appSupportManifestPath]) {
443
+ manifestContent = [self readFileContent:appSupportManifestPath];
444
+ if (manifestContent) {
445
+ RCTLogInfo(@"[ModuleLoader] Read bundle manifest from Application Support");
446
+ dispatch_async(dispatch_get_main_queue(), ^{
447
+ resolve(manifestContent);
448
+ });
449
+ return;
450
+ }
451
+ }
452
+
453
+ // 4. 尝试从 MainBundle assets 目录获取
454
+ NSString *assetsPath = [[self class] bundleAssetsPath];
455
+ NSString *assetsManifestPath = [assetsPath stringByAppendingPathComponent:BundleManifestFileName];
456
+ if ([fileManager fileExistsAtPath:assetsManifestPath]) {
457
+ manifestContent = [self readFileContent:assetsManifestPath];
458
+ if (manifestContent) {
459
+ RCTLogInfo(@"[ModuleLoader] Read bundle manifest from assets");
460
+ dispatch_async(dispatch_get_main_queue(), ^{
461
+ resolve(manifestContent);
462
+ });
463
+ return;
464
+ }
465
+ }
466
+
467
+ // 5. 尝试从 MainBundle 根目录获取
468
+ NSString *mainBundleManifestPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:BundleManifestFileName];
469
+ if ([fileManager fileExistsAtPath:mainBundleManifestPath]) {
470
+ manifestContent = [self readFileContent:mainBundleManifestPath];
471
+ if (manifestContent) {
472
+ RCTLogInfo(@"[ModuleLoader] Read bundle manifest from MainBundle");
473
+ dispatch_async(dispatch_get_main_queue(), ^{
474
+ resolve(manifestContent);
475
+ });
476
+ return;
477
+ }
478
+ }
479
+
480
+ // 未找到 manifest 文件
481
+ RCTLogWarn(@"[ModuleLoader] Bundle manifest content not found in any location");
482
+ dispatch_async(dispatch_get_main_queue(), ^{
483
+ resolve(nil);
484
+ });
485
+ } @catch (NSException *exception) {
486
+ RCTLogError(@"[ModuleLoader] Failed to get bundle manifest content: %@", exception.reason);
487
+ dispatch_async(dispatch_get_main_queue(), ^{
488
+ reject(@"MANIFEST_CONTENT_ERROR", @"Failed to read manifest content", nil);
489
+ });
490
+ }
491
+ });
492
+ }
493
+
494
+ @end
495
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bm-fe/react-native-multi-bundle",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.5",
4
4
  "description": "React Native 多 Bundle 系统 - 支持模块按需加载和独立更新",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -20,11 +20,16 @@
20
20
  "src",
21
21
  "scripts",
22
22
  "templates",
23
- "android/moduleloader",
23
+ "android/moduleloader/src",
24
+ "android/moduleloader/*.gradle",
25
+ "android/moduleloader/*.pro",
26
+ "ios/ModuleLoader",
24
27
  "react-native.config.js",
28
+ "react-native-multi-bundle.podspec",
25
29
  "README.md",
26
30
  "INTEGRATION.md",
27
- "LICENSE"
31
+ "LICENSE",
32
+ "!android/moduleloader/build"
28
33
  ],
29
34
  "bin": {
30
35
  "multi-bundle-build": "./scripts/build-multi-bundle.js"
@@ -45,47 +50,61 @@
45
50
  },
46
51
  "peerDependencies": {
47
52
  "react": ">=18.0.0",
48
- "react-native": ">=0.70.0"
53
+ "react-native": ">=0.70.0",
54
+ "@react-navigation/native": ">=6.0.0",
55
+ "@react-navigation/native-stack": ">=6.0.0",
56
+ "react-native-fs": ">=2.0.0",
57
+ "react-native-safe-area-context": ">=4.0.0",
58
+ "react-native-screens": ">=3.0.0"
49
59
  },
50
- "dependencies": {
51
- "@react-navigation/native": "^7.1.21",
52
- "@react-navigation/native-stack": "^7.6.4",
53
- "react-native-fs": "^2.20.0",
54
- "react-native-safe-area-context": "^5.6.2",
55
- "react-native-screens": "^4.18.0"
60
+ "peerDependenciesMeta": {
61
+ "@react-navigation/native": {
62
+ "optional": true
63
+ },
64
+ "@react-navigation/native-stack": {
65
+ "optional": true
66
+ },
67
+ "react-native-fs": {
68
+ "optional": true
69
+ },
70
+ "react-native-safe-area-context": {
71
+ "optional": true
72
+ },
73
+ "react-native-screens": {
74
+ "optional": true
75
+ }
56
76
  },
77
+ "dependencies": {},
57
78
  "devDependencies": {
58
- "@babel/core": "^7.28.5",
59
- "@babel/preset-env": "^7.28.5",
60
- "@babel/runtime": "^7.28.4",
61
- "@react-native-community/cli": "20.0.2",
62
- "@react-native-community/cli-platform-android": "20.0.2",
63
- "@react-native-community/cli-platform-ios": "20.0.2",
64
- "@react-native/babel-preset": "0.82.1",
65
- "@react-native/eslint-config": "0.82.1",
66
- "@react-native/gradle-plugin": "0.82.1",
67
- "@react-native/metro-config": "0.82.1",
68
- "@react-native/typescript-config": "0.82.1",
69
- "@rnx-kit/babel-preset-metro-react-native": "^3.0.0",
79
+ "@babel/core": "7.25.2",
80
+ "@babel/preset-env": "7.25.2",
81
+ "@babel/runtime": "7.25.0",
82
+ "@react-native-community/cli": "18.0.0",
83
+ "@react-native-community/cli-platform-android": "18.0.0",
84
+ "@react-native-community/cli-platform-ios": "18.0.0",
85
+ "@react-native/babel-preset": "0.79.0",
86
+ "@react-native/eslint-config": "0.79.0",
87
+ "@react-native/gradle-plugin": "0.79.0",
88
+ "@react-native/metro-config": "0.79.0",
89
+ "@react-native/typescript-config": "0.79.0",
70
90
  "@testing-library/react-native": "^13.3.3",
71
- "@types/jest": "^30.0.0",
72
- "@types/react": "^19.2.6",
73
- "@types/react-test-renderer": "^19.1.0",
74
- "babel-jest": "^30.2.0",
91
+ "@types/jest": "29.5.13",
92
+ "@types/react": "19.0.0",
93
+ "@types/react-test-renderer": "19.0.0",
75
94
  "babel-plugin-transform-inline-environment-variables": "^0.4.4",
76
- "eslint": "^9.39.1",
77
- "jest": "^30.2.0",
78
- "patch-package": "^8.0.1",
95
+ "eslint": "8.22.0",
96
+ "jest": "29.7.0",
97
+ "patch-package": "8.0.0",
79
98
  "postinstall-postinstall": "^2.1.0",
80
- "prettier": "3.6.2",
81
- "react-native": "^0.82.1",
82
- "react": "19.1.1",
83
- "react-test-renderer": "^19.1.1",
99
+ "prettier": "3.2.5",
100
+ "react": "19.0.0",
101
+ "react-native": "0.79.5",
102
+ "react-test-renderer": "19.0.0",
84
103
  "standard-version": "^9.5.0",
85
- "typescript": "5.9.3"
104
+ "typescript": "5.5.4"
86
105
  },
87
106
  "engines": {
88
- "node": ">=18"
107
+ "node": ">=18.0.0 <22.0.0"
89
108
  },
90
109
  "publishConfig": {
91
110
  "access": "public"
@@ -0,0 +1,29 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "react-native-multi-bundle"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.description = <<-DESC
10
+ React Native 多 Bundle 系统 - 支持模块按需加载和独立更新。
11
+ 提供 iOS 原生模块 ModuleLoader,用于在同一个 JS 运行时中动态加载子 bundle。
12
+ DESC
13
+ s.homepage = "https://github.com/AntonyMei/react-native-multiple-bundle-demo"
14
+ s.license = { :type => package["license"], :file => "LICENSE" }
15
+ s.author = package["author"]
16
+ s.platforms = { :ios => "13.0" }
17
+ s.source = { :git => "https://github.com/AntonyMei/react-native-multiple-bundle-demo.git", :tag => "v#{s.version}" }
18
+
19
+ s.source_files = "ios/ModuleLoader/**/*.{h,m,mm}"
20
+
21
+ # 编译选项
22
+ s.pod_target_xcconfig = {
23
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++17",
24
+ "DEFINES_MODULE" => "YES"
25
+ }
26
+
27
+ # 依赖 React Native Core
28
+ s.dependency "React-Core"
29
+ end
@@ -16,7 +16,10 @@ module.exports = {
16
16
  packageInstance: 'new ModuleLoaderPackage()',
17
17
  buildTypes: ['debug', 'release'],
18
18
  },
19
- ios: null, // iOS 暂未实现
19
+ ios: {
20
+ // iOS 使用 podspec 自动链接
21
+ podspecPath: './react-native-multi-bundle.podspec',
22
+ },
20
23
  },
21
24
  },
22
25
  };
@@ -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,47 +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
- // 生产环境:从 Native 模块获取 manifest
64
- // if (!__DEV__) {
65
- // try {
66
- // const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
67
- // if (nativeManifest) {
68
- // return convertNativeManifestToBundleManifest(nativeManifest);
69
- // } else {
70
- // // Native 模块返回 null 或 undefined
71
- // throw new Error(
72
- // '[LocalBundleManager] Native module returned null manifest. ' +
73
- // 'Please ensure bundle-manifest.json exists in the app bundle.'
74
- // );
75
- // }
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
- console.log("ReactNativeJS","nativeManifest getCurrentBundleManifestContent")
87
- const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
88
- console.log("ReactNativeJS","nativeManifest "+nativeManifest)
89
- if (nativeManifest) {
90
- return convertNativeManifestToBundleManifest(nativeManifest);
91
- } else {
92
- // Native 模块返回 null 或 undefined
93
- throw new Error(
94
- '[LocalBundleManager] Native module returned null manifest. ' +
95
- 'Please ensure bundle-manifest.json exists in the app bundle.'
96
- );
97
- }
64
+ // 先尝试从 Native 模块获取 manifest(支持 CodePush 等热更新场景)
65
+ try {
66
+ console.log('[LocalBundleManager] Trying to get manifest from Native module...');
67
+ const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
68
+
69
+ if (nativeManifest) {
70
+ console.log('[LocalBundleManager] Got manifest from Native module');
71
+ return convertNativeManifestToBundleManifest(nativeManifest);
72
+ }
73
+
74
+ console.log('[LocalBundleManager] Native module returned null manifest');
75
+ } catch (error) {
76
+ console.warn(`[LocalBundleManager] Failed to get manifest from Native: ${error}`);
77
+ }
98
78
 
99
- // 开发环境:从开发服务器获取 manifest
79
+ // 生产环境下,如果 Native 返回 null,抛出异常
80
+ if (!__DEV__) {
81
+ throw new Error(
82
+ '[LocalBundleManager] Native module returned null manifest. ' +
83
+ 'Please ensure bundle-manifest.json exists in the app bundle.'
84
+ );
85
+ }
86
+
87
+ // 开发环境:降级到从开发服务器获取 manifest
88
+ console.log('[LocalBundleManager] Dev mode: trying to fetch manifest from dev server...');
89
+
100
90
  const config = getGlobalConfig();
101
91
  const devServer = config?.devServer;
102
92
 
@@ -105,9 +95,11 @@ async function getCurrentBundleManifest(): Promise<BundleManifest> {
105
95
  const protocol = devServer.protocol || 'http';
106
96
  const platform = Platform.OS;
107
97
  const manifestUrl = `${protocol}://${devServer.host}:${devServer.port}/bundle-manifest.json?platform=${platform}`;
98
+ console.log(`[LocalBundleManager] Fetching manifest from: ${manifestUrl}`);
108
99
  const response = await fetch(manifestUrl);
109
100
  if (response.ok) {
110
101
  const manifest: BundleManifest = await response.json();
102
+ console.log('[LocalBundleManager] Got manifest from dev server');
111
103
  return manifest;
112
104
  } else {
113
105
  console.warn(
@@ -125,9 +117,11 @@ async function getCurrentBundleManifest(): Promise<BundleManifest> {
125
117
  const host = Platform.OS === 'android' ? '10.0.2.2' : 'localhost';
126
118
  const platform = Platform.OS;
127
119
  const manifestUrl = `http://${host}:8081/bundle-manifest.json?platform=${platform}`;
120
+ console.log(`[LocalBundleManager] Fetching manifest from default: ${manifestUrl}`);
128
121
  const response = await fetch(manifestUrl);
129
122
  if (response.ok) {
130
123
  const manifest: BundleManifest = await response.json();
124
+ console.log('[LocalBundleManager] Got manifest from default dev server');
131
125
  return manifest;
132
126
  } else {
133
127
  console.warn(
@@ -208,23 +208,23 @@ async function loadModule(moduleId: string): Promise<void> {
208
208
  moduleState[moduleId] = 'loading';
209
209
 
210
210
  // Step 3:选择 ModuleLoader
211
- let loader: any = injectedModuleLoader;
211
+ let loader = NativeModules.ModuleLoader;;
212
212
 
213
- if (!loader) {
214
- // 开发环境:优先使用 ModuleLoaderMock(HTTP 模式)
215
- if (__DEV__) {
216
- loader = ModuleLoaderMock;
217
- } else {
218
- // 生产环境:使用 Native CodePush 模块
219
- if (NativeModules?.ModuleLoader.loadBusinessBundle) {
220
- loader = NativeModules.ModuleLoader;
221
- } else {
222
- throw new Error(
223
- 'ModuleLoader not available: Native ModuleLoader module not found in production'
224
- );
225
- }
226
- }
227
- }
213
+ // if (!loader) {
214
+ // // 开发环境:优先使用 ModuleLoaderMock(HTTP 模式)
215
+ // if (__DEV__) {
216
+ // loader = ModuleLoaderMock;
217
+ // } else {
218
+ // // 生产环境:使用 Native CodePush 模块
219
+ // if (NativeModules?.ModuleLoader.loadBusinessBundle) {
220
+ // loader = NativeModules.ModuleLoader;
221
+ // } else {
222
+ // throw new Error(
223
+ // 'ModuleLoader not available: Native ModuleLoader module not found in production'
224
+ // );
225
+ // }
226
+ // }
227
+ // }
228
228
 
229
229
  // 传入 bundleId(即 moduleId)和 bundlePath(从 manifest 中获取)
230
230
  const bundlePath = moduleMeta[moduleId].file;