diff --git a/deps.ts b/deps.ts index 5b86a25..8120f72 100644 --- a/deps.ts +++ b/deps.ts @@ -1,4 +1,4 @@ -export * as path from "jsr:@std/path@1.0.8"; +export * as path from "https://deno.land/std@0.224.0/path/mod.ts"; export * as html from "jsr:@std/html@1.0.3"; export * as astring from "jsr:@davidbonnet/astring@1.8.6"; diff --git a/src/environment.ts b/src/environment.ts index bfa4c72..cd5040f 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -1,7 +1,7 @@ import tokenize, { Token } from "./tokenizer.ts"; import type { Loader } from "./loader.ts"; -import { transformTemplateCode } from "./transformer.ts"; +import { TransformError, transformTemplateCode } from "./transformer.ts"; export interface TemplateResult { content: string; @@ -139,21 +139,8 @@ export class Environment { try { code = transformTemplateCode(code, dataVarname); } catch (error) { - const end = (error as ParseError).start; - const lastPos = code.slice(0, end).lastIndexOf("__pos = "); - const lastPosEnd = code.slice(lastPos).indexOf(";"); - - if (lastPos > -1 && lastPosEnd > lastPos) { - const pos = parseInt( - code.slice(lastPos + 8, lastPos + lastPosEnd), - 10, - ); - throw this.createError( - path || "", - source, - pos, - new Error((error as ParseError).message), - ); + if (error instanceof TransformError) { + throw this.createError(path, source, error.pos, error); } throw error; @@ -192,7 +179,7 @@ export class Environment { const { position, error } = result; if (error) { - throw this.createError(path || "unknown", source, position, error); + throw this.createError(path, source, position, error); } for (const tokenPreprocessor of this.tokenPreprocessors) { @@ -325,19 +312,17 @@ export class Environment { } createError( - path: string, - source: string, - position: number, + path: string = "unknown", + source: string = "", + position: number = 0, cause: Error, ): Error { - if (!source) { - return cause; - } - const [line, column, code] = errorLine(source, position); return new Error( - `Error in the template ${path}:${line}:${column}\n\n${code.trim()}\n\n> ${cause.message}\n`, + `Error in the template ${path}:${line}:${column}\n\n${code.trim()}\n\n${ + cause.message.replaceAll(/^/gm, "> ") + }\n`, { cause }, ); } @@ -386,9 +371,3 @@ export function errorLine( function checkAsync(fn: () => unknown): boolean { return fn.constructor?.name === "AsyncFunction"; } - -interface ParseError extends Error { - start: number; - end: number; - range: [number, number]; -} diff --git a/src/transformer.ts b/src/transformer.ts index 551e1ec..73355f6 100644 --- a/src/transformer.ts +++ b/src/transformer.ts @@ -1,5 +1,27 @@ import { astring, ESTree, meriyah, walker } from "../deps.ts"; +// Declare types +interface ParseError extends Error { + start: number; + end: number; + range: [number, number]; + loc: Record<"start" | "end", Record<"line" | "column", number>>; + description: string; +} + +interface TransformErrorOptions extends ErrorOptions { + pos?: number; +} + +export class TransformError extends Error { + pos?: number; + constructor(message: string, options?: TransformErrorOptions) { + super(message); + this.name = "TransformError"; + this.pos = options?.pos; + } +} + // List of identifiers that are in globalThis // but should be accessed as templateState.identifier const INCLUDE_GLOBAL = [ @@ -119,7 +141,29 @@ export function transformTemplateCode( return code; } - const parsed = meriyah.parseScript(code, { module: true }) as ESTree.Program; + let parsed; + try { + parsed = meriyah.parseScript(code, { module: true }) as ESTree.Program; + } catch (error) { + const { message, start, loc } = error as ParseError; + + // Use information from `meriyah` to annotate the part of + // the compiled template function that triggered the ParseError + const annotation = code.split("\n")[loc.start.line - 1] + "\n" + + " ".repeat(loc.start.column) + "\x1b[31m^\x1b[0m"; + + // Grab the last instance of Vento's `__pos` variable before the + // error was thrown. Pass this back to Vento's createError to + // tie this error with problmatic template code + const matches = [...code.slice(0, start).matchAll(/__pos = (\d+);/g)]; + const pos = Number(matches.at(-1)?.[1]); + + throw new TransformError( + `[meriyah] ${message}\nthrown while parsing compiled template function:\n\n${annotation}`, + { pos }, + ); + } + const tracker = new ScopeTracker(); const exclude = [ @@ -128,11 +172,11 @@ export function transformTemplateCode( ]; if (parsed.type !== "Program") { - throw new Error("Expected a program"); + throw new TransformError("[meriyah] Expected a program"); } if (parsed.body.length === 0) { - throw new Error("Empty program"); + throw new TransformError("[meriyah] Empty program"); } // Transforms an identifier to a MemberExpression @@ -166,7 +210,7 @@ export function transformTemplateCode( } walker.walk(parsed, { - enter(node) { + enter(node: ESTree.Node) { switch (node.type) { // Track variable declarations case "VariableDeclaration":