@bm-fe/react-native-multi-bundle 1.0.0-beta.1 → 1.0.0-beta.11
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 +104 -62
- package/android/moduleloader/build.gradle +93 -0
- package/android/moduleloader/src/main/AndroidManifest.xml +4 -0
- package/android/moduleloader/src/main/AndroidManifestNew.xml +3 -0
- package/android/moduleloader/src/main/java/com/bitmart/exchange/module/loader/ModuleLoaderModule.kt +555 -0
- package/android/moduleloader/src/main/java/com/bitmart/exchange/module/loader/ModuleLoaderPackage.kt +25 -0
- package/ios/ModuleLoader/ModuleLoaderModule.h +12 -0
- package/ios/ModuleLoader/ModuleLoaderModule.mm +488 -0
- package/package.json +58 -32
- package/react-native-multi-bundle.podspec +29 -0
- package/react-native.config.js +26 -0
- package/scripts/build-multi-bundle.js +36 -6
- package/scripts/sync-bundles-to-assets.js +3 -3
- package/src/multi-bundle/LocalBundleManager.ts +28 -15
- package/src/multi-bundle/ModuleRegistry.ts +1 -1
- package/src/multi-bundle/README.md +2 -0
- package/templates/metro.config.js.template +377 -6
|
@@ -0,0 +1,488 @@
|
|
|
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
|
+
#import <objc/runtime.h>
|
|
7
|
+
#import <jsi/jsi.h>
|
|
8
|
+
|
|
9
|
+
using namespace facebook;
|
|
10
|
+
|
|
11
|
+
@implementation ModuleLoaderModule
|
|
12
|
+
|
|
13
|
+
RCT_EXPORT_MODULE(ModuleLoader);
|
|
14
|
+
|
|
15
|
+
// 合成 bridge 属性(RCTBridgeModule 协议要求)
|
|
16
|
+
@synthesize bridge = _bridge;
|
|
17
|
+
|
|
18
|
+
// Bundle manifest file name
|
|
19
|
+
static NSString *const BundleManifestFileName = @"bundle-manifest.json";
|
|
20
|
+
|
|
21
|
+
#pragma mark - Bundle Path Resolution
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 获取 bundle 文件的完整路径
|
|
25
|
+
*/
|
|
26
|
+
- (NSString *)getFullBundlePath:(NSString *)bundlePath {
|
|
27
|
+
NSString *fileName = [bundlePath lastPathComponent];
|
|
28
|
+
NSString *relativePath = bundlePath;
|
|
29
|
+
|
|
30
|
+
if ([bundlePath containsString:@"modules/"]) {
|
|
31
|
+
fileName = [bundlePath lastPathComponent];
|
|
32
|
+
relativePath = [NSString stringWithFormat:@"modules/%@", fileName];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
NSMutableArray<NSString *> *searchPaths = [NSMutableArray array];
|
|
36
|
+
|
|
37
|
+
// CodePush 路径
|
|
38
|
+
NSString *codePushPath = [self getCodePushBundlePath:bundlePath];
|
|
39
|
+
if (codePushPath) {
|
|
40
|
+
[searchPaths addObject:codePushPath];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// MainBundle/Bundles/modules/xxx.jsbundle
|
|
44
|
+
NSString *path1 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:[@"Bundles/" stringByAppendingString:relativePath]];
|
|
45
|
+
if (path1) [searchPaths addObject:path1];
|
|
46
|
+
|
|
47
|
+
// MainBundle/Bundles/xxx.jsbundle
|
|
48
|
+
NSString *path2 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:[@"Bundles/" stringByAppendingString:fileName]];
|
|
49
|
+
if (path2) [searchPaths addObject:path2];
|
|
50
|
+
|
|
51
|
+
// MainBundle 根目录
|
|
52
|
+
NSString *path3 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:bundlePath];
|
|
53
|
+
if (path3) [searchPaths addObject:path3];
|
|
54
|
+
|
|
55
|
+
NSString *path4 = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:fileName];
|
|
56
|
+
if (path4) [searchPaths addObject:path4];
|
|
57
|
+
|
|
58
|
+
// resourcePath
|
|
59
|
+
NSString *path5 = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:bundlePath];
|
|
60
|
+
if (path5) [searchPaths addObject:path5];
|
|
61
|
+
|
|
62
|
+
NSString *path6 = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:fileName];
|
|
63
|
+
if (path6) [searchPaths addObject:path6];
|
|
64
|
+
|
|
65
|
+
// Documents 目录
|
|
66
|
+
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
|
67
|
+
if (documentsPath) {
|
|
68
|
+
NSString *path7 = [documentsPath stringByAppendingPathComponent:bundlePath];
|
|
69
|
+
if (path7) [searchPaths addObject:path7];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (NSString *fullPath in searchPaths) {
|
|
73
|
+
if (fullPath && [[NSFileManager defaultManager] fileExistsAtPath:fullPath]) {
|
|
74
|
+
RCTLogInfo(@"[ModuleLoader] Found bundle at: %@", fullPath);
|
|
75
|
+
return fullPath;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
RCTLogWarn(@"[ModuleLoader] Bundle not found: %@", bundlePath);
|
|
80
|
+
for (NSString *path in searchPaths) {
|
|
81
|
+
if (path) RCTLogWarn(@"[ModuleLoader] - %@", path);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return nil;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
- (NSString *)getCodePushBundlePath:(NSString *)bundlePath {
|
|
88
|
+
return nil;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#pragma mark - Bridge Access
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 获取当前的 RCTBridge(类似 Android 的 ReactContext)
|
|
95
|
+
*/
|
|
96
|
+
- (RCTBridge *)getCurrentBridge {
|
|
97
|
+
// 方法 1: 使用模块自带的 bridge(最可靠)
|
|
98
|
+
if (self.bridge) {
|
|
99
|
+
RCTLogInfo(@"[ModuleLoader] Using self.bridge: %@", NSStringFromClass([self.bridge class]));
|
|
100
|
+
return self.bridge;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 方法 2: 使用静态方法获取当前 bridge
|
|
104
|
+
RCTBridge *currentBridge = [RCTBridge currentBridge];
|
|
105
|
+
if (currentBridge) {
|
|
106
|
+
RCTLogInfo(@"[ModuleLoader] Using [RCTBridge currentBridge]: %@", NSStringFromClass([currentBridge class]));
|
|
107
|
+
return currentBridge;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 方法 3: 从 AppDelegate 获取
|
|
111
|
+
UIApplication *app = [UIApplication sharedApplication];
|
|
112
|
+
id delegate = app.delegate;
|
|
113
|
+
|
|
114
|
+
if ([delegate respondsToSelector:@selector(bridge)]) {
|
|
115
|
+
RCTBridge *bridge = [delegate performSelector:@selector(bridge)];
|
|
116
|
+
if (bridge) {
|
|
117
|
+
RCTLogInfo(@"[ModuleLoader] Using AppDelegate.bridge: %@", NSStringFromClass([bridge class]));
|
|
118
|
+
return bridge;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 方法 4: 通过 rootViewFactory
|
|
123
|
+
if ([delegate respondsToSelector:@selector(rootViewFactory)]) {
|
|
124
|
+
id factory = [delegate performSelector:@selector(rootViewFactory)];
|
|
125
|
+
if (factory && [factory respondsToSelector:@selector(bridge)]) {
|
|
126
|
+
RCTBridge *bridge = [factory performSelector:@selector(bridge)];
|
|
127
|
+
if (bridge) {
|
|
128
|
+
RCTLogInfo(@"[ModuleLoader] Using rootViewFactory.bridge: %@", NSStringFromClass([bridge class]));
|
|
129
|
+
return bridge;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
RCTLogWarn(@"[ModuleLoader] Could not get bridge from any source");
|
|
135
|
+
return nil;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#pragma mark - Bundle Loading (JSI)
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 通过 JSI Runtime 加载 bundle
|
|
142
|
+
* 这是 RN 新架构 (RCTBridgeProxy) 下加载子 bundle 的唯一方式
|
|
143
|
+
*/
|
|
144
|
+
- (BOOL)loadScriptViaJSI:(NSString *)filePath
|
|
145
|
+
sourceURL:(NSURL *)sourceURL
|
|
146
|
+
bridge:(id)bridge
|
|
147
|
+
error:(NSError **)outError {
|
|
148
|
+
|
|
149
|
+
RCTLogInfo(@"[ModuleLoader] Loading script via JSI Runtime");
|
|
150
|
+
|
|
151
|
+
// 读取 bundle 数据
|
|
152
|
+
NSError *readError = nil;
|
|
153
|
+
NSData *bundleData = [NSData dataWithContentsOfFile:filePath options:0 error:&readError];
|
|
154
|
+
if (readError || !bundleData) {
|
|
155
|
+
if (outError) {
|
|
156
|
+
*outError = readError ?: [NSError errorWithDomain:@"ModuleLoader"
|
|
157
|
+
code:1001
|
|
158
|
+
userInfo:@{NSLocalizedDescriptionKey: @"Failed to read bundle file"}];
|
|
159
|
+
}
|
|
160
|
+
return NO;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
RCTLogInfo(@"[ModuleLoader] Bundle data loaded: %lu bytes", (unsigned long)bundleData.length);
|
|
164
|
+
|
|
165
|
+
// 获取 JSI runtime
|
|
166
|
+
void *runtimePtr = nil;
|
|
167
|
+
|
|
168
|
+
// 方法 1: 使用 runtime 方法
|
|
169
|
+
SEL runtimeSel = NSSelectorFromString(@"runtime");
|
|
170
|
+
if ([bridge respondsToSelector:runtimeSel]) {
|
|
171
|
+
NSMethodSignature *sig = [bridge methodSignatureForSelector:runtimeSel];
|
|
172
|
+
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
|
|
173
|
+
[invocation setTarget:bridge];
|
|
174
|
+
[invocation setSelector:runtimeSel];
|
|
175
|
+
[invocation invoke];
|
|
176
|
+
[invocation getReturnValue:&runtimePtr];
|
|
177
|
+
RCTLogInfo(@"[ModuleLoader] Got runtime via runtime method: %p", runtimePtr);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 方法 2: 直接访问 _runtime ivar
|
|
181
|
+
if (!runtimePtr) {
|
|
182
|
+
Ivar runtimeIvar = class_getInstanceVariable([bridge class], "_runtime");
|
|
183
|
+
if (runtimeIvar) {
|
|
184
|
+
runtimePtr = (__bridge void *)object_getIvar(bridge, runtimeIvar);
|
|
185
|
+
RCTLogInfo(@"[ModuleLoader] Got runtime via _runtime ivar: %p", runtimePtr);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!runtimePtr) {
|
|
190
|
+
if (outError) {
|
|
191
|
+
*outError = [NSError errorWithDomain:@"ModuleLoader"
|
|
192
|
+
code:1003
|
|
193
|
+
userInfo:@{NSLocalizedDescriptionKey: @"Could not get JSI runtime from bridge"}];
|
|
194
|
+
}
|
|
195
|
+
return NO;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 转换为 jsi::Runtime
|
|
199
|
+
jsi::Runtime *runtime = static_cast<jsi::Runtime *>(runtimePtr);
|
|
200
|
+
|
|
201
|
+
// 转换 NSData 为字符串
|
|
202
|
+
NSString *scriptString = [[NSString alloc] initWithData:bundleData encoding:NSUTF8StringEncoding];
|
|
203
|
+
if (!scriptString) {
|
|
204
|
+
if (outError) {
|
|
205
|
+
*outError = [NSError errorWithDomain:@"ModuleLoader"
|
|
206
|
+
code:1004
|
|
207
|
+
userInfo:@{NSLocalizedDescriptionKey: @"Failed to convert bundle data to string"}];
|
|
208
|
+
}
|
|
209
|
+
return NO;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
std::string script = std::string([scriptString UTF8String]);
|
|
213
|
+
std::string sourceURLStr = std::string([[sourceURL absoluteString] UTF8String]);
|
|
214
|
+
|
|
215
|
+
// 尝试获取 dispatchToJSThread 或使用 invokeAsync
|
|
216
|
+
__block BOOL success = NO;
|
|
217
|
+
__block NSError *evalError = nil;
|
|
218
|
+
|
|
219
|
+
// 创建执行 block
|
|
220
|
+
void (^executeBlock)(void) = ^{
|
|
221
|
+
@try {
|
|
222
|
+
auto buffer = std::make_shared<jsi::StringBuffer>(script);
|
|
223
|
+
runtime->evaluateJavaScript(buffer, sourceURLStr);
|
|
224
|
+
RCTLogInfo(@"[ModuleLoader] ✅ Bundle executed via JSI evaluateJavaScript");
|
|
225
|
+
success = YES;
|
|
226
|
+
} @catch (NSException *e) {
|
|
227
|
+
RCTLogError(@"[ModuleLoader] JSI evaluateJavaScript failed: %@", e.reason);
|
|
228
|
+
evalError = [NSError errorWithDomain:@"ModuleLoader"
|
|
229
|
+
code:1005
|
|
230
|
+
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"JSI evaluation failed: %@", e.reason]}];
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// 方法 1: 使用 invokeAsync (RCTBridge 的标准方法)
|
|
235
|
+
SEL invokeAsyncSel = NSSelectorFromString(@"invokeAsync:");
|
|
236
|
+
if ([bridge respondsToSelector:invokeAsyncSel]) {
|
|
237
|
+
RCTLogInfo(@"[ModuleLoader] Using invokeAsync: to dispatch to JS thread");
|
|
238
|
+
|
|
239
|
+
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
240
|
+
|
|
241
|
+
void (^wrappedBlock)(void) = ^{
|
|
242
|
+
executeBlock();
|
|
243
|
+
dispatch_semaphore_signal(semaphore);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
#pragma clang diagnostic push
|
|
247
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
248
|
+
[bridge performSelector:invokeAsyncSel withObject:wrappedBlock];
|
|
249
|
+
#pragma clang diagnostic pop
|
|
250
|
+
|
|
251
|
+
// 等待执行完成 (最多 10 秒)
|
|
252
|
+
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC));
|
|
253
|
+
|
|
254
|
+
if (success) {
|
|
255
|
+
return YES;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 方法 2: 直接执行 (如果已经在 JS 线程或可以同步执行)
|
|
260
|
+
if (!success) {
|
|
261
|
+
RCTLogInfo(@"[ModuleLoader] Executing directly on current thread");
|
|
262
|
+
|
|
263
|
+
@try {
|
|
264
|
+
auto buffer = std::make_shared<jsi::StringBuffer>(script);
|
|
265
|
+
runtime->evaluateJavaScript(buffer, sourceURLStr);
|
|
266
|
+
RCTLogInfo(@"[ModuleLoader] ✅ Bundle executed via direct JSI evaluateJavaScript");
|
|
267
|
+
return YES;
|
|
268
|
+
} @catch (NSException *e) {
|
|
269
|
+
RCTLogError(@"[ModuleLoader] Direct JSI evaluateJavaScript failed: %@", e.reason);
|
|
270
|
+
if (outError) {
|
|
271
|
+
*outError = [NSError errorWithDomain:@"ModuleLoader"
|
|
272
|
+
code:1005
|
|
273
|
+
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"JSI evaluation failed: %@", e.reason]}];
|
|
274
|
+
}
|
|
275
|
+
return NO;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (evalError && outError) {
|
|
280
|
+
*outError = evalError;
|
|
281
|
+
}
|
|
282
|
+
return success;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#pragma mark - Main Load Method
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 加载子 bundle
|
|
289
|
+
* 使用 JSI Runtime 直接执行 JavaScript 代码
|
|
290
|
+
*/
|
|
291
|
+
RCT_EXPORT_METHOD(loadBusinessBundle:(NSString *)bundleId
|
|
292
|
+
bundlePath:(NSString *)bundlePath
|
|
293
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
294
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
295
|
+
{
|
|
296
|
+
RCTLogInfo(@"[ModuleLoader] loadBusinessBundle: bundleId=%@, bundlePath=%@", bundleId, bundlePath);
|
|
297
|
+
|
|
298
|
+
NSString *fullPath = [self getFullBundlePath:bundlePath];
|
|
299
|
+
if (!fullPath) {
|
|
300
|
+
resolve(@{@"success": @NO, @"errorMessage": @"BUNDLE_PATH_NOT_FOUND"});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
305
|
+
// 获取 bridge
|
|
306
|
+
RCTBridge *bridge = [self getCurrentBridge];
|
|
307
|
+
if (!bridge) {
|
|
308
|
+
RCTLogError(@"[ModuleLoader] Bridge not available");
|
|
309
|
+
resolve(@{@"success": @NO, @"errorMessage": @"BRIDGE_NOT_AVAILABLE"});
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
RCTLogInfo(@"[ModuleLoader] Bridge class: %@", NSStringFromClass([bridge class]));
|
|
314
|
+
|
|
315
|
+
// 使用 JSI Runtime 加载脚本
|
|
316
|
+
NSURL *sourceURL = [NSURL fileURLWithPath:fullPath];
|
|
317
|
+
NSError *loadError = nil;
|
|
318
|
+
|
|
319
|
+
BOOL success = [self loadScriptViaJSI:fullPath sourceURL:sourceURL bridge:bridge error:&loadError];
|
|
320
|
+
|
|
321
|
+
if (success) {
|
|
322
|
+
RCTLogInfo(@"[ModuleLoader] ✅ Bundle loaded successfully: %@", bundleId);
|
|
323
|
+
resolve(@{@"success": @YES});
|
|
324
|
+
} else {
|
|
325
|
+
RCTLogError(@"[ModuleLoader] ❌ Failed to load bundle: %@", loadError.localizedDescription);
|
|
326
|
+
resolve(@{@"success": @NO, @"errorMessage": loadError.localizedDescription ?: @"LOAD_FAILED"});
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#pragma mark - Bundle Manifest Methods
|
|
332
|
+
|
|
333
|
+
+ (NSString *)getApplicationSupportDirectory {
|
|
334
|
+
return [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex:0];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
+ (NSString *)bundleAssetsPath {
|
|
338
|
+
return [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"assets"];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
- (NSString *)readFileContent:(NSString *)filePath {
|
|
342
|
+
@try {
|
|
343
|
+
NSError *error;
|
|
344
|
+
NSString *content = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&error];
|
|
345
|
+
if (error) {
|
|
346
|
+
RCTLogWarn(@"[ModuleLoader] Failed to read file: %@ - %@", filePath, error.localizedDescription);
|
|
347
|
+
return nil;
|
|
348
|
+
}
|
|
349
|
+
return content;
|
|
350
|
+
} @catch (NSException *exception) {
|
|
351
|
+
RCTLogWarn(@"[ModuleLoader] Failed to read file: %@ - %@", filePath, exception.reason);
|
|
352
|
+
return nil;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
RCT_EXPORT_METHOD(getCurrentBundleManifest:(RCTPromiseResolveBlock)resolve
|
|
357
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
358
|
+
{
|
|
359
|
+
@try {
|
|
360
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
361
|
+
|
|
362
|
+
// 1. MainBundle/Bundles 目录
|
|
363
|
+
NSString *bundlesPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Bundles"];
|
|
364
|
+
NSString *bundlesManifestPath = [bundlesPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
365
|
+
if ([fileManager fileExistsAtPath:bundlesManifestPath]) {
|
|
366
|
+
RCTLogInfo(@"[ModuleLoader] Found manifest at Bundles: %@", bundlesManifestPath);
|
|
367
|
+
resolve(bundlesManifestPath);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 2. Documents 目录
|
|
372
|
+
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
|
373
|
+
if (documentsPath) {
|
|
374
|
+
NSString *documentsManifestPath = [documentsPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
375
|
+
if ([fileManager fileExistsAtPath:documentsManifestPath]) {
|
|
376
|
+
resolve(documentsManifestPath);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 3. Application Support 目录
|
|
382
|
+
NSString *appSupportPath = [[self class] getApplicationSupportDirectory];
|
|
383
|
+
NSString *appSupportManifestPath = [appSupportPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
384
|
+
if ([fileManager fileExistsAtPath:appSupportManifestPath]) {
|
|
385
|
+
resolve(appSupportManifestPath);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 4. MainBundle assets 目录
|
|
390
|
+
NSString *assetsPath = [[self class] bundleAssetsPath];
|
|
391
|
+
NSString *assetsManifestPath = [assetsPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
392
|
+
if ([fileManager fileExistsAtPath:assetsManifestPath]) {
|
|
393
|
+
resolve(assetsManifestPath);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// 5. MainBundle 根目录
|
|
398
|
+
NSString *mainBundleManifestPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:BundleManifestFileName];
|
|
399
|
+
if ([fileManager fileExistsAtPath:mainBundleManifestPath]) {
|
|
400
|
+
resolve(mainBundleManifestPath);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
RCTLogWarn(@"[ModuleLoader] Bundle manifest not found");
|
|
405
|
+
resolve(nil);
|
|
406
|
+
} @catch (NSException *exception) {
|
|
407
|
+
RCTLogError(@"[ModuleLoader] Failed to get manifest: %@", exception.reason);
|
|
408
|
+
reject(@"MANIFEST_ERROR", @"Failed to get manifest path", nil);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
RCT_EXPORT_METHOD(getCurrentBundleManifestContent:(RCTPromiseResolveBlock)resolve
|
|
413
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
414
|
+
{
|
|
415
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
416
|
+
@try {
|
|
417
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
418
|
+
NSString *manifestContent = nil;
|
|
419
|
+
|
|
420
|
+
// 1. MainBundle/Bundles 目录
|
|
421
|
+
NSString *bundlesPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Bundles"];
|
|
422
|
+
NSString *bundlesManifestPath = [bundlesPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
423
|
+
if ([fileManager fileExistsAtPath:bundlesManifestPath]) {
|
|
424
|
+
manifestContent = [self readFileContent:bundlesManifestPath];
|
|
425
|
+
if (manifestContent) {
|
|
426
|
+
RCTLogInfo(@"[ModuleLoader] Read manifest from Bundles");
|
|
427
|
+
dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 2. Documents 目录
|
|
433
|
+
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
|
434
|
+
if (documentsPath) {
|
|
435
|
+
NSString *documentsManifestPath = [documentsPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
436
|
+
if ([fileManager fileExistsAtPath:documentsManifestPath]) {
|
|
437
|
+
manifestContent = [self readFileContent:documentsManifestPath];
|
|
438
|
+
if (manifestContent) {
|
|
439
|
+
dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 3. Application Support 目录
|
|
446
|
+
NSString *appSupportPath = [[self class] getApplicationSupportDirectory];
|
|
447
|
+
NSString *appSupportManifestPath = [appSupportPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
448
|
+
if ([fileManager fileExistsAtPath:appSupportManifestPath]) {
|
|
449
|
+
manifestContent = [self readFileContent:appSupportManifestPath];
|
|
450
|
+
if (manifestContent) {
|
|
451
|
+
dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 4. MainBundle assets 目录
|
|
457
|
+
NSString *assetsPath = [[self class] bundleAssetsPath];
|
|
458
|
+
NSString *assetsManifestPath = [assetsPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
459
|
+
if ([fileManager fileExistsAtPath:assetsManifestPath]) {
|
|
460
|
+
manifestContent = [self readFileContent:assetsManifestPath];
|
|
461
|
+
if (manifestContent) {
|
|
462
|
+
dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
|
|
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
|
+
dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
RCTLogWarn(@"[ModuleLoader] Bundle manifest content not found");
|
|
478
|
+
dispatch_async(dispatch_get_main_queue(), ^{ resolve(nil); });
|
|
479
|
+
} @catch (NSException *exception) {
|
|
480
|
+
RCTLogError(@"[ModuleLoader] Failed to get manifest content: %@", exception.reason);
|
|
481
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
482
|
+
reject(@"MANIFEST_CONTENT_ERROR", @"Failed to read manifest content", nil);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
@end
|
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.11",
|
|
4
4
|
"description": "React Native 多 Bundle 系统 - 支持模块按需加载和独立更新",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -20,9 +20,16 @@
|
|
|
20
20
|
"src",
|
|
21
21
|
"scripts",
|
|
22
22
|
"templates",
|
|
23
|
+
"android/moduleloader/src",
|
|
24
|
+
"android/moduleloader/*.gradle",
|
|
25
|
+
"android/moduleloader/*.pro",
|
|
26
|
+
"ios/ModuleLoader",
|
|
27
|
+
"react-native.config.js",
|
|
28
|
+
"react-native-multi-bundle.podspec",
|
|
23
29
|
"README.md",
|
|
24
30
|
"INTEGRATION.md",
|
|
25
|
-
"LICENSE"
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"!android/moduleloader/build"
|
|
26
33
|
],
|
|
27
34
|
"bin": {
|
|
28
35
|
"multi-bundle-build": "./scripts/build-multi-bundle.js"
|
|
@@ -35,50 +42,69 @@
|
|
|
35
42
|
"version:minor": "standard-version --release-as minor",
|
|
36
43
|
"version:major": "standard-version --release-as major",
|
|
37
44
|
"version:beta": "npm version prerelease --preid=beta --no-git-tag-version --no-scripts",
|
|
45
|
+
"prepublishOnly": "rm -rf android/moduleloader/build",
|
|
38
46
|
"publish:beta": "npm publish --tag beta",
|
|
39
47
|
"release:beta": "npm run version:beta && npm run publish:beta",
|
|
40
|
-
"release": "npm run version && npm publish"
|
|
48
|
+
"release": "npm run version && npm publish",
|
|
49
|
+
"android": "react-native run-android"
|
|
41
50
|
},
|
|
42
51
|
"peerDependencies": {
|
|
43
52
|
"react": ">=18.0.0",
|
|
44
|
-
"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"
|
|
45
59
|
},
|
|
46
|
-
"
|
|
47
|
-
"@react-navigation/native":
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"react-native-
|
|
51
|
-
|
|
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
|
+
}
|
|
52
76
|
},
|
|
77
|
+
"dependencies": {},
|
|
53
78
|
"devDependencies": {
|
|
54
|
-
"@babel/core": "
|
|
55
|
-
"@babel/preset-env": "
|
|
56
|
-
"@babel/runtime": "
|
|
57
|
-
"@react-native-community/cli": "
|
|
58
|
-
"@react-native-community/cli-platform-android": "
|
|
59
|
-
"@react-native-community/cli-platform-ios": "
|
|
60
|
-
"@react-native/babel-preset": "0.
|
|
61
|
-
"@react-native/eslint-config": "0.
|
|
62
|
-
"@react-native/
|
|
63
|
-
"@react-native/
|
|
64
|
-
"@
|
|
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",
|
|
65
90
|
"@testing-library/react-native": "^13.3.3",
|
|
66
|
-
"@types/jest": "
|
|
67
|
-
"@types/react": "
|
|
68
|
-
"@types/react-test-renderer": "
|
|
69
|
-
"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",
|
|
70
94
|
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
|
71
|
-
"eslint": "
|
|
72
|
-
"jest": "
|
|
73
|
-
"patch-package": "
|
|
95
|
+
"eslint": "8.22.0",
|
|
96
|
+
"jest": "29.7.0",
|
|
97
|
+
"patch-package": "8.0.0",
|
|
74
98
|
"postinstall-postinstall": "^2.1.0",
|
|
75
|
-
"prettier": "3.
|
|
76
|
-
"react
|
|
99
|
+
"prettier": "3.2.5",
|
|
100
|
+
"react": "19.0.0",
|
|
101
|
+
"react-native": "0.79.5",
|
|
102
|
+
"react-test-renderer": "19.0.0",
|
|
77
103
|
"standard-version": "^9.5.0",
|
|
78
|
-
"typescript": "5.
|
|
104
|
+
"typescript": "5.5.4"
|
|
79
105
|
},
|
|
80
106
|
"engines": {
|
|
81
|
-
"node": ">=18"
|
|
107
|
+
"node": ">=18.0.0 <22.0.0"
|
|
82
108
|
},
|
|
83
109
|
"publishConfig": {
|
|
84
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
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native 配置文件
|
|
3
|
+
* 用于配置原生模块的自动链接
|
|
4
|
+
*
|
|
5
|
+
* 当此包被安装到其他 RN 项目时,React Native CLI 会自动读取此配置
|
|
6
|
+
* 并将 ModuleLoaderPackage 自动注册到 PackageList 中
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
// 配置原生模块自动链接
|
|
11
|
+
dependency: {
|
|
12
|
+
platforms: {
|
|
13
|
+
android: {
|
|
14
|
+
sourceDir: './android/moduleloader',
|
|
15
|
+
packageImportPath: 'import com.bitmart.exchange.module.loader.ModuleLoaderPackage;',
|
|
16
|
+
packageInstance: 'new ModuleLoaderPackage()',
|
|
17
|
+
buildTypes: ['debug', 'release'],
|
|
18
|
+
},
|
|
19
|
+
ios: {
|
|
20
|
+
// iOS 使用 podspec 自动链接
|
|
21
|
+
podspecPath: './react-native-multi-bundle.podspec',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|