Skip to content

Commit 5eb02d8

Browse files
motiz88facebook-github-bot
authored andcommitted
Move react-native-symbolicate to metro repo
Summary: Previously open-sourced as `react-native-symbolicate`, this package is more closely coupled with Metro and should therefore be part of the latter. Reviewed By: cpojer Differential Revision: D14208031 fbshipit-source-id: 8bae2100149142394e12d499dc4b8a256f07647e
1 parent dd0b429 commit 5eb02d8

File tree

15 files changed

+706
-1
lines changed

15 files changed

+706
-1
lines changed

β€Ž.eslintrc.jsβ€Ž

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ module.exports = {
4040
node: true,
4141
},
4242
},
43+
{
44+
files: ['packages/metro-source-map/**/*.js'],
45+
env: {
46+
node: true,
47+
},
48+
},
4349
{
4450
files: ['scripts/**/*.js'],
4551
rules: {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "metro-symbolicate",
3+
"version": "0.52.0",
4+
"description": "A tool to find the source location from JS bundles and stack traces.",
5+
"license": "MIT",
6+
"main": "./src/symbolicate.js",
7+
"bin": "./src/symbolicate.js",
8+
"repository": {
9+
"type": "git",
10+
"url": "git@github.com:facebook/metro.git"
11+
},
12+
"scripts": {
13+
"prepare-release": "test -d build && rm -rf src.real && mv src src.real && mv build src",
14+
"cleanup-release": "test ! -e build && mv src build && mv src.real src"
15+
},
16+
"engines": {
17+
"node": ">=8.3"
18+
},
19+
"keywords": [
20+
"metro"
21+
],
22+
"dependencies": {
23+
"source-map": "^0.5.6",
24+
"through2": "^2.0.1"
25+
}
26+
}
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
'use strict';
11+
12+
/* eslint-disable no-console */
13+
14+
const fs = require('fs');
15+
16+
const UNKNOWN_MODULE_IDS = {
17+
segmentId: 0,
18+
localId: undefined,
19+
};
20+
21+
/*
22+
* If the file name of a stack frame is numeric (+ ".js"), we assume it's a
23+
* lazily injected module coming from a "random access bundle". We are using
24+
* special source maps for these bundles, so that we can symbolicate stack
25+
* traces for multiple injected files with a single source map.
26+
*
27+
* There is also a convention for callsites that are in split segments of a
28+
* bundle, named either `seg-3.js` for segment #3 for example, or `seg-3_5.js`
29+
* for module #5 of segment #3 of a segmented RAM bundle.
30+
*/
31+
function parseFileName(str) {
32+
const modMatch = str.match(/^(\d+).js$/);
33+
if (modMatch != null) {
34+
return {segmentId: 0, localId: Number(modMatch[1])};
35+
}
36+
const segMatch = str.match(/^seg-(\d+)(?:_(\d+))?.js$/);
37+
if (segMatch != null) {
38+
return {
39+
segmentId: Number(segMatch[1]),
40+
localId: segMatch[2] && Number(segMatch[2]),
41+
};
42+
}
43+
return UNKNOWN_MODULE_IDS;
44+
}
45+
46+
/*
47+
* A helper function to return a mapping {line, column} object for a given input
48+
* line and column, and optionally a module ID.
49+
*/
50+
function getOriginalPositionFor(lineNumber, columnNumber, moduleIds, context) {
51+
var moduleLineOffset = 0;
52+
var metadata = context.segments[moduleIds.segmentId];
53+
const {localId} = moduleIds;
54+
if (localId != null) {
55+
const {moduleOffsets} = metadata;
56+
if (!moduleOffsets) {
57+
throw new Error(
58+
'Module ID given for a source map that does not have ' +
59+
'an x_facebook_offsets field',
60+
);
61+
}
62+
if (moduleOffsets[localId] == null) {
63+
throw new Error('Unknown module ID: ' + localId);
64+
}
65+
moduleLineOffset = moduleOffsets[localId];
66+
}
67+
return metadata.consumer.originalPositionFor({
68+
line: Number(lineNumber) + moduleLineOffset,
69+
column: Number(columnNumber),
70+
});
71+
}
72+
73+
function createContext(SourceMapConsumer, sourceMapContent) {
74+
var sourceMapJson = JSON.parse(sourceMapContent.replace(/^\)\]\}'/, ''));
75+
return {
76+
segments: Object.entries(sourceMapJson.x_facebook_segments || {}).reduce(
77+
(acc, seg) => {
78+
acc[seg[0]] = {
79+
consumer: new SourceMapConsumer(seg[1]),
80+
moduleOffsets: seg[1].x_facebook_offsets || {},
81+
};
82+
return acc;
83+
},
84+
{
85+
'0': {
86+
consumer: new SourceMapConsumer(sourceMapJson),
87+
moduleOffsets: sourceMapJson.x_facebook_offsets || {},
88+
},
89+
},
90+
),
91+
};
92+
}
93+
94+
// parse stack trace with String.replace
95+
// replace the matched part of stack trace to symbolicated result
96+
// sample stack trace:
97+
// IOS: foo@4:18131, Android: bar:4:18063
98+
// sample stack trace with module id:
99+
// IOS: foo@123.js:4:18131, Android: bar:123.js:4:18063
100+
// sample result:
101+
// IOS: foo.js:57:foo, Android: bar.js:75:bar
102+
function symbolicate(stackTrace, context) {
103+
return stackTrace.replace(
104+
/(?:([^@: \n]+)(@|:))?(?:(?:([^@: \n]+):)?(\d+):(\d+)|\[native code\])/g,
105+
function(match, func, delimiter, fileName, line, column) {
106+
var original = getOriginalPositionFor(
107+
line,
108+
column,
109+
parseFileName(fileName || ''),
110+
context,
111+
);
112+
return original.source + ':' + original.line + ':' + original.name;
113+
},
114+
);
115+
}
116+
117+
// Taking in a map like
118+
// trampoline offset (optional js function name)
119+
// JS_0158_xxxxxxxxxxxxxxxxxxxxxx fe 91081
120+
// JS_0159_xxxxxxxxxxxxxxxxxxxxxx Ft 68651
121+
// JS_0160_xxxxxxxxxxxxxxxxxxxxxx value 50700
122+
// JS_0161_xxxxxxxxxxxxxxxxxxxxxx setGapAtCursor 0
123+
// JS_0162_xxxxxxxxxxxxxxxxxxxxxx (unknown) 50818
124+
// JS_0163_xxxxxxxxxxxxxxxxxxxxxx value 108267
125+
126+
function symbolicateProfilerMap(mapFile, context) {
127+
return fs
128+
.readFileSync(mapFile, 'utf8')
129+
.split('\n')
130+
.slice(0, -1)
131+
.map(function(line) {
132+
const line_list = line.split(' ');
133+
const trampoline = line_list[0];
134+
const js_name = line_list[1];
135+
const offset = parseInt(line_list[2], 10);
136+
137+
if (!offset) {
138+
return trampoline + ' ' + trampoline;
139+
}
140+
141+
var original = getOriginalPositionFor(
142+
1,
143+
offset,
144+
UNKNOWN_MODULE_IDS,
145+
context,
146+
);
147+
148+
return (
149+
trampoline +
150+
' ' +
151+
(original.name || js_name) +
152+
'::' +
153+
[original.source, original.line, original.column].join(':')
154+
);
155+
})
156+
.join('\n');
157+
}
158+
159+
function symbolicateAttribution(obj, context) {
160+
var loc = obj.location;
161+
var line = loc.line || 1;
162+
var column = loc.column || loc.virtualOffset;
163+
var file = loc.filename ? parseFileName(loc.filename) : UNKNOWN_MODULE_IDS;
164+
var original = getOriginalPositionFor(line, column, file, context);
165+
obj.location = {
166+
file: original.source,
167+
line: original.line,
168+
column: original.column,
169+
};
170+
}
171+
172+
// Symbolicate chrome trace "stackFrames" section.
173+
// Each frame in it has three fields: name, funcVirtAddr(optional), offset(optional).
174+
// funcVirtAddr and offset are only available if trace is generated from
175+
// hbc bundle without debug info.
176+
function symbolicateChromeTrace(traceFile, context) {
177+
const contentJson = JSON.parse(fs.readFileSync(traceFile, 'utf8'));
178+
if (contentJson.stackFrames == null) {
179+
console.error('Unable to locate `stackFrames` section in trace.');
180+
process.exit(1);
181+
}
182+
console.log(
183+
'Processing ' + Object.keys(contentJson.stackFrames).length + ' frames',
184+
);
185+
Object.values(contentJson.stackFrames).forEach(function(entry) {
186+
let line;
187+
let column;
188+
189+
// Function entrypoint line/column; used for symbolicating function name.
190+
let funcLine;
191+
let funcColumn;
192+
193+
if (entry.funcVirtAddr != null && entry.offset != null) {
194+
// Without debug information.
195+
const funcVirtAddr = parseInt(entry.funcVirtAddr, 10);
196+
const offsetInFunction = parseInt(entry.offset, 10);
197+
// Main bundle always use hard-coded line value 1.
198+
// TODO: support multiple bundle/module.
199+
line = 1;
200+
column = funcVirtAddr + offsetInFunction;
201+
funcLine = 1;
202+
funcColumn = funcVirtAddr;
203+
} else if (entry.line != null && entry.column != null) {
204+
// For hbc bundle with debug info, name field may already have source
205+
// information for the bundle; we still can use babel/metro/prepack
206+
// source map to symbolicate the bundle frame addresses further to its
207+
// original source code.
208+
line = entry.line;
209+
column = entry.column;
210+
211+
funcLine = entry.funcLine;
212+
funcColumn = entry.funcColumn;
213+
} else {
214+
// Native frames.
215+
return;
216+
}
217+
218+
// Symbolicate original file/line/column.
219+
const addressOriginal = getOriginalPositionFor(
220+
line,
221+
column,
222+
UNKNOWN_MODULE_IDS,
223+
context,
224+
);
225+
226+
let frameName = entry.name;
227+
// Symbolicate function name.
228+
if (funcLine != null && funcColumn != null) {
229+
const funcOriginal = getOriginalPositionFor(
230+
funcLine,
231+
funcColumn,
232+
UNKNOWN_MODULE_IDS,
233+
context,
234+
);
235+
if (funcOriginal.name != null) {
236+
frameName = funcOriginal.name;
237+
}
238+
} else {
239+
// No function line/column info.
240+
console.warn(
241+
'Warning: no function prolog line/column info; name may be wrong',
242+
);
243+
}
244+
245+
// Output format is: funcName(file:line:column)
246+
const sourceLocation = `(${addressOriginal.source}:${
247+
addressOriginal.line
248+
}:${addressOriginal.column})`;
249+
entry.name = frameName + sourceLocation;
250+
});
251+
console.log('Writing to ' + traceFile);
252+
fs.writeFileSync(traceFile, JSON.stringify(contentJson));
253+
}
254+
255+
module.exports = {
256+
createContext,
257+
getOriginalPositionFor,
258+
parseFileName,
259+
symbolicate,
260+
symbolicateProfilerMap,
261+
symbolicateAttribution,
262+
symbolicateChromeTrace,
263+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{"functionId":1,"location":{"virtualOffset":22},"usage":[]}
2+
{"functionId":2,"location":{"virtualOffset":99},"usage":[]}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"stackFrames": {
3+
"1": {
4+
"funcVirtAddr": "0",
5+
"offset": "0",
6+
"name": "global+0",
7+
"category": "JavaScript"
8+
},
9+
"2": {
10+
"funcVirtAddr": "0",
11+
"offset": "55",
12+
"name": "global+55",
13+
"category": "JavaScript"
14+
},
15+
"3": {
16+
"funcVirtAddr": "67",
17+
"offset": "16",
18+
"name": "entryPoint+16",
19+
"category": "JavaScript",
20+
"parent": 2
21+
},
22+
"4": {
23+
"funcVirtAddr": "89",
24+
"offset": "0",
25+
"name": "helper+0",
26+
"category": "JavaScript",
27+
"parent": 3
28+
},
29+
"5": {
30+
"funcVirtAddr": "89",
31+
"offset": "146",
32+
"name": "helper+146",
33+
"category": "JavaScript",
34+
"parent": 3
35+
},
36+
"6": {
37+
"name": "[Native]4367295792",
38+
"category": "Native",
39+
"parent": 5
40+
}
41+
}
42+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"version": 3,
3+
"sources": [
4+
"temp/bench.js"
5+
],
6+
"mappings": "AACC,uDAmBU,YAnBY,gBACd,MAGU,wFAKA,WACT,iBACC,IAAD,YACU,cAAA,YAHY,eAAb;"
7+
}

β€Žpackages/metro-symbolicate/src/__tests__/__fixtures__/testfile.js.mapβ€Ž

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
JS_0000_xxxxxxxxxxxxxxxxxxxxxx (unknown) 373
2+
JS_0001_xxxxxxxxxxxxxxxxxxxxxx garbage 296
3+
JS_0002_xxxxxxxxxxxxxxxxxxxxxx name 1234
4+
JS_0003_xxxxxxxxxxxxxxxxxxxxxx (unknown) 5678

β€Žpackages/metro-symbolicate/src/__tests__/__fixtures__/testfile.sectioned.js.mapβ€Ž

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
throws6@thrower.min.js:1:161
2+
thrower.min.js:1:488
3+
forEach@[native code]
4+
o@thrower.min.js:1:464
5+
throws4@thrower.min.js:1:276
6+
eval code
7+
eval@[native code]
8+
t@thrower.min.js:1:420
9+
throws2@thrower.min.js:1:333
10+
throws1@thrower.min.js:1:362
11+
throws0@thrower.min.js:1:391
12+
thrower.min.js:1:506
13+
global code@thrower.min.js:1:534

0 commit comments

Comments
Β (0)