summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/astro/src/compiler/codegen/index.ts49
-rw-r--r--packages/astro/src/compiler/utils.ts84
2 files changed, 123 insertions, 10 deletions
diff --git a/packages/astro/src/compiler/codegen/index.ts b/packages/astro/src/compiler/codegen/index.ts
index b31fbf4a0..fe0905f8c 100644
--- a/packages/astro/src/compiler/codegen/index.ts
+++ b/packages/astro/src/compiler/codegen/index.ts
@@ -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}}`;
diff --git a/packages/astro/src/compiler/utils.ts b/packages/astro/src/compiler/utils.ts
index acbdf9c96..232f1b747 100644
--- a/packages/astro/src/compiler/utils.ts
+++ b/packages/astro/src/compiler/utils.ts
@@ -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;
+}