2026-06-16 12:20:04 +08:00
|
|
|
|
'use strict'
|
|
|
|
|
|
|
2026-06-19 02:58:27 +08:00
|
|
|
|
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'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-16 12:20:04 +08:00
|
|
|
|
/**
|
2026-06-16 17:39:18 +08:00
|
|
|
|
* withBugCollect(metroConfig)
|
2026-06-16 12:20:04 +08:00
|
|
|
|
* 包裹 Metro 配置,打 Release 包时自动上传 SourceMap。
|
|
|
|
|
|
*/
|
2026-06-16 17:39:18 +08:00
|
|
|
|
function withBugCollect(metroConfig) {
|
2026-06-19 02:58:27 +08:00
|
|
|
|
const projectRoot = metroConfig.projectRoot || process.cwd()
|
|
|
|
|
|
|
2026-06-16 12:20:04 +08:00
|
|
|
|
return {
|
|
|
|
|
|
...metroConfig,
|
|
|
|
|
|
serializer: {
|
|
|
|
|
|
...metroConfig.serializer,
|
|
|
|
|
|
customSerializer: async (entryPoint, preModules, graph, options) => {
|
|
|
|
|
|
// 调用原始 serializer
|
|
|
|
|
|
const baseSerializer = metroConfig.serializer?.customSerializer
|
|
|
|
|
|
const result = baseSerializer
|
|
|
|
|
|
? await baseSerializer(entryPoint, preModules, graph, options)
|
|
|
|
|
|
: undefined
|
|
|
|
|
|
|
|
|
|
|
|
// 仅 Release 包上传 SourceMap(dev 模式跳过)
|
|
|
|
|
|
if (!options.dev) {
|
2026-06-19 02:58:27 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
2026-06-16 12:20:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-16 17:39:18 +08:00
|
|
|
|
module.exports = { withBugCollect }
|