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

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.
Files changed (32) hide show
  1. package/INTEGRATION.md +371 -0
  2. package/LICENSE +21 -0
  3. package/README.md +240 -0
  4. package/package.json +86 -0
  5. package/scripts/build-multi-bundle.js +483 -0
  6. package/scripts/release-codepush-dev.sh +90 -0
  7. package/scripts/release-codepush.js +420 -0
  8. package/scripts/sync-bundles-to-assets.js +252 -0
  9. package/src/index.ts +40 -0
  10. package/src/multi-bundle/DevModeConfig.ts +23 -0
  11. package/src/multi-bundle/HMRClient.ts +157 -0
  12. package/src/multi-bundle/LocalBundleManager.ts +155 -0
  13. package/src/multi-bundle/ModuleErrorFallback.tsx +85 -0
  14. package/src/multi-bundle/ModuleLoaderMock.ts +169 -0
  15. package/src/multi-bundle/ModuleLoadingPlaceholder.tsx +34 -0
  16. package/src/multi-bundle/ModuleRegistry.ts +295 -0
  17. package/src/multi-bundle/README.md +343 -0
  18. package/src/multi-bundle/config.ts +141 -0
  19. package/src/multi-bundle/createModuleLoader.tsx +92 -0
  20. package/src/multi-bundle/createModuleRouteLoader.tsx +31 -0
  21. package/src/multi-bundle/devUtils.ts +48 -0
  22. package/src/multi-bundle/init.ts +131 -0
  23. package/src/multi-bundle/metro-config-helper.js +140 -0
  24. package/src/multi-bundle/preloadModule.ts +33 -0
  25. package/src/multi-bundle/routeRegistry.ts +118 -0
  26. package/src/types/global.d.ts +14 -0
  27. package/templates/metro.config.js.template +45 -0
  28. package/templates/multi-bundle.config.json.template +31 -0
  29. package/templates/native/android/ModuleLoaderModule.kt +227 -0
  30. package/templates/native/android/ModuleLoaderPackage.kt +26 -0
  31. package/templates/native/ios/ModuleLoaderModule.h +13 -0
  32. package/templates/native/ios/ModuleLoaderModule.m +60 -0
@@ -0,0 +1,118 @@
1
+ /**
2
+ * routeRegistry - 路由注册系统
3
+ * 支持动态路由注册,避免硬编码导入
4
+ */
5
+
6
+ import React from 'react';
7
+ import { ModuleRegistry } from './ModuleRegistry';
8
+ import { createModuleRouteLoader } from './createModuleRouteLoader';
9
+
10
+ /**
11
+ * 路由注册表
12
+ * 存储已注册的路由组件
13
+ */
14
+ const routeRegistry: Map<string, React.ComponentType<any>> = new Map();
15
+
16
+ /**
17
+ * 路由键格式:`${moduleId}:${routeKey}`
18
+ */
19
+ function getRouteKey(moduleId: string, routeKey: string): string {
20
+ return `${moduleId}:${routeKey}`;
21
+ }
22
+
23
+ /**
24
+ * 注册模块路由
25
+ *
26
+ * @param moduleId 模块 ID
27
+ * @param routeKey 路由 key(对应模块 exports.routes[routeKey])
28
+ * @param component 路由组件(可选,如果不提供则使用 createModuleRouteLoader 自动生成延迟加载组件)
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * // 方式1:自动从模块 exports 中提取(延迟加载)
33
+ * registerModuleRoute('home', 'Home');
34
+ *
35
+ * // 方式2:手动提供组件
36
+ * registerModuleRoute('home', 'Home', MyHomeComponent);
37
+ * ```
38
+ */
39
+ export function registerModuleRoute(
40
+ moduleId: string,
41
+ routeKey: string,
42
+ component?: React.ComponentType<any>
43
+ ): React.ComponentType<any> {
44
+ const key = getRouteKey(moduleId, routeKey);
45
+
46
+ if (component) {
47
+ // 直接注册提供的组件
48
+ routeRegistry.set(key, component);
49
+ return component;
50
+ } else {
51
+ // 使用 createModuleRouteLoader 自动生成(延迟加载)
52
+ // 注意:这里返回的是组件,而不是加载器函数
53
+ // 为了兼容性,我们调用加载器函数获取组件
54
+ const createLoader = createModuleRouteLoader(moduleId, routeKey);
55
+ const lazyComponent = createLoader();
56
+ routeRegistry.set(key, lazyComponent);
57
+ return lazyComponent;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 获取已注册的路由组件
63
+ *
64
+ * @param moduleId 模块 ID
65
+ * @param routeKey 路由 key
66
+ * @returns 路由组件,如果未注册则返回 undefined
67
+ */
68
+ export function getModuleRoute(
69
+ moduleId: string,
70
+ routeKey: string
71
+ ): React.ComponentType<any> | undefined {
72
+ const key = getRouteKey(moduleId, routeKey);
73
+ return routeRegistry.get(key);
74
+ }
75
+
76
+ /**
77
+ * 检查路由是否已注册
78
+ */
79
+ export function hasModuleRoute(moduleId: string, routeKey: string): boolean {
80
+ const key = getRouteKey(moduleId, routeKey);
81
+ return routeRegistry.has(key);
82
+ }
83
+
84
+ /**
85
+ * 批量注册路由
86
+ *
87
+ * @param routes 路由配置数组
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * registerRoutes([
92
+ * { moduleId: 'home', routeKey: 'Home' },
93
+ * { moduleId: 'details', routeKey: 'Details' },
94
+ * ]);
95
+ * ```
96
+ */
97
+ export function registerRoutes(
98
+ routes: Array<{ moduleId: string; routeKey: string; component?: React.ComponentType<any> }>
99
+ ): void {
100
+ routes.forEach(({ moduleId, routeKey, component }) => {
101
+ registerModuleRoute(moduleId, routeKey, component);
102
+ });
103
+ }
104
+
105
+ /**
106
+ * 清除所有已注册的路由(主要用于测试)
107
+ */
108
+ export function clearRoutes(): void {
109
+ routeRegistry.clear();
110
+ }
111
+
112
+ /**
113
+ * 获取所有已注册的路由键
114
+ */
115
+ export function getAllRouteKeys(): string[] {
116
+ return Array.from(routeRegistry.keys());
117
+ }
118
+
@@ -0,0 +1,14 @@
1
+ import { ModuleRegistry } from '../multi-bundle/ModuleRegistry';
2
+ export {};
3
+
4
+ declare global {
5
+ var __ModuleRegistry: typeof ModuleRegistry;
6
+
7
+ // React Native 环境中的 process.env 类型定义
8
+ namespace NodeJS {
9
+ interface ProcessEnv {
10
+ MODULE_DEBUG?: string;
11
+ NODE_ENV?: 'development' | 'production' | 'test';
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Metro 配置模板
3
+ *
4
+ * 使用说明:
5
+ * 1. 将此文件复制到你的项目根目录,重命名为 metro.config.js
6
+ * 2. 根据你的项目结构调整 modulePaths 和 sharedDependencies
7
+ * 3. 确保已安装 @bm-fe/react-native-multi-bundle
8
+ */
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
13
+ const baseJSBundle = require('metro/private/DeltaBundler/Serializers/baseJSBundle');
14
+ const bundleToString = require('metro/private/lib/bundleToString');
15
+ const {
16
+ getConfigFromEnv,
17
+ isModulePath: isModulePathHelper,
18
+ isSharedDependencyPath: isSharedDependencyPathHelper,
19
+ extractModuleNameFromPath: extractModuleNameFromPathHelper,
20
+ extractModuleNameFromEntry: extractModuleNameFromEntryHelper,
21
+ } = require('@bm-fe/react-native-multi-bundle/src/multi-bundle/metro-config-helper');
22
+
23
+ const projectRoot = __dirname;
24
+ const defaultConfig = getDefaultConfig(__dirname);
25
+
26
+ // ⚠️ 重要:根据你的项目结构调整以下配置
27
+ const MULTI_BUNDLE_CONFIG_FILE = path.join(projectRoot, 'multi-bundle.config.json');
28
+
29
+ // 通过环境变量或默认值配置模块路径和共享依赖
30
+ // 也可以通过环境变量覆盖:
31
+ // RN_MULTI_BUNDLE_MODULE_PATHS='["src/modules/**"]'
32
+ // RN_MULTI_BUNDLE_SHARED_DEPS='["src/multi-bundle/**","src/navigation/**"]'
33
+ const multiBundleConfig = getConfigFromEnv();
34
+ const modulePaths = multiBundleConfig.modulePaths || ['src/modules/**'];
35
+ const sharedDependencies = multiBundleConfig.sharedDependencies || [
36
+ 'node_modules/@bm-fe/react-native-multi-bundle/**',
37
+ 'src/navigation/**'
38
+ ];
39
+
40
+ // ... 其余配置代码与主 metro.config.js 相同
41
+ // 请参考 @bm-fe/react-native-multi-bundle 的完整实现
42
+
43
+ module.exports = mergeConfig(defaultConfig, {
44
+ // 你的自定义配置
45
+ });
@@ -0,0 +1,31 @@
1
+ {
2
+ "formatVersion": 1,
3
+ "platforms": ["ios", "android"],
4
+ "main": {
5
+ "entry": "src/main-entry.tsx",
6
+ "version": "1.0.0"
7
+ },
8
+ "modules": [
9
+ {
10
+ "id": "home",
11
+ "entry": "src/modules/Home/index.ts",
12
+ "version": "1.0.0",
13
+ "dependencies": [],
14
+ "lazy": true
15
+ },
16
+ {
17
+ "id": "details",
18
+ "entry": "src/modules/Details/index.ts",
19
+ "version": "1.0.0",
20
+ "dependencies": [],
21
+ "lazy": true
22
+ },
23
+ {
24
+ "id": "settings",
25
+ "entry": "src/modules/Settings/index.ts",
26
+ "version": "1.0.0",
27
+ "dependencies": [],
28
+ "lazy": true
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,227 @@
1
+ package com.yourpackage // ⚠️ 修改为你的包名
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
+
12
+ /**
13
+ * ModuleLoader Native 模块
14
+ * 用于在 React Native 运行时中动态加载子 bundle
15
+ *
16
+ * 支持新架构(ReactHostImpl)和旧架构(CatalystInstance)
17
+ *
18
+ * 集成步骤:
19
+ * 1. 将此文件复制到你的 Android 项目的 java/com/yourpackage/ 目录
20
+ * 2. 修改包名为你的实际包名
21
+ * 3. 在 MainApplication.kt 中注册 ModuleLoaderPackage
22
+ */
23
+ @ReactModule(name = ModuleLoaderModule.NAME)
24
+ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
25
+
26
+ companion object {
27
+ const val NAME = "ModuleLoader"
28
+ private const val TAG = "ModuleLoaderModule"
29
+
30
+ // 缓存反射获取的类和方法,避免重复反射
31
+ private var reactHostImplClass: Class<*>? = null
32
+ private var reactInstanceClass: Class<*>? = null
33
+
34
+ init {
35
+ try {
36
+ reactHostImplClass = Class.forName("com.facebook.react.runtime.ReactHostImpl")
37
+ reactInstanceClass = Class.forName("com.facebook.react.runtime.ReactInstance")
38
+ } catch (e: ClassNotFoundException) {
39
+ Log.w(TAG, "New architecture classes not found, will use old architecture")
40
+ }
41
+ }
42
+ }
43
+
44
+ // 记录已加载的 bundle,避免重复加载
45
+ private val loadedBundles = mutableSetOf<String>()
46
+
47
+ override fun getName(): String = NAME
48
+
49
+ /**
50
+ * 加载子 bundle
51
+ *
52
+ * @param bundleId 模块 ID(如 "home", "details", "settings")
53
+ * @param bundlePath bundle 文件路径(如 "modules/home.bundle")
54
+ * @param promise 加载结果回调
55
+ */
56
+ @ReactMethod
57
+ fun loadBusinessBundle(bundleId: String, bundlePath: String, promise: Promise) {
58
+ Log.d(TAG, "loadBusinessBundle: bundleId=$bundleId, bundlePath=$bundlePath")
59
+
60
+ // 已经加载过的 bundle,直接返回成功
61
+ if (loadedBundles.contains(bundleId)) {
62
+ Log.d(TAG, "Bundle already loaded: $bundleId")
63
+ val result = Arguments.createMap().apply {
64
+ putBoolean("success", true)
65
+ }
66
+ promise.resolve(result)
67
+ return
68
+ }
69
+
70
+ try {
71
+ // 获取 ReactApplication
72
+ val application = reactContext.applicationContext as? android.app.Application
73
+ val reactApplication = application as? com.facebook.react.ReactApplication
74
+
75
+ if (reactApplication == null) {
76
+ Log.e(TAG, "Application is not a ReactApplication")
77
+ val result = Arguments.createMap().apply {
78
+ putBoolean("success", false)
79
+ putString("errorMessage", "Application is not a ReactApplication")
80
+ }
81
+ promise.resolve(result)
82
+ return
83
+ }
84
+
85
+ val reactHost = reactApplication.reactHost
86
+
87
+ // 构建 assets 路径
88
+ val assetUrl = "assets://$bundlePath"
89
+ Log.d(TAG, "Loading bundle from: $assetUrl")
90
+
91
+ // 创建 JSBundleLoader
92
+ val bundleLoader = JSBundleLoader.createAssetLoader(
93
+ reactContext,
94
+ assetUrl,
95
+ false // 异步加载
96
+ )
97
+
98
+ // 检查是否是新架构(ReactHostImpl)
99
+ val hostImplClass = reactHostImplClass
100
+ if (reactHost != null && hostImplClass != null && hostImplClass.isInstance(reactHost)) {
101
+ loadBundleViaReflection(reactHost, bundleLoader, bundleId, promise)
102
+ } else {
103
+ // 旧架构回退方案:使用 CatalystInstance
104
+ Log.d(TAG, "Falling back to CatalystInstance (old architecture)")
105
+ loadBundleWithCatalystInstance(bundleId, bundlePath, promise)
106
+ }
107
+
108
+ } catch (e: Exception) {
109
+ Log.e(TAG, "Failed to load bundle: $bundleId", e)
110
+ val result = Arguments.createMap().apply {
111
+ putBoolean("success", false)
112
+ putString("errorMessage", e.message ?: "Unknown error")
113
+ }
114
+ promise.resolve(result)
115
+ }
116
+ }
117
+
118
+ /**
119
+ * 新架构方案:完全通过反射获取 ReactInstance 并加载 bundle
120
+ */
121
+ private fun loadBundleViaReflection(
122
+ reactHost: Any,
123
+ bundleLoader: JSBundleLoader,
124
+ bundleId: String,
125
+ promise: Promise
126
+ ) {
127
+ try {
128
+ val hostImplClass = reactHostImplClass ?: throw Exception("ReactHostImpl class not found")
129
+ val instanceClass = reactInstanceClass ?: throw Exception("ReactInstance class not found")
130
+
131
+ // 通过反射获取 private 的 reactInstance 字段
132
+ val reactInstanceField = hostImplClass.getDeclaredField("reactInstance")
133
+ reactInstanceField.isAccessible = true
134
+ val reactInstance = reactInstanceField.get(reactHost)
135
+
136
+ if (reactInstance == null) {
137
+ Log.e(TAG, "ReactInstance is null")
138
+ val result = Arguments.createMap().apply {
139
+ putBoolean("success", false)
140
+ putString("errorMessage", "ReactInstance not available")
141
+ }
142
+ promise.resolve(result)
143
+ return
144
+ }
145
+
146
+ // 通过反射调用 ReactInstance.loadJSBundle 方法
147
+ val loadJSBundleMethod = instanceClass.getMethod("loadJSBundle", JSBundleLoader::class.java)
148
+ loadJSBundleMethod.invoke(reactInstance, bundleLoader)
149
+
150
+ // 标记为已加载
151
+ loadedBundles.add(bundleId)
152
+ Log.d(TAG, "Bundle loaded successfully via ReactInstance: $bundleId")
153
+
154
+ val result = Arguments.createMap().apply {
155
+ putBoolean("success", true)
156
+ }
157
+ promise.resolve(result)
158
+
159
+ } catch (e: Exception) {
160
+ Log.e(TAG, "Failed to load bundle via ReactInstance: $bundleId", e)
161
+ val result = Arguments.createMap().apply {
162
+ putBoolean("success", false)
163
+ putString("errorMessage", e.message ?: "Unknown error")
164
+ }
165
+ promise.resolve(result)
166
+ }
167
+ }
168
+
169
+ /**
170
+ * 旧架构回退方案:使用 CatalystInstance 加载 bundle
171
+ */
172
+ @Suppress("DEPRECATION")
173
+ private fun loadBundleWithCatalystInstance(bundleId: String, bundlePath: String, promise: Promise) {
174
+ try {
175
+ val catalystInstance = reactContext.catalystInstance
176
+ if (catalystInstance == null) {
177
+ Log.e(TAG, "CatalystInstance is null")
178
+ val result = Arguments.createMap().apply {
179
+ putBoolean("success", false)
180
+ putString("errorMessage", "CatalystInstance not available")
181
+ }
182
+ promise.resolve(result)
183
+ return
184
+ }
185
+
186
+ val assetPath = "assets://$bundlePath"
187
+ catalystInstance.loadScriptFromAssets(
188
+ reactContext.assets,
189
+ assetPath,
190
+ false
191
+ )
192
+
193
+ loadedBundles.add(bundleId)
194
+ Log.d(TAG, "Bundle loaded successfully via CatalystInstance: $bundleId")
195
+
196
+ val result = Arguments.createMap().apply {
197
+ putBoolean("success", true)
198
+ }
199
+ promise.resolve(result)
200
+ } catch (e: Exception) {
201
+ Log.e(TAG, "Failed to load bundle via CatalystInstance: $bundleId", e)
202
+ val result = Arguments.createMap().apply {
203
+ putBoolean("success", false)
204
+ putString("errorMessage", e.message ?: "Unknown error")
205
+ }
206
+ promise.resolve(result)
207
+ }
208
+ }
209
+
210
+ /**
211
+ * 检查 bundle 是否已加载
212
+ */
213
+ @ReactMethod
214
+ fun isBundleLoaded(bundleId: String, promise: Promise) {
215
+ promise.resolve(loadedBundles.contains(bundleId))
216
+ }
217
+
218
+ /**
219
+ * 获取已加载的 bundle 列表
220
+ */
221
+ @ReactMethod
222
+ fun getLoadedBundles(promise: Promise) {
223
+ val array = Arguments.createArray()
224
+ loadedBundles.forEach { array.pushString(it) }
225
+ promise.resolve(array)
226
+ }
227
+ }
@@ -0,0 +1,26 @@
1
+ package com.yourpackage // ⚠️ 修改为你的包名
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
+ * 集成步骤:
13
+ * 1. 将此文件复制到你的 Android 项目的 java/com/yourpackage/ 目录
14
+ * 2. 修改包名为你的实际包名
15
+ * 3. 在 MainApplication.kt 的 getPackages() 方法中添加:
16
+ * packages.add(ModuleLoaderPackage())
17
+ */
18
+ class ModuleLoaderPackage : ReactPackage {
19
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
20
+ return listOf(ModuleLoaderModule(reactContext))
21
+ }
22
+
23
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
24
+ return emptyList()
25
+ }
26
+ }
@@ -0,0 +1,13 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ /**
4
+ * ModuleLoader iOS Native 模块
5
+ *
6
+ * 集成步骤:
7
+ * 1. 将此文件添加到你的 iOS 项目
8
+ * 2. 在 Xcode 中创建对应的 .m 实现文件
9
+ * 3. 在 AppDelegate.m 中注册模块
10
+ */
11
+ @interface ModuleLoaderModule : NSObject <RCTBridgeModule>
12
+
13
+ @end
@@ -0,0 +1,60 @@
1
+ #import "ModuleLoaderModule.h"
2
+ #import <React/RCTBridge.h>
3
+ #import <React/RCTBundleURLProvider.h>
4
+
5
+ /**
6
+ * ModuleLoader iOS Native 模块实现
7
+ *
8
+ * 注意:iOS 端的 bundle 加载方式与 Android 不同
9
+ * 这里提供一个基础实现,你可能需要根据实际需求调整
10
+ */
11
+ @implementation ModuleLoaderModule
12
+
13
+ RCT_EXPORT_MODULE(ModuleLoader);
14
+
15
+ // 记录已加载的 bundle
16
+ static NSMutableSet<NSString *> *loadedBundles;
17
+
18
+ + (void)initialize {
19
+ if (self == [ModuleLoaderModule class]) {
20
+ loadedBundles = [NSMutableSet set];
21
+ }
22
+ }
23
+
24
+ RCT_EXPORT_METHOD(loadBusinessBundle:(NSString *)bundleId
25
+ bundlePath:(NSString *)bundlePath
26
+ resolver:(RCTPromiseResolveBlock)resolve
27
+ rejecter:(RCTPromiseRejectBlock)reject)
28
+ {
29
+ // 检查是否已加载
30
+ if ([loadedBundles containsObject:bundleId]) {
31
+ resolve(@{@"success": @YES});
32
+ return;
33
+ }
34
+
35
+ // TODO: 实现 iOS bundle 加载逻辑
36
+ // iOS 端的 bundle 加载方式与 Android 不同
37
+ // 你可能需要使用 RCTBridge 的相关 API 来加载 bundle
38
+
39
+ NSLog(@"[ModuleLoader] Loading bundle: %@ from %@", bundleId, bundlePath);
40
+
41
+ // 标记为已加载
42
+ [loadedBundles addObject:bundleId];
43
+
44
+ resolve(@{@"success": @YES});
45
+ }
46
+
47
+ RCT_EXPORT_METHOD(isBundleLoaded:(NSString *)bundleId
48
+ resolver:(RCTPromiseResolveBlock)resolve
49
+ rejecter:(RCTPromiseRejectBlock)reject)
50
+ {
51
+ resolve(@([loadedBundles containsObject:bundleId]));
52
+ }
53
+
54
+ RCT_EXPORT_METHOD(getLoadedBundles:(RCTPromiseResolveBlock)resolve
55
+ rejecter:(RCTPromiseRejectBlock)reject)
56
+ {
57
+ resolve([loadedBundles allObjects]);
58
+ }
59
+
60
+ @end