@bm-fe/react-native-multi-bundle 1.0.0-beta.1 → 1.0.0-beta.10

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.
@@ -200,6 +200,7 @@ function removePreludeFromBundle(bundlePath) {
200
200
  function ensureOutputDir(platform) {
201
201
  const platformDir = path.join(OUTPUT_DIR, platform);
202
202
  const modulesDir = path.join(platformDir, 'modules');
203
+ const assetsDir = path.join(platformDir, 'assets');
203
204
 
204
205
  if (!fs.existsSync(platformDir)) {
205
206
  fs.mkdirSync(platformDir, { recursive: true });
@@ -207,6 +208,9 @@ function ensureOutputDir(platform) {
207
208
  if (!fs.existsSync(modulesDir)) {
208
209
  fs.mkdirSync(modulesDir, { recursive: true });
209
210
  }
211
+ if (!fs.existsSync(assetsDir)) {
212
+ fs.mkdirSync(assetsDir, { recursive: true });
213
+ }
210
214
 
211
215
  return platformDir;
212
216
  }
@@ -233,6 +237,16 @@ function buildBundle(entryFile, outputFile, platform, env = 'development', progr
233
237
  // ⭐ 关键:判断是否是模块 bundle(通过检查 entry 路径)
234
238
  const isModuleBundle = entry.includes('src/modules/');
235
239
 
240
+ // 计算 assets 输出目录(统一放在平台目录下的 assets 文件夹)
241
+ const platformDir = path.dirname(outputFile);
242
+ const assetsDest = path.join(platformDir, 'assets');
243
+ const assetsDestRelative = path.relative(PROJECT_ROOT, assetsDest);
244
+
245
+ // 确保 assets 目录存在
246
+ if (!fs.existsSync(assetsDest)) {
247
+ fs.mkdirSync(assetsDest, { recursive: true });
248
+ }
249
+
236
250
  const commandParts = [
237
251
  // 主 bundle 和模块 bundle 都设置 RN_MULTI_BUNDLE_BUILD,但使用不同的 RN_BUILD_TYPE
238
252
  'RN_MULTI_BUNDLE_BUILD=true',
@@ -242,6 +256,7 @@ function buildBundle(entryFile, outputFile, platform, env = 'development', progr
242
256
  `--bundle-output ${out}`,
243
257
  `--platform ${platform}`,
244
258
  `--dev ${dev}`,
259
+ `--assets-dest ${assetsDestRelative}`,
245
260
  '--reset-cache',
246
261
  ];
247
262
 
@@ -4,8 +4,8 @@
4
4
  * 同步 Bundle 到 Assets 目录脚本
5
5
  * 将构建好的 bundle 文件同步到 Android/iOS 的 assets 目录
6
6
  *
7
- * Android: android/app/src/main/assets/bundles/
8
- * iOS: ios/DemoProject/RNModules/Bundles/
7
+ * Android: android/app/src/main/assets/
8
+ * iOS: ios/DemoProject/Bundles/
9
9
  */
10
10
 
11
11
  const fs = require('fs');
@@ -14,7 +14,7 @@ const path = require('path');
14
14
  const PROJECT_ROOT = path.join(__dirname, '..');
15
15
  const BUILD_DIR = path.join(PROJECT_ROOT, 'build/bundles');
16
16
  const ANDROID_ASSETS_DIR = path.join(PROJECT_ROOT, 'android/app/src/main/assets');
17
- const IOS_ASSETS_DIR = path.join(PROJECT_ROOT, 'ios/DemoProject/RNModules/Bundles');
17
+ const IOS_ASSETS_DIR = path.join(PROJECT_ROOT, 'ios/DemoProject/Bundles');
18
18
 
19
19
  /**
20
20
  * ANSI 颜色工具函数
@@ -56,14 +56,37 @@ function convertNativeManifestToBundleManifest(nativeManifest: any): BundleManif
56
56
 
57
57
  /**
58
58
  * 获取当前激活包的 manifest
59
- * - 开发环境:从开发服务器获取 manifest(HTTP 模式)
60
- * - 生产环境:从 Native CodePush 模块获取 manifest(直接返回 JSON 对象)
59
+ * - 优先从 Native 模块获取(支持 CodePush 等热更新场景)
60
+ * - 如果 Native 返回 null,在开发环境下降级到开发服务器获取
61
+ * - 生产环境下如果 Native 返回 null,抛出异常
61
62
  */
62
63
  async function getCurrentBundleManifest(): Promise<BundleManifest> {
63
64
  // 生产环境:从 Native 模块获取 manifest
64
- if (!__DEV__) {
65
- try {
66
- const nativeManifest = await NativeModules.CodePush.getCurrentBundleManifestContent();
65
+ // if (!__DEV__) {
66
+ // try {
67
+ // const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
68
+ // if (nativeManifest) {
69
+ // return convertNativeManifestToBundleManifest(nativeManifest);
70
+ // } else {
71
+ // // Native 模块返回 null 或 undefined
72
+ // throw new Error(
73
+ // '[LocalBundleManager] Native module returned null manifest. ' +
74
+ // 'Please ensure bundle-manifest.json exists in the app bundle.'
75
+ // );
76
+ // }
77
+ // } catch (error) {
78
+ // console.error(
79
+ // `[LocalBundleManager] Failed to get manifest from Native: ${error}`
80
+ // );
81
+ // // 生产环境无法获取 manifest 时抛出异常
82
+ // throw new Error(
83
+ // `[LocalBundleManager] Failed to get manifest from Native module: ${error}`
84
+ // );
85
+ // }
86
+ // }
87
+ console.log("ReactNativeJS","nativeManifest getCurrentBundleManifestContent")
88
+ const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
89
+ console.log("ReactNativeJS","nativeManifest "+nativeManifest)
67
90
  if (nativeManifest) {
68
91
  return convertNativeManifestToBundleManifest(nativeManifest);
69
92
  } else {
@@ -73,16 +96,6 @@ async function getCurrentBundleManifest(): Promise<BundleManifest> {
73
96
  'Please ensure bundle-manifest.json exists in the app bundle.'
74
97
  );
75
98
  }
76
- } catch (error) {
77
- console.error(
78
- `[LocalBundleManager] Failed to get manifest from Native: ${error}`
79
- );
80
- // 生产环境无法获取 manifest 时抛出异常
81
- throw new Error(
82
- `[LocalBundleManager] Failed to get manifest from Native module: ${error}`
83
- );
84
- }
85
- }
86
99
 
87
100
  // 开发环境:从开发服务器获取 manifest
88
101
  const config = getGlobalConfig();
@@ -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
  }
@@ -341,3 +341,5 @@ function preloadModule(moduleId: string): Promise<void>
341
341
  - [使用指南](../docs/使用指南.md)
342
342
  - [多 Bundle 架构设计](../docs/React%20Native%20多%20bundle%20技术方案.md)
343
343
 
344
+
345
+
@@ -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);