Browse Source

Add support for multiple paths on inputs and templates

Craig Fletcher 11 months ago
parent
commit
c1d966cfb9
8 changed files with 148 additions and 65 deletions
  1. 26 6
      README.md
  2. 33 23
      eslint.config.js
  3. 7 1
      package.json
  4. 10 6
      src/defaults.js
  5. 2 2
      src/index.js
  6. 20 20
      src/lib.js
  7. 11 7
      src/processors.js
  8. 39 0
      src/util.js

+ 26 - 6
README.md

@@ -45,7 +45,9 @@ You can also build a self-contained executable if you like chunky binaries with
 A working example of how to configure can be found in `src/defaults.js`, with the processors separated out to
 A working example of how to configure can be found in `src/defaults.js`, with the processors separated out to
 `src/processors.js` for tidiness. 
 `src/processors.js` for tidiness. 
 
 
-Your `rhedyn.config.js` should export an object with "tasks" and "opts". 
+Your `rhedyn.config.js` should export an object with "tasks" and "opts". These are detailed below.
+
+If you want to extend the default config, it can be imported as `defaultConfig` from this package.
 
 
 #### opts
 #### opts
 
 
@@ -53,6 +55,19 @@ The `opts` object in your config is for anything related to the project as a who
 means things like the base output directory, where to find templates for the renderer, etc. It'll be passed to the
 means things like the base output directory, where to find templates for the renderer, etc. It'll be passed to the
 processor function as a key on the `meta` object.
 processor function as a key on the `meta` object.
 
 
+`opts` can also have an `include` property, an object containing task keys (e.g. "styles") and paths to include for
+that task, such as additional styles or templates. The path can be anywhere, not just local to the project - I use it to
+include basic typography styles that I know I'll want in every project, but it could just as easily be used to produce a
+sort-of "theme" that could be shared across multiple projects.
+
+  ```
+    opts: {
+      baseDir: 'dist/',
+      templatesDirs: ['templates/'],
+      defaultTemplate: 'default',
+      include: { styles: ['~/.rhedyn/styles/'] }
+    },
+  ```
 
 
 #### tasks
 #### tasks
 
 
@@ -61,7 +76,7 @@ Tasks should be an array of objects that look something like this:
   ```
   ```
   {
   {
     name: "styles",
     name: "styles",
-    inputDir: "styles/",
+    inputDirs: ["styles/"],
     outputDir: "static/styles/",
     outputDir: "static/styles/",
     inputFileExtension: ".scss",
     inputFileExtension: ".scss",
     outputFileExtension: ".css",
     outputFileExtension: ".css",
@@ -70,7 +85,10 @@ Tasks should be an array of objects that look something like this:
   ```
   ```
 
 
 All properties are mandatory, and simply specify where we're reading the files in from and their extensions then where
 All properties are mandatory, and simply specify where we're reading the files in from and their extensions then where
-to put them once we've processed them. `processor` is a function that takes a file path as it's first argument, which is
+to put them once we've processed them. `inputDirs` is an array of paths, so you could include multiple paths here if you
+wish (some SCSS from a node module, for example).
+
+`processor` is a function that takes a file path as it's first argument, which is
 the file path to process, and `meta` as the second argument. `meta` contains the `opts` object, along with the
 the file path to process, and `meta` as the second argument. `meta` contains the `opts` object, along with the
 already-processed filepaths from other tasks:
 already-processed filepaths from other tasks:
 
 
@@ -78,8 +96,9 @@ already-processed filepaths from other tasks:
   {
   {
     opts: {
     opts: {
       baseDir: 'dist/',
       baseDir: 'dist/',
-      templatesDir: 'templates/',
-      defaultTemplate: 'default'
+      templateDirs: ['templates/'],
+      defaultTemplate: 'default',
+      include: { styles: ['~/.rhedyn/styles/'] }
     },
     },
     resources: { styles: [ 
     resources: { styles: [ 
       { path: 'static/styles/main.css', detail: {} } 
       { path: 'static/styles/main.css', detail: {} } 
@@ -93,4 +112,5 @@ A processor is just a function that takes a filepath and meta, returns an object
 optionally `detail`. `detail` can contain any properties you like, and is generally for annotating things like "date" or
 optionally `detail`. `detail` can contain any properties you like, and is generally for annotating things like "date" or
 "author" that might be extracted from the source file (think frontmatter and the like).
 "author" that might be extracted from the source file (think frontmatter and the like).
 
 
-Some examples can be found in `src/processors.js`.
+Some examples can be found in `src/processors.js`, and you can find some utility functions exported from this package as
+`utils`. The sample processors are also exported from this module, as `processors`.

+ 33 - 23
eslint.config.js

@@ -1,37 +1,47 @@
-import js from '@eslint/js'
+import globals from "globals"
+import js from "@eslint/js"
 
 
 export default [
 export default [
   js.configs.recommended,
   js.configs.recommended,
   {
   {
+    languageOptions: {
+      globals: {
+        ...globals.node,
+      },
+    },
     rules: {
     rules: {
-      /**
-       * Custom rules.
-       * docs: https://eslint.org/docs/rules
-       */
-      'quotes': ['error', 'double'],
-      'indent': [
-        'error', 2, { 'SwitchCase': 1 },
-      ],
-      'no-multi-spaces': 'error',
-      'prefer-const': 'error',
-      'array-bracket-newline': [
-        'error', {
+      "array-bracket-newline": [
+        "error",
+        {
           minItems: 3,
           minItems: 3,
           multiline: true,
           multiline: true,
         },
         },
       ],
       ],
-      'array-element-newline': ['error', 'consistent'],
-      'comma-dangle': ['error', 'always-multiline'],
-      'no-process-env': 'off',
-      'no-undef': 'error',
-      'no-multiple-empty-lines': [
-        'error', {
-          'max': 1,
+      "array-element-newline": ["error", "consistent"],
+      "comma-dangle": ["error", "always-multiline"],
+      indent: [
+        "error", 2, { SwitchCase: 1 },
+      ],
+      "no-console": "off",
+      "no-multi-spaces": "error",
+      "no-multiple-empty-lines": [
+        "error",
+        {
+          max: 1,
         },
         },
       ],
       ],
-      'no-var': 'error',
-      'object-curly-spacing': ['error', 'always'],
-      'semi': ['error', 'never'],
+      "no-process-env": "off",
+      "no-undef": "error",
+      "no-var": "error",
+      "object-curly-spacing": ["error", "always"],
+      "prefer-const": "error",
+      quotes: ["error", "double"],
+      semi: ["error", "never"],
+      "sort-imports": ["error", { ignoreCase: true }],
+      "sort-keys": [
+        "error", "asc", { caseSensitive: true, natural: true },
+      ],
+      "sort-vars": ["error", { ignoreCase: true }],
     },
     },
   },
   },
 ]
 ]

+ 7 - 1
package.json

@@ -5,11 +5,17 @@
   "main": "src/index.js",
   "main": "src/index.js",
   "scripts": {
   "scripts": {
     "generate": "node src/index.js",
     "generate": "node src/index.js",
-    "build": "nexe -i src/index.js -o dist/rhedyn --build --python=$(which python3)"
+    "build": "nexe -i src/index.js -o dist/rhedyn --build --python=$(which python3)",
+    "lint-fix": "eslint --fix src/*.js"
   },
   },
   "bin": {
   "bin": {
     "rhedyn": "src/index.js"
     "rhedyn": "src/index.js"
   },
   },
+  "exports": {
+    "./utils": "./src/util.js",
+    "./defaultConfig": "./src/defaults.js",
+    "./processors": "./src/processors.js"
+  },
   "keywords": [],
   "keywords": [],
   "author": "",
   "author": "",
   "license": "ISC",
   "license": "ISC",

+ 10 - 6
src/defaults.js

@@ -1,18 +1,19 @@
 import { compileSass, renderMarkdownWithTemplate } from "./processors.js"
 import { compileSass, renderMarkdownWithTemplate } from "./processors.js"
+
 export const tasks = [
 export const tasks = [
   {
   {
+    inputDirs: ["styles/"],
+    inputFileExtension: ".scss",
     name: "styles",
     name: "styles",
-    inputDir: "styles/",
     outputDir: "static/styles/",
     outputDir: "static/styles/",
-    inputFileExtension: ".scss",
     outputFileExtension: ".css",
     outputFileExtension: ".css",
     processor: compileSass,
     processor: compileSass,
   },
   },
   {
   {
+    inputDirs: ["markdown/"],
+    inputFileExtension: ".md",
     name: "pages",
     name: "pages",
-    inputDir: "markdown/",
     outputDir: "./",
     outputDir: "./",
-    inputFileExtension: ".md",
     outputFileExtension: ".html",
     outputFileExtension: ".html",
     processor: renderMarkdownWithTemplate,
     processor: renderMarkdownWithTemplate,
   },
   },
@@ -20,13 +21,16 @@ export const tasks = [
 
 
 export const opts = {
 export const opts = {
   baseDir: "dist/",
   baseDir: "dist/",
-  templatesDir: "templates/",
   defaultTemplate: "default",
   defaultTemplate: "default",
+  include: {
+    styles: ["~/.rhedyn/styles/"],
+  },
+  templateDirs: ["templates/", "~/.rhedyn/templates/"],
 }
 }
 
 
 const defaults = {
 const defaults = {
-  tasks,
   opts,
   opts,
+  tasks,
 }
 }
 
 
 export default defaults
 export default defaults

+ 2 - 2
src/index.js

@@ -1,9 +1,9 @@
 #!/usr/bin/env node
 #!/usr/bin/env node
 
 
+import * as defaultConfig from "./defaults.js"
 import { getConfig, processFiles } from "./lib.js"
 import { getConfig, processFiles } from "./lib.js"
-import defaults from "./defaults.js"
 
 
-const { opts, tasks } = await getConfig() || { ...defaults }
+const { opts, tasks } = await getConfig() || { ...defaultConfig }
 
 
 console.log(`Processing ${tasks.length} tasks`)
 console.log(`Processing ${tasks.length} tasks`)
 tasks.reduce(
 tasks.reduce(

+ 20 - 20
src/lib.js

@@ -1,5 +1,10 @@
-import path from "path"
+import {
+  readDirectoryRecursively,
+  removeBasePaths,
+  resolvePath,
+} from "./util.js"
 import fs from "fs"
 import fs from "fs"
+import path from "path"
 import process from "node:process"
 import process from "node:process"
 
 
 export async function getConfig() {
 export async function getConfig() {
@@ -10,35 +15,30 @@ export async function getConfig() {
       return config.default || config
       return config.default || config
     } catch (err) {
     } catch (err) {
       console.error("Error reading rhedyn.config.js:", err)
       console.error("Error reading rhedyn.config.js:", err)
-      return
+      throw new Error("Failed reading config file")
     }
     }
   } else {
   } else {
     return
     return
   }
   }
 }
 }
 
 
-export function readDirectoryRecursively(dir, files = []) {
-  const contents = fs.readdirSync(dir, { withFileTypes: true })
-  for (const item of contents) {
-    const itemPath = path.join(dir, item.name)
-    if (item.isDirectory()) {
-      readDirectoryRecursively(itemPath, files)
-    } else {
-      files.push(itemPath)
-    }
-  }
-  return files
-}
-
 export function processFiles(config, meta) {
 export function processFiles(config, meta) {
-  const filesToProcess = readDirectoryRecursively(config.inputDir)
+  const includes = meta.opts?.include?.[config.name] || []
+  const pathsToInclude = [...config.inputDirs, ...includes].map(resolvePath)
+  const filesToProcess = pathsToInclude
+    .map(dirPath => {
+      return readDirectoryRecursively(dirPath)
+    })
+    .flat()
+
   return filesToProcess.map(filePath => {
   return filesToProcess.map(filePath => {
     const fileOutputPath = path.join(
     const fileOutputPath = path.join(
       meta.opts.baseDir,
       meta.opts.baseDir,
       config.outputDir,
       config.outputDir,
-      filePath
-        .replace(config.inputDir, "")
-        .replace(config.inputFileExtension, config.outputFileExtension),
+      removeBasePaths(pathsToInclude, filePath).replace(
+        config.inputFileExtension,
+        config.outputFileExtension,
+      ),
     )
     )
     const fileOutputDir = path.dirname(fileOutputPath)
     const fileOutputDir = path.dirname(fileOutputPath)
     if (!fs.existsSync(fileOutputDir)) {
     if (!fs.existsSync(fileOutputDir)) {
@@ -49,8 +49,8 @@ export function processFiles(config, meta) {
     fs.writeFileSync(fileOutputPath, result)
     fs.writeFileSync(fileOutputPath, result)
 
 
     return {
     return {
-      path: fileOutputPath.replace(meta.opts.baseDir, ""),
       detail,
       detail,
+      path: fileOutputPath.replace(meta.opts.baseDir, ""),
     }
     }
   })
   })
 }
 }

+ 11 - 7
src/processors.js

@@ -1,10 +1,10 @@
+import * as sass from "sass"
+import { firstFound } from "./util.js"
 import fs from "fs"
 import fs from "fs"
-import path from "path"
-import matter from "gray-matter"
-import { marked } from "marked"
 import handlebars from "handlebars"
 import handlebars from "handlebars"
-import * as sass from "sass"
+import { marked } from "marked"
 import markedCodePreview from "marked-code-preview"
 import markedCodePreview from "marked-code-preview"
+import matter from "gray-matter"
 
 
 const markedRenderer = marked.use({ gfm: true }).use(markedCodePreview)
 const markedRenderer = marked.use({ gfm: true }).use(markedCodePreview)
 
 
@@ -12,9 +12,10 @@ export function renderMarkdownWithTemplate(filePath, meta) {
   const content = fs.readFileSync(filePath, "utf8")
   const content = fs.readFileSync(filePath, "utf8")
   const { data, content: markdown } = matter(content)
   const { data, content: markdown } = matter(content)
   const templateName = data.template || meta.opts.defaultTemplate
   const templateName = data.template || meta.opts.defaultTemplate
+
   const template = handlebars.compile(
   const template = handlebars.compile(
     fs.readFileSync(
     fs.readFileSync(
-      path.join(meta.opts.templatesDir, `${templateName}.hbs`),
+      firstFound(meta.opts.templateDirs, `${templateName}.hbs`),
       "utf8",
       "utf8",
     ),
     ),
   )
   )
@@ -23,10 +24,13 @@ export function renderMarkdownWithTemplate(filePath, meta) {
     ...meta,
     ...meta,
     content: markedRenderer(markdown),
     content: markedRenderer(markdown),
   })
   })
-  return { result: html, detail: data }
+  return {
+    detail: data,
+    result: html,
+  }
 }
 }
 
 
-export function compileSass(filePath, meta) {
+export function compileSass(filePath) {
   return {
   return {
     result: sass.compile(filePath, { style: "compressed" }).css.toString(),
     result: sass.compile(filePath, { style: "compressed" }).css.toString(),
   }
   }

+ 39 - 0
src/util.js

@@ -0,0 +1,39 @@
+import fs from "fs"
+import os from "node:os"
+import path from "path"
+
+export function readDirectoryRecursively(dir, files = []) {
+  if (!fs.existsSync(dir)) {
+    return files
+  }
+  const contents = fs.readdirSync(dir, { withFileTypes: true })
+  for (const item of contents) {
+    const itemPath = path.join(dir, item.name)
+    if (item.isDirectory()) {
+      readDirectoryRecursively(itemPath, files)
+    } else {
+      files.push(itemPath)
+    }
+  }
+  return files
+}
+
+export function resolvePath(unresolvedPath) {
+  return path.resolve(unresolvedPath.replace(/^~/, os.homedir()))
+}
+
+export function firstFound(dirs, fileName) {
+  for (const dir of dirs) {
+    const filePath = resolvePath(path.join(dir, fileName))
+    if (fs.existsSync(filePath)) {
+      return filePath
+    }
+  }
+  return null
+}
+
+export function removeBasePaths(baseDirs, fullPath) {
+  return baseDirs.reduce((cleanedPath, dir) => {
+    return cleanedPath.replace(dir, "")
+  }, fullPath)
+}