aboutsummaryrefslogtreecommitdiff
path: root/deno/service-manager
diff options
context:
space:
mode:
Diffstat (limited to 'deno/service-manager')
-rw-r--r--deno/service-manager/deno.json12
-rw-r--r--deno/service-manager/main.ts39
-rw-r--r--deno/service-manager/template.ts122
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);
+ }
+}