refactor: modernize symbolicator runtime and security

这个提交包含在:
徐勤民 2026-07-27 12:44:10 +08:00
父节点 76384bbd0c
当前提交 b1141bcacc
共有 17 个文件被更改,包括 2676 次插入122 次删除

9
.dockerignore 普通文件
查看文件

@ -0,0 +1,9 @@
.git
.gitignore
node_modules
dist
coverage
test
docs
*.log
.DS_Store

5
.gitignore vendored 普通文件
查看文件

@ -0,0 +1,5 @@
node_modules/
dist/
coverage/
*.log
.DS_Store

查看文件

@ -1,33 +1,36 @@
FROM node:20-alpine AS builder
FROM node:24.18.0-alpine AS dependencies
WORKDIR /app
# Install dependencies
COPY package.json ./
RUN npm install
COPY package.json package-lock.json ./
RUN npm ci
FROM dependencies AS builder
# Copy source
COPY tsconfig.json ./
COPY src/ ./src/
# Build
RUN npm run build
# Production image
FROM node:20-alpine
FROM node:24.18.0-alpine AS production-dependencies
WORKDIR /app
# Install dependencies
COPY package.json ./
RUN npm install --production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
# Copy built files
COPY --from=builder /app/dist ./dist
FROM node:24.18.0-alpine AS runtime
# Install atos for iOS symbolication (if needed)
RUN apk add --no-cache binutils
ENV NODE_ENV=production
WORKDIR /app
COPY --from=production-dependencies --chown=node:node /app/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/dist ./dist
EXPOSE 3000
USER node
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["node", "-e", "fetch('http://127.0.0.1:3000/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
CMD ["node", "dist/index.js"]

23
Jenkinsfile vendored
查看文件

@ -27,7 +27,8 @@ pipeline {
branches: [[name: 'main']],
extensions: [[$class: 'CleanBeforeCheckout']],
userRemoteConfigs: [[
url: 'https://xuqinmin12:28115f261568d510d230727b3e82c0167414286e@xuqinmin.com/xuqmGroup/XuqmGroup-Symbolicator.git'
url: 'ssh://git@xuqinmin.com:2222/xuqmGroup/XuqmGroup-Symbolicator.git',
credentialsId: 'jenkins-ssh-key'
]]
])
}
@ -109,15 +110,17 @@ print('versions.json updated: symbolicator = ${env.IMAGE_VERSION}')
stage('Commit Version') {
steps {
script {
bat """
chcp 65001 >nul
git config user.email "jenkins@xuqm.com"
git config user.name "Jenkins CI"
git add ${env.VERSION_FILE}
git diff --cached --quiet || git commit -m "ci: bump ${env.IMAGE_NAME} to ${env.IMAGE_VERSION} [skip ci]"
git push origin HEAD:main
"""
sshagent(credentials: ['jenkins-ssh-key']) {
script {
bat """
chcp 65001 >nul
git config user.email "jenkins@xuqm.com"
git config user.name "Jenkins CI"
git add ${env.VERSION_FILE}
git diff --cached --quiet || git commit -m "ci: bump ${env.IMAGE_NAME} to ${env.IMAGE_VERSION} [skip ci]"
git push origin HEAD:main
"""
}
}
}
}

44
README.md 普通文件
查看文件

@ -0,0 +1,44 @@
# Xuqm Symbolicator
BugCollect 的独立符号化服务,提供 React Native、Electron、Vue、Android、iOS 和 Flutter
堆栈处理入口。服务只负责符号化,不保存上传内容。
## 运行基线
- 生产容器Node.js 24 LTS
- 本地开发Node.js 22.22 及以上受支持版本
- 安装:`npm ci`
- 构建:`npm run build`
- 测试:`npm test`
- 完整检查:`npm run check`
## HTTP 契约
- `GET /health`:返回服务状态。
- `POST /api/symbolicate`:保持现有 JSON 请求与 JSON 响应结构。
- 配置 `SYMBOLICATOR_API_KEY` 后,请求必须通过 `x-api-key` 请求头认证;未配置时保持
既有开发模式行为。
- 请求体最大 105 MiB。业务字段仍分别限制堆栈 100 KB、Sourcemap 50 MB 和
ProGuard/R8 mapping 100 MB,超限请求不会进入解析器。
服务不提供 multipart 文件上传接口。`multer` 仅保留为现有依赖契约并升级至安全版本,
不得在未定义文件数量、单文件大小、总大小和临时文件清理规则前直接启用。
## 容器
```bash
docker build -t xuqm-symbolicator:local .
docker run --rm -p 3000:3000 xuqm-symbolicator:local
```
镜像使用锁文件安装、多阶段构建和非 root 用户运行。健康检查访问容器内
`http://127.0.0.1:3000/health`
当前 Linux 容器不内置 macOS `atos` 或 Flutter SDK。因此 iOS/Flutter 请求接口仍保留,
但对应外部工具必须由实际部署环境提供;不能用 TypeScript 构建结果替代这两类运行验证。
## 安全边界
- 日志不得输出 API Key、请求体、Sourcemap、mapping、堆栈内容或宿主文件路径。
- 不得把来自请求的路径或命令参数拼接进 shell;现有外部工具调用统一使用参数数组。
- 依赖变更必须同步提交 `package-lock.json` 并使用 `npm ci` 验证。

查看文件

@ -0,0 +1,49 @@
# Symbolicator 实施交接
## 当前架构
- `src/index.ts`Express 应用工厂、公共中间件、健康检查和统一错误边界。
- `src/routes/symbolicate.ts`:认证和 `/api/symbolicate` 路由编排。
- `src/parsers/`:各平台解析器;路由层不复制解析规则。
- `test/`:使用 Node 内置测试运行器验证 HTTP 契约和关键解析器。
- `Dockerfile`Node 24 LTS 多阶段、锁文件安装、非 root 运行和容器健康检查。
## 本次现代化决策
1. Express 5、source-map 0.8 和 TypeScript 7 已通过当前源码构建与契约测试,不保留旧版
并行实现。
2. multer 升级到 2.2.0,消除旧依赖链安全问题;当前没有 multipart HTTP 契约,因此不注册
上传中间件。
3. 全局请求体上限由 200 MiB 收敛为 105 MiB,覆盖现有 100 MB mapping 业务上限且避免
无边界内存占用。
4. 应用工厂使测试无需修改全局环境变量或占用固定端口;生产入口仍是
`node dist/index.js`
5. 错误日志只记录错误类型,不记录请求体、凭据、符号文件和用户提供的路径。
## 接手验证
```bash
npm ci
npm run check
npm audit --audit-level=low
docker build -t xuqm-symbolicator:local .
```
最后一项仅在 Docker daemon 可用时执行。iOS `atos` 和 Flutter SDK 不属于该 Linux
镜像,相关平台仍需在具有对应工具链的实际运行环境单独验证。
## 2026-07-27 验证记录
- `npm ci`:通过,安装过程的依赖安全审计为 0 个漏洞。
- `npm run check`:通过,共 8 项构建、HTTP 契约、解析器、安全和 Dockerfile 静态测试。
- `npm start` + `GET /health`:构建产物启动成功并返回既有健康响应。
- `node:24.18.0-alpine`:官方镜像清单包含 amd64、arm64 和 s390x。
- `docker build`:未执行;本机安装了 Docker CLI,但 Docker daemon 未运行。
- iOS/Flutter 外部工具链:未验证;当前 Linux 容器不提供 `atos` 和 Flutter SDK。
## 禁止事项
- 不得恢复 `npm install` 容器构建或忽略锁文件。
- 不得把 Jenkins 仓库地址改回包含 Token 的 URL;现有 SSH credentials 方案是基线。
- 不得在日志中输出认证信息或完整异常输入。
- 不得在没有显式限额与清理策略时启用 multipart 上传。

2201
package-lock.json 自动生成的 普通文件

文件差异内容过多而无法显示 加载差异

查看文件

@ -3,23 +3,28 @@
"version": "1.0.0",
"description": "BugCollect 符号化服务 - 支持 RN/Android/iOS/Flutter",
"main": "dist/index.js",
"engines": {
"node": ">=22.22.0 <27"
},
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"test": "node --import tsx --test test/**/*.test.ts",
"check": "npm run build && npm test",
"start": "node dist/index.js"
},
"dependencies": {
"express": "^4.18.2",
"source-map": "^0.7.4",
"multer": "^1.4.5-lts.1",
"cors": "^2.8.5"
"cors": "2.8.6",
"express": "5.2.1",
"multer": "2.2.0",
"source-map": "0.8.0"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/multer": "^1.4.11",
"@types/cors": "^2.8.17",
"@types/node": "^20.10.0",
"typescript": "^5.3.2",
"tsx": "^4.6.2"
"@types/cors": "2.8.19",
"@types/express": "5.0.6",
"@types/multer": "2.2.0",
"@types/node": "24.13.3",
"tsx": "4.23.1",
"typescript": "7.0.2"
}
}

查看文件

@ -1,31 +1,62 @@
import express from 'express'
import cors from 'cors'
import { symbolicateRouter } from './routes/symbolicate'
import { createSymbolicateRouter } from './routes/symbolicate'
const app = express()
const PORT = process.env.PORT || 3000
const DEFAULT_BODY_LIMIT = '105mb'
// Middleware
app.use(cors())
app.use(express.json({ limit: '200mb' }))
app.use(express.text({ limit: '200mb' }))
export interface AppOptions {
apiKey?: string
bodyLimit?: string
}
// Routes
app.use('/api', symbolicateRouter)
/**
*
*
* 使
*
*/
export function createApp(options: AppOptions = {}) {
const app = express()
const bodyLimit = options.bodyLimit || DEFAULT_BODY_LIMIT
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', service: 'xuqm-symbolicator', version: '1.0.0' })
})
app.disable('x-powered-by')
app.use(cors())
app.use(express.json({ limit: bodyLimit }))
app.use(express.text({ limit: bodyLimit }))
app.use('/api', createSymbolicateRouter(options.apiKey))
// Error handler
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error('Error:', err.message)
res.status(500).json({ success: false, error: err.message })
})
app.get('/health', (_req, res) => {
res.json({ status: 'ok', service: 'xuqm-symbolicator', version: '1.0.0' })
})
app.listen(PORT, () => {
console.log(`Symbolicator service running on port ${PORT}`)
})
app.use((
err: Error & { type?: string; status?: number; statusCode?: number },
_req: express.Request,
res: express.Response,
_next: express.NextFunction
) => {
const isBodyTooLarge =
err.type === 'entity.too.large' ||
err.status === 413 ||
err.statusCode === 413
const status = isBodyTooLarge ? 413 : 500
const message = isBodyTooLarge ? 'request body too large' : 'internal server error'
// 只记录错误类型,不记录请求体、API Key、符号文件内容或宿主文件路径。
console.error('Symbolicator request failed:', err.name)
res.status(status).json({ success: false, error: message })
})
return app
}
export const app = createApp()
if (require.main === module) {
app.listen(PORT, () => {
console.log(`Symbolicator service running on port ${PORT}`)
})
}
export default app

查看文件

@ -59,24 +59,24 @@ export function symbolicateWithDsym(
// 验证输入参数(防止命令注入)
if (!validatePath(dsymPath)) {
console.error('Invalid dsymPath:', dsymPath)
console.error('Invalid dSYM path')
return frames.map(f => `${f.function} (${f.file}:${f.line})`)
}
if (!validateAddress(loadAddress)) {
console.error('Invalid loadAddress:', loadAddress)
console.error('Invalid dSYM load address')
return frames.map(f => `${f.function} (${f.file}:${f.line})`)
}
if (!validateArch(arch)) {
console.error('Invalid arch:', arch)
console.error('Invalid dSYM architecture')
return frames.map(f => `${f.function} (${f.file}:${f.line})`)
}
// 验证所有地址格式
for (const addr of addresses) {
if (!validateAddress(addr)) {
console.error('Invalid address:', addr)
console.error('Invalid dSYM frame address')
return frames.map(f => `${f.function} (${f.file}:${f.line})`)
}
}
@ -95,8 +95,9 @@ export function symbolicateWithDsym(
}
return `${f.function} (${f.file}:${f.line})`
})
} catch (err) {
console.error('atos symbolication failed:', err)
} catch {
// 子进程异常对象可能包含完整文件路径和调用参数,不可直接写入日志。
console.error('atos symbolication failed')
return frames.map(f => `${f.function} (${f.file}:${f.line})`)
}
}

查看文件

@ -46,12 +46,12 @@ export function symbolicateWithFlutter(
// 验证路径安全性
if (!validatePath(debugSymbolsPath)) {
console.error('Invalid debugSymbolsPath:', debugSymbolsPath)
console.error('Invalid Flutter debug symbols path')
return stacktrace
}
if (!validatePath(flutterPath)) {
console.error('Invalid flutterPath:', flutterPath)
console.error('Invalid Flutter executable path')
return stacktrace
}
@ -74,8 +74,9 @@ export function symbolicateWithFlutter(
// 读取符号化结果
const result = readFileSync(tmpOutput, 'utf-8')
return result
} catch (err) {
console.error('Flutter symbolization failed:', err)
} catch {
// 子进程异常对象可能包含完整文件路径和调用参数,不可直接写入日志。
console.error('Flutter symbolization failed')
return stacktrace
} finally {
// 清理临时文件

查看文件

@ -25,27 +25,40 @@ export async function symbolicateWithSourcemap(
const rawSourceMap = JSON.parse(sourcemapContent)
const consumer = await new SourceMapConsumer(rawSourceMap)
const results: SymbolicatedFrame[] = []
try {
const results: SymbolicatedFrame[] = []
for (const frame of frames) {
try {
const original = consumer.originalPositionFor({
line: frame.line,
column: frame.column,
})
if (original.source) {
results.push({
originalFile: original.source,
originalLine: original.line || 0,
originalColumn: original.column || 0,
originalFunction: original.name || frame.function,
minifiedFile: frame.file,
minifiedLine: frame.line,
minifiedColumn: frame.column,
for (const frame of frames) {
try {
const original = consumer.originalPositionFor({
line: frame.line,
column: frame.column,
})
} else {
// 无法映射,保留原始帧
if (original.source) {
results.push({
originalFile: original.source,
originalLine: original.line || 0,
originalColumn: original.column || 0,
originalFunction: original.name || frame.function,
minifiedFile: frame.file,
minifiedLine: frame.line,
minifiedColumn: frame.column,
})
} else {
// 无法映射,保留原始帧
results.push({
originalFile: frame.file,
originalLine: frame.line,
originalColumn: frame.column,
originalFunction: frame.function,
minifiedFile: frame.file,
minifiedLine: frame.line,
minifiedColumn: frame.column,
})
}
} catch {
// 单帧解析失败不影响其余堆栈,保留该帧原始位置。
results.push({
originalFile: frame.file,
originalLine: frame.line,
@ -56,22 +69,12 @@ export async function symbolicateWithSourcemap(
minifiedColumn: frame.column,
})
}
} catch (err) {
// 解析失败,保留原始帧
results.push({
originalFile: frame.file,
originalLine: frame.line,
originalColumn: frame.column,
originalFunction: frame.function,
minifiedFile: frame.file,
minifiedLine: frame.line,
minifiedColumn: frame.column,
})
}
}
consumer.destroy()
return results
return results
} finally {
consumer.destroy()
}
}
/**

查看文件

@ -3,36 +3,50 @@
*/
import { Router, Request, Response, NextFunction } from 'express'
import { timingSafeEqual } from 'node:crypto'
import { parseStackFrames, Platform } from '../parsers/stack-parser'
import { symbolicateWithSourcemap, formatSymbolicatedStack, SymbolicatedFrame } from '../parsers/sourcemap'
import { parseProguardMapping, deobfuscateStack } from '../parsers/proguard'
import { symbolicateWithDsym } from '../parsers/dsym'
import { symbolicateWithFlutter } from '../parsers/flutter'
export const symbolicateRouter = Router()
function authMiddleware(apiKey: string) {
return (req: Request, res: Response, next: NextFunction) => {
// 如果没有配置 API Key,则跳过认证开发模式
if (!apiKey) {
return next()
}
// API Key 认证中间件
const API_KEY = process.env.SYMBOLICATOR_API_KEY || ''
const providedKey = req.headers['x-api-key']
if (typeof providedKey !== 'string' || !sameSecret(providedKey, apiKey)) {
return res.status(401).json({
success: false,
error: 'Unauthorized: invalid or missing API key',
})
}
function authMiddleware(req: Request, res: Response, next: NextFunction) {
// 如果没有配置 API Key,则跳过认证开发模式
if (!API_KEY) {
return next()
}
const providedKey = req.headers['x-api-key'] as string
if (!providedKey || providedKey !== API_KEY) {
return res.status(401).json({
success: false,
error: 'Unauthorized: invalid or missing API key',
})
}
next()
}
// 应用认证中间件
symbolicateRouter.use(authMiddleware)
function sameSecret(provided: string, expected: string) {
const providedBuffer = Buffer.from(provided)
const expectedBuffer = Buffer.from(expected)
return providedBuffer.length === expectedBuffer.length &&
timingSafeEqual(providedBuffer, expectedBuffer)
}
/**
*
*/
export function createSymbolicateRouter(
apiKey = process.env.SYMBOLICATOR_API_KEY || ''
) {
const router = Router()
router.use(authMiddleware(apiKey))
router.post('/symbolicate', handleSymbolicate)
return router
}
interface SymbolicateRequest {
/** 平台类型 */
@ -70,8 +84,15 @@ interface SymbolicateRequest {
*
*
*/
symbolicateRouter.post('/symbolicate', async (req: Request, res: Response) => {
async function handleSymbolicate(req: Request, res: Response) {
try {
if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
return res.status(400).json({
success: false,
error: 'platform and stacktrace are required',
})
}
const {
platform,
stacktrace,
@ -85,7 +106,8 @@ symbolicateRouter.post('/symbolicate', async (req: Request, res: Response) => {
debugSymbolsPath,
} = req.body as SymbolicateRequest
if (!platform || !stacktrace) {
if (typeof platform !== 'string' || typeof stacktrace !== 'string' ||
!platform || !stacktrace) {
return res.status(400).json({
success: false,
error: 'platform and stacktrace are required',
@ -177,13 +199,14 @@ symbolicateRouter.post('/symbolicate', async (req: Request, res: Response) => {
...result,
})
} catch (err: any) {
console.error('Symbolication error:', err)
// 输入文件内容可能包含业务代码,日志仅记录异常类型。
console.error('Symbolication failed:', err?.name || 'Error')
return res.status(500).json({
success: false,
error: err.message,
error: err instanceof Error ? err.message : 'symbolication failed',
})
}
})
}
/**
* JS Sourcemap

15
test/dockerfile.test.ts 普通文件
查看文件

@ -0,0 +1,15 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { test } from 'node:test'
const dockerfile = readFileSync(new URL('../Dockerfile', import.meta.url), 'utf8')
test('Dockerfile 使用锁文件、多阶段和非 root 运行', () => {
assert.match(dockerfile, /^FROM node:24\.18\.0-alpine AS dependencies$/m)
assert.match(dockerfile, /COPY package\.json package-lock\.json \.\//)
assert.equal(/\bRUN npm install\b/.test(dockerfile), false)
assert.ok((dockerfile.match(/\bRUN npm ci\b/g) || []).length >= 2)
assert.match(dockerfile, /^USER node$/m)
assert.match(dockerfile, /^HEALTHCHECK /m)
assert.match(dockerfile, /^CMD \["node", "dist\/index\.js"\]$/m)
})

91
test/http.test.ts 普通文件
查看文件

@ -0,0 +1,91 @@
import assert from 'node:assert/strict'
import { Server } from 'node:http'
import { AddressInfo } from 'node:net'
import { afterEach, test } from 'node:test'
import { createApp } from '../src'
const servers: Server[] = []
afterEach(async () => {
await Promise.all(
servers.splice(0).map(server => new Promise<void>((resolve, reject) => {
server.close(error => error ? reject(error) : resolve())
}))
)
})
async function start(options: Parameters<typeof createApp>[0] = {}) {
const server = createApp(options).listen(0, '127.0.0.1')
servers.push(server)
await new Promise<void>(resolve => server.once('listening', resolve))
const { port } = server.address() as AddressInfo
return `http://127.0.0.1:${port}`
}
test('health 保持稳定响应契约', async () => {
const baseUrl = await start()
const response = await fetch(`${baseUrl}/health`)
assert.equal(response.status, 200)
assert.deepEqual(await response.json(), {
status: 'ok',
service: 'xuqm-symbolicator',
version: '1.0.0',
})
assert.equal(response.headers.get('x-powered-by'), null)
})
test('配置 API Key 时拒绝未认证请求', async () => {
const baseUrl = await start({ apiKey: 'test-only-key' })
const response = await fetch(`${baseUrl}/api/symbolicate`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ platform: 'android', stacktrace: 'sample' }),
})
assert.equal(response.status, 401)
assert.deepEqual(await response.json(), {
success: false,
error: 'Unauthorized: invalid or missing API key',
})
})
test('Android 反混淆接口保持既有请求响应结构', async () => {
const baseUrl = await start()
const response = await fetch(`${baseUrl}/api/symbolicate`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
platform: 'android',
stacktrace: 'at a.b.c.a(r8-map-id-sample:13)',
mappingFile: [
'com.example.Sample -> a.b.c:',
' 13:13:void com.example.Sample.run():42:42 -> a',
].join('\n'),
}),
})
assert.equal(response.status, 200)
const body = await response.json() as { success: boolean; symbolicated: string }
assert.equal(body.success, true)
assert.match(body.symbolicated, /com\.example\.Sample\.[\w$]+\(Sample\.kt:42\)/)
})
test('请求体超限返回受控错误且不进入业务处理', async () => {
const baseUrl = await start({ bodyLimit: '1kb' })
const response = await fetch(`${baseUrl}/api/symbolicate`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
platform: 'rn',
stacktrace: 'x'.repeat(2048),
sourcemap: '{}',
}),
})
assert.equal(response.status, 413)
assert.deepEqual(await response.json(), {
success: false,
error: 'request body too large',
})
})

37
test/security.test.ts 普通文件
查看文件

@ -0,0 +1,37 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { test } from 'node:test'
function source(relativePath: string) {
return readFileSync(new URL(`../${relativePath}`, import.meta.url), 'utf8')
}
test('日志不直接输出认证值、请求体或用户文件路径', () => {
const files = [
'src/index.ts',
'src/routes/symbolicate.ts',
'src/parsers/dsym.ts',
'src/parsers/flutter.ts',
].map(source).join('\n')
const forbidden = [
/console\.(?:log|error|warn)\([^)]*apiKey/,
/console\.(?:log|error|warn)\([^)]*req\.body/,
/console\.(?:log|error|warn)\([^)]*dsymPath/,
/console\.(?:log|error|warn)\([^)]*debugSymbolsPath/,
/console\.(?:log|error|warn)\([^)]*,\s*err\s*[),]/,
]
for (const pattern of forbidden) {
assert.equal(pattern.test(files), false, `发现不安全日志模式:${pattern}`)
}
})
test('HTTP 输入和未来文件上传依赖均保持显式上限', () => {
assert.match(source('src/index.ts'), /DEFAULT_BODY_LIMIT = '105mb'/)
const packageJson = JSON.parse(source('package.json')) as {
dependencies: Record<string, string>
}
assert.equal(packageJson.dependencies.multer, '2.2.0')
})

33
test/sourcemap.test.ts 普通文件
查看文件

@ -0,0 +1,33 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { SourceMapGenerator } from 'source-map'
import { symbolicateWithSourcemap } from '../src/parsers/sourcemap'
test('source-map 0.8 可解析并释放 consumer', async () => {
const generator = new SourceMapGenerator({ file: 'index.bundle' })
generator.addMapping({
generated: { line: 1, column: 0 },
original: { line: 8, column: 2 },
source: 'src/example.ts',
name: 'run',
})
const result = await symbolicateWithSourcemap([
{
function: 'a',
file: 'index.bundle',
line: 1,
column: 0,
},
], generator.toString())
assert.deepEqual(result, [{
originalFile: 'src/example.ts',
originalLine: 8,
originalColumn: 2,
originalFunction: 'run',
minifiedFile: 'index.bundle',
minifiedLine: 1,
minifiedColumn: 0,
}])
})