'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 os = require('os'); const VIRTUAL_MODULE_ID = '@xuqm/autoinit-config'; 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 文件作为虚拟模块 const tmpDir = path.join(os.tmpdir(), 'xuqm-sdk-config'); fs.mkdirSync(tmpDir, { recursive: true }); const virtualFile = path.join(tmpDir, '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; 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); }, }, }); } module.exports = { withXuqmConfig };