diff options
Diffstat (limited to 'deno/service-manager')
-rw-r--r-- | deno/service-manager/deno.json | 12 | ||||
-rw-r--r-- | deno/service-manager/main.ts | 39 | ||||
-rw-r--r-- | deno/service-manager/template.ts | 122 |
3 files changed, 173 insertions, 0 deletions
diff --git a/deno/service-manager/deno.json b/deno/service-manager/deno.json new file mode 100644 index 0000000..9f30853 --- /dev/null +++ b/deno/service-manager/deno.json @@ -0,0 +1,12 @@ +{ + "version": "0.1.0", + "tasks": { + "run": "deno run -A main.ts", + "compile": "deno compile -o out/manage -A main.ts" + }, + "imports": { + "@std/dotenv": "jsr:@std/dotenv@^0.225.5", + "@std/fs": "jsr:@std/fs@^1.0.18", + "mustache": "npm:mustache@^4.2.0" + } +} diff --git a/deno/service-manager/main.ts b/deno/service-manager/main.ts new file mode 100644 index 0000000..93f4c1b --- /dev/null +++ b/deno/service-manager/main.ts @@ -0,0 +1,39 @@ +import { parseArgs } from "@std/cli"; +import { loadVariables, TemplateDir } from "./template.ts"; +import { join } from "@std/path"; + +if (import.meta.main) { + const args = parseArgs(Deno.args, { + string: ["project-dir"], + boolean: ["no-dry-run"], + }); + + if (args._.length === 0) { + throw new Error("You must specify a command."); + } + + const projectDir = args["project-dir"]; + if (projectDir == null) { + throw new Error("You must specify project-dir."); + } + + const command = String(args._[0]); + + switch (command) { + case "gen-tmpl": + new TemplateDir( + join(projectDir, "services/templates"), + ).generateWithVariableFiles( + [ + join(projectDir, "data/config"), + join(projectDir, "services/config.template"), + ], + args["no-dry-run"] === true + ? join(projectDir, "services/generated") + : undefined, + ); + break; + default: + throw new Error(command + " is not a valid command."); + } +} diff --git a/deno/service-manager/template.ts b/deno/service-manager/template.ts new file mode 100644 index 0000000..0b043a1 --- /dev/null +++ b/deno/service-manager/template.ts @@ -0,0 +1,122 @@ +import { dirname, join, relative } from "@std/path"; +import { copySync, existsSync, walkSync } from "@std/fs"; +import { parse } from "@std/dotenv"; +import { distinct } from "@std/collections"; +// @ts-types="npm:@types/mustache" +import Mustache from "mustache"; + +Mustache.tags = ["@@", "@@"]; +Mustache.escape = (value) => String(value); + +function getVariableKeys(original: string): string[] { + return distinct( + Mustache.parse(original) + .filter(function (v) { + return v[0] === "name"; + }) + .map(function (v) { + return v[1]; + }), + ); +} + +export function loadVariables(files: string[]): Record<string, string> { + const vars: Record<string, string> = {}; + for (const file of files) { + const text = Deno.readTextFileSync(file); + for (const [key, valueText] of Object.entries(parse(text))) { + getVariableKeys(valueText).forEach((name) => { + if (!(name in vars)) { + throw new Error( + `Variable ${name} is not defined yet, perhaps due to typos or wrong order.`, + ); + } + }); + vars[key] = Mustache.render(valueText, vars); + } + } + return vars; +} + +const TEMPLATE_FILE_EXT = ".template"; + +export class TemplateDir { + templates: { path: string; ext: string; text: string; vars: string[] }[] = []; + plains: { path: string }[] = []; + + constructor(public dir: string) { + console.log("Scanning template dir:"); + Array.from( + walkSync(dir, { includeDirs: false, followSymlinks: true }), + ).forEach(({ path }) => { + path = relative(this.dir, path); + if (path.endsWith(TEMPLATE_FILE_EXT)) { + console.log(` (template) ${path}`); + const text = Deno.readTextFileSync(join(dir, path)); + this.templates.push({ + path, + ext: TEMPLATE_FILE_EXT, + text, + vars: getVariableKeys(text), + }); + } else { + console.log(` (plain) ${path}`); + this.plains.push({ path }); + } + }); + console.log("Done scanning template dir."); + } + + allNeededVars() { + return distinct(this.templates.flatMap((t) => t.vars)); + } + + generate(vars: Record<string, string>, generatedDir?: string) { + console.log( + `Generating, template dir: ${this.dir}, generated dir: ${generatedDir ?? "[dry-run]"}:`, + ); + + const undefinedVars = this.allNeededVars().filter((v) => !(v in vars)); + if (undefinedVars.length !== 0) { + throw new Error( + `Needed variables are not defined: ${undefinedVars.join(", ")}`, + ); + } + + if (generatedDir != null) { + if (existsSync(generatedDir)) { + console.log(` delete old generated dir ${generatedDir}`); + Deno.removeSync(generatedDir, { recursive: true }); + } + + for (const file of this.plains) { + const [source, destination] = [ + join(this.dir, file.path), + join(generatedDir, file.path), + ]; + console.log(` copy ${source} to ${destination} ...`); + Deno.mkdirSync(dirname(destination), { recursive: true }); + copySync(source, destination); + } + for (const file of this.templates) { + const [source, destination] = [ + join(this.dir, file.path), + join(generatedDir, file.path.slice(0, -file.ext.length)), + ]; + console.log(` generate ${source} to ${destination} ...`); + const rendered = Mustache.render(file.text, vars); + Deno.mkdirSync(dirname(destination), { recursive: true }); + Deno.writeTextFileSync(destination, rendered); + } + } + console.log(`Done generating.`); + } + + generateWithVariableFiles(varFiles: string[], generatedDir?: string) { + console.log("Scanning defined vars:"); + const vars = loadVariables(varFiles); + Object.keys(vars).forEach((name) => console.log(` ${name}`)); + console.log("Done scanning defined vars."); + this.generate(vars, generatedDir); + } +} |