diff --git a/packages/common/metro/index.js b/packages/common/metro/index.js new file mode 100644 index 0000000..58e838a --- /dev/null +++ b/packages/common/metro/index.js @@ -0,0 +1,96 @@ +'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 withXuqmConfig(metroConfig) { + const projectRoot = metroConfig.projectRoot ?? process.cwd(); + const configFile = findConfigFile(projectRoot); + + if (!configFile) { + // 未找到配置文件,返回原配置(autoInit 会静默跳过) + return metroConfig; + } + + // 读取加密配置内容 + const encryptedContent = fs.readFileSync(configFile, 'utf-8').trim(); + + // 生成临时 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 { + ...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 }; diff --git a/packages/common/package.json b/packages/common/package.json index 26cad39..6992b76 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -6,8 +6,10 @@ "main": "src/index.ts", "react-native": "src/index.ts", "types": "src/index.ts", + "metro": "metro/index.js", "files": [ - "src" + "src", + "metro" ], "private": false, "publishConfig": { diff --git a/packages/common/src/autoInit.ts b/packages/common/src/autoInit.ts index dc4dda7..872e987 100644 --- a/packages/common/src/autoInit.ts +++ b/packages/common/src/autoInit.ts @@ -3,13 +3,16 @@ * * 对齐 Android SDK 的 ContentProvider 模式。 * - * React Native 没有 ContentProvider,所以用 Metro moduleNameMapper 桥接: - * 1. 宿主把加密配置放到 src/assets/xuqm/config.xuqmconfig.ts - * 2. 宿主 babel.config.js 添加 alias: '@xuqm/autoinit-config' → 该文件 + * 推荐方式(withXuqmConfig Metro 插件): + * 1. 宿主将 .xuqmconfig 文件放入 src/assets/config/ + * 2. metro.config.js 使用 withXuqmConfig(metroConfig) 包装 * 3. 本模块 tryRequire('@xuqm/autoinit-config') 自动读取并初始化 - * 4. 宿主代码零初始化调用 * - * 如果 alias 未配置(require 失败),静默跳过,不崩溃。 + * 兼容方式(手动 alias): + * 1. 宿主把加密配置放到 src/assets/xuqm/config.xuqmconfig.ts + * 2. 宿主 babel/metro config 添加 alias: '@xuqm/autoinit-config' → 该文件 + * + * 如果配置未找到(require 失败),静默跳过,不崩溃。 */ import {isInitialized} from './config'