XuqmGroup-RNSDK/packages/common/metro/index.js

169 行
5.8 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

'use strict'
/**
* Metro 插件:自动发现 .xuqmconfig 配置文件。
*
* 宿主只需将平台下载的 .xuqmconfig 文件放入 src/assets/config/,
* SDK 自动读取并完成初始化,无需重命名或创建 .ts 包装文件。
*
* 对齐 Android SDK 的 ConfigFileReader 逻辑:
* 优先 config.xuqmconfig,否则取第一个 *.xuqmconfig。
*
* @param {import('@react-native/metro-config').MetroConfig} metroConfig
* @returns {import('@react-native/metro-config').MetroConfig}
*/
const fs = require('fs')
const path = require('path')
const VIRTUAL_MODULE_ID = '@xuqm/autoinit-config'
const AUTO_INIT_MODULE = path.resolve(__dirname, '../src/internal.ts')
const CONFIG_DIR_CANDIDATES = [
'src/assets/config',
'src/assets/xuqm',
'assets/config',
'assets/xuqm',
]
function findConfigFile(projectRoot) {
for (const dir of CONFIG_DIR_CANDIDATES) {
const absDir = path.resolve(projectRoot, dir)
if (!fs.existsSync(absDir)) continue
const files = fs.readdirSync(absDir)
// 优先 config.xuqmconfig 或 config.xuqm
const preferred = files.find(f => f === 'config.xuqmconfig' || f === 'config.xuqm')
if (preferred) {
return path.join(absDir, preferred)
}
// 否则取第一个 .xuqmconfig
const fallback = files.find(f => f.endsWith('.xuqmconfig'))
if (fallback) {
return path.join(absDir, fallback)
}
}
return null
}
function applyBugCollect(metroConfig) {
// 不再自动链式 withBugCollect基于 customSerializer 的 sourcemap 上传在 Metro 0.84
// 下不可靠exports 限制无法获取默认 serializer,会破坏 dev 打包)。
// sourcemap 上传改由独立后处理脚本react-native bundle 后单独上传)实现。
return metroConfig
}
/** 写入目标文件(仅在缺失或内容不一致时),返回是否发生写入。 */
function syncFile(content, destPath, label) {
try {
if (fs.existsSync(destPath) && fs.readFileSync(destPath, 'utf-8').trim() === content) {
return false
}
fs.mkdirSync(path.dirname(destPath), { recursive: true })
fs.writeFileSync(destPath, content, 'utf-8')
console.log(`[XuqmConfig] 已同步 .xuqmconfig → ${label}`)
return true
} catch (e) {
console.warn(`[XuqmConfig] 同步 .xuqmconfig 到 ${label} 失败:${e.message}`)
return false
}
}
/**
* 单点放置:宿主只在 RN 侧放置一份 .xuqmconfig,构建/运行时自动同步到
* 原生位置Android assets/config,供 XuqmMergedProvider 自动初始化;iOS 资源目录)。
* 缺失或内容不一致才复制,幂等。
*/
function syncNativeConfig(projectRoot, content) {
const fileName = 'config.xuqmconfig'
// Android原生 ContentProvider 扫描 assets/config 下的 .xuqmconfig
if (fs.existsSync(path.resolve(projectRoot, 'android'))) {
syncFile(
content,
path.resolve(projectRoot, 'android/app/src/main/assets/config', fileName),
'android assets/config',
)
}
// iOS复制到 app 资源目录(需在 Xcode「Copy Bundle Resources」中引用一次
const iosDir = path.resolve(projectRoot, 'ios')
if (fs.existsSync(iosDir)) {
try {
const appDir = fs
.readdirSync(iosDir, { withFileTypes: true })
.filter(d => d.isDirectory() && fs.existsSync(path.join(iosDir, d.name, 'Info.plist')))
.map(d => d.name)[0]
if (appDir) {
syncFile(content, path.join(iosDir, appDir, 'config', fileName), `ios ${appDir}/config`)
}
} catch {
// 忽略 iOS 同步异常,不阻断打包
}
}
}
function withXuqmConfig(metroConfig) {
const projectRoot = metroConfig.projectRoot ?? process.cwd()
const configFile = findConfigFile(projectRoot)
if (!configFile) {
// 未找到配置文件,返回原配置autoInit 会静默跳过)
return applyBugCollect(metroConfig)
}
// 读取加密配置内容
const encryptedContent = fs.readFileSync(configFile, 'utf-8').trim()
// 单点放置:自动同步到原生 Android/iOS缺失或不一致才复制
syncNativeConfig(projectRoot, encryptedContent)
// 生成临时 TS 文件作为虚拟模块
// Metro 0.84+ only hashes files under projectRoot/watchFolders. Keep the
// generated transport module in the ignored package-manager cache instead
// of the system temp directory; the .xuqmconfig file remains the sole source.
const generatedDir = path.join(projectRoot, 'node_modules', '.cache', 'xuqm-sdk')
fs.mkdirSync(generatedDir, { recursive: true })
const virtualFile = path.join(generatedDir, 'autoinit-config.ts')
fs.writeFileSync(
virtualFile,
`// Auto-generated by @xuqm/rn-common/metro — do not edit\nexport const ENCRYPTED_CONFIG = ${JSON.stringify(encryptedContent)};\n`,
'utf-8',
)
// 拦截模块解析,将 @xuqm/autoinit-config 指向虚拟文件
const existingResolveRequest = metroConfig.resolver?.resolveRequest
const existingGetModulesRunBeforeMainModule =
metroConfig.serializer?.getModulesRunBeforeMainModule
return applyBugCollect({
...metroConfig,
resolver: {
...metroConfig.resolver,
resolveRequest: (context, moduleName, platform) => {
if (moduleName === VIRTUAL_MODULE_ID) {
return {
type: 'sourceFile',
filePath: virtualFile,
}
}
if (existingResolveRequest) {
return existingResolveRequest(context, moduleName, platform)
}
return context.resolveRequest(context, moduleName, platform)
},
},
serializer: {
...metroConfig.serializer,
getModulesRunBeforeMainModule: entryFilePath => {
const existingModules = existingGetModulesRunBeforeMainModule
? existingGetModulesRunBeforeMainModule(entryFilePath)
: []
return [...new Set([...existingModules, AUTO_INIT_MODULE])]
},
},
})
}
module.exports = { withXuqmConfig }