@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.
@@ -0,0 +1,555 @@
1
+ package com.bitmart.exchange.module.loader
2
+
3
+ import android.util.Log
4
+ import com.facebook.react.bridge.Promise
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
7
+ import com.facebook.react.bridge.ReactMethod
8
+ import com.facebook.react.bridge.Arguments
9
+ import com.facebook.react.bridge.JSBundleLoader
10
+ import com.facebook.react.module.annotations.ReactModule
11
+ import kotlinx.coroutines.CoroutineScope
12
+ import kotlinx.coroutines.Dispatchers
13
+ import kotlinx.coroutines.SupervisorJob
14
+ import kotlinx.coroutines.launch
15
+ import kotlinx.coroutines.withContext
16
+ import org.json.JSONObject
17
+ import java.io.BufferedReader
18
+ import java.io.File
19
+ import java.io.FileOutputStream
20
+ import java.io.FileReader
21
+ import java.io.IOException
22
+ import java.io.InputStream
23
+ import java.io.InputStreamReader
24
+
25
+ /**
26
+ * 简单的 Bundle 加载模块
27
+ *
28
+ * 新架构下通过反射获取 ReactInstance,然后调用 loadJSBundle 方法
29
+ * 注意:ReactInstance 是 internal 类,必须完全通过反射操作
30
+ *
31
+ * Bundle 加载优先级:
32
+ * 1. CodePush 目录(热更新后的 bundle)
33
+ * 2. Assets(APK 内置的 bundle)
34
+ */
35
+ @ReactModule(name = ModuleLoaderModule.NAME)
36
+ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
37
+
38
+ companion object {
39
+ const val NAME = "ModuleLoader"
40
+ private const val TAG = "ModuleLoaderModule"
41
+
42
+ // CodePush 相关常量
43
+ private const val CODE_PUSH_FOLDER_PREFIX = "CodePush"
44
+ private const val STATUS_FILE = "codepush.json"
45
+ private const val CURRENT_PACKAGE_KEY = "currentPackage"
46
+
47
+ // 缓存反射获取的类和方法,避免重复反射
48
+ private var reactHostImplClass: Class<*>? = null
49
+ private var reactInstanceClass: Class<*>? = null
50
+
51
+ init {
52
+ try {
53
+ reactHostImplClass = Class.forName("com.facebook.react.runtime.ReactHostImpl")
54
+ reactInstanceClass = Class.forName("com.facebook.react.runtime.ReactInstance")
55
+ } catch (e: ClassNotFoundException) {
56
+ Log.w(TAG, "New architecture classes not found, will use old architecture")
57
+ }
58
+ }
59
+ }
60
+
61
+ // 协程作用域,用于异步操作
62
+ private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
63
+
64
+ override fun getName(): String = NAME
65
+
66
+ /**
67
+ * 加载子 bundle
68
+ *
69
+ * 加载策略(按优先级):
70
+ * 1. 旧架构:CatalystInstance(当前 RN 0.79.5)
71
+ * 2. 新架构:ReactHostImpl(完全 Bridgeless)
72
+ */
73
+ @ReactMethod
74
+ fun loadBusinessBundle(bundleId: String, bundlePath: String, promise: Promise) {
75
+ Log.d(TAG, "loadBusinessBundle: bundleId=$bundleId, bundlePath=$bundlePath")
76
+
77
+ // 1️⃣ 优先:旧架构 CatalystInstance
78
+ if (tryLoadWithCatalystInstance(bundleId, bundlePath, promise)) {
79
+ return
80
+ }
81
+
82
+ // 2️⃣ 其次:新架构 ReactHostImpl
83
+ if (tryLoadWithReactHost(bundleId, bundlePath, promise)) {
84
+ return
85
+ }
86
+
87
+ // 都失败
88
+ Log.e(TAG, "No suitable runtime found")
89
+ promise.resolve(Arguments.createMap().apply {
90
+ putBoolean("success", false)
91
+ putString("errorMessage", "No CatalystInstance or ReactHostImpl available")
92
+ })
93
+ }
94
+
95
+ /**
96
+ * 尝试使用旧架构 CatalystInstance 加载
97
+ * @return true 如果成功处理(包括成功加载或失败),false 如果 CatalystInstance 不可用
98
+ */
99
+ @Suppress("DEPRECATION")
100
+ private fun tryLoadWithCatalystInstance(bundleId: String, bundlePath: String, promise: Promise): Boolean {
101
+ val catalystInstance = reactContext.catalystInstance ?: return false
102
+
103
+ Log.d(TAG, "Using CatalystInstance (same JS Runtime guaranteed)")
104
+ try {
105
+ // 1️⃣ 优先从 CodePush 目录加载
106
+ val codePushBundlePath = getCodePushBundlePath(bundlePath)
107
+ if (codePushBundlePath != null) {
108
+ Log.d(TAG, "Loading bundle from CodePush: $codePushBundlePath")
109
+ catalystInstance.loadScriptFromFile(codePushBundlePath, codePushBundlePath, false)
110
+ Log.d(TAG, "✓ Bundle loaded from CodePush: $bundleId")
111
+ promise.resolve(Arguments.createMap().apply { putBoolean("success", true) })
112
+ return true
113
+ }
114
+
115
+ // 2️⃣ 回退到 assets 加载
116
+ val assetPath = "assets://$bundlePath"
117
+ Log.d(TAG, "Loading bundle from assets: $assetPath")
118
+ catalystInstance.loadScriptFromAssets(reactContext.assets, assetPath, false)
119
+ Log.d(TAG, "✓ Bundle loaded from assets: $bundleId")
120
+ promise.resolve(Arguments.createMap().apply { putBoolean("success", true) })
121
+ } catch (e: Exception) {
122
+ Log.e(TAG, "Failed to load bundle: $bundleId", e)
123
+ promise.resolve(Arguments.createMap().apply {
124
+ putBoolean("success", false)
125
+ putString("errorMessage", e.message ?: "Unknown error")
126
+ })
127
+ }
128
+ return true
129
+ }
130
+
131
+ /**
132
+ * 尝试使用新架构 ReactHostImpl 加载
133
+ * @return true 如果成功处理,false 如果 ReactHostImpl 不可用
134
+ */
135
+ private fun tryLoadWithReactHost(bundleId: String, bundlePath: String, promise: Promise): Boolean {
136
+ val application = reactContext.applicationContext as? android.app.Application
137
+ val reactApplication = application as? com.facebook.react.ReactApplication ?: return false
138
+
139
+ val reactHost = reactApplication.reactHost
140
+ val hostImplClass = reactHostImplClass
141
+
142
+ if (reactHost == null || hostImplClass == null || !hostImplClass.isInstance(reactHost)) {
143
+ return false
144
+ }
145
+
146
+ Log.d(TAG, "Using ReactHostImpl (new architecture)")
147
+
148
+ // 1️⃣ 优先从 CodePush 目录加载
149
+ val codePushBundlePath = getCodePushBundlePath(bundlePath)
150
+ val bundleLoader = if (codePushBundlePath != null) {
151
+ Log.d(TAG, "Loading bundle from CodePush: $codePushBundlePath")
152
+ JSBundleLoader.createFileLoader(codePushBundlePath)
153
+ } else {
154
+ // 2️⃣ 回退到 assets 加载
155
+ val assetUrl = "assets://$bundlePath"
156
+ Log.d(TAG, "Loading bundle from assets: $assetUrl")
157
+ JSBundleLoader.createAssetLoader(reactContext, assetUrl, false)
158
+ }
159
+
160
+ loadBundleViaReflection(reactHost, bundleLoader, bundleId, promise)
161
+ return true
162
+ }
163
+
164
+ /**
165
+ * 新架构方案:完全通过反射获取 ReactInstance 并加载 bundle
166
+ */
167
+ private fun loadBundleViaReflection(
168
+ reactHost: Any,
169
+ bundleLoader: JSBundleLoader,
170
+ bundleId: String,
171
+ promise: Promise
172
+ ) {
173
+ try {
174
+ val hostImplClass = reactHostImplClass ?: throw Exception("ReactHostImpl class not found")
175
+ val instanceClass = reactInstanceClass ?: throw Exception("ReactInstance class not found")
176
+
177
+ // 通过反射获取 private 的 reactInstance 字段
178
+ val reactInstanceField = hostImplClass.getDeclaredField("reactInstance")
179
+ reactInstanceField.isAccessible = true
180
+ val reactInstance = reactInstanceField.get(reactHost)
181
+
182
+ if (reactInstance == null) {
183
+ Log.e(TAG, "ReactInstance is null")
184
+ val result = Arguments.createMap().apply {
185
+ putBoolean("success", false)
186
+ putString("errorMessage", "ReactInstance not available")
187
+ }
188
+ promise.resolve(result)
189
+ return
190
+ }
191
+
192
+ // 通过反射调用 ReactInstance.loadJSBundle 方法
193
+ val loadJSBundleMethod = instanceClass.getMethod("loadJSBundle", JSBundleLoader::class.java)
194
+ loadJSBundleMethod.invoke(reactInstance, bundleLoader)
195
+
196
+ Log.d(TAG, "Bundle loaded successfully via ReactInstance: $bundleId")
197
+
198
+ val result = Arguments.createMap().apply {
199
+ putBoolean("success", true)
200
+ }
201
+ promise.resolve(result)
202
+
203
+ } catch (e: Exception) {
204
+ Log.e(TAG, "Failed to load bundle via ReactInstance: $bundleId", e)
205
+ val result = Arguments.createMap().apply {
206
+ putBoolean("success", false)
207
+ putString("errorMessage", e.message ?: "Unknown error")
208
+ }
209
+ promise.resolve(result)
210
+ }
211
+ }
212
+
213
+ // ==================== CodePush Bundle Path Resolution ====================
214
+
215
+ /**
216
+ * 获取 CodePush 基础目录
217
+ * 路径: <filesDir>/CodePush/
218
+ */
219
+ private fun getCodePushPath(): String {
220
+ return "${reactContext.filesDir.absolutePath}/$CODE_PUSH_FOLDER_PREFIX"
221
+ }
222
+
223
+ /**
224
+ * 获取当前 CodePush 包的 hash
225
+ * 从 <CodePushPath>/codepush.json 读取 currentPackage 字段
226
+ */
227
+ private fun getCurrentPackageHash(): String? {
228
+ return try {
229
+ val statusFilePath = "${getCodePushPath()}/$STATUS_FILE"
230
+ val statusFile = File(statusFilePath)
231
+
232
+ if (!statusFile.exists()) {
233
+ Log.d(TAG, "CodePush status file not found: $statusFilePath")
234
+ return null
235
+ }
236
+
237
+ val content = statusFile.readText()
238
+ val json = JSONObject(content)
239
+ val packageHash = json.optString(CURRENT_PACKAGE_KEY, null)
240
+
241
+ if (packageHash.isNullOrEmpty()) {
242
+ Log.d(TAG, "No current package hash in CodePush status")
243
+ return null
244
+ }
245
+
246
+ Log.d(TAG, "Current CodePush package hash: $packageHash")
247
+ packageHash
248
+ } catch (e: Exception) {
249
+ Log.e(TAG, "Error reading CodePush status: ${e.message}")
250
+ null
251
+ }
252
+ }
253
+
254
+ /**
255
+ * 获取 CodePush 包目录路径
256
+ * 路径: <CodePushPath>/<packageHash>/
257
+ */
258
+ private fun getPackageFolderPath(packageHash: String): String {
259
+ return "${getCodePushPath()}/$packageHash"
260
+ }
261
+
262
+ /**
263
+ * 从 CodePush 目录查找子 bundle 文件
264
+ *
265
+ * 查找顺序:
266
+ * 1. <packageFolder>/modules/<bundleFileName>
267
+ * 2. <packageFolder>/<bundlePath>
268
+ * 3. <packageFolder>/<bundleFileName>
269
+ *
270
+ * @param bundlePath 原始 bundle 路径,如 "modules/home.jsbundle"
271
+ * @return 完整的文件路径,如果不存在返回 null
272
+ */
273
+ private fun getCodePushBundlePath(bundlePath: String): String? {
274
+ val packageHash = getCurrentPackageHash() ?: return null
275
+ val packageFolder = getPackageFolderPath(packageHash)
276
+
277
+ // 检查包目录是否存在
278
+ val packageDir = File(packageFolder)
279
+ if (!packageDir.exists()) {
280
+ Log.d(TAG, "CodePush package folder not found: $packageFolder")
281
+ return null
282
+ }
283
+
284
+ // 提取文件名
285
+ val bundleFileName = File(bundlePath).name
286
+
287
+ // 查找路径列表
288
+ val searchPaths = listOf(
289
+ // 1. <packageFolder>/modules/<bundleFileName>
290
+ "$packageFolder/modules/$bundleFileName",
291
+ // 2. <packageFolder>/<bundlePath> (完整相对路径)
292
+ "$packageFolder/$bundlePath",
293
+ // 3. <packageFolder>/<bundleFileName> (仅文件名)
294
+ "$packageFolder/$bundleFileName"
295
+ )
296
+
297
+ for (path in searchPaths) {
298
+ val file = File(path)
299
+ if (file.exists() && file.isFile) {
300
+ Log.d(TAG, "Found bundle in CodePush: $path")
301
+ return path
302
+ }
303
+ }
304
+
305
+ Log.d(TAG, "Bundle not found in CodePush package, searched: $searchPaths")
306
+ return null
307
+ }
308
+
309
+ // ==================== Bundle Manifest Support ====================
310
+
311
+ /**
312
+ * 获取当前 bundle manifest 文件路径
313
+ *
314
+ * 查找优先级:
315
+ * 1. CodePush 当前包目录(热更新后的 manifest)
316
+ * 2. assets-base 目录(初始复制的 manifest)
317
+ */
318
+ @ReactMethod
319
+ fun getCurrentBundleManifest(promise: Promise) {
320
+ try {
321
+ // 1️⃣ 优先从 CodePush 当前包目录获取
322
+ val packageHash = getCurrentPackageHash()
323
+ if (packageHash != null) {
324
+ val packageFolder = getPackageFolderPath(packageHash)
325
+ val codePushManifestPath = "$packageFolder/bundle-manifest.json"
326
+ val codePushManifestFile = File(codePushManifestPath)
327
+
328
+ if (codePushManifestFile.exists()) {
329
+ Log.d(TAG, "Found manifest in CodePush package: $codePushManifestPath")
330
+ promise.resolve(codePushManifestPath)
331
+ return
332
+ }
333
+ }
334
+
335
+ // 2️⃣ 确保 assets-base 目录已初始化
336
+ ensureCodePushDirectoryInitialized()
337
+
338
+ // 从 assets-base 目录获取 manifest
339
+ val codePushDir = getCodePushPath()
340
+ val assetsBaseManifestPath = "$codePushDir/assets-base/bundle-manifest.json"
341
+ val assetsBaseManifestFile = File(assetsBaseManifestPath)
342
+
343
+ if (assetsBaseManifestFile.exists()) {
344
+ Log.d(TAG, "Found manifest in assets-base: $assetsBaseManifestPath")
345
+ promise.resolve(assetsBaseManifestPath)
346
+ return
347
+ }
348
+
349
+ // 如果都不存在,返回 null
350
+ Log.w(TAG, "Bundle manifest not found in any location")
351
+ promise.resolve(null)
352
+ } catch (e: Exception) {
353
+ Log.e(TAG, "Failed to get current bundle manifest: ${e.message}")
354
+ promise.reject("MANIFEST_ERROR", "Failed to get manifest path", e)
355
+ }
356
+ }
357
+
358
+ /**
359
+ * 获取当前 bundle manifest 文件内容
360
+ *
361
+ * 查找优先级:
362
+ * 1. CodePush 当前包目录(热更新后的 manifest)
363
+ * 2. assets-base 目录(初始复制的 manifest)
364
+ */
365
+ @ReactMethod
366
+ fun getCurrentBundleManifestContent(promise: Promise) {
367
+ moduleScope.launch {
368
+ try {
369
+ val manifestContent = withContext(Dispatchers.IO) {
370
+ // 1️⃣ 优先从 CodePush 当前包目录读取
371
+ val packageHash = getCurrentPackageHash()
372
+ if (packageHash != null) {
373
+ val packageFolder = getPackageFolderPath(packageHash)
374
+ val codePushManifestPath = "$packageFolder/bundle-manifest.json"
375
+ val codePushManifestFile = File(codePushManifestPath)
376
+
377
+ if (codePushManifestFile.exists()) {
378
+ val content = readFileContent(codePushManifestFile)
379
+ if (content != null) {
380
+ Log.d(TAG, "✓ Read bundle manifest from CodePush package")
381
+ return@withContext content
382
+ }
383
+ }
384
+ }
385
+
386
+ // 2️⃣ 确保 assets-base 目录已初始化
387
+ ensureCodePushDirectoryInitialized()
388
+
389
+ // 从 assets-base 目录读取 manifest
390
+ val codePushDir = getCodePushPath()
391
+ val assetsBaseManifestPath = "$codePushDir/assets-base/bundle-manifest.json"
392
+ val assetsBaseManifestFile = File(assetsBaseManifestPath)
393
+
394
+ if (assetsBaseManifestFile.exists()) {
395
+ val content = readFileContent(assetsBaseManifestFile)
396
+ if (content != null) {
397
+ Log.d(TAG, "✓ Read bundle manifest from assets-base directory")
398
+ return@withContext content
399
+ }
400
+ }
401
+
402
+ // 如果都不存在,返回 null
403
+ Log.w(TAG, "Bundle manifest content not found in any location")
404
+ null
405
+ }
406
+ promise.resolve(manifestContent)
407
+ } catch (e: Exception) {
408
+ Log.e(TAG, "Failed to get bundle manifest content: ${e.message}")
409
+ promise.reject("MANIFEST_CONTENT_ERROR", "Failed to read manifest content", e)
410
+ }
411
+ }
412
+ }
413
+
414
+ /**
415
+ * 确保 CodePush 目录已从 assets 初始化
416
+ */
417
+ private fun ensureCodePushDirectoryInitialized() {
418
+ try {
419
+ val codePushDir = "${reactContext.filesDir.absolutePath}/CodePush"
420
+ val assetsBaseDir = "$codePushDir/assets-base"
421
+
422
+ // 检查 assets-base 目录是否有 bundle-manifest.json 和 modules
423
+ val manifestFile = File("$assetsBaseDir/bundle-manifest.json")
424
+ val modulesDir = File("$assetsBaseDir/modules")
425
+
426
+ // 如果任一不存在,从 assets 复制
427
+ if (!manifestFile.exists() || !modulesDir.exists()) {
428
+ Log.d(TAG, "Initializing CodePush directory from assets...")
429
+ copyAssetsToCodePushDirectory(codePushDir)
430
+ }
431
+ } catch (e: Exception) {
432
+ Log.e(TAG, "Failed to initialize CodePush directory: ${e.message}")
433
+ }
434
+ }
435
+
436
+ /**
437
+ * 从 assets 复制 bundle-manifest.json 和 modules 目录到 CodePush 目录
438
+ */
439
+ private fun copyAssetsToCodePushDirectory(codePushDir: String) {
440
+ try {
441
+ // 创建 assets-base 目录
442
+ val defaultPackageDir = "$codePushDir/assets-base"
443
+ val defaultPackageDirFile = File(defaultPackageDir)
444
+ if (!defaultPackageDirFile.exists()) {
445
+ defaultPackageDirFile.mkdirs()
446
+ }
447
+
448
+ // 1. 复制 bundle-manifest.json
449
+ try {
450
+ val manifestAssetPath = "bundle-manifest.json"
451
+ val manifestInput = reactContext.assets.open(manifestAssetPath)
452
+ val manifestOutput = File("$defaultPackageDir/bundle-manifest.json")
453
+ copyInputStreamToFile(manifestInput, manifestOutput)
454
+ Log.d(TAG, "✓ Copied bundle-manifest.json to CodePush directory")
455
+ } catch (e: IOException) {
456
+ Log.w(TAG, "Warning: Could not copy bundle-manifest.json: ${e.message}")
457
+ }
458
+
459
+ // 2. 复制 modules 目录
460
+ try {
461
+ val modulesAssetPath = "modules"
462
+ val modulesOutputDir = File("$defaultPackageDir/modules")
463
+ if (!modulesOutputDir.exists()) {
464
+ modulesOutputDir.mkdirs()
465
+ }
466
+
467
+ copyAssetFolder(modulesAssetPath, modulesOutputDir.absolutePath)
468
+ Log.d(TAG, "✓ Copied modules directory to CodePush directory")
469
+ } catch (e: IOException) {
470
+ Log.w(TAG, "Warning: Could not copy modules directory: ${e.message}")
471
+ }
472
+
473
+ } catch (e: Exception) {
474
+ Log.e(TAG, "Failed to copy assets to CodePush directory: ${e.message}")
475
+ }
476
+ }
477
+
478
+ /**
479
+ * 递归复制 asset 文件夹
480
+ */
481
+ @Throws(IOException::class)
482
+ private fun copyAssetFolder(assetPath: String, targetPath: String) {
483
+ val assets = reactContext.assets.list(assetPath)
484
+ if (assets.isNullOrEmpty()) {
485
+ // 这是一个文件,不是目录
486
+ val input = reactContext.assets.open(assetPath)
487
+ val output = File(targetPath)
488
+ copyInputStreamToFile(input, output)
489
+ } else {
490
+ // 这是一个目录
491
+ val dir = File(targetPath)
492
+ if (!dir.exists()) {
493
+ dir.mkdirs()
494
+ }
495
+
496
+ for (asset in assets) {
497
+ val subAssetPath = "$assetPath/$asset"
498
+ val subTargetPath = "$targetPath/$asset"
499
+ copyAssetFolder(subAssetPath, subTargetPath)
500
+ }
501
+ }
502
+ }
503
+
504
+ /**
505
+ * 将 InputStream 复制到文件
506
+ */
507
+ @Throws(IOException::class)
508
+ private fun copyInputStreamToFile(input: InputStream, output: File) {
509
+ var outputStream: FileOutputStream? = null
510
+ try {
511
+ outputStream = FileOutputStream(output)
512
+ val buffer = ByteArray(4096)
513
+ var bytesRead: Int
514
+ while (input.read(buffer).also { bytesRead = it } != -1) {
515
+ outputStream.write(buffer, 0, bytesRead)
516
+ }
517
+ } finally {
518
+ try {
519
+ input.close()
520
+ } catch (e: IOException) {
521
+ // Ignore
522
+ }
523
+ try {
524
+ outputStream?.close()
525
+ } catch (e: IOException) {
526
+ // Ignore
527
+ }
528
+ }
529
+ }
530
+
531
+ /**
532
+ * 读取文件内容
533
+ */
534
+ private fun readFileContent(file: File): String? {
535
+ var reader: BufferedReader? = null
536
+ return try {
537
+ reader = BufferedReader(FileReader(file))
538
+ val content = StringBuilder()
539
+ var line: String?
540
+ while (reader.readLine().also { line = it } != null) {
541
+ content.append(line).append("\n")
542
+ }
543
+ content.toString()
544
+ } catch (e: IOException) {
545
+ Log.e(TAG, "Failed to read file: ${file.absolutePath} - ${e.message}")
546
+ null
547
+ } finally {
548
+ try {
549
+ reader?.close()
550
+ } catch (e: IOException) {
551
+ // Ignore
552
+ }
553
+ }
554
+ }
555
+ }
@@ -0,0 +1,25 @@
1
+ package com.bitmart.exchange.module.loader
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ /**
9
+ * ModuleLoader ReactPackage
10
+ * 注册 ModuleLoaderModule 到 React Native
11
+ */
12
+ class ModuleLoaderPackage : ReactPackage {
13
+ @Suppress("DEPRECATION")
14
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
15
+ return listOf(ModuleLoaderModule(reactContext))
16
+ }
17
+
18
+ @Suppress("DEPRECATION")
19
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
20
+ return emptyList()
21
+ }
22
+ }
23
+
24
+
25
+
@@ -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
+