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 | e956c5c35ad1ccdc9ae1ec5d4b66e45541985e1a (patch) | |
| tree | abc95edc46d9b6cdefa57e0b3bce8c54a8d6e042 /deno | |
| parent | 0b702d027973ea26d7e9618d4edc181d4cd1fc31 (diff) | |
| download | crupest-e956c5c35ad1ccdc9ae1ec5d4b66e45541985e1a.tar.gz crupest-e956c5c35ad1ccdc9ae1ec5d4b66e45541985e1a.tar.bz2 crupest-e956c5c35ad1ccdc9ae1ec5d4b66e45541985e1a.zip | |
refactor(deno): done template generation.
Diffstat (limited to 'deno')
| -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 | 
6 files changed, 215 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); +  } +} | 
