@ackplus/nest-dynamic-templates
Version:
A powerful and flexible dynamic template rendering library for NestJS applications with support for Nunjucks, Handlebars, EJS, Pug, MJML, Markdown, and more.
162 lines • 5.86 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.diagnoseRenderError = diagnoseRenderError;
const template_errors_1 = require("./template.errors");
function diagnoseRenderError(cause, source, context, meta) {
const err = toError(cause);
const contextKeys = context ? Object.keys(context) : [];
const location = extractLocation(err.message, meta);
const missingVariable = extractMissingVariable(err.message, source, location);
const snippet = location ? lineAt(source, location.line) : undefined;
const reason = missingVariable
? `variable "${missingVariable}" is undefined`
: cleanMessage(err.message);
const subject = meta.templateName ? `template "${meta.templateName}"` : 'inline content';
const tags = [
[meta.engine, meta.language].filter(Boolean).join(' → '),
meta.scope && `scope=${meta.scope}`,
meta.locale && `locale=${meta.locale}`,
].filter(Boolean);
const where = tags.length ? ` [${tags.join(', ')}]` : '';
const message = `Failed to render ${subject}${where}: ${stripTrailingDot(reason)}.`;
const hint = missingVariable
? buildMissingVarHint(missingVariable, contextKeys)
: stageHint(meta.stage, meta.engine, meta.language);
return new template_errors_1.TemplateRenderError({
message,
stage: meta.stage,
engine: meta.engine,
language: meta.language,
templateName: meta.templateName,
scope: meta.scope,
scopeId: meta.scopeId,
locale: meta.locale,
missingVariable,
contextKeys,
location,
snippet,
hint,
}, err);
}
function buildMissingVarHint(variable, contextKeys) {
const provided = contextKeys.length
? `The render context has: [${contextKeys.join(', ')}].`
: `The render context was empty.`;
return `Pass "${variable}" in the render context. ${provided} To make it optional, guard it in the template, e.g. {{ ${variable} or "" }}.`;
}
function stageHint(stage, engine, language) {
switch (stage) {
case 'template-engine':
return `Check the ${engine ?? 'template'} syntax and that every referenced variable is provided.`;
case 'language-engine':
return `Check that the content is valid ${language ?? 'markup'} after variable interpolation.`;
case 'layout':
return `The error happened while rendering the layout wrapper around this template.`;
case 'subject':
return `The error happened while rendering the subject line.`;
default:
return undefined;
}
}
function toError(cause) {
if (cause instanceof Error)
return cause;
if (typeof cause === 'string')
return new Error(cause);
try {
return new Error(JSON.stringify(cause));
}
catch {
return new Error(String(cause));
}
}
function extractLocation(message, meta) {
const patterns = [
/\[Line (\d+),\s*Column (\d+)\]/i,
/Parse error on line (\d+)/i,
/(?:ejs|pug):(\d+)/i,
/>>\s*(\d+)\s*\|/,
/\bon line (\d+)(?::(\d+))?/i,
/\bline (\d+)(?::(\d+))?/i,
];
for (const re of patterns) {
const m = message.match(re);
if (m) {
const line = Number(m[1]);
const column = m[2] !== undefined ? Number(m[2]) : undefined;
if (Number.isFinite(line) && line > 0) {
return column !== undefined && Number.isFinite(column)
? { line, column }
: { line };
}
}
}
return undefined;
}
function extractMissingVariable(message, source, location) {
const refError = message.match(/(\b[\w$][\w.$]*) is not defined/);
if (refError)
return refError[1];
const njkCall = message.match(/Unable to call `?([\w.$]+)`?, which is undefined/i);
if (njkCall)
return njkCall[1];
if (/attempted to output null or undefined value/i.test(message) && location) {
return expressionAt(source, location);
}
return undefined;
}
const TOKEN_RE = /\{\{-?\s*([\s\S]*?)\s*-?\}\}|<%[=\-_]?\s*([\s\S]*?)\s*[-_]?%>|#\{\s*([\s\S]*?)\s*\}/g;
function expressionAt(source, location) {
const lineText = rawLineAt(source, location.line);
if (!lineText)
return undefined;
const tokens = [];
TOKEN_RE.lastIndex = 0;
let m;
while ((m = TOKEN_RE.exec(lineText)) !== null) {
const expr = (m[1] ?? m[2] ?? m[3] ?? '').trim();
if (expr)
tokens.push({ start: m.index, expr });
}
if (tokens.length === 0)
return undefined;
let chosen = tokens[0];
if (location.column !== undefined) {
const col = location.column;
for (const t of tokens) {
if (t.start <= col)
chosen = t;
}
}
return leadingVariable(chosen.expr);
}
function leadingVariable(expr) {
const beforeFilter = expr.split('|')[0].trim();
const path = beforeFilter.match(/^[\w$][\w.$]*(?:\[[^\]]+\])*/);
return path ? path[0] : undefined;
}
function lineAt(source, line) {
const raw = rawLineAt(source, line);
if (raw === undefined)
return undefined;
const trimmed = raw.trim();
return trimmed.length > 200 ? `${trimmed.slice(0, 197)}...` : trimmed;
}
function rawLineAt(source, line) {
if (!source)
return undefined;
const lines = source.split(/\r?\n/);
return lines[line - 1];
}
function cleanMessage(message) {
return message
.replace(/^Error:\s*/i, '')
.replace(/\(unknown path\)\s*/gi, '')
.replace(/\[Line \d+,\s*Column \d+\]\s*/gi, '')
.replace(/\s+/g, ' ')
.trim();
}
function stripTrailingDot(s) {
return s.replace(/\.+$/, '');
}
//# sourceMappingURL=diagnose.js.map