aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuqian Yang <crupest@crupest.life>2025-06-09 22:02:33 +0800
committerYuqian Yang <crupest@crupest.life>2025-06-10 15:25:12 +0800
commit133bbc64f891afc1ae6c965176cddbc9050f9e80 (patch)
treed4411565d41e214df0aefd79bfe9240ab8f0fb0f
parentcc013584714e8ce1f583ce391f8123881e3c0297 (diff)
downloadcrupest-133bbc64f891afc1ae6c965176cddbc9050f9e80.tar.gz
crupest-133bbc64f891afc1ae6c965176cddbc9050f9e80.tar.bz2
crupest-133bbc64f891afc1ae6c965176cddbc9050f9e80.zip
refactor(deno): done template generation.
-rw-r--r--deno/deno.json8
-rw-r--r--deno/deno.lock36
-rw-r--r--deno/mail-relay/deno.json1
-rw-r--r--deno/service-manager/deno.json13
-rw-r--r--deno/service-manager/main.ts39
-rw-r--r--deno/service-manager/template.ts122
-rw-r--r--services/config.template3
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