sourcemap
metro v0.66.2 react-native-community/cli v6.4.0 mozilla/source-map 0.5.7
source-map的基本使用生产与解析
bundle
打包
npx react-native bundle --entry-file index.js --platform ios --dev false --bundle-output RNBundle/index.ios.bundle --assets-dest RNBundle/ --sourcemap-output RNBundle/index.ios.bundle.map --verbose
metro-symbolicate解析bundle
export default class TestClass {
testFunc() {
a = a + 1;
console.log('hello');
let b = b + 1;
}
}
sourcemap解析命令 npx metro-symbolicate ./index.bundle.map < ./track.js
track.js文件是crash的堆栈
ReferenceError: Property 'a' doesn't exist, js engine: hermes
value
index.ios.bundle:420:242
anonymous
index.ios.bundle:6:179
h
index.ios.bundle:2:1585
d
index.ios.bundle:2:958
o
index.ios.bundle:2:496
global
index.ios.bundle:423:3
mozilla/source-map解析bundle
最终的调用的是source-map相关内容,如下测试代码
const sourceMap = require('source-map');
const fs = require('fs');
/**
* 异步解析行、列对应的函数信息
* @param {number} line 行
* @param {number} column 列
* @returns Promise
*/
const asyncSymbolicate = async (line = 0, column = 0) => {
return new Promise((resolve, reject) => {
if (line < 0 || column < 0) {
reject('invalid params');
return;
}
fs.readFile('./index.bundle.map', 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
const smc = new sourceMap.SourceMapConsumer(data);
smc.then(consumer => {
console.log('line'+line)
const info = consumer.originalPositionFor({
line: line,
column: column
});
console.log('info'+JSON.stringify(info))
resolve(info);
}).catch(err => {
reject(err);
});
}
});
});
};
测试一个crash,变量a未定义
a = a + 1
输入的内容,分别表示文件路径、行、列
index.ios.bundle:420:296
输出
{
source: '/AwesomeProject/awesomeproject/test.js',
line: 3,
column: 4,
name: 'a'
}
hermes 解析
若hermes产物含 sourcemap, 可以直接用react-native打包出来的sourcemap进行解析,方式跟源码包一样。
hermes source map剥离
若hermes 打包时选择-output-source-map 则 sourcemap会从hermes包内剥离,减少hermes包的大小,但解析前要先合并sourcemap
首先基于源码打包后的文件,进行hermes打包
./node_modules/hermes-engine/osx-bin/hermesc -emit-binary -out BCRNBundle/temp/index.ios.bundle BCRNBundle/index.ios.bundle -output-source-map
BCRNBundle/index.ios.bundle为源码包路径,执行命令后在temp目录下产生新的index.ios.bundle和map文件
然后进行sourcemap合并
node ./node_modules/react-native/scripts/compose-source-maps.js RNBundle/index.ios.bundle.map BCRNBundle/hbc/index.ios.bundle.map -o BCRNBundle/index.ios.bundle.map
解析方式同源码解析
参考文档 https://reactnative.cn/docs/symbolication
ram-bundle打包
npx react-native ram-bundle --platform ios --dev true --entry-file index.js --bundle-output release/ios/RNBundle/index.ios.bundle --assets-dest release/ios/RNBundle --sourcemap-output release/ios/ios-release.bundle.map
crash 堆栈
516.js:15:13
可以使用metro-symbolicate解析crash
metro-source-map
source map 生成
describe('build map from raw mappings', () => {
it('returns a `Generator` instance', () => {
expect(fromRawMappings([])).toBeInstanceOf(Generator);
});
it('returns a working source map containing all mappings', () => {
const input = [
{
code: lines(11),
functionMap: {names: ['<global>'], mappings: 'AAA'},
map: [
[1, 2],
[3, 4, 5, 6, 'apples'],
[7, 8, 9, 10],
[11, 12, 13, 14, 'pears'],
],
source: 'code1',
path: 'path1',
},
{
code: lines(3),
functionMap: {names: ['<global>'], mappings: 'AAA'},
map: [
[1, 2],
[3, 4, 15, 16, 'bananas'],
],
source: 'code2',
path: 'path2',
},
{
code: lines(23),
functionMap: null,
map: [
[11, 12],
[13, 14, 15, 16, 'bananas'],
[17, 18, 19, 110],
[21, 112, 113, 114, 'pears'],
],
source: 'code3',
path: 'path3',
},
];
expect(fromRawMappings(input).toMap()).toEqual({
mappings:
'E;;IAIMA;;;;QAII;;;;YAIIC;E;;ICEEC;;;;;;;;;;;Y;;cCAAA;;;;kBAI8F;;;;gHA8FID',
names: ['apples', 'pears', 'bananas'],
sources: ['path1', 'path2', 'path3'],
sourcesContent: ['code1', 'code2', 'code3'],
x_facebook_sources: [
[{names: ['<global>'], mappings: 'AAA'}],
[{names: ['<global>'], mappings: 'AAA'}],
null,
],
version: 3,
});
});
从原始的map生成source map
- fromRawMappings流程,返回值Generator
- 调用addMappingsForFile函数,传入map参数
- 遍历map数组,调用addMapping函数
- 最后调用Generator的相关函数,比如addSimpleMapping函数,传入generatedLine、generatedColumn
- 调用B64Builder的startSegment函数,传入参数为当前列减去上一列。(base64相关操作)
- Generator的toMap函数,返回source map
source map 格式
见source-map.js 文件MixedSourceMap类型
bundle sourcemap格式,见BasicSourceMap
sourcemap是一个json文件,字段解释
- sources,数组,rn的所有文件路径
- sourcesContent,sources对应的文件内容数组
- x_facebook_sources,里面是与sources对应一致的函数名数组
- names,该文件的函数名数组
- mappings,
- names,所有的函数数组
- mappings,mapping一个分号为1行,逗号为一个segment
ram-bundle sourcemap格式,见IndexMap
- sections,元素为json,包含以下两个字段,相当于把bundle source map切割为很多sections
- map,这里的字段和bundle打包一样
- offset
- x_facebook_offsets,所有文件的offset
- x_metro_module_paths,所有文件路径
source map 解析
it('performs lookup correctly in a non-indexed map', () => {
const map = {
version: 3,
names: ['first', 'second'],
sources: ['foo.js', 'bar.js'],
mappings: 'AAAAA,CCCCC',
};
const consumer = new Consumer(map);
expect(
consumer.originalPositionFor({line: add1(0), column: add0(0)}),
).toEqual(
objectContaining({source: 'foo.js', line: 1, column: 0, name: 'first'}),
);
});
Consumer的初始化
- DelegatingConsumer初始化,传入MixedSourceMap
- 若有mapping,初始化MappingsConsumer(basic),若无mappings,初始化SectionsConsumer(index),它们都继承AbstractConsumer
调用originalPositionFor函数
对于MappingsConsumer
- 调用_decodeAndCacheMappings函数,解码sourcemap文件,得到mappings对象
- 调用greatestLowerBound函数,遍历mappings,比对generatedLine,generatedColumn(堆栈),拿到原文件的line、column
compose source map
见composeSourceMaps.js文件,上文compose-source-maps.js合并源码map和hermes map,调用的就是composeSourceMaps.js文件的composeSourceMaps函数
mozilla/source-map
source map spec https://sourcemaps.info/spec.html
source map解析
mozilla/source-map的流程与metro-source-map类似
- SourceMapConsumer(bundle BasicSourceMapConsumer,ram-bundle IndexedSourceMapConsumer),mozilla/source-map
- originalPositionFor,返回source(源文件绝对路径),参数line,colum,数据来自于mapping
mapping格式
{
generatedLine: 1099,
generatedColumn: 25,
source: null,
originalLine: 11,
originalColumn: 19,
name: null
}
generatedLine,generatedColumn是crash时堆栈的行列,originalLine,originalColumn为解析出来的源文件行列。 source为sourcemap文件中sources数组的位置,这里可以取出源文件绝对路径
originalPositionFor的过程,mozilla/source-map,调用_findMapping函数遍历mapping,找到对应的originalLine和originalColumn