Переглянути джерело

Replace ejs with nunjucks

Loris Leiva 1 рік тому
батько
коміт
8f3260d552
4 змінених файлів з 88 додано та 141 видалено
  1. 3 31
      index.ts
  2. 2 1
      package.json
  3. 34 62
      pnpm-lock.yaml
  4. 49 47
      utils/renderTemplate.ts

+ 3 - 31
index.ts

@@ -1,18 +1,14 @@
 #!/usr/bin/env node
 
 import chalk from "chalk";
-import ejs from "ejs";
 import * as fs from "node:fs";
 import * as path from "node:path";
 
-import {
-  postOrderDirectoryTraverse,
-  preOrderDirectoryTraverse,
-} from "./utils/directoryTraverse";
+import { postOrderDirectoryTraverse } from "./utils/directoryTraverse";
 import { generateReadme } from "./utils/generateReadme";
 import { logBanner, logEnd, logStart } from "./utils/getLogs";
 import { RenderContext, getRenderContext } from "./utils/getRenderContext";
-import renderTemplate from "./utils/renderTemplate";
+import { renderTemplate } from "./utils/renderTemplate";
 
 init().catch((e) => {
   console.error(e);
@@ -25,10 +21,9 @@ async function init() {
   logStart(ctx);
 
   // Prepare rendering function and accumulate callbacks.
-  const callbacks = [];
   const render = function render(templateName: string) {
     const directory = path.resolve(ctx.templateDirectory, templateName);
-    renderTemplate(directory, ctx.targetDirectory, callbacks);
+    renderTemplate(ctx, directory, ctx.targetDirectory);
   };
 
   // Render base template.
@@ -39,29 +34,6 @@ async function init() {
   //   render("config/jsx");
   // }
 
-  // An external data store for callbacks to share data
-  const dataStore = {};
-  // Process callbacks
-  for (const cb of callbacks) {
-    await cb(dataStore);
-  }
-
-  // EJS template rendering
-  preOrderDirectoryTraverse(
-    ctx.targetDirectory,
-    () => {},
-    (filepath) => {
-      if (filepath.endsWith(".ejs")) {
-        const template = fs.readFileSync(filepath, "utf-8");
-        const dest = filepath.replace(/\.ejs$/, "");
-        const content = ejs.render(template, dataStore[dest]);
-
-        fs.writeFileSync(dest, content);
-        fs.unlinkSync(filepath);
-      }
-    }
-  );
-
   // README generation
   fs.writeFileSync(
     path.resolve(ctx.targetDirectory, "README.md"),

+ 2 - 1
package.json

@@ -35,11 +35,12 @@
     "@tsconfig/node20": "^20.1.2",
     "@types/eslint": "^8.56.5",
     "@types/node": "^20.11.24",
+    "@types/nunjucks": "^3.2.6",
     "@types/prompts": "^2.4.9",
     "chalk": "^5.3.0",
-    "ejs": "^3.1.9",
     "esbuild": "^0.18.20",
     "gradient-string": "^2.0.2",
+    "nunjucks": "^3.2.4",
     "prettier": "^3.2.5",
     "prompts": "^2.4.2",
     "zx": "^7.2.3"

+ 34 - 62
pnpm-lock.yaml

@@ -14,21 +14,24 @@ devDependencies:
   '@types/node':
     specifier: ^20.11.24
     version: 20.11.24
+  '@types/nunjucks':
+    specifier: ^3.2.6
+    version: 3.2.6
   '@types/prompts':
     specifier: ^2.4.9
     version: 2.4.9
   chalk:
     specifier: ^5.3.0
     version: 5.3.0
-  ejs:
-    specifier: ^3.1.9
-    version: 3.1.9
   esbuild:
     specifier: ^0.18.20
     version: 0.18.20
   gradient-string:
     specifier: ^2.0.2
     version: 2.0.2
+  nunjucks:
+    specifier: ^3.2.4
+    version: 3.2.4
   prettier:
     specifier: ^3.2.5
     version: 3.2.5
@@ -308,6 +311,10 @@ packages:
       undici-types: 5.26.5
     dev: true
 
+  /@types/nunjucks@3.2.6:
+    resolution: {integrity: sha512-pHiGtf83na1nCzliuAdq8GowYiXvH5l931xZ0YEHaLMNFgynpEqx+IPStlu7UaDkehfvl01e4x/9Tpwhy7Ue3w==}
+    dev: true
+
   /@types/prompts@2.4.9:
     resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==}
     dependencies:
@@ -327,6 +334,10 @@ packages:
     resolution: {integrity: sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g==}
     dev: true
 
+  /a-sync-waterfall@1.0.1:
+    resolution: {integrity: sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==}
+    dev: true
+
   /ansi-styles@4.3.0:
     resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
     engines: {node: '>=8'}
@@ -334,25 +345,8 @@ packages:
       color-convert: 2.0.1
     dev: true
 
-  /async@3.2.5:
-    resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
-    dev: true
-
-  /balanced-match@1.0.2:
-    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
-    dev: true
-
-  /brace-expansion@1.1.11:
-    resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
-    dependencies:
-      balanced-match: 1.0.2
-      concat-map: 0.0.1
-    dev: true
-
-  /brace-expansion@2.0.1:
-    resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
-    dependencies:
-      balanced-match: 1.0.2
+  /asap@2.0.6:
+    resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
     dev: true
 
   /braces@3.0.2:
@@ -386,8 +380,9 @@ packages:
     resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
     dev: true
 
-  /concat-map@0.0.1:
-    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+  /commander@5.1.0:
+    resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
+    engines: {node: '>= 6'}
     dev: true
 
   /data-uri-to-buffer@4.0.1:
@@ -406,14 +401,6 @@ packages:
     resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
     dev: true
 
-  /ejs@3.1.9:
-    resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==}
-    engines: {node: '>=0.10.0'}
-    hasBin: true
-    dependencies:
-      jake: 10.8.7
-    dev: true
-
   /esbuild@0.18.20:
     resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
     engines: {node: '>=12'}
@@ -481,12 +468,6 @@ packages:
       web-streams-polyfill: 3.3.3
     dev: true
 
-  /filelist@1.0.4:
-    resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
-    dependencies:
-      minimatch: 5.1.6
-    dev: true
-
   /fill-range@7.0.1:
     resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
     engines: {node: '>=8'}
@@ -580,17 +561,6 @@ packages:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
     dev: true
 
-  /jake@10.8.7:
-    resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==}
-    engines: {node: '>=10'}
-    hasBin: true
-    dependencies:
-      async: 3.2.5
-      chalk: 4.1.2
-      filelist: 1.0.4
-      minimatch: 3.1.2
-    dev: true
-
   /jsonfile@6.1.0:
     resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
     dependencies:
@@ -621,19 +591,6 @@ packages:
       picomatch: 2.3.1
     dev: true
 
-  /minimatch@3.1.2:
-    resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
-    dependencies:
-      brace-expansion: 1.1.11
-    dev: true
-
-  /minimatch@5.1.6:
-    resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
-    engines: {node: '>=10'}
-    dependencies:
-      brace-expansion: 2.0.1
-    dev: true
-
   /minimist@1.2.8:
     resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
     dev: true
@@ -652,6 +609,21 @@ packages:
       formdata-polyfill: 4.0.10
     dev: true
 
+  /nunjucks@3.2.4:
+    resolution: {integrity: sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==}
+    engines: {node: '>= 6.9.0'}
+    hasBin: true
+    peerDependencies:
+      chokidar: ^3.3.0
+    peerDependenciesMeta:
+      chokidar:
+        optional: true
+    dependencies:
+      a-sync-waterfall: 1.0.1
+      asap: 2.0.6
+      commander: 5.1.0
+    dev: true
+
   /path-type@4.0.0:
     resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
     engines: {node: '>=8'}

+ 49 - 47
utils/renderTemplate.ts

@@ -1,44 +1,48 @@
 import * as fs from "node:fs";
 import * as path from "node:path";
-import { pathToFileURL } from "node:url";
+import nunjucks, { ConfigureOptions } from "nunjucks";
 
 import { deepMerge } from "./deepMerge";
+import { RenderContext } from "./getRenderContext";
 import { sortDependencies } from "./sortDependencies";
+import {
+  camelCase,
+  kebabCase,
+  pascalCase,
+  snakeCase,
+  titleCase,
+} from "./strings";
 
 /**
- * Renders a template folder/file to the file system,
- * by recursively copying all files under the `src` directory,
+ * Renders a template folder/file to the provided destination,
+ * by recursively copying all files under the source directory,
  * with the following exception:
- *   - `_filename` should be renamed to `.filename`
- *   - Fields in `package.json` should be recursively merged
- * @param {string} src source filename to copy
- * @param {string} dest destination filename of the copy operation
+ *   - `_filename` are renamed to `.filename`.
+ *   - `package.json` files are merged.
+ *   - `.gitignore` files are concatenated.
+ *   - `.njk` files are rendered as nunjucks templates
+ *     and saved without the `.njk` extension.
  */
-function renderTemplate(src: string, dest: string, callbacks: Array<Function>) {
+export function renderTemplate(ctx: RenderContext, src: string, dest: string) {
   const stats = fs.statSync(src);
 
+  // Recursively render directories.
   if (stats.isDirectory()) {
-    // skip node_module
-    if (path.basename(src) === "node_modules") {
-      return;
-    }
+    // Skip node_module.
+    if (path.basename(src) === "node_modules") return;
 
-    // if it's a directory, render its subdirectories and files recursively
+    // Render its subdirectories and files recursively.
     fs.mkdirSync(dest, { recursive: true });
     for (const file of fs.readdirSync(src)) {
-      renderTemplate(
-        path.resolve(src, file),
-        path.resolve(dest, file),
-        callbacks
-      );
+      renderTemplate(ctx, path.resolve(src, file), path.resolve(dest, file));
     }
     return;
   }
 
   const filename = path.basename(src);
 
+  // Merge package.json files.
   if (filename === "package.json" && fs.existsSync(dest)) {
-    // merge instead of overwriting
     const existing = JSON.parse(fs.readFileSync(dest, "utf8"));
     const newPackage = JSON.parse(fs.readFileSync(src, "utf8"));
     const pkg = sortDependencies(deepMerge(existing, newPackage));
@@ -46,47 +50,45 @@ function renderTemplate(src: string, dest: string, callbacks: Array<Function>) {
     return;
   }
 
-  if (filename === "extensions.json" && fs.existsSync(dest)) {
-    // merge instead of overwriting
-    const existing = JSON.parse(fs.readFileSync(dest, "utf8"));
-    const newExtensions = JSON.parse(fs.readFileSync(src, "utf8"));
-    const extensions = deepMerge(existing, newExtensions);
-    fs.writeFileSync(dest, JSON.stringify(extensions, null, 2) + "\n");
-    return;
-  }
-
+  // Rename `_file` to `.file`.
   if (filename.startsWith("_")) {
-    // rename `_file` to `.file`
     dest = path.resolve(path.dirname(dest), filename.replace(/^_/, "."));
   }
 
+  // Append to existing .gitignore.
   if (filename === "_gitignore" && fs.existsSync(dest)) {
-    // append to existing .gitignore
     const existing = fs.readFileSync(dest, "utf8");
     const newGitignore = fs.readFileSync(src, "utf8");
     fs.writeFileSync(dest, existing + "\n" + newGitignore);
     return;
   }
 
-  // data file for EJS templates
-  if (filename.endsWith(".data.mjs")) {
-    // use dest path as key for the data store
-    dest = dest.replace(/\.data\.mjs$/, "");
-
-    // Add a callback to the array for late usage when template files are being processed
-    callbacks.push(async (dataStore) => {
-      const getData = (await import(pathToFileURL(src).toString())).default;
-
-      // Though current `getData` are all sync, we still retain the possibility of async
-      dataStore[dest] = await getData({
-        oldData: dataStore[dest] || {},
-      });
-    });
-
-    return; // skip copying the data file
+  // Render nunjucks templates.
+  if (filename.endsWith(".njk")) {
+    dest = dest.replace(/\.njk$/, "");
+    fs.writeFileSync(dest, resolveNunjunksTemplate(src, ctx));
+    return;
   }
 
   fs.copyFileSync(src, dest);
 }
 
-export default renderTemplate;
+function resolveNunjunksTemplate(
+  file: string,
+  context?: object,
+  options?: ConfigureOptions
+): string {
+  const directory = path.dirname(file);
+  const filename = path.basename(file);
+  const env = nunjucks.configure(directory, {
+    trimBlocks: true,
+    autoescape: false,
+    ...options,
+  });
+  env.addFilter("pascalCase", pascalCase);
+  env.addFilter("camelCase", camelCase);
+  env.addFilter("snakeCase", snakeCase);
+  env.addFilter("kebabCase", kebabCase);
+  env.addFilter("titleCase", titleCase);
+  return env.render(filename, context);
+}