@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.
package/android/moduleloader/src/main/java/com/bitmart/exchange/module/loader/ModuleLoaderModule.kt
CHANGED
|
@@ -23,8 +23,7 @@ import java.io.InputStreamReader
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* 简单的 Bundle 加载模块
|
|
26
|
-
*
|
|
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
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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
|
-
//
|
|
72
|
-
if (
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
101
|
+
promise.resolve(Arguments.createMap().apply {
|
|
122
102
|
putBoolean("success", false)
|
|
123
103
|
putString("errorMessage", e.message ?: "Unknown error")
|
|
124
|
-
}
|
|
125
|
-
|
|
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
|
|
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
|
@@ -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);
|