@fromjs/backend
Version:
334 lines (300 loc) • 9.79 kB
text/typescript
import { prettifyAndMapFrameObject } from "./prettify";
import { RequestHandler } from "../RequestHandler";
var StackTraceGPS = require("stacktrace-gps");
var ErrorStackParser = require("error-stack-parser");
var request = require("request");
// var { prettifyAndMapFrameObject } = require("./prettify");
export interface ResolvedStackFrameCodeLine {
text: string;
}
export interface ResolvedStackFrame {
code: {
previousLines: ResolvedStackFrameCodeLine[];
line: ResolvedStackFrameCodeLine;
nextLines: ResolvedStackFrameCodeLine[];
};
columnNumber: number;
lineNumber: number;
fileName: string;
}
function makeLine(fullLine, focusColumn) {
try {
var text = fullLine;
var firstCharIndex = 0;
var lastCharIndex = fullLine.length;
if (fullLine.length > 500) {
firstCharIndex = focusColumn - 200;
if (firstCharIndex < 0) {
firstCharIndex = 0;
}
lastCharIndex = firstCharIndex + 400;
text = fullLine.slice(firstCharIndex, lastCharIndex);
}
return {
length: fullLine.length,
firstCharIndex,
lastCharIndex,
text,
};
} catch (err) {
return {
text: err.toString(),
};
}
}
class StackFrameResolver {
_cache = {};
_gps: any = null;
_nonProxyGps: any = null;
_requestHandler: RequestHandler;
_sourceObjectUrlCache = {};
constructor(requestHandler) {
this._requestHandler = requestHandler;
this._gps = new StackTraceGPS({ ajax: this.getAjax("proxy") });
this._nonProxyGps = new StackTraceGPS({ ajax: this.getAjax("normal") });
}
getAjax(type: "proxy" | "normal") {
const ajax = (url) => {
console.log("get", { url });
// if (
// type === "normal" &&
// url.includes("http://fromjs-temporary-url.com:5555")
// ) {
// // We use this port for eval scripts, which are only available through the proxy
// return this._gps._get(url);
// }
return new Promise((resolve, reject) => {
const options: any = {};
// if (type === "proxy") {
// options.proxy = "http://127.0.0.1:" + this._proxyPort;
// }
if (type === "proxy") {
this._requestHandler
.handleRequest({
url,
method: "GET",
headers: {},
postData: null,
})
.then((r) => {
resolve(r.body);
});
return;
}
var r = request.defaults(options);
r(
{
url,
},
function (err, res, body) {
if (err) {
console.error("request source maping error", err, url);
reject(err);
} else {
resolve(body);
}
}
);
});
};
return ajax.bind(this);
}
resolveSourceCode(frameObject, options = { prettify: false }) {
const { prettify } = options;
return this._fetchCode(frameObject).then(async (code) => {
if (prettify) {
const {
formatted,
mappedFrameObject,
} = await prettifyAndMapFrameObject(code, frameObject);
frameObject = mappedFrameObject;
code = formatted;
}
frameObject.code = this.getSourceCodeObject(frameObject, code);
return frameObject;
});
}
_fetchCode(frameObject) {
return this._gps._get(frameObject.fileName);
}
getSourceCodeObject(frameObject, code) {
// Make it possible to look up full source code
// Easy to do it this way but pretty inelegant because
// - it duplicates the cache data (though probably not taking much extra space)
// - it only works if you first resolve a frame
this._sourceObjectUrlCache[frameObject.fileName] = code;
if (
code.includes("ENOTFOUND") &&
code.includes("fromjs-temporary-url.com:5555")
) {
return {
line: makeLine(
"Failed to fetch source code - this can happen if the server is restarted and you're trying to look at e.g. an eval script",
0
),
nextLines: [],
previousLines: [],
};
}
var lines = code.split("\n");
if (!lines[frameObject.lineNumber - 1]) {
console.log(
"line doesn't exist - maybe because the frame being mapped is inside init.js and doesn't map anywhere?"
);
}
const NUMBER_OF_LINES_TO_LOAD = 20;
return {
line: makeLine(
lines[frameObject.lineNumber - 1],
frameObject.columnNumber
),
previousLines: lines
.slice(
Math.max(frameObject.lineNumber - 1 - NUMBER_OF_LINES_TO_LOAD, 0),
frameObject.lineNumber - 1
)
.map((l) => makeLine(l, 0)),
nextLines: lines
.slice(
frameObject.lineNumber,
frameObject.lineNumber + 1 + NUMBER_OF_LINES_TO_LOAD
)
.map((l) => makeLine(l, 0)),
};
}
getFullSourceCode(url) {
let res = this._sourceObjectUrlCache[url];
if (res === undefined) {
res = this._sourceObjectUrlCache[url + "?dontprocess"];
}
if (res === undefined) {
throw Error("url not found in cache");
}
return res;
}
resolveFrameFromLoc(loc, prettifyIfNoSourceMap) {
if (!loc.start) {
return Promise.resolve({
line: makeLine("Loc has no start value " + JSON.stringify(loc), 0),
nextLines: [],
previousLines: [],
});
}
const frameObject: any = {};
frameObject.fileName = loc.url + "?dontprocess";
frameObject.lineNumber = loc.start.line;
frameObject.columnNumber = loc.start.column;
// copied from stacktrace-gps
function _findSourceMappingURL(source) {
var sourceMappingUrlRegExp = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/gm;
var lastSourceMappingUrl;
var matchSourceMappingUrl;
while ((matchSourceMappingUrl = sourceMappingUrlRegExp.exec(source))) {
// jshint ignore:line
lastSourceMappingUrl = matchSourceMappingUrl[1];
}
if (lastSourceMappingUrl) {
return lastSourceMappingUrl;
} else {
throw new Error("sourceMappingURL not found");
}
}
return new Promise((resolve) => {
// Currently only supports the happy path, and assumes
// sourcemap uses sourcescontent instead of URL refs
const finishWithoutSourceMaps = () => {
return this.resolveSourceCode(frameObject, {
prettify: prettifyIfNoSourceMap,
}).then((frameObjectWithSourceCode) => {
// frameObject.__debugOnly_FrameString = frameString;
resolve(frameObjectWithSourceCode);
});
};
if (
!frameObject.fileName.includes("http://fromjs-temporary-url.com:5555/")
) {
this._nonProxyGps
.pinpoint(frameObject)
.then((pinpointedFrameObject) => {
return this._nonProxyGps
._get(frameObject.fileName)
.then((unSourcemappedCode) => {
let smUrl = _findSourceMappingURL(unSourcemappedCode);
if (!(smUrl.includes("http") || smUrl.includes("https"))) {
const basePath = frameObject.fileName
.split("/")
.slice(0, -1)
.join("/");
smUrl = basePath + "/" + smUrl;
}
return this._nonProxyGps.sourceMapConsumerCache[smUrl].then(
(smConsumer) => {
const sourcesIndex = smConsumer.sources.indexOf(
pinpointedFrameObject.fileName
);
const code = smConsumer.sourcesContent[sourcesIndex];
pinpointedFrameObject.code = this.getSourceCodeObject(
pinpointedFrameObject,
code
);
resolve(pinpointedFrameObject);
}
);
});
})
.catch((err) => {
finishWithoutSourceMaps();
});
} else {
finishWithoutSourceMaps();
}
});
}
_resolveFrame(frameString, prettify) {
return new Promise<ResolvedStackFrame>((resolve, reject) => {
var cacheKey = frameString + (prettify ? "Pretty" : "Nonpretty");
if (this._cache[cacheKey]) {
return resolve(this._cache[cacheKey]);
}
var frameObject = ErrorStackParser.parse({ stack: frameString })[0];
const finish = (frame: ResolvedStackFrame) => {
frame.fileName = frame.fileName.replace(".dontprocess", "");
this._cache[cacheKey] = frame;
resolve(frame);
};
var gps = this._gps;
gps.pinpoint(frameObject).then(
(newFrame) => {
if (!prettify) {
this.resolveSourceCode(newFrame).then((newFrameWithSourceCode) => {
finish(newFrameWithSourceCode);
});
} else {
// this._fetchCode(newFrame).then(code => {
// var { mappedFrameObject, formatted } = prettifyAndMapFrameObject(
// code,
// newFrame
// );
// mappedFrameObject.code = getSourceCodeObject(
// mappedFrameObject,
// formatted
// );
// finish(mappedFrameObject);
// });
}
},
function () {
console.log("Pinpoint failed!", arguments);
reject("pinpoint failed");
}
);
});
}
resolveFrame(frameString) {
return this._resolveFrame(frameString, false);
}
resolveFrameAndPrettify(frameString) {
return this._resolveFrame(frameString, true);
}
}
export default StackFrameResolver;