'use strict' const fs = require('fs') const path = require('path') const https = require('https') const http = require('http') /** * 读取配置文件获取 bugCollectApiUrl */ function readConfig(projectRoot) { // 1. 尝试读取 xuqm.config.js const configJsPath = path.join(projectRoot, 'xuqm.config.js') if (fs.existsSync(configJsPath)) { try { const config = require(configJsPath) if (config.bugCollectApiUrl) return config.bugCollectApiUrl if (config.platformUrl) return config.platformUrl.replace(/\/$/, '') + '/api' } catch (e) { console.warn('[BugCollect] Failed to read xuqm.config.js:', e.message) } } // 2. 尝试读取 .xuqmconfig (加密配置文件) const configDirs = ['config', 'xuqm'] for (const dir of configDirs) { const configDir = path.join(projectRoot, 'src', 'assets', dir) if (!fs.existsSync(configDir)) continue const files = fs.readdirSync(configDir) const configFile = files.find(f => f.endsWith('.xuqmconfig') || f.endsWith('.xuqm')) if (configFile) { // 加密配置文件需要解密,这里先跳过 // 实际使用时应该通过 SDK 解密 console.warn('[BugCollect] Encrypted config found but cannot be read directly') } } // 3. 从环境变量读取 if (process.env.BUG_COLLECT_API_URL) return process.env.BUG_COLLECT_API_URL return null } /** * 上传 SourceMap 文件 */ function uploadSourceMap(apiUrl, appKey, platform, appVersion, sourceMapPath) { return new Promise((resolve, reject) => { const url = new URL(`${apiUrl}/bugcollect/v1/sourcemaps/upload`) const isHttps = url.protocol === 'https:' const client = isHttps ? https : http // 读取 sourcemap 文件 const fileContent = fs.readFileSync(sourceMapPath) const fileName = path.basename(sourceMapPath) // 构建 multipart/form-data const boundary = '----BugCollectBoundary' + Date.now() const parts = [] // appKey parts.push(`--${boundary}\r\nContent-Disposition: form-data; name="appKey"\r\n\r\n${appKey}`) // platform parts.push(`--${boundary}\r\nContent-Disposition: form-data; name="platform"\r\n\r\n${platform}`) // appVersion parts.push(`--${boundary}\r\nContent-Disposition: form-data; name="appVersion"\r\n\r\n${appVersion}`) // bundleName parts.push(`--${boundary}\r\nContent-Disposition: form-data; name="bundleName"\r\n\r\nindex`) // file parts.push(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: application/json\r\n\r\n`) const header = Buffer.from(parts.join('\r\n') + '\r\n') const footer = Buffer.from(`\r\n--${boundary}--\r\n`) const body = Buffer.concat([header, fileContent, footer]) const options = { hostname: url.hostname, port: url.port || (isHttps ? 443 : 80), path: url.pathname, method: 'POST', headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}`, 'Content-Length': body.length, }, } const req = client.request(options, (res) => { let data = '' res.on('data', chunk => data += chunk) res.on('end', () => { if (res.statusCode >= 200 && res.statusCode < 300) { console.log(`[BugCollect] SourceMap uploaded successfully: ${fileName}`) resolve(data) } else { console.warn(`[BugCollect] SourceMap upload failed: ${res.statusCode} ${data}`) reject(new Error(`Upload failed: ${res.statusCode}`)) } }) }) req.on('error', (err) => { console.warn('[BugCollect] SourceMap upload error:', err.message) reject(err) }) req.write(body) req.end() }) } /** * 获取应用版本号 */ function getAppVersion(projectRoot) { try { const packageJson = require(path.join(projectRoot, 'package.json')) return packageJson.version || '1.0.0' } catch { return '1.0.0' } } /** * withBugCollect(metroConfig) * 包裹 Metro 配置,打 Release 包时自动上传 SourceMap。 */ function withBugCollect(metroConfig) { const projectRoot = metroConfig.projectRoot || process.cwd() return { ...metroConfig, serializer: { ...metroConfig.serializer, customSerializer: async (entryPoint, preModules, graph, options) => { // 仅在宿主已有 customSerializer 时委托;否则保持 result 未定义并交回 Metro。 // 注意:本插件应通过显式包裹在已有 serializer 之上使用(见 README), // 不建议自动链式启用(Metro 默认 serializer 不可经 exports 获取)。 const baseSerializer = metroConfig.serializer?.customSerializer const result = baseSerializer ? await baseSerializer(entryPoint, preModules, graph, options) : undefined // 仅 Release 包上传 SourceMap(dev 模式跳过) if (!options.dev) { try { // 获取配置 const apiUrl = readConfig(projectRoot) if (!apiUrl) { console.warn('[BugCollect] No API URL configured, skipping SourceMap upload') return result } // 从 graph 中获取 appKey const appKey = graph.transformer?.config?.xuqmAppKey || process.env.XUQM_APP_KEY || '' if (!appKey) { console.warn('[BugCollect] No appKey configured, skipping SourceMap upload') return result } // 获取平台和版本 const platform = options.platform || 'android' const appVersion = getAppVersion(projectRoot) // 查找 sourcemap 文件路径 // Metro 会在 options.sourceMapUrl 中指定 sourcemap 路径 const sourceMapUrl = options.sourceMapUrl let sourceMapPath = null if (sourceMapUrl) { // 从 URL 中提取本地路径 if (sourceMapUrl.startsWith('file://')) { sourceMapPath = sourceMapUrl.replace('file://', '') } else if (sourceMapUrl.startsWith('/')) { sourceMapPath = sourceMapUrl } } // 如果没有从 URL 获取,尝试默认路径 if (!sourceMapPath || !fs.existsSync(sourceMapPath)) { const defaultPaths = [ path.join(projectRoot, `${platform}.bundle.map`), path.join(projectRoot, 'index.bundle.map'), path.join(projectRoot, 'bundle.map'), ] sourceMapPath = defaultPaths.find(p => fs.existsSync(p)) } if (!sourceMapPath || !fs.existsSync(sourceMapPath)) { console.warn('[BugCollect] SourceMap file not found, skipping upload') return result } // 异步上传(不阻塞构建) uploadSourceMap(apiUrl, appKey, platform, appVersion, sourceMapPath) .catch(err => { console.warn('[BugCollect] SourceMap upload failed:', err.message) }) } catch (err) { console.warn('[BugCollect] SourceMap upload error:', err.message) } } return result }, }, } } module.exports = { withBugCollect }