UNPKG

@lbu/code-gen

Version:

Generate various boring parts of your server

158 lines (142 loc) 3.92 kB
import { readFileSync } from "fs"; import path from "path"; import { inspect } from "util"; import { AppError, camelToSnakeCase, isNil, processDirectoryRecursiveSync, } from "@lbu/stdlib"; import { lowerCaseFirst, upperCaseFirst } from "./utils.js"; /** * @type {{context: Object<string, Function>, globals: Object<string, Function>}} */ export const templateContext = { globals: { isNil, upperCaseFirst, lowerCaseFirst, camelToSnakeCase, inspect: (arg) => inspect(arg, { sorted: true, colors: false, depth: 15 }), quote: (x) => `"${x}"`, }, context: {}, }; /** * @param {string} name * @param {string} str * @param {object} [opts={}] * @param {boolean} [opts.debug] */ export function compileTemplate(name, str, opts = {}) { if (isNil(name) || isNil(str)) { throw new TypeError("Both name and string are required"); } const compiled = str .split("\n") .map((it) => { const cleaned = it.replace(/[\r\n\t]/g, " "); const markStarts = cleaned.split("{{").join("\t"); const evaluate = markStarts .replace(/((^|}})[^\t]*)'/g, "$1\r") .replace(/\t=(.*?)}}/g, "',$1,'"); // Syntax fixes, e.g start new push when necessary return evaluate .split("\t") .join("');") .split("}}") .join("p.push('") .split("\r") .join("\\'"); }) .join("\\n"); const debugString = opts.debug ? "console.dir({ contextKeys: Object.keys(_ctx), data: it }, {colors: true, depth: null});" : ""; try { templateContext.context[name] = new Function( "_ctx", "it", ` const p = []; ${debugString} with (_ctx) { with (it) { p.push('${compiled}'); }} return p.join('') .replace(/[ \\t\\r]+/g, ' ') // Replace all multiple spaces, with a single .replace(/^[ \\t\\r]+/gm, '') // Replace spaces at start of sentence with nothing .replace(/^(\\s*\\r?\\n){1,}/gm, '\\n') // Replace multiple new lines .replace(/^\\s*\\*\\s*([^\\n\\/\\*]+)\\s*\\n+/gm, ' * $1\\n ') // replace empty lines in JSDoc .replace(/\\n\\n/gm, '\\n') // Remove empty lines .replace(/\\(\\(newline\\)\\)/g, '\\n\\n'); // Controlled newlines `, ); } catch (e) { const err = new Error(`Error while compiling ${name} template`); err.originalErr = e; err.templateName = name; err.compiled = compiled; throw err; } } /** * @param {string} dir * @param {string} extension * @param {ProcessDirectoryOptions} [opts] */ export function compileTemplateDirectory(dir, extension, opts) { const ext = extension[0] !== "." ? `.${extension}` : extension; processDirectoryRecursiveSync( dir, (file) => { if (!file.endsWith(ext)) { return; } const content = readFileSync(file, "utf-8"); const name = path.parse(file).name; compileTemplate(name, content); }, opts, ); } /** * @param {string} name * @param {*} data * @returns {string} */ export function executeTemplate(name, data) { if (isNil(templateContext)) { throw new TypeError( "TemplateContext is required, please create a new one with `newTemplateContext()`", ); } if (isNil(templateContext.context[name])) { throw new Error(`Unknown template: ${name}`); } try { return templateContext.context[name](getExecutionContext(), data).trim(); } catch (e) { throw new AppError( "codeGen.executeTemplate.error", 500, { message: "Error while executing template", template: name, }, e, ); } } /** * Combine globals and registered templates into a single object */ function getExecutionContext() { const result = { ...templateContext.globals, }; for (const [key, item] of Object.entries(templateContext.context)) { result[key] = item.bind(undefined, result); } return result; }