diff options
author | Yuqian Yang <crupest@crupest.life> | 2025-06-09 22:02:33 +0800 |
---|---|---|
committer | Yuqian Yang <crupest@crupest.life> | 2025-06-10 15:25:12 +0800 |
commit | 133bbc64f891afc1ae6c965176cddbc9050f9e80 (patch) | |
tree | d4411565d41e214df0aefd79bfe9240ab8f0fb0f | |
parent | cc013584714e8ce1f583ce391f8123881e3c0297 (diff) | |
download | crupest-133bbc64f891afc1ae6c965176cddbc9050f9e80.tar.gz crupest-133bbc64f891afc1ae6c965176cddbc9050f9e80.tar.bz2 crupest-133bbc64f891afc1ae6c965176cddbc9050f9e80.zip |
refactor(deno): done template generation.
-rw-r--r-- | deno/deno.json | 8 | ||||
-rw-r--r-- | deno/deno.lock | 36 | ||||
-rw-r--r-- | deno/mail-relay/deno.json | 1 | ||||
-rw-r--r-- | deno/service-manager/deno.json | 13 | ||||
-rw-r--r-- | deno/service-manager/main.ts | 39 | ||||
-rw-r--r-- | deno/service-manager/template.ts | 122 | ||||
-rw-r--r-- | services/config.template | 3 |
7 files changed, 218 insertions, 4 deletions
diff --git a/deno/deno.json b/deno/deno.json index 0199e14..71ad398 100644 --- a/deno/deno.json +++ b/deno/deno.json @@ -1,15 +1,17 @@ { - "workspace": [ "./base", "./mail-relay" ], + "workspace": [ "./base", "./service-manager", "./mail-relay" ], "tasks": { - "compile:mail-relay": "deno task --cwd=mail-relay compile" + "compile:mail-relay": "deno task --cwd=mail-relay compile", + "compile:service-manager": "deno task --cwd=service-manager compile" }, "imports": { "@std/cli": "jsr:@std/cli@^1.0.19", + "@std/collections": "jsr:@std/collections@^1.1.1", "@std/csv": "jsr:@std/csv@^1.0.6", "@std/encoding": "jsr:@std/encoding@^1.0.10", "@std/expect": "jsr:@std/expect@^1.0.16", "@std/io": "jsr:@std/io@^0.225.2", "@std/path": "jsr:@std/path@^1.1.0", - "@std/testing": "jsr:@std/testing@^1.0.13", + "@std/testing": "jsr:@std/testing@^1.0.13" } } diff --git a/deno/deno.lock b/deno/deno.lock index 336e6e0..357b31f 100644 --- a/deno/deno.lock +++ b/deno/deno.lock @@ -8,14 +8,17 @@ "jsr:@std/async@^1.0.13": "1.0.13", "jsr:@std/bytes@^1.0.5": "1.0.6", "jsr:@std/cli@^1.0.19": "1.0.19", + "jsr:@std/collections@^1.1.1": "1.1.1", "jsr:@std/csv@^1.0.6": "1.0.6", "jsr:@std/data-structures@^1.0.8": "1.0.8", + "jsr:@std/dotenv@~0.225.5": "0.225.5", "jsr:@std/encoding@1": "1.0.10", "jsr:@std/encoding@^1.0.10": "1.0.10", "jsr:@std/expect@^1.0.16": "1.0.16", "jsr:@std/fmt@1": "1.0.8", "jsr:@std/fs@1": "1.0.17", "jsr:@std/fs@^1.0.17": "1.0.17", + "jsr:@std/fs@^1.0.18": "1.0.18", "jsr:@std/internal@^1.0.6": "1.0.7", "jsr:@std/internal@^1.0.7": "1.0.7", "jsr:@std/internal@^1.0.8": "1.0.8", @@ -30,10 +33,13 @@ "npm:@aws-sdk/client-sesv2@^3.821.0": "3.824.0", "npm:@hono/zod-validator@0.7": "0.7.0_hono@4.7.11_zod@3.25.51", "npm:@smithy/fetch-http-handler@^5.0.4": "5.0.4", + "npm:@types/lodash@*": "4.17.17", + "npm:@types/mustache@*": "4.2.6", "npm:@types/node@*": "22.15.15", "npm:email-addresses@5": "5.0.0", "npm:hono@^4.7.11": "4.7.11", "npm:kysely@~0.28.2": "0.28.2", + "npm:mustache@^4.2.0": "4.2.0", "npm:zod@^3.25.48": "3.25.51" }, "jsr": { @@ -71,6 +77,9 @@ "@std/cli@1.0.19": { "integrity": "b3601a54891f89f3f738023af11960c4e6f7a45dc76cde39a6861124cba79e88" }, + "@std/collections@1.1.1": { + "integrity": "eff6443fbd9d5a6697018fb39c5d13d5f662f0045f21392d640693d0008ab2af" + }, "@std/csv@1.0.6": { "integrity": "52ef0e62799a0028d278fa04762f17f9bd263fad9a8e7f98c14fbd371d62d9fd", "dependencies": [ @@ -80,6 +89,9 @@ "@std/data-structures@1.0.8": { "integrity": "2fb7219247e044c8fcd51341788547575653c82ae2c759ff209e0263ba7d9b66" }, + "@std/dotenv@0.225.5": { + "integrity": "9ce6f9d0ec3311f74a32535aa1b8c62ed88b1ab91b7f0815797d77a6f60c922f" + }, "@std/encoding@1.0.10": { "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" }, @@ -99,6 +111,12 @@ "jsr:@std/path@^1.0.9" ] }, + "@std/fs@1.0.18": { + "integrity": "24bcad99eab1af4fde75e05da6e9ed0e0dce5edb71b7e34baacf86ffe3969f3a", + "dependencies": [ + "jsr:@std/path@^1.1.0" + ] + }, "@std/internal@1.0.7": { "integrity": "39eeb5265190a7bc5d5591c9ff019490bd1f2c3907c044a11b0d545796158a0f" }, @@ -1153,6 +1171,12 @@ "tslib" ] }, + "@types/lodash@4.17.17": { + "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==" + }, + "@types/mustache@4.2.6": { + "integrity": "sha512-t+8/QWTAhOFlrF1IVZqKnMRJi84EgkIK5Kh0p2JV4OLywUvCwJPFxbJAl7XAow7DVIHsF+xW9f1MVzg0L6Szjw==" + }, "@types/node@22.15.15": { "integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==", "dependencies": [ @@ -1178,6 +1202,10 @@ "kysely@0.28.2": { "integrity": "sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==" }, + "mustache@4.2.0": { + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": true + }, "strnum@1.1.2": { "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==" }, @@ -1198,6 +1226,7 @@ "workspace": { "dependencies": [ "jsr:@std/cli@^1.0.19", + "jsr:@std/collections@^1.1.1", "jsr:@std/csv@^1.0.6", "jsr:@std/encoding@^1.0.10", "jsr:@std/expect@^1.0.16", @@ -1218,6 +1247,13 @@ "npm:kysely@~0.28.2", "npm:zod@^3.25.48" ] + }, + "service-manager": { + "dependencies": [ + "jsr:@std/dotenv@~0.225.5", + "jsr:@std/fs@^1.0.18", + "npm:mustache@^4.2.0" + ] } } } diff --git a/deno/mail-relay/deno.json b/deno/mail-relay/deno.json index e03ba93..9105747 100644 --- a/deno/mail-relay/deno.json +++ b/deno/mail-relay/deno.json @@ -2,7 +2,6 @@ "version": "0.1.0", "tasks": { "run": "deno run -A aws/app.ts", - "test": "deno test -A", "compile": "deno compile -o out/crupest-relay -A aws/app.ts" }, "imports": { diff --git a/deno/service-manager/deno.json b/deno/service-manager/deno.json new file mode 100644 index 0000000..2ba8394 --- /dev/null +++ b/deno/service-manager/deno.json @@ -0,0 +1,13 @@ + +{ + "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..be6cb60 --- /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); + } +} diff --git a/services/config.template b/services/config.template index 89448ce..6f69c6d 100644 --- a/services/config.template +++ b/services/config.template @@ -1,3 +1,6 @@ +CRUPEST_GITHUB=https://github.com/crupest +CRUPEST_SERVICES_DIR=services +CRUPEST_DATA_DIR=data CRUPEST_ROOT_URL=https://@@CRUPEST_DOMAIN@@ CRUPEST_MAIL_SERVER_DOMAIN=mail.@@CRUPEST_DOMAIN@@ CRUPEST_DOCKER_DIR=@@CRUPEST_SERVICES_DIR@@/docker |