edgejs-cli
Version:
CLI utility to generate files based on edge templates
224 lines (223 loc) • 7.61 kB
JavaScript
import { parseArgs } from "node:util";
import DeepMerge from "@fastify/deepmerge";
import path from "node:path";
import { Edge, Template } from "edge.js";
import yaml from "js-yaml";
import { globby } from 'globby';
import pMap from "p-map";
import fs from "node:fs/promises";
import dedent from "dedent";
const args = await getArgs();
const edge = bootstrapEdge();
const dMerge = DeepMerge();
const templates = await collectTemplates();
if (!(templates === null || templates === void 0 ? void 0 : templates.length))
fail("No templates could be found");
const context = await parseContext(args.dataPath);
await generateFiles(templates, context);
// ---
async function parseContext(ctxPath) {
if (!ctxPath)
return {};
const rawContext = await fs.readFile(ctxPath, "utf-8").catch(e => {
fail(`Failed to read context file: ${ctxPath}`);
});
if (rawContext && ctxPath.endsWith(".json")) {
try {
return JSON.parse(rawContext);
}
catch (e) {
console.error(e);
fail(`Failed to parse context file as json: ${ctxPath}`);
}
}
if (rawContext && (ctxPath.endsWith(".yaml") || ctxPath.endsWith(".yml"))) {
try {
return yaml.load(rawContext);
}
catch (e) {
console.error(e);
fail(`Failed to parse context file as yaml: ${ctxPath}`);
}
}
fail(`Unsupported context file extension: ${ctxPath}`);
}
async function collectTemplates() {
var _a, _b;
const inputDir = (_a = args.inputDir) !== null && _a !== void 0 ? _a : process.cwd();
if (args.inputPath) {
const tmpl = getTemplate(args.inputPath);
const inputStat = await fs.stat(tmpl.path);
if (!inputStat.isFile()) {
fail(`Expected inputPath to point to a file`);
}
edge.registerTemplate(tmpl.key, {
template: await fs.readFile(tmpl.path, "utf-8")
});
return [tmpl];
}
const inputExt = (_b = args.inputExtension) !== null && _b !== void 0 ? _b : "edge";
const inputStat = await fs.stat(inputDir);
if (!inputStat.isDirectory()) {
fail(`Expected inputPath to point to a file`);
}
edge.mount(path.resolve(inputDir));
const pattern = `**/*.${inputExt}`;
const templatePaths = await globby([pattern], {
cwd: inputDir
});
return compact(templatePaths.map(p => {
if (isIgnored(p))
return null;
return getTemplate(p);
}));
}
async function generateFiles(templates, context) {
const failedKeys = [];
await pMap(templates, (t) => generateFile(t, context).catch(e => {
console.error(`Failure generating ${t}:`, e);
failedKeys.push(t.key);
}), { concurrency: 5 });
if (failedKeys.length)
fail(`Failed to generate: ${failedKeys.join(", ")}`);
}
async function generateFile(template, baseContext) {
var _a, _b;
const context = await getContext(template, baseContext);
const content = await edge.render(template.key, context);
if (template.isMulti) {
return generateMulti(template, content);
}
const outPath = (_a = args.outputPath) !== null && _a !== void 0 ? _a : (args.skipOutputExtension
? template.key
: `${template.key}.${(_b = args.outputExtension) !== null && _b !== void 0 ? _b : "html"}`);
await writeFile(outPath, content, template.path);
}
async function generateMulti(template, content) {
var _a;
const xml2js = await import("xml2js");
const tree = await xml2js.parseStringPromise(`<root>${content}</root>`);
if (!Array.isArray(tree.root.file))
throw new Error(`Invalid format: ${template.path} - file tag missing`);
for (const f of tree.root.file) {
if (args.outputPath)
fail("outputPath can not be used with multi templates");
const outPath = (_a = f === null || f === void 0 ? void 0 : f.$) === null || _a === void 0 ? void 0 : _a.path;
if (typeof outPath !== "string") {
throw new Error(`Invalid format: ${template.path} - path attribute missing`);
}
let fileContent = f._ || "";
if (isAttrTrue(f.$.dedent))
fileContent = dedent(fileContent);
if (isAttrTrue(f.$.trim) || isAttrTrue(f.$.strip))
fileContent = fileContent.trim();
await writeFile(outPath, fileContent, template.path);
}
}
function isAttrTrue(val) {
const lVal = val === null || val === void 0 ? void 0 : val.toLowerCase();
return lVal === "true" || lVal === "yes";
}
async function writeFile(outPath, content, sourcePath) {
var _a;
const outDir = (_a = args.outputDir) !== null && _a !== void 0 ? _a : process.cwd();
const finalOutPath = path.resolve(outDir, outPath);
console.log(`Generating file: ${sourcePath} -> ${finalOutPath}`);
await fs.mkdir(path.dirname(finalOutPath), {
recursive: true
});
await fs.writeFile(finalOutPath, content, {
encoding: "utf-8"
});
}
async function getContext(template, baseContext) {
var _a;
if (!args.relativeContextPath)
return baseContext;
const localContext = await parseContext(path.resolve((_a = args.inputPath) !== null && _a !== void 0 ? _a : process.cwd(), path.dirname(template.path), args.relativeContextPath));
return dMerge(baseContext, localContext);
}
function bootstrapEdge() {
const edge = Edge.create({
cache: false
});
if (args.skipEscaping) {
Template.prototype.escape = (input) => input;
}
return edge;
}
async function getArgs() {
const { values } = parseArgs({
options: {
inputPath: {
type: "string",
short: "i"
},
inputDir: {
type: "string",
short: "I"
},
outputPath: {
type: "string",
short: "o"
},
outputDir: {
type: "string",
short: "O"
},
dataPath: {
type: "string",
short: "d"
},
relativeContextPath: {
type: "string",
short: "c"
},
inputExtension: {
type: "string",
},
outputExtension: {
type: "string",
},
skipOutputExtension: {
type: "boolean",
},
skipEscaping: {
type: "boolean",
},
}
});
if (values.outputDir && values.outputPath) {
fail(`outputDir or outputPath can not be used together`);
}
if (values.inputDir && values.outputPath) {
fail(`outputDir must be used instead of outputPath if inputDir is passed`);
}
return values;
}
function fail(msg) {
console.error(msg);
process.exit(1);
}
function isIgnored(p) {
return p.startsWith(".") || p.startsWith("_");
}
function getFileNameWithoutExt(p) {
return path.basename(p, path.extname(p));
}
function getTemplate(subPath) {
var _a;
const inputDir = (_a = args.inputDir) !== null && _a !== void 0 ? _a : process.cwd();
const inputPath = path.resolve(inputDir, subPath);
const baseName = getFileNameWithoutExt(subPath);
const multiMatch = baseName.match(/^(.*)\.multi$/);
const key = path.join(path.dirname(subPath), baseName);
return {
path: inputPath,
key,
isMulti: !!multiMatch
};
}
function compact(arr) {
return arr.filter(Boolean);
}