跳到主要内容

解析 SourceMap

2024年04月30日
柏拉文
越努力,越幸运

一、认识


通过 source-map 来实现代码还原。代码还原工作流如下:

  1. 通过 Webpack 或者 Vite 打包具有错误的文件

  2. 运行 bundle.js 控制台会出现错误,此时会有报错信息中的行列数

  3. 根据报错信息中的行列数, 通过 source-map 来反解出错的源代码位置

二、思路


1. 实例化 SourceMapConsumer 对象

const sourcemapData = fs.readFileSync("./dist/bundle.js.map").toString();

const sourcemapConsumer = new SourceMapConsumer(
sourcemapData,
null,
"webpack://parse-sourcemap/"
);

2. 根据压缩后的代码报错行列数获取源代码的位置信息

const originalPosition = consumer.originalPositionFor({
line: compressedLine,
column: compressedColumn,
});

3. 读出原始文件内容,结合原始报错行列号,锁定原始错误位置

const {
source,
line: originalLine,
column: originalColumn,
} = originalPosition;

if (source) {
const sourceContent = consumer.sourceContentFor(source);
}

三、实现


Webpack 打包器为例 (Vite) 类似

3.1 index.js

const a = 1;
console.log(a);

const array = [1, 2, 3, 4, 5];

array.forEach((value, key) => {
console.log("key", key);
console.log("value", value);
});

function foo() {
console.log("foo");
}

foo();

console.log(c);

3.2 package.json

{
"name": "webpack-sourceMap",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "webpack build -c ./webpack.config.js"
},
"devDependencies": {
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"source-map": "^0.7.4"
}
}

3.3 webpack.config.js

const Path = require("path");

function resolve(path) {
return Path.resolve(__dirname, path);
}

module.exports = {
mode: "production",
entry: resolve("./index.js"),
devtool: "source-map",
output: {
clean: true,
path: resolve("build"),
filename: "[name]-[contenthash].js",
},
};

3.4 recover-sourcemap.js

const Fs = require("fs");
const { SourceMapConsumer } = require("source-map");

function recoverSourceMap(options) {
return new Promise((resolve, reject) => {
const { type, sourcemapData, compressedLine, compressedColumn } = options;
const sourcemapConsumer = new SourceMapConsumer(sourcemapData, null, type);
sourcemapConsumer.then((consumer) => {
const originalPosition = consumer.originalPositionFor({
line: compressedLine,
column: compressedColumn,
});

const {
source,
line: originalLine,
column: originalColumn,
} = originalPosition;

if (source) {
const sourceContent = consumer.sourceContentFor(source);
if (sourceContent) {
const lines = sourceContent.split("\n");
const codeLine = lines[originalPosition.line - 1];
resolve({
status: "success",
message: "成功还原代码",
sourceCodeInfo: {
source,
originalLine,
originalColumn,
codeLine,
},
});
}
} else {
reject({ status: "failed", message: "无法找到原始源文件位置。" });
}
});
});
}

async function run() {
const sourcemapData = Fs.readFileSync(
"./build/main-6910fe9c709975f8f52b.js.map"
).toString();
const result = await recoverSourceMap({
sourcemapData,
type: "webpack://parse-sourcemap/",
compressedLine: Number(1), // 混肴代码报错 row
compressedColumn: Number(123), // 混肴代码报错 col
});

if (!result.sourceCodeInfo) {
return;
}
const { source, codeLine, originalLine } = result.sourceCodeInfo;

console.log(`压缩后代码位置:行 1,列 123`);
console.log(`原始源文件:${source}`);
console.log(`原始源文件行号:${originalLine}`);
console.log("出问题代码行 -> Code line:", codeLine);
}

run();

四、测试


  1. 执行 pnpm build 打包生成 bundle.js

  2. 执行 node bundle.js,此时, bundle.js 会报错,控制台出现详细的报错信息,可以拿到混肴后的报错位置,行号、列号

  3. 得到报错后的行号、列号,修改 run 中的 compressedLinecompressedColumn 参数, 并执行 node recover-sourcemap.js

此时,成功通过混肴后的行号、列号找到源代码中对应的位置。