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

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.
@@ -56,47 +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
- // 生产环境:从 Native 模块获取 manifest
64
- // if (!__DEV__) {
65
- // try {
66
- // const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
67
- // if (nativeManifest) {
68
- // return convertNativeManifestToBundleManifest(nativeManifest);
69
- // } else {
70
- // // Native 模块返回 null 或 undefined
71
- // throw new Error(
72
- // '[LocalBundleManager] Native module returned null manifest. ' +
73
- // 'Please ensure bundle-manifest.json exists in the app bundle.'
74
- // );
75
- // }
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
- console.log("ReactNativeJS","nativeManifest getCurrentBundleManifestContent")
87
- const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
88
- console.log("ReactNativeJS","nativeManifest "+nativeManifest)
89
- if (nativeManifest) {
90
- return convertNativeManifestToBundleManifest(nativeManifest);
91
- } else {
92
- // Native 模块返回 null 或 undefined
93
- throw new Error(
94
- '[LocalBundleManager] Native module returned null manifest. ' +
95
- 'Please ensure bundle-manifest.json exists in the app bundle.'
96
- );
97
- }
64
+ // 先尝试从 Native 模块获取 manifest(支持 CodePush 等热更新场景)
65
+ try {
66
+ console.log('[LocalBundleManager] Trying to get manifest from Native module...');
67
+ const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
68
+
69
+ if (nativeManifest) {
70
+ console.log('[LocalBundleManager] Got manifest from Native module');
71
+ return convertNativeManifestToBundleManifest(nativeManifest);
72
+ }
73
+
74
+ console.log('[LocalBundleManager] Native module returned null manifest');
75
+ } catch (error) {
76
+ console.warn(`[LocalBundleManager] Failed to get manifest from Native: ${error}`);
77
+ }
98
78
 
99
- // 开发环境:从开发服务器获取 manifest
79
+ // 生产环境下,如果 Native 返回 null,抛出异常
80
+ if (!__DEV__) {
81
+ throw new Error(
82
+ '[LocalBundleManager] Native module returned null manifest. ' +
83
+ 'Please ensure bundle-manifest.json exists in the app bundle.'
84
+ );
85
+ }
86
+
87
+ // 开发环境:降级到从开发服务器获取 manifest
88
+ console.log('[LocalBundleManager] Dev mode: trying to fetch manifest from dev server...');
89
+
100
90
  const config = getGlobalConfig();
101
91
  const devServer = config?.devServer;
102
92
 
@@ -105,9 +95,11 @@ async function getCurrentBundleManifest(): Promise<BundleManifest> {
105
95
  const protocol = devServer.protocol || 'http';
106
96
  const platform = Platform.OS;
107
97
  const manifestUrl = `${protocol}://${devServer.host}:${devServer.port}/bundle-manifest.json?platform=${platform}`;
98
+ console.log(`[LocalBundleManager] Fetching manifest from: ${manifestUrl}`);
108
99
  const response = await fetch(manifestUrl);
109
100
  if (response.ok) {
110
101
  const manifest: BundleManifest = await response.json();
102
+ console.log('[LocalBundleManager] Got manifest from dev server');
111
103
  return manifest;
112
104
  } else {
113
105
  console.warn(
@@ -125,9 +117,11 @@ async function getCurrentBundleManifest(): Promise<BundleManifest> {
125
117
  const host = Platform.OS === 'android' ? '10.0.2.2' : 'localhost';
126
118
  const platform = Platform.OS;
127
119
  const manifestUrl = `http://${host}:8081/bundle-manifest.json?platform=${platform}`;
120
+ console.log(`[LocalBundleManager] Fetching manifest from default: ${manifestUrl}`);
128
121
  const response = await fetch(manifestUrl);
129
122
  if (response.ok) {
130
123
  const manifest: BundleManifest = await response.json();
124
+ console.log('[LocalBundleManager] Got manifest from default dev server');
131
125
  return manifest;
132
126
  } else {
133
127
  console.warn(
@@ -208,23 +208,23 @@ async function loadModule(moduleId: string): Promise<void> {
208
208
  moduleState[moduleId] = 'loading';
209
209
 
210
210
  // Step 3:选择 ModuleLoader
211
- let loader: any = injectedModuleLoader;
211
+ let loader = NativeModules.ModuleLoader;;
212
212
 
213
- if (!loader) {
214
- // 开发环境:优先使用 ModuleLoaderMock(HTTP 模式)
215
- if (__DEV__) {
216
- loader = ModuleLoaderMock;
217
- } else {
218
- // 生产环境:使用 Native CodePush 模块
219
- if (NativeModules?.ModuleLoader.loadBusinessBundle) {
220
- loader = NativeModules.ModuleLoader;
221
- } else {
222
- throw new Error(
223
- 'ModuleLoader not available: Native CodePush module not found in production'
224
- );
225
- }
226
- }
227
- }
213
+ // if (!loader) {
214
+ // // 开发环境:优先使用 ModuleLoaderMock(HTTP 模式)
215
+ // if (__DEV__) {
216
+ // loader = ModuleLoaderMock;
217
+ // } else {
218
+ // // 生产环境:使用 Native CodePush 模块
219
+ // if (NativeModules?.ModuleLoader.loadBusinessBundle) {
220
+ // loader = NativeModules.ModuleLoader;
221
+ // } else {
222
+ // throw new Error(
223
+ // 'ModuleLoader not available: Native ModuleLoader module not found in production'
224
+ // );
225
+ // }
226
+ // }
227
+ // }
228
228
 
229
229
  // 传入 bundleId(即 moduleId)和 bundlePath(从 manifest 中获取)
230
230
  const bundlePath = moduleMeta[moduleId].file;
@@ -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);