// Text we look for in an error stack to guess whether a given line represents a import { scope as scopeFn, trailingSlash } from "@weborigami/async-tree"; import codeFragment from "./codeFragment.js"; import { typos } from "./typos.js"; // function in the Origami source code. const origamiSourceSignals = [ "async-tree/src/", "language/src/", "origami/src/", "at Scope.evaluate", ]; export async function builtinReferenceError(tree, builtins, key) { const messages = [ `"${key}" is being called as if it were a builtin function, but it's not.`, ]; // See if the key is in scope (but not as a builtin) const scope = scopeFn(tree); const value = await scope.get(key); if (value === undefined) { const typos = await formatScopeTypos(builtins, key); messages.push(typos); } else { messages.push(`Use "${key}/" instead.`); } const message = messages.join(" "); return new ReferenceError(message); } /** * Format an error for display in the console. * * @param {Error} error */ export function formatError(error) { let message; if (error.stack) { // Display the stack only until we reach the Origami source code. message = ""; let lines = error.stack.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (maybeOrigamiSourceCode(line)) { break; } if (message) { message += "\n"; } message += lines[i]; } } else { message = error.toString(); } // Add location let location = /** @type {any} */ (error).location; if (location) { const fragment = codeFragment(location); let { source, start } = location; message += `\nevaluating: ${fragment}`; if (typeof source === "object" && source.url) { message += `\n at ${source.url.href}:${start.line}:${start.column}`; } else if (source.text.includes("\n")) { message += `\n at line ${start.line}, column ${start.column}`; } } return message; } export async function formatScopeTypos(scope, key) { const keys = await scopeTypos(scope, key); // Don't match deprecated keys const filtered = keys.filter((key) => !key.startsWith("@")); if (filtered.length === 0) { return ""; } const quoted = filtered.map((key) => `"${key}"`); const list = quoted.join(", "); return `Maybe you meant ${list}?`; } export function maybeOrigamiSourceCode(text) { return origamiSourceSignals.some((signal) => text.includes(signal)); } export async function scopeReferenceError(scope, key) { const messages = [ `"${key}" is not in scope.`, await formatScopeTypos(scope, key), ]; const message = messages.join(" "); return new ReferenceError(message); } // Return all possible typos for `key` in scope async function scopeTypos(scope, key) { const scopeKeys = [...(await scope.keys())]; const normalizedScopeKeys = scopeKeys.map((key) => trailingSlash.remove(key)); const normalizedKey = trailingSlash.remove(key); return typos(normalizedKey, normalizedScopeKeys); }