生产环境的 JavaScript bundle 往往只有一行或几行很长的代码,变量名也都被改掉了。如果构建时同时产出了 source map(单独的 .map 文件,或者写在 bundle 末尾的内联 data: URL),这个 map 就记录了 bundle 里每个位置对应原始代码的哪个文件、哪一行哪一列。很多 map 还会把原始文件本身塞进一个叫 sourcesContent 的字段里。
这个工具在浏览器里读取 source map,给你三样东西:一份 map 内容报告;用 sourcesContent 重建出来的原始源码目录(可以浏览、预览、单独下载某个文件,或者整体下载成 .zip);以及位置查询——把 bundle 中的位置或粘贴进来的报错堆栈,换算成原始文件、行号、列号和名称。整个过程不上传任何文件。
能做什么
输入。 拖入或选择一个或多个文件:一个 .map 文件,一个 .js、.mjs、.cjs 或 .css bundle,或者两者一起。其他扩展名的文件会按内容判断(map 以 { 开头)。加入 bundle 时,页面会读取它最后一条 sourceMappingURL 注释:
- 内联 map(
//# sourceMappingURL=data:application/json;base64,...)会被解码直接使用,所以单独上传这个 bundle 就够了; - 外部引用(
//# sourceMappingURL=app.js.map)会在报告里列出,并提示你把那个文件一起上传。页面不会替你去加载它。
如果上传了多个 map,优先使用 bundle 声明的那个(其次是 <bundle>.map,再其次是第一个),其余的会出现在警告里。上限:所有文件合计 256 MiB,300 万个映射段,200 万行生成代码,20 万个源文件,索引 map 最多 1 万个 section、嵌套 64 层,分析超时时间 2 分钟。
报告。
- map 字段:格式(普通 map,或带
sections的索引 map,会被展平)、file、sourceRoot、源文件数量以及其中带sourcesContent的数量、名称数量、映射段数和生成代码行数。 - 忽略列表:有多少源文件在
ignoreList或更早的x_google_ignoreList里(打包工具标记为第三方或生成的代码)。这些源文件在目录树和查询结果里会打上标记。 - 打包工具线索:统计看起来像 webpack(
webpack://)、Turbopack(turbopack://)、Vite(/@fs/、/@vite/)、Rollup(\0虚拟模块、commonjsHelpers)、Rolldown、esbuild(<stdin>)或 Parcel(/__parcel_source_root/)的源文件名。这只是从文件名得到的线索,不能证明一定是哪个工具打的包。 - node_modules 占比:有多少源文件位于
node_modules/目录下。 - Bundle:大小、行数,以及它的
sourceMappingURL写了什么。
原始源码。 每个带内容的源文件都会出现在一棵可折叠、可搜索的目录树里。路径会先规整:拼上 sourceRoot,去掉 webpack://、file:// 之类的 URL 协议,解析掉 . 和 ..,保证路径不会跑出目录树。你可以以文本方式预览、单独下载一个文件,或者点 全部下载为 .zip。map 里只有名字、没带内容的源文件会单独列出。
位置查询。 在查询框里粘贴一行或多行:
- 位置,例如
1:2446(行号和列号都从 1 开始,与 DevTools 和堆栈里显示的一致); - Chrome 或 Node 的堆栈行(
at explode (https://example.com/assets/app.js:1:2446)),或者 Firefox、Safari 的堆栈行(explode@https://example.com/assets/app.js:1:2446)。
每条结果给出原始的 路径:行:列、map 里记录的原始名称(如果有),以及目标位置附近几行原始代码,并标出目标行。如果上传了 bundle,还会显示该位置附近的生成代码。指向其他文件的堆栈帧会标为其他文件;如果没有任何一帧指向上传的 bundle(比如文件改过名),则所有帧都会被映射。
原理
source map 第 3 版由 ECMA-426 定义。mappings 字段是一长串 Base64 VLQ 数字:; 分隔生成代码的行,, 分隔映射段,每个段有 1、4 或 5 个数(生成列号,然后是源文件索引、原始行、原始列,以及可选的名称索引)。
在真正解码之前,页面先检查 map 的结构,再把 mappings 不解码地扫一遍。这一遍会统计映射段数和行数,拒绝 Base64 以外的字符、只有 2 或 3 个数的段,以及超过 32 位的数字,一旦超过上限就立刻停下。索引 map 逐个 section 做同样的检查:偏移必须严格递增;通过 url 引用其他 map 的 section 会被拒绝,因为页面不从网络加载任何东西。之后用 @jridgewell/trace-mapping 0.3.31 解码,并逐段检查解码结果是否落在它所属 section 的源文件和名称范围内。
查询时,取不晚于你给出位置的最大生成位置(可以跨行往前找),并返回这个位置上的所有映射段。这条规则、预检的内容和顺序都参照了 morluto 的开源项目 REA(MIT 许可)在 JavaScript 分析中追踪 source map 位置的做法。
.zip 在页面里直接生成:不压缩(stored),文件名用 UTF-8,每个文件带 CRC-32。写入之前会清理每条路径,保证解压时不会写到目标文件夹之外:没有绝对路径,没有 ..,没有空文件名,没有 Windows 不允许的字符;重名的文件(包括只差大小写的)会加上 ~2 这样的后缀。
文件如何保持私密
文件不会上传:
- 解析、解码、查询和打包
.zip都在你设备上的 Web Worker 里完成。文件里的 URL,包括 bundle 的sourceMappingURL和索引 map section 的url,都不会被请求。 - 源码、文件名、路径和代码片段都以纯文本显示,文件里的任何内容都不会被当作 HTML 解析或执行。
- 统计事件只有粗粒度信息:成功或错误码、map 的来源(
file、inline或none)、源文件数量区间(如11-100)、大小区间和耗时区间。不会包含文件名、源文件路径、位置、堆栈或代码。完整列表见隐私页。 - 工具页不展示广告。
如何自己验证
- 在选择文件之前,打开浏览器开发者工具,切到 Network(网络) 面板。分析一个 map,做一次位置查询,再下载
.zip。你只会看到页面资源和 worker 脚本/_astro/sourcemap-worker-*.js的请求,没有任何请求携带你的文件。 - 或者先打开页面,再断网(关掉 Wi-Fi,或者在 Network 面板勾选 Offline),然后分析文件,依然可以正常工作。
- worker 运行在内容安全策略
default-src 'none'; script-src 'self'; connect-src 'none'之下,浏览器不允许它发出任何网络请求。
局限
- 没有 map 就无法还原。 没有 source map 时,这个工具不会做反压缩、变量重命名或拆分模块。那需要 Wakaru(
unminify/unpack)这类反混淆工具,不在本工具范围内;bundle 源码还原教程介绍了 REA 如何调用它。 - 只有 map 带了内容才有原始代码。 关闭了
sourcesContent的 map(例如 webpack 的nosources-source-map)只能给出文件名和位置,没有原始代码。 - 原始代码是打包工具看到的版本。 对 TypeScript、Vue 或 JSX,通常就是原始文件,但有些 loader 存的是已经转换过的中间版本。
- 不会加载外部 map。 如果 bundle 指向单独的
.map文件,请自己把它加进来。section 使用url的索引 map 需要先合并成一个文件。 - section 互相重叠的 map 会被拒绝,和 REA 的做法一致,即使有些工具能接受。
- 列号。 查询使用 UTF-16 列号,与 JavaScript 引擎和 source map 格式一致。位于一行最后一个映射之后的位置,会归到这最后一个映射上。
- 查询上限。 每次最多映射 200 个位置或堆栈行,其余的只计数、不映射。超过 1024 个字符的行会被忽略。
- zip 格式。 不支持 ZIP64:一个压缩包最多 65,534 个文件、4 GiB。
常见问题
网站的 source map 在哪里找?
看脚本最后一行有没有 sourceMappingURL 注释,或者响应头里有没有 SourceMap。在 DevTools 的 Sources(源代码) 面板里,有 map 时能看到映射出来的原始文件。只下载你有权分析的网站的 map。
为什么有些文件只有名字没有内容?
map 里列出了它们,但对应的 sourcesContent 是 null 或缺失。它们会出现在“没有内容的源文件”列表里;位置查询仍然能告诉你它们的文件、行和列。
堆栈映射到了错误的位置。
确认 map 和抛错的 bundle 来自同一次构建(不同部署里同名的 bundle 对不上),并且列号是浏览器给出的,而不是来自格式化过的文件副本。
为什么有些路径里带 ~2?
两个源文件在路径清理后落到了同一个位置(比如 webpack://app/./src/a.js 和 webpack:///src/a.js,或者 A.js 和 a.js)。第二个会被改名,保证在任何系统上解压时两个文件都能保留下来。
能把原始位置反查到 bundle 里吗?
这个工具不支持。它只把生成代码的位置映射到原始位置,这正是看报错堆栈和读压缩代码时需要的方向。
合规
请只分析你自己的项目、你获得授权评估的项目或开源软件的 bundle 和 map。尊重被还原代码的许可证(包括其中限制逆向工程的条款)以及适用的法律。参见免责声明。
相关教程
- 用 REA 从 bundle 还原 JavaScript 源码:source map、webpack chunk 恢复、Wakaru 拆包和命令行下的数据流追踪。
- 用 REA 追踪网站 API 调用:浏览器、HAR 与 bundle:抓取页面的脚本和 map,再顺着其中的 API 调用往下追;流量可以用 HAR 分析工具归纳。
- ASAR 解包与 Electron IPC 分析:Electron 应用常把
.map文件打进app.asar,在那里取出来,再放到这里打开。 - 发布前自查:你的 npm 包和前端产物会不会泄露 source map:先用本工具看看自己的 map 会暴露什么,再把 map 挡在 npm 包和部署目录之外。
- 用 source map 把压缩后的 JS 报错堆栈还原到源码行号:构建 hidden map,把粘贴进来的线上报错堆栈逐帧还原。