@bm-fe/react-native-multi-bundle 1.0.0-beta.3 → 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 +71 -6
- package/android/moduleloader/src/main/java/com/bitmart/exchange/module/loader/ModuleLoaderModule.kt +65 -126
- package/ios/ModuleLoader/ModuleLoaderModule.h +12 -0
- package/ios/ModuleLoader/ModuleLoaderModule.mm +495 -0
- package/package.json +54 -35
- package/react-native-multi-bundle.podspec +29 -0
- package/react-native.config.js +4 -1
- package/scripts/sync-bundles-to-assets.js +3 -3
- package/src/multi-bundle/LocalBundleManager.ts +32 -38
- package/src/multi-bundle/ModuleRegistry.ts +16 -16
- package/templates/metro.config.js.template +377 -6
|
@@ -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.
|
|
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
|
-
"
|
|
51
|
-
"@react-navigation/native":
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"react-native-
|
|
55
|
-
|
|
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": "
|
|
59
|
-
"@babel/preset-env": "
|
|
60
|
-
"@babel/runtime": "
|
|
61
|
-
"@react-native-community/cli": "
|
|
62
|
-
"@react-native-community/cli-platform-android": "
|
|
63
|
-
"@react-native-community/cli-platform-ios": "
|
|
64
|
-
"@react-native/babel-preset": "0.
|
|
65
|
-
"@react-native/eslint-config": "0.
|
|
66
|
-
"@react-native/gradle-plugin": "0.
|
|
67
|
-
"@react-native/metro-config": "0.
|
|
68
|
-
"@react-native/typescript-config": "0.
|
|
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": "
|
|
72
|
-
"@types/react": "
|
|
73
|
-
"@types/react-test-renderer": "
|
|
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": "
|
|
77
|
-
"jest": "
|
|
78
|
-
"patch-package": "
|
|
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.
|
|
81
|
-
"react
|
|
82
|
-
"react": "
|
|
83
|
-
"react-test-renderer": "
|
|
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.
|
|
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
|
package/react-native.config.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* 同步 Bundle 到 Assets 目录脚本
|
|
5
5
|
* 将构建好的 bundle 文件同步到 Android/iOS 的 assets 目录
|
|
6
6
|
*
|
|
7
|
-
* Android: android/app/src/main/assets/
|
|
8
|
-
* iOS: ios/DemoProject/
|
|
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/
|
|
17
|
+
const IOS_ASSETS_DIR = path.join(PROJECT_ROOT, 'ios/DemoProject/Bundles');
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* ANSI 颜色工具函数
|