feat(#455): improve error logging inside of expressions (#476)

This commit is contained in:
Nate Moore 2021-06-16 16:15:25 -05:00 committed by GitHub
parent e7b2a6d8dd
commit 03ebdc3387
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 123 additions and 10 deletions

View file

@ -13,7 +13,7 @@ import _babelGenerator from '@babel/generator';
import babelParser from '@babel/parser';
import { codeFrameColumns } from '@babel/code-frame';
import * as babelTraverse from '@babel/traverse';
import { error, warn } from '../../logger.js';
import { error, warn, parseError } from '../../logger.js';
import { fetchContent } from './content.js';
import { isFetchContent } from './utils.js';
import { yellow } from 'kleur/colors';
@ -21,6 +21,8 @@ import { isComponentTag } from '../utils';
import { renderMarkdown } from '@astrojs/markdown-support';
import { transform } from '../transform/index.js';
import { PRISM_IMPORT } from '../transform/prism.js';
import { positionAt } from '../utils';
import { readFileSync } from 'fs';
const traverse: typeof babelTraverse.default = (babelTraverse.default as any).default;
@ -170,14 +172,38 @@ function getComponentWrapper(_name: string, { url, importSpecifier }: ComponentI
}
/** Evaluate expression (safely) */
function compileExpressionSafe(raw: string): string {
let { code } = transformSync(raw, {
loader: 'tsx',
jsxFactory: 'h',
jsxFragment: 'Fragment',
charset: 'utf8',
});
return code;
function compileExpressionSafe(raw: string, { state, compileOptions, location }: { state: CodegenState, compileOptions: CompileOptions, location: { start: number, end: number } }): string|null {
try {
let { code } = transformSync(raw, {
loader: 'tsx',
jsxFactory: 'h',
jsxFragment: 'Fragment',
charset: 'utf8'
});
return code;
} catch ({ errors }) {
const err = new Error() as any;
const e = errors[0];
err.filename = state.filename;
const text = readFileSync(state.filename).toString();
const start = positionAt(location.start, text);
start.line += e.location.line;
start.character += e.location.column + 1;
err.start = { line: start.line, column: start.character };
const end = { ...start };
end.character += e.location.length;
const frame = codeFrameColumns(text, {
start: { line: start.line, column: start.character },
end: { line: end.line, column: end.character },
})
err.frame = frame;
err.message = e.text;
parseError(compileOptions.logging, err);
return null;
}
}
interface CompileResult {
@ -473,8 +499,11 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
raw += children[nextChildIndex++];
}
}
const location = { start: node.start, end: node.end };
// TODO Do we need to compile this now, or should we compile the entire module at the end?
let code = compileExpressionSafe(raw).trim().replace(/\;$/, '');
let code = compileExpressionSafe(raw, { state, compileOptions, location });
if (code === null) throw new Error(`Unable to compile expression`);
code = code.trim().replace(/\;$/, '');
if (!FALSY_EXPRESSIONS.has(code)) {
if (state.markers.insideMarkdown) {
buffers[curr] += `{${code}}`;

View file

@ -2,3 +2,87 @@
export function isComponentTag(tag: string) {
return /^[A-Z]/.test(tag) || /^[a-z]+\./.test(tag);
}
export interface Position {
line: number;
character: number;
}
/** Clamps a number between min and max */
export function clamp(num: number, min: number, max: number): number {
return Math.max(min, Math.min(max, num));
}
/**
* Get the line and character based on the offset
* @param offset The index of the position
* @param text The text for which the position should be retrived
*/
export function positionAt(offset: number, text: string): Position {
offset = clamp(offset, 0, text.length);
const lineOffsets = getLineOffsets(text);
let low = 0;
let high = lineOffsets.length;
if (high === 0) {
return { line: 0, character: offset };
}
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (lineOffsets[mid] > offset) {
high = mid;
} else {
low = mid + 1;
}
}
// low is the least x for which the line offset is larger than the current offset
// or array.length if no line offset is larger than the current offset
const line = low - 1;
return { line, character: offset - lineOffsets[line] };
}
/**
* Get the offset of the line and character position
* @param position Line and character position
* @param text The text for which the offset should be retrived
*/
export function offsetAt(position: Position, text: string): number {
const lineOffsets = getLineOffsets(text);
if (position.line >= lineOffsets.length) {
return text.length;
} else if (position.line < 0) {
return 0;
}
const lineOffset = lineOffsets[position.line];
const nextLineOffset = position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : text.length;
return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
}
/** Get the offset of all lines */
function getLineOffsets(text: string) {
const lineOffsets = [];
let isLineStart = true;
for (let i = 0; i < text.length; i++) {
if (isLineStart) {
lineOffsets.push(i);
isLineStart = false;
}
const ch = text.charAt(i);
isLineStart = ch === '\r' || ch === '\n';
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
i++;
}
}
if (isLineStart && text.length > 0) {
lineOffsets.push(text.length);
}
return lineOffsets;
}