UNPKG

@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
"use strict"; 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