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

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.
@@ -23,8 +23,7 @@ import java.io.InputStreamReader
23
23
 
24
24
  /**
25
25
  * 简单的 Bundle 加载模块
26
- * 用于在同一个 JS 运行时中动态加载子 bundle
27
- *
26
+ *
28
27
  * 新架构下通过反射获取 ReactInstance,然后调用 loadJSBundle 方法
29
28
  * 注意:ReactInstance 是 internal 类,必须完全通过反射操作
30
29
  */
@@ -34,11 +33,11 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
34
33
  companion object {
35
34
  const val NAME = "ModuleLoader"
36
35
  private const val TAG = "ModuleLoaderModule"
37
-
36
+
38
37
  // 缓存反射获取的类和方法,避免重复反射
39
38
  private var reactHostImplClass: Class<*>? = null
40
39
  private var reactInstanceClass: Class<*>? = null
41
-
40
+
42
41
  init {
43
42
  try {
44
43
  reactHostImplClass = Class.forName("com.facebook.react.runtime.ReactHostImpl")
@@ -49,9 +48,6 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
49
48
  }
50
49
  }
51
50
 
52
- // 记录已加载的 bundle,避免重复加载
53
- private val loadedBundles = mutableSetOf<String>()
54
-
55
51
  // 协程作用域,用于异步操作
56
52
  private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
57
53
 
@@ -59,71 +55,77 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
59
55
 
60
56
  /**
61
57
  * 加载子 bundle
62
- *
63
- * @param bundleId 模块 ID(如 "home", "details", "settings")
64
- * @param bundlePath bundle 文件路径(如 "modules/home.bundle"
65
- * @param promise 加载结果回调
58
+ *
59
+ * 加载策略(按优先级):
60
+ * 1. 旧架构:CatalystInstance(当前 RN 0.79.5
61
+ * 2. 新架构:ReactHostImpl(完全 Bridgeless)
66
62
  */
67
63
  @ReactMethod
68
64
  fun loadBusinessBundle(bundleId: String, bundlePath: String, promise: Promise) {
69
65
  Log.d(TAG, "loadBusinessBundle: bundleId=$bundleId, bundlePath=$bundlePath")
70
66
 
71
- // 已经加载过的 bundle,直接返回成功
72
- if (loadedBundles.contains(bundleId)) {
73
- Log.d(TAG, "Bundle already loaded: $bundleId")
74
- val result = Arguments.createMap().apply {
75
- putBoolean("success", true)
76
- }
77
- promise.resolve(result)
67
+ // 1️⃣ 优先:旧架构 CatalystInstance
68
+ if (tryLoadWithCatalystInstance(bundleId, bundlePath, promise)) {
78
69
  return
79
70
  }
80
71
 
81
- try {
82
- // 获取 ReactApplication
83
- val application = reactContext.applicationContext as? android.app.Application
84
- val reactApplication = application as? com.facebook.react.ReactApplication
85
-
86
- if (reactApplication == null) {
87
- Log.e(TAG, "Application is not a ReactApplication")
88
- val result = Arguments.createMap().apply {
89
- putBoolean("success", false)
90
- putString("errorMessage", "Application is not a ReactApplication")
91
- }
92
- promise.resolve(result)
93
- return
94
- }
72
+ // 2️⃣ 其次:新架构 ReactHostImpl
73
+ if (tryLoadWithReactHost(bundleId, bundlePath, promise)) {
74
+ return
75
+ }
95
76
 
96
- val reactHost = reactApplication.reactHost
97
-
98
- // 构建 assets 路径
99
- val assetUrl = "assets://$bundlePath"
100
- Log.d(TAG, "Loading bundle from: $assetUrl")
101
-
102
- // 创建 JSBundleLoader
103
- val bundleLoader = JSBundleLoader.createAssetLoader(
104
- reactContext,
105
- assetUrl,
106
- false // 异步加载
107
- )
108
-
109
- // 检查是否是新架构(ReactHostImpl)
110
- val hostImplClass = reactHostImplClass
111
- if (reactHost != null && hostImplClass != null && hostImplClass.isInstance(reactHost)) {
112
- loadBundleViaReflection(reactHost, bundleLoader, bundleId, promise)
113
- } else {
114
- // 旧架构回退方案:使用 CatalystInstance
115
- Log.d(TAG, "Falling back to CatalystInstance (old architecture)")
116
- loadBundleWithCatalystInstance(bundleId, bundlePath, promise)
117
- }
77
+ // 都失败
78
+ Log.e(TAG, "No suitable runtime found")
79
+ promise.resolve(Arguments.createMap().apply {
80
+ putBoolean("success", false)
81
+ putString("errorMessage", "No CatalystInstance or ReactHostImpl available")
82
+ })
83
+ }
84
+
85
+ /**
86
+ * 尝试使用旧架构 CatalystInstance 加载
87
+ * @return true 如果成功处理(包括成功加载或失败),false 如果 CatalystInstance 不可用
88
+ */
89
+ @Suppress("DEPRECATION")
90
+ private fun tryLoadWithCatalystInstance(bundleId: String, bundlePath: String, promise: Promise): Boolean {
91
+ val catalystInstance = reactContext.catalystInstance ?: return false
118
92
 
93
+ Log.d(TAG, "Using CatalystInstance (same JS Runtime guaranteed)")
94
+ try {
95
+ val assetPath = "assets://$bundlePath"
96
+ catalystInstance.loadScriptFromAssets(reactContext.assets, assetPath, false)
97
+ Log.d(TAG, "Bundle loaded successfully: $bundleId")
98
+ promise.resolve(Arguments.createMap().apply { putBoolean("success", true) })
119
99
  } catch (e: Exception) {
120
100
  Log.e(TAG, "Failed to load bundle: $bundleId", e)
121
- val result = Arguments.createMap().apply {
101
+ promise.resolve(Arguments.createMap().apply {
122
102
  putBoolean("success", false)
123
103
  putString("errorMessage", e.message ?: "Unknown error")
124
- }
125
- promise.resolve(result)
104
+ })
105
+ }
106
+ return true
107
+ }
108
+
109
+ /**
110
+ * 尝试使用新架构 ReactHostImpl 加载
111
+ * @return true 如果成功处理,false 如果 ReactHostImpl 不可用
112
+ */
113
+ private fun tryLoadWithReactHost(bundleId: String, bundlePath: String, promise: Promise): Boolean {
114
+ val application = reactContext.applicationContext as? android.app.Application
115
+ val reactApplication = application as? com.facebook.react.ReactApplication ?: return false
116
+
117
+ val reactHost = reactApplication.reactHost
118
+ val hostImplClass = reactHostImplClass
119
+
120
+ if (reactHost == null || hostImplClass == null || !hostImplClass.isInstance(reactHost)) {
121
+ return false
126
122
  }
123
+
124
+ Log.d(TAG, "Using ReactHostImpl (new architecture)")
125
+ val assetUrl = "assets://$bundlePath"
126
+ val bundleLoader = JSBundleLoader.createAssetLoader(reactContext, assetUrl, false)
127
+ loadBundleViaReflection(reactHost, bundleLoader, bundleId, promise)
128
+ return true
127
129
  }
128
130
 
129
131
  /**
@@ -138,12 +140,12 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
138
140
  try {
139
141
  val hostImplClass = reactHostImplClass ?: throw Exception("ReactHostImpl class not found")
140
142
  val instanceClass = reactInstanceClass ?: throw Exception("ReactInstance class not found")
141
-
143
+
142
144
  // 通过反射获取 private 的 reactInstance 字段
143
145
  val reactInstanceField = hostImplClass.getDeclaredField("reactInstance")
144
146
  reactInstanceField.isAccessible = true
145
147
  val reactInstance = reactInstanceField.get(reactHost)
146
-
148
+
147
149
  if (reactInstance == null) {
148
150
  Log.e(TAG, "ReactInstance is null")
149
151
  val result = Arguments.createMap().apply {
@@ -157,59 +159,16 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
157
159
  // 通过反射调用 ReactInstance.loadJSBundle 方法
158
160
  val loadJSBundleMethod = instanceClass.getMethod("loadJSBundle", JSBundleLoader::class.java)
159
161
  loadJSBundleMethod.invoke(reactInstance, bundleLoader)
160
-
161
- // 标记为已加载
162
- loadedBundles.add(bundleId)
162
+
163
163
  Log.d(TAG, "Bundle loaded successfully via ReactInstance: $bundleId")
164
164
 
165
165
  val result = Arguments.createMap().apply {
166
166
  putBoolean("success", true)
167
167
  }
168
168
  promise.resolve(result)
169
-
170
- } catch (e: Exception) {
171
- Log.e(TAG, "Failed to load bundle via ReactInstance: $bundleId", e)
172
- val result = Arguments.createMap().apply {
173
- putBoolean("success", false)
174
- putString("errorMessage", e.message ?: "Unknown error")
175
- }
176
- promise.resolve(result)
177
- }
178
- }
179
-
180
- /**
181
- * 旧架构回退方案:使用 CatalystInstance 加载 bundle
182
- */
183
- @Suppress("DEPRECATION")
184
- private fun loadBundleWithCatalystInstance(bundleId: String, bundlePath: String, promise: Promise) {
185
- try {
186
- val catalystInstance = reactContext.catalystInstance
187
- if (catalystInstance == null) {
188
- Log.e(TAG, "CatalystInstance is null")
189
- val result = Arguments.createMap().apply {
190
- putBoolean("success", false)
191
- putString("errorMessage", "CatalystInstance not available")
192
- }
193
- promise.resolve(result)
194
- return
195
- }
196
-
197
- val assetPath = "assets://$bundlePath"
198
- catalystInstance.loadScriptFromAssets(
199
- reactContext.assets,
200
- assetPath,
201
- false
202
- )
203
169
 
204
- loadedBundles.add(bundleId)
205
- Log.d(TAG, "Bundle loaded successfully via CatalystInstance: $bundleId")
206
-
207
- val result = Arguments.createMap().apply {
208
- putBoolean("success", true)
209
- }
210
- promise.resolve(result)
211
170
  } catch (e: Exception) {
212
- Log.e(TAG, "Failed to load bundle via CatalystInstance: $bundleId", e)
171
+ Log.e(TAG, "Failed to load bundle via ReactInstance: $bundleId", e)
213
172
  val result = Arguments.createMap().apply {
214
173
  putBoolean("success", false)
215
174
  putString("errorMessage", e.message ?: "Unknown error")
@@ -218,24 +177,6 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
218
177
  }
219
178
  }
220
179
 
221
- /**
222
- * 检查 bundle 是否已加载
223
- */
224
- @ReactMethod
225
- fun isBundleLoaded(bundleId: String, promise: Promise) {
226
- promise.resolve(loadedBundles.contains(bundleId))
227
- }
228
-
229
- /**
230
- * 获取已加载的 bundle 列表
231
- */
232
- @ReactMethod
233
- fun getLoadedBundles(promise: Promise) {
234
- val array = Arguments.createArray()
235
- loadedBundles.forEach { array.pushString(it) }
236
- promise.resolve(array)
237
- }
238
-
239
180
  // ==================== Bundle Manifest Support ====================
240
181
 
241
182
  /**
@@ -253,7 +194,7 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
253
194
  val codePushDir = "${reactContext.filesDir.absolutePath}/CodePush"
254
195
  val assetsBaseManifestPath = "$codePushDir/assets-base/bundle-manifest.json"
255
196
  val assetsBaseManifestFile = File(assetsBaseManifestPath)
256
-
197
+
257
198
  if (assetsBaseManifestFile.exists()) {
258
199
  promise.resolve(assetsBaseManifestPath)
259
200
  return
@@ -283,7 +224,7 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
283
224
  val codePushDir = "${reactContext.filesDir.absolutePath}/CodePush"
284
225
  val assetsBaseManifestPath = "$codePushDir/assets-base/bundle-manifest.json"
285
226
  val assetsBaseManifestFile = File(assetsBaseManifestPath)
286
-
227
+
287
228
  if (assetsBaseManifestFile.exists()) {
288
229
  val content = readFileContent(assetsBaseManifestFile)
289
230
  if (content != null) {
@@ -445,5 +386,3 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
445
386
  }
446
387
  }
447
388
  }
448
-
449
-
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",
3
+ "version": "1.0.0-beta.4",
4
4
  "description": "React Native 多 Bundle 系统 - 支持模块按需加载和独立更新",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -220,7 +220,7 @@ async function loadModule(moduleId: string): Promise<void> {
220
220
  loader = NativeModules.ModuleLoader;
221
221
  } else {
222
222
  throw new Error(
223
- 'ModuleLoader not available: Native CodePush module not found in production'
223
+ 'ModuleLoader not available: Native ModuleLoader module not found in production'
224
224
  );
225
225
  }
226
226
  }
@@ -12,6 +12,7 @@ const fs = require('fs');
12
12
  const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
13
13
  const baseJSBundle = require('metro/private/DeltaBundler/Serializers/baseJSBundle');
14
14
  const bundleToString = require('metro/private/lib/bundleToString');
15
+ const { sourceMapStringNonBlocking } = require('metro/src/DeltaBundler/Serializers/sourceMapString');
15
16
  const {
16
17
  getConfigFromEnv,
17
18
  isModulePath: isModulePathHelper,
@@ -22,6 +23,7 @@ const {
22
23
 
23
24
  const projectRoot = __dirname;
24
25
  const defaultConfig = getDefaultConfig(__dirname);
26
+ const { assetExts, sourceExts } = defaultConfig.resolver;
25
27
 
26
28
  // ⚠️ 重要:根据你的项目结构调整以下配置
27
29
  const MULTI_BUNDLE_CONFIG_FILE = path.join(projectRoot, 'multi-bundle.config.json');
@@ -33,13 +35,41 @@ const MULTI_BUNDLE_CONFIG_FILE = path.join(projectRoot, 'multi-bundle.config.jso
33
35
  const multiBundleConfig = getConfigFromEnv();
34
36
  const modulePaths = multiBundleConfig.modulePaths || ['src/modules/**'];
35
37
  const sharedDependencies = multiBundleConfig.sharedDependencies || [
36
- 'node_modules/@bm-fe/react-native-multi-bundle/**',
37
38
  'src/navigation/**'
38
39
  ];
39
40
 
40
41
  // ⚠️ 注意:这是"请求级别"的标记,由 middleware 动态设置
41
42
  let isDevModuleRequest = false;
42
43
 
44
+ const isMultiBundleBuild = process.env.RN_MULTI_BUNDLE_BUILD === 'true';
45
+ const buildType = process.env.RN_BUILD_TYPE || 'main'; // 'main' 或 'module'
46
+
47
+ /**
48
+ * Metro/Node 里拿到的路径多数是绝对路径,而 multi-bundle 的 glob helper
49
+ * 使用的是 ^...$ 的“全匹配”,所以必须先转换成项目相对路径再做匹配。
50
+ */
51
+ function toProjectRelativePath(filePath) {
52
+ if (!filePath || typeof filePath !== 'string') return filePath;
53
+
54
+ // 统一分隔符,避免 Windows/Metro 混用反斜杠
55
+ const normalized = filePath.replace(/\\/g, '/');
56
+
57
+ // 已经是相对路径(例如 dev server req.url 截出来的 path)就直接用
58
+ if (!path.isAbsolute(filePath)) {
59
+ return normalized.replace(/^\.\//, '');
60
+ }
61
+
62
+ // 绝对路径:转成相对 projectRoot 的路径,再规范化
63
+ return path.relative(projectRoot, filePath).replace(/\\/g, '/');
64
+ }
65
+
66
+ function isReactNativeRuntime(modulePath) {
67
+ return (
68
+ modulePath.includes('__prelude__') ||
69
+ modulePath.endsWith('/react-native/Libraries/Core/InitializeCore.js')
70
+ );
71
+ }
72
+
43
73
  /**
44
74
  * 加载 multi-bundle.config.json 配置
45
75
  */
@@ -104,6 +134,252 @@ function generateManifestFromConfig(config, platform) {
104
134
  return manifest;
105
135
  }
106
136
 
137
+ /**
138
+ * 检查模块是否是共享依赖(应该使用 segmentId=0,以便跨 bundle 访问)
139
+ * 使用配置化的共享依赖路径
140
+ */
141
+ function isSharedModuleForSegmentId(modulePath) {
142
+ // 共享依赖应该始终使用 segmentId=0,这样主 bundle 和模块 bundle 都能访问
143
+ // ⚠️ 关键修复:检查 node_modules 时,需要同时处理有前导斜杠和没有前导斜杠的情况
144
+ if (
145
+ modulePath.includes('/node_modules/') ||
146
+ modulePath.includes('node_modules/')
147
+ ) {
148
+ const result = sharedNodeModules.some(
149
+ dep =>
150
+ modulePath.includes(`/node_modules/${dep}`) ||
151
+ modulePath.includes(`node_modules/${dep}`)
152
+ );
153
+ return result;
154
+ }
155
+
156
+ // 使用配置化的共享依赖路径
157
+ return isSharedDependencyPathHelper(
158
+ toProjectRelativePath(modulePath),
159
+ sharedDependencies
160
+ );
161
+ }
162
+
163
+ // 模块名到 segmentId 的映射(确保同一模块的 segmentId 稳定)
164
+ // 主 bundle 使用 segmentId = 0,模块 bundle 从 1 开始
165
+ // ⭐ 使用基于模块名的哈希算法,确保同一模块名总是得到相同的 segmentId,不依赖于构建顺序
166
+ function generateSegmentIdForModule(moduleName) {
167
+ // 使用 djb2 哈希算法,确保稳定性
168
+ let hash = 5381;
169
+ for (let i = 0; i < moduleName.length; i++) {
170
+ hash = (hash << 5) + hash + moduleName.charCodeAt(i);
171
+ }
172
+
173
+ // 确保 segmentId 在 1-1023 范围内(0 保留给主 bundle)
174
+ // 使用模运算确保范围,但保留足够的空间避免冲突
175
+ const segmentId = 1 + (Math.abs(hash) % 1000); // 1-1000 范围,足够大多数项目使用
176
+ return segmentId;
177
+ }
178
+
179
+ /**
180
+ * 基于文件路径生成稳定的 localId
181
+ * 使用 djb2 哈希算法,确保同一路径总是得到相同的 localId
182
+ * 范围:0-1048575(20位),避免哈希碰撞
183
+ */
184
+ function generateStableLocalId(relativePath) {
185
+ // djb2 哈希算法
186
+ let hash = 5381;
187
+ for (let i = 0; i < relativePath.length; i++) {
188
+ hash = (hash << 5) + hash + relativePath.charCodeAt(i);
189
+ }
190
+
191
+ // 确保 localId 在 0-1048575 范围内(20位)
192
+ // 之前使用 16 位 (65536) 导致了大量哈希碰撞,导致运行时加载错误的模块
193
+ const localId = Math.abs(hash) % 1048576;
194
+ return localId;
195
+ }
196
+
197
+ /**
198
+ * 获取模块的 segmentId
199
+ * - 共享依赖(react-native、react 等):始终返回 0,确保跨 bundle 访问
200
+ * - 主 bundle 的模块:返回 0
201
+ * - 模块 bundle 的业务模块:返回对应的 segmentId(1, 2, 3...)
202
+ */
203
+ function getSegmentIdForModule(modulePath) {
204
+ // ⭐ 关键:共享依赖始终使用 segmentId=0,确保主 bundle 和模块 bundle 使用相同的模块 ID
205
+ if (isSharedModuleForSegmentId(modulePath)) {
206
+ return 0;
207
+ }
208
+
209
+ const moduleName = extractModuleNameFromPathHelper(
210
+ toProjectRelativePath(modulePath),
211
+ modulePaths
212
+ );
213
+ if (moduleName) {
214
+ // 这是模块 bundle 中的业务模块
215
+ // ⭐ 使用基于模块名的哈希算法,确保同一模块名总是得到相同的 segmentId
216
+ // 不依赖于构建顺序,确保 HTTP 模式和 Native 模式下的 segmentId 一致
217
+ const segmentId = generateSegmentIdForModule(moduleName);
218
+ return segmentId;
219
+ }
220
+ // 主 bundle 的模块使用 segmentId = 0
221
+ return 0;
222
+ }
223
+
224
+ function createStableModuleIdFactory() {
225
+ const fileToId = new Map();
226
+
227
+ // 存储已分配的入口 id,避免重复计算
228
+ const entryIdCache = new Map();
229
+
230
+ return modulePath => {
231
+ // 用相对路径作为 key,确保多入口一致
232
+ let relativePath = path.relative(projectRoot, modulePath);
233
+ relativePath = relativePath.replace(/\\/g, '/');
234
+ // ⚠️ 关键修复:使用全局替换,处理所有 node_modules 路径(包括嵌套的)
235
+ // 同时处理以 node_modules/ 开头的情况(没有前导 /)
236
+ relativePath = relativePath
237
+ .replace(/\/node_modules\//g, '/nm/')
238
+ .replace(/^node_modules\//, 'nm/');
239
+
240
+ // ⭐ 关键:共享依赖使用稳定的哈希算法生成模块 ID,确保跨 bundle 一致性
241
+ // 不再依赖全局 Map,因为每次构建都是新进程
242
+ // 使用基于路径的稳定哈希,确保同一路径总是得到相同的模块 ID
243
+ if (isSharedModuleForSegmentId(modulePath)) {
244
+ // 共享依赖总是使用 segmentId=0,localId 基于路径哈希
245
+ const segmentId = 0;
246
+ const localId = generateStableLocalId(relativePath);
247
+ // 使用 20 位左移,给 localId 留出 20 位空间 (100万+ 文件)
248
+ const stableId = (segmentId << 20) | localId;
249
+
250
+ // 检查当前构建中是否已经分配过(避免重复计算)
251
+ if (fileToId.has(relativePath)) {
252
+ return fileToId.get(relativePath);
253
+ }
254
+
255
+ fileToId.set(relativePath, stableId);
256
+ return stableId;
257
+ }
258
+
259
+ if (fileToId.has(relativePath)) {
260
+ return fileToId.get(relativePath);
261
+ }
262
+
263
+ // 检查是否是模块入口文件
264
+ const moduleName = extractModuleNameFromEntryHelper(
265
+ toProjectRelativePath(modulePath),
266
+ modulePaths
267
+ );
268
+ if (moduleName) {
269
+ // 为模块入口文件分配唯一的 id
270
+ if (!entryIdCache.has(moduleName)) {
271
+ const entryId = generateEntryIdForModule(moduleName);
272
+ entryIdCache.set(moduleName, entryId);
273
+ fileToId.set(relativePath, entryId);
274
+ // console.log(`[ModuleId] Entry ${relativePath} (${moduleName}) -> ${entryId}`);
275
+ return entryId;
276
+ } else {
277
+ // 如果同一个模块的入口文件再次出现(不应该发生),使用缓存的 id
278
+ const entryId = entryIdCache.get(moduleName);
279
+ fileToId.set(relativePath, entryId);
280
+ return entryId;
281
+ }
282
+ }
283
+
284
+ // ⭐ 关键:根据模块路径判断它属于哪个 bundle,分配对应的 segmentId
285
+ const segmentId = getSegmentIdForModule(modulePath);
286
+
287
+ // ⭐ 关键修复:使用基于路径的稳定 localId,而不是递增的 nextId
288
+ // 这确保 HTTP 模式和构建模式下,同一文件总是得到相同的模块 ID
289
+ const localId = generateStableLocalId(relativePath);
290
+
291
+ // 使用 Metro 的 packModuleId 格式:segmentId << 20 | localId
292
+ // 以前是 16 位,现在扩大到 20 位以减少碰撞
293
+ const finalModuleId = (segmentId << 20) | localId;
294
+
295
+ // ⚠️ 重要:存储 finalModuleId 而不是 localId,确保缓存正确
296
+ fileToId.set(relativePath, finalModuleId);
297
+
298
+ return finalModuleId;
299
+ };
300
+ }
301
+
302
+ /**
303
+ * 判断是否是模块 bundle(通过 entryPoint 路径)
304
+ * 使用配置化的模块路径规则
305
+ */
306
+ function isModuleBundleEntry(entryPoint) {
307
+ if (!entryPoint) return false;
308
+
309
+ return isModulePathHelper(toProjectRelativePath(entryPoint), modulePaths);
310
+ }
311
+
312
+ /**
313
+ * 自定义 serializer:对模块 bundle 移除 prelude 代码
314
+ *
315
+ * @param {string} entryPoint - 入口文件路径
316
+ * @param {Array} prepend - prelude 模块列表
317
+ * @param {Object} graph - 依赖图
318
+ * @param {Object} bundleOptions - bundle 选项
319
+ * @returns {Promise<string | {code: string, map?: string}>}
320
+ */
321
+ async function customSerializer(entryPoint, prepend, graph, bundleOptions) {
322
+ const isModuleBundle = isModuleBundleEntry(entryPoint) || isDevModuleRequest;
323
+
324
+ // 调用默认 serializer 生成 bundle
325
+ let bundle = baseJSBundle(entryPoint, prepend, graph, bundleOptions);
326
+ // Sentry 会在 options 上挂 sentryBundleCallback,用于注入 Debug ID
327
+ if (
328
+ bundleOptions &&
329
+ typeof bundleOptions.sentryBundleCallback === 'function'
330
+ ) {
331
+ bundle = bundleOptions.sentryBundleCallback(bundle);
332
+ }
333
+ const result = bundleToString(bundle);
334
+
335
+ // ⚠️ 重要:如果你使用自定义 serializer,Sentry 需要我们返回 map(JSON 字符串)
336
+ // 否则它会在内部 JSON.parse(undefined) 并报错。
337
+ // 这里复用 Metro 的 sourcemap 生成逻辑(与 Metro Server 中的默认行为一致)
338
+ let map = null;
339
+ try {
340
+ const sortedModules = [...graph.dependencies.values()].sort(
341
+ (a, b) =>
342
+ bundleOptions.createModuleId(a.path) -
343
+ bundleOptions.createModuleId(b.path)
344
+ );
345
+ map = await sourceMapStringNonBlocking([...prepend, ...sortedModules], {
346
+ excludeSource: false,
347
+ processModuleFilter: bundleOptions.processModuleFilter,
348
+ shouldAddToIgnoreList: bundleOptions.shouldAddToIgnoreList,
349
+ getSourceUrl: bundleOptions.getSourceUrl,
350
+ });
351
+ } catch (e) {
352
+ // 兜底:保证 map 永远是合法 JSON 字符串,避免 Sentry 解析失败
353
+ map = '{}';
354
+ }
355
+
356
+ // 如果是模块 bundle,移除 prelude 代码
357
+ if (isModuleBundle) {
358
+ // 找到第一个 __d( 调用的位置
359
+ // Prelude 代码从 bundle 开头到第一个 __d( 之前
360
+ const firstModuleDefIndex = result.code.indexOf('__d(');
361
+
362
+ if (firstModuleDefIndex > 0) {
363
+ const preludeCode = result.code.substring(0, firstModuleDefIndex);
364
+
365
+ // 移除 prelude:从开头到第一个 __d( 之前的所有内容
366
+ // 保留第一个 __d( 及其之后的所有内容
367
+ const codeWithoutPrelude = result.code.substring(firstModuleDefIndex);
368
+
369
+ return {
370
+ code: codeWithoutPrelude,
371
+ map, // 返回 sourcemap 字符串(Sentry 需要)
372
+ };
373
+ } else {
374
+ // 如果没有找到 __d(,说明可能不是标准的 bundle 格式,返回原样
375
+ return { ...result, map };
376
+ }
377
+ }
378
+
379
+ // 主 bundle:使用默认行为,保留完整的 prelude
380
+ return { ...result, map };
381
+ }
382
+
107
383
  const config = {
108
384
  projectRoot: path.resolve(__dirname),
109
385
 
@@ -186,12 +462,107 @@ const config = {
186
462
  };
187
463
  },
188
464
  },
465
+ // 你的自定义配置
466
+ transformer: {
467
+ babelTransformerPath: require.resolve('./lingui-svg-transformer.js'),
468
+ getTransformOptions: async () => ({
469
+ transform: {
470
+ experimentalImportSupport: true,
471
+ inlineRequires: true,
472
+ },
473
+ }),
474
+ },
475
+ resolver: {
476
+ assetExts: [...assetExts.filter(ext => ext !== 'svg'), 'pdf'],
477
+ sourceExts: [...sourceExts, 'svg', 'po'],
478
+ },
479
+ serializer: {
480
+ // 1️⃣ 先继承默认 serializer,后面在这个基础上做"定制"
481
+ ...defaultConfig.serializer,
482
+
483
+ // ⭐ 自定义 serializer:对模块 bundle 移除 prelude 代码
484
+ // 注意:Metro 的 serializer 配置支持 customSerializer 函数
485
+ // 如果 defaultConfig 已经有 customSerializer,需要包装它
486
+ customSerializer: defaultConfig.serializer?.customSerializer
487
+ ? async (entryPoint, prepend, graph, bundleOptions) => {
488
+ // 先调用默认的 serializer
489
+ await defaultConfig.serializer.customSerializer(
490
+ entryPoint,
491
+ prepend,
492
+ graph,
493
+ bundleOptions
494
+ );
495
+ // 然后调用我们的处理逻辑
496
+ return await customSerializer(
497
+ entryPoint,
498
+ prepend,
499
+ graph,
500
+ bundleOptions
501
+ );
502
+ }
503
+ : customSerializer,
504
+
505
+ // 2️⃣ Dev / Prod 统一的 moduleId 工厂(你原来的逻辑)
506
+ createModuleIdFactory: createStableModuleIdFactory,
507
+
508
+ // 3️⃣ 按业务 bundle 过滤模块:把 RN runtime 和公共依赖抽到 base.bundle 里
509
+ processModuleFilter: module => {
510
+ const modulePath = module.path;
511
+
512
+ // ⭐ 关键:只有在构建模块 bundle 时才过滤共享依赖
513
+ // 主 bundle 应该包含所有依赖,包括共享依赖
514
+ const isModuleBundleBuild = buildType === 'module' || isDevModuleRequest;
189
515
 
190
- // ... 其余配置代码(serializer、transformer 等)请参考完整实现
191
- // 完整的 Metro 配置包含:
192
- // - serializer: 自定义模块 ID 工厂、processModuleFilter、customSerializer
193
- // - transformer: getTransformOptions
194
- // 这些配置较为复杂,建议参考 @bm-fe/react-native-multi-bundle 的完整实现
516
+ if (!isModuleBundleBuild) {
517
+ // bundle:包含所有模块,不过滤共享依赖
518
+ return true;
519
+ }
520
+
521
+ // 模块 bundle:过滤共享依赖,让它们从主 bundle 中加载
522
+ // 过滤 RN runtime(shared runtime 装到 base.bundle 中)
523
+ if (isReactNativeRuntime(modulePath)) {
524
+ return false;
525
+ }
526
+
527
+ // 过滤公共依赖(React / RN / shared 代码)
528
+ // 这些模块会在主 bundle 中,模块 bundle 通过全局的 __r 访问它们
529
+ if (isSharedModuleForSegmentId(modulePath)) {
530
+ return false;
531
+ }
532
+
533
+ // 其他模块保留在业务 bundle 中
534
+ return true;
535
+ },
536
+
537
+ // 4️⃣ ⭐ 关键点:对"业务 bundle"不再注入 prelude,避免重复定义 global.__r 等
538
+ getModulesRunBeforeMainModule: entryFilePath => {
539
+ // 判断是否是模块 bundle:
540
+ // 1. 通过 entryFilePath 路径(使用配置化的模块路径规则)- 最可靠的方式
541
+ // 2. 通过开发服务器请求标记(仅开发模式)
542
+ // 注意:不能仅依赖 isMultiBundleBuild,因为主 bundle 和模块 bundle 都会设置这个环境变量
543
+
544
+ const isModuleBundle =
545
+ isDevModuleRequest || isModuleBundleEntry(entryFilePath);
546
+
547
+ if (isModuleBundle) {
548
+ return [];
549
+ }
550
+
551
+ // 对主 bundle:使用默认行为,保留唯一一份 runtime 初始化
552
+ if (
553
+ defaultConfig.serializer &&
554
+ typeof defaultConfig.serializer.getModulesRunBeforeMainModule ===
555
+ 'function'
556
+ ) {
557
+ return defaultConfig.serializer.getModulesRunBeforeMainModule(
558
+ entryFilePath
559
+ );
560
+ }
561
+
562
+ // 兜底:默认返回空数组(大部分版本其实不会走到这里)
563
+ return [];
564
+ },
565
+ },
195
566
  };
196
567
 
197
568
  module.exports = mergeConfig(defaultConfig, config);