UNPKG

@compas/code-gen

Version:

Generate various boring parts of your server

161 lines (145 loc) 3.93 kB
import { readFileSync } from "fs"; import path from "path"; import { AppError, camelToSnakeCase, isNil, processDirectoryRecursiveSync, } from "@compas/stdlib"; import { formatDocString } from "./generator/comments.js"; import { lowerCaseFirst, upperCaseFirst } from "./utils.js"; /** * @type {{context: Record<string, Function>, globals: Record<string, Function>}} */ export const templateContext = { globals: { isNil, upperCaseFirst, lowerCaseFirst, camelToSnakeCase, quote: (x) => `"${x}"`, formatDocString, }, context: {}, }; /** * @param {string} name * @param {string} str * @param {{ debug?: boolean }} [opts={}] */ 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') // Reformat lines in JSDoc .replace(/\\n\\n/gm, '\\n') // Remove empty lines .replace(/\\(\\(newline\\)\\)/g, '\\n\\n'); // Controlled newlines `, ); } catch (e) { throw AppError.serverError( { message: `Error while compiling template`, templateName: name, compiled, }, // @ts-ignore e, ); } } /** * @param {string} dir * @param {string} extension * @param {import("@compas/stdlib").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 (/** @type {any} */ 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; }