diff --git a/packages/bugcollect/metro/index.js b/packages/bugcollect/metro/index.js index 885fed2..78962ff 100644 --- a/packages/bugcollect/metro/index.js +++ b/packages/bugcollect/metro/index.js @@ -1,11 +1,137 @@ '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。 - * 当前为存根实现,后续补全 SourceMap 上传逻辑。 */ function withBugCollect(metroConfig) { + const projectRoot = metroConfig.projectRoot || process.cwd() + return { ...metroConfig, serializer: { @@ -19,10 +145,66 @@ function withBugCollect(metroConfig) { // 仅 Release 包上传 SourceMap(dev 模式跳过) if (!options.dev) { - // TODO: 补全 SourceMap 上传逻辑 - // 1. 读取 .xuqmconfig 或 xuqm.config.js 获取 bugCollectApiUrl - // 2. 读取 sourceMapUrl 对应的 .map 文件 - // 3. 上传到 bugCollectApiUrl/bugcollect/v1/sourcemaps/upload + 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