Skip to content

Commit

Permalink
Add pretty stack and code-frame to error output (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
marvinhagemeister authored Jul 5, 2020
1 parent 8dd3d04 commit 93bc270
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 65 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"babel-plugin-istanbul": "^6.0.0",
"chalk": "^2.3.0",
"dlv": "^1.1.3",
"errorstacks": "^1.3.0",
"expect": "^24.9.0",
"istanbul-instrumenter-loader": "^3.0.1",
"jasmine-core": "^3.5.0",
Expand All @@ -67,7 +68,8 @@
"minimatch": "^3.0.4",
"puppeteer": "^4.0.1",
"sade": "^1.7.3",
"script-loader": "^0.7.2"
"script-loader": "^0.7.2",
"simple-code-frame": "^1.0.0"
},
"peerDependencies": {
"webpack": ">=4"
Expand Down
170 changes: 106 additions & 64 deletions src/lib/util.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import { createCodeFrame } from 'simple-code-frame';
import { parseStackTrace } from 'errorstacks';

export function moduleDir(name) {
let file = require.resolve(name),
Expand Down Expand Up @@ -36,13 +38,40 @@ export function dedupe(value, index, arr) {
return arr.indexOf(value) === index;
}

let firstFile;
export function indent(str, level) {
const space = ' '.repeat(level);
return str
.split('\n')
.map((line) => space + line)
.join('\n');
}

/**
* Colorize a pre-formatted code frame
* @param {string} str
*/
export function highlightCodeFrame(str) {
return str
.split('\n')
.map((line) => {
if (/^>\s(.*)/.test(line)) {
return line.replace(/^>(.*)/, (_, content) => {
return chalk.bold.redBright('>') + chalk.white(content);
});
} else if (/^\s+\|\s+\^/.test(line)) {
return line
.replace('|', chalk.dim('|'))
.replace('^', chalk.bold.redBright('^'));
}
return chalk.dim(line);
})
.join('\n');
}

export function cleanStack(str, cwd = process.cwd()) {
str = str.replace(/^[\s\S]+\n\n([A-Za-z]*Error: )/g, '$1');

firstFile = null;

let clean = str.replace(
let stack = str.replace(
new RegExp(
`( |\\()(https?:\\/\\/localhost:\\d+\\/base\\/|webpack:///|${cwd.replace(
/([\\/[\]()*+$!^.,?])/g,
Expand All @@ -52,71 +81,84 @@ export function cleanStack(str, cwd = process.cwd()) {
),
replacer
);
if (firstFile != null) {
let [filename, line, char] = firstFile;
if (line) {
let read;
try {
read = fs.readFileSync(path.resolve(cwd, filename), 'utf8');
} catch (e) {}
if (read) {
let start = Math.max(0, char - 40);
let startLine = Math.max(0, line - 3);
read = read.split('\n');
clean = clean.replace(/\n +/g, '\n ');
if (line < read.length) {
clean +=
'\n\n' +
chalk.white(
highlight(
outdent(
[
read.slice(startLine, line).join('\n'),
read[line].substr(start, char + 30),
new Array(char).join('-') + '^',
read.slice(line + 1, line + 4).join('\n'),
].join('\n'),
' ',
process.stdout.columns - 10
),
line - startLine,
2
)
);
}
}

let frames = parseStackTrace(stack);

// Some frameworks mess with the stack. Use a simple heuristic
// to find the beginning of the proper stack.
let message = stack;
if (frames.length) {
let lines = stack.split('\n');
let stackStart = lines.indexOf(frames[0].raw);
if (stackStart > 0) {
message = lines
.slice(0, stackStart)
.map((s) => s.trim())
.join('\n');
}
}
return chalk.red(clean);
}

function replacer(str, before, root, filename, position) {
if (firstFile == null) {
let [line, char] = position.match(/\d+/g) || [];
firstFile = [filename, line - 1, char | 0];
/**
* The nearest location where the user's code triggered the error.
* @type {import('errorstacks').StackFrame}
*/
let nearestFrame;

stack = frames
.map((frame) => {
// Only show frame for errors in the user's code
if (!nearestFrame && !/node_modules/.test(frame.fileName)) {
nearestFrame = frame;
}

// Native traces don't have an error location
if (!frame.name || frame.type === 'native') {
return chalk.gray(frame.raw.trim());
}

const {
sourceFileName,
column,
fileName,
line,
name,
sourceColumn,
sourceLine,
} = frame;

const loc = chalk.cyanBright(`${fileName}:${line}:${column}`);
const originalLoc =
sourceFileName !== ''
? chalk.gray(' <- ') +
chalk.gray(`${sourceFileName}:${sourceLine}:${sourceColumn}`)
: '';
return chalk.gray(`at ${name} (${loc}${originalLoc})`);
})
.join('\n');

let codeFrame = '';
if (nearestFrame) {
try {
const { fileName, line, column } = nearestFrame;
if (fileName) {
const content = fs.readFileSync(fileName, 'utf-8');
codeFrame = createCodeFrame(content, line, column - 1, {
before: 2,
after: 2,
});
codeFrame = highlightCodeFrame(codeFrame);
codeFrame = indent(codeFrame, 2) + '\n';
}
} catch (err) {
// eslint-disable-next-line no-console
console.log('INTERNAL WARNING: Failed to read stack frame code: ' + err);
}
}
return before + chalk.blue('./' + filename + chalk.dim(position));
}

function highlight(text, line, count) {
let lines = text.split('\n');
return (
chalk.dim(lines.slice(0, line).join('\n')) +
'\n' +
lines.slice(line, line + count).join('\n') +
'\n' +
chalk.dim(lines.slice(line + count).join('\n'))
);
message = indent(chalk.reset(message), 2);
return `\n${message}\n\n${codeFrame}${indent(stack, 4)}\n`;
}

function outdent(str, prefix = '', width = 80) {
str = str.replace(/(^\n+|\n+$)/g, '').replace(/\t/, ' ');
let indents = str.match(/^[ -]+/gm) || [];
let minLength = indents.reduce(
(indent, value) => Math.min(indent, value.length),
indents[0] ? indents[0].length : 0
);
str = str.replace(/^[ -]+/gm, (str) => prefix + str.substring(minLength));
str = str.replace(/^.*$/gm, (str) => str.substring(0, width));
return str;
function replacer(str, before, root, filename, position) {
return before + './' + filename + position;
}

0 comments on commit 93bc270

Please sign in to comment.