@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.
- package/INTEGRATION.md +104 -62
- package/android/moduleloader/build.gradle +93 -0
- package/android/moduleloader/src/main/AndroidManifest.xml +4 -0
- package/android/moduleloader/src/main/AndroidManifestNew.xml +3 -0
- package/android/moduleloader/src/main/java/com/bitmart/exchange/module/loader/ModuleLoaderModule.kt +555 -0
- package/android/moduleloader/src/main/java/com/bitmart/exchange/module/loader/ModuleLoaderPackage.kt +25 -0
- package/ios/ModuleLoader/ModuleLoaderModule.h +12 -0
- package/ios/ModuleLoader/ModuleLoaderModule.mm +488 -0
- package/package.json +58 -32
- package/react-native-multi-bundle.podspec +29 -0
- package/react-native.config.js +26 -0
- package/scripts/build-multi-bundle.js +36 -6
- package/scripts/sync-bundles-to-assets.js +3 -3
- package/src/multi-bundle/LocalBundleManager.ts +28 -15
- package/src/multi-bundle/ModuleRegistry.ts +1 -1
- package/src/multi-bundle/README.md +2 -0
- package/templates/metro.config.js.template +377 -6
|
@@ -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
|
}
|
|
@@ -214,7 +218,7 @@ function ensureOutputDir(platform) {
|
|
|
214
218
|
/**
|
|
215
219
|
* 构建单个 bundle
|
|
216
220
|
*/
|
|
217
|
-
function buildBundle(entryFile, outputFile, platform, env = 'development', progressInfo = null) {
|
|
221
|
+
function buildBundle(entryFile, outputFile, platform, env = 'development', assetsDestDir = null, enableSourcemap = false, progressInfo = null) {
|
|
218
222
|
const isProduction = env === 'production';
|
|
219
223
|
const dev = !isProduction;
|
|
220
224
|
const startTime = Date.now();
|
|
@@ -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 目录(所有 bundle 共享同一个 assets 目录)
|
|
241
|
+
// 如果未指定,则使用平台目录下的 assets 文件夹
|
|
242
|
+
const assetsDest = assetsDestDir || path.join(path.dirname(outputFile), '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,11 +256,12 @@ 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
|
|
|
248
|
-
//
|
|
249
|
-
if (
|
|
263
|
+
// 如果启用了 sourcemap,添加 sourcemap 输出
|
|
264
|
+
if (enableSourcemap) {
|
|
250
265
|
const sourcemapOutput = `${out}.map`;
|
|
251
266
|
commandParts.push(`--sourcemap-output ${sourcemapOutput}`);
|
|
252
267
|
}
|
|
@@ -400,16 +415,19 @@ function printBuildSummary(results, platform, env, totalDuration) {
|
|
|
400
415
|
/**
|
|
401
416
|
* 主构建函数
|
|
402
417
|
*/
|
|
403
|
-
function buildMultiBundle(platform, env = 'development') {
|
|
418
|
+
function buildMultiBundle(platform, env = 'development', enableSourcemap = false) {
|
|
404
419
|
const startTime = Date.now();
|
|
405
420
|
|
|
406
421
|
// 构建开始日志
|
|
407
422
|
console.log('\n' + bold('🚀 Building Multi-Bundle'));
|
|
408
|
-
console.log(`${bold('Platform:')} ${blue(platform)} | ${bold('Environment:')} ${yellow(env)}\n`);
|
|
423
|
+
console.log(`${bold('Platform:')} ${blue(platform)} | ${bold('Environment:')} ${yellow(env)} | ${bold('Sourcemap:')} ${enableSourcemap ? green('enabled') : gray('disabled')}\n`);
|
|
409
424
|
|
|
410
425
|
const config = loadMultiBundleConfig();
|
|
411
426
|
const outputDir = ensureOutputDir(platform);
|
|
412
427
|
|
|
428
|
+
// 计算统一的 assets 目录(所有 bundle 共享)
|
|
429
|
+
const sharedAssetsDir = path.join(outputDir, 'assets');
|
|
430
|
+
|
|
413
431
|
const results = [];
|
|
414
432
|
const totalBundles = 1 + config.modules.length; // main + modules
|
|
415
433
|
let currentIndex = 0;
|
|
@@ -424,6 +442,8 @@ function buildMultiBundle(platform, env = 'development') {
|
|
|
424
442
|
mainOutput,
|
|
425
443
|
platform,
|
|
426
444
|
env,
|
|
445
|
+
sharedAssetsDir, // 传递统一的 assets 目录
|
|
446
|
+
enableSourcemap, // 传递 sourcemap 配置
|
|
427
447
|
{ current: currentIndex, total: totalBundles, name: mainFileName }
|
|
428
448
|
);
|
|
429
449
|
results.push(mainResult);
|
|
@@ -439,6 +459,8 @@ function buildMultiBundle(platform, env = 'development') {
|
|
|
439
459
|
moduleOutput,
|
|
440
460
|
platform,
|
|
441
461
|
env,
|
|
462
|
+
sharedAssetsDir, // 所有模块 bundle 也使用同一个 assets 目录
|
|
463
|
+
enableSourcemap, // 所有模块 bundle 也使用相同的 sourcemap 配置
|
|
442
464
|
{ current: currentIndex, total: totalBundles, name: moduleFileName }
|
|
443
465
|
);
|
|
444
466
|
results.push(moduleResult);
|
|
@@ -470,6 +492,14 @@ function main() {
|
|
|
470
492
|
env = args[envIndex + 1];
|
|
471
493
|
}
|
|
472
494
|
|
|
495
|
+
// 解析 sourcemap 参数(默认关闭)
|
|
496
|
+
let enableSourcemap = false;
|
|
497
|
+
if (args.includes('--sourcemap')) {
|
|
498
|
+
enableSourcemap = true;
|
|
499
|
+
} else if (args.includes('--no-sourcemap')) {
|
|
500
|
+
enableSourcemap = false;
|
|
501
|
+
}
|
|
502
|
+
|
|
473
503
|
// 验证平台参数
|
|
474
504
|
if (!['ios', 'android'].includes(platform)) {
|
|
475
505
|
console.error(`\n${red('✗')} ${red(bold('Invalid platform'))}`);
|
|
@@ -486,7 +516,7 @@ function main() {
|
|
|
486
516
|
}
|
|
487
517
|
|
|
488
518
|
try {
|
|
489
|
-
buildMultiBundle(platform, env);
|
|
519
|
+
buildMultiBundle(platform, env, enableSourcemap);
|
|
490
520
|
} catch (error) {
|
|
491
521
|
console.error(`\n${red('✗')} ${red(bold('Build failed'))}`);
|
|
492
522
|
if (error.message) {
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* 同步 Bundle 到 Assets 目录脚本
|
|
5
5
|
* 将构建好的 bundle 文件同步到 Android/iOS 的 assets 目录
|
|
6
6
|
*
|
|
7
|
-
* Android: android/app/src/main/assets/
|
|
8
|
-
* iOS: ios/DemoProject/
|
|
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/
|
|
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
|
-
* -
|
|
60
|
-
* -
|
|
59
|
+
* - 优先从 Native 模块获取(支持 CodePush 等热更新场景)
|
|
60
|
+
* - 如果 Native 返回 null,在开发环境下降级到开发服务器获取
|
|
61
|
+
* - 生产环境下如果 Native 返回 null,抛出异常
|
|
61
62
|
*/
|
|
62
63
|
async function getCurrentBundleManifest(): Promise<BundleManifest> {
|
|
63
64
|
// 生产环境:从 Native 模块获取 manifest
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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);
|