|
| 1 | +import * as fs from 'fs-extra' |
| 2 | +import { isBinaryFile } from 'isbinaryfile' |
| 3 | +import * as path from 'path' |
| 4 | +import * as ejs from 'ejs' |
| 5 | +import fm from 'front-matter' |
| 6 | + |
| 7 | +export interface Action { |
| 8 | + templateDir: string |
| 9 | + target: string |
| 10 | + overwrite?: boolean |
| 11 | +} |
| 12 | + |
| 13 | +export interface CodeGenResult { |
| 14 | + status: 'add' | 'overwrite' | 'skipped' |
| 15 | + type: 'text' | 'binary' |
| 16 | + file: string |
| 17 | + content: string |
| 18 | +} |
| 19 | + |
| 20 | +export interface CodeGenResults { |
| 21 | + files: Array<CodeGenResult> |
| 22 | + failed: Array<Error> |
| 23 | +} |
| 24 | + |
| 25 | +/** |
| 26 | + * Utility for generating files from ejs templates or for general scaffolding purposes. |
| 27 | + * Given a templte directory, all files within will be moved to the target directory specified whilst |
| 28 | + * maintaining the folder heirarchy. It supports both text and binary files, with text files having the |
| 29 | + * additional ablity to be rendered with .ejs support meaning any arguments passed in can be interpolated |
| 30 | + * into the file. For custom file naming, front-matter can be used to specify the output fileName. |
| 31 | + */ |
| 32 | +export async function codeGenerator ( |
| 33 | + action: Action, |
| 34 | + args: { [key: string]: any }, |
| 35 | +): Promise<CodeGenResults> { |
| 36 | + const templateFiles = await allFilesInDir(action.templateDir) |
| 37 | + const codeGenResults: CodeGenResults = { files: [], failed: [] } |
| 38 | + |
| 39 | + for (const file of templateFiles) { |
| 40 | + const isBinary = await isBinaryFile(file) |
| 41 | + const parsedFile = path.parse(file) |
| 42 | + |
| 43 | + const processBinaryFile = async () => { |
| 44 | + const rawFileContent = await fs.readFile(file) |
| 45 | + const computedPath = computePath( |
| 46 | + action.templateDir, |
| 47 | + action.target, |
| 48 | + file, |
| 49 | + args, |
| 50 | + ) |
| 51 | + |
| 52 | + return { computedPath, content: rawFileContent, type: 'binary' } as const |
| 53 | + } |
| 54 | + |
| 55 | + const processTextFile = async () => { |
| 56 | + const fileContent = (await fs.readFile(file)).toString() |
| 57 | + const { body, renderedAttributes } = frontMatter(fileContent, args) |
| 58 | + const computedPath = computePath( |
| 59 | + action.templateDir, |
| 60 | + action.target, |
| 61 | + path.join( |
| 62 | + parsedFile.dir, |
| 63 | + renderedAttributes.fileName || parsedFile.base, |
| 64 | + ), |
| 65 | + args, |
| 66 | + ) |
| 67 | + const renderedTemplate = ejs.render(body, args) |
| 68 | + |
| 69 | + return { computedPath, content: renderedTemplate, type: 'text' } as const |
| 70 | + } |
| 71 | + |
| 72 | + try { |
| 73 | + const { content, computedPath, type } = isBinary |
| 74 | + ? await processBinaryFile() |
| 75 | + : await processTextFile() |
| 76 | + |
| 77 | + const exists = await fileExists(computedPath) |
| 78 | + const status = !exists |
| 79 | + ? 'add' |
| 80 | + : exists && action.overwrite |
| 81 | + ? 'overwrite' |
| 82 | + : 'skipped' |
| 83 | + |
| 84 | + if (status === 'add' || status === 'overwrite') { |
| 85 | + await fs.outputFile(computedPath, content) |
| 86 | + } |
| 87 | + |
| 88 | + codeGenResults.files.push({ |
| 89 | + file: computedPath, |
| 90 | + type, |
| 91 | + status, |
| 92 | + content: content.toString(), |
| 93 | + }) |
| 94 | + } catch (e) { |
| 95 | + codeGenResults.failed.push(e as Error) |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + return codeGenResults |
| 100 | +} |
| 101 | + |
| 102 | +function computePath ( |
| 103 | + srcFolder: string, |
| 104 | + target: string, |
| 105 | + filePath: string, |
| 106 | + substitutions: { [k: string]: any }, |
| 107 | +): string { |
| 108 | + const relativeFromSrcFolder = path.relative(srcFolder, filePath) |
| 109 | + let computedPath = path.join(target, relativeFromSrcFolder) |
| 110 | + |
| 111 | + Object.entries(substitutions).forEach(([propertyName, value]) => { |
| 112 | + computedPath = computedPath.split(`{{${propertyName}}}`).join(value) |
| 113 | + }) |
| 114 | + |
| 115 | + return computedPath |
| 116 | +} |
| 117 | + |
| 118 | +async function allFilesInDir (parent: string): Promise<string[]> { |
| 119 | + let res: string[] = [] |
| 120 | + |
| 121 | + for (const dir of await fs.readdir(parent)) { |
| 122 | + const child = path.join(parent, dir) |
| 123 | + const isDir = (await fs.stat(child)).isDirectory() |
| 124 | + |
| 125 | + if (!isDir) { |
| 126 | + res.push(child) |
| 127 | + } else { |
| 128 | + res = [...res, ...(await allFilesInDir(child))] |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + return res |
| 133 | +} |
| 134 | + |
| 135 | +function frontMatter (content: string, args: { [key: string]: any }) { |
| 136 | + const { attributes, body } = fm(content, { allowUnsafe: true }) as { |
| 137 | + attributes: { [key: string]: string } |
| 138 | + body: string |
| 139 | + } |
| 140 | + const renderedAttributes = Object.entries(attributes).reduce( |
| 141 | + (acc, [key, val]) => ({ ...acc, [key]: ejs.render(val, args) }), |
| 142 | + {} as { [key: string]: string }, |
| 143 | + ) |
| 144 | + |
| 145 | + return { body, renderedAttributes } |
| 146 | +} |
| 147 | + |
| 148 | +async function fileExists (absolute: string) { |
| 149 | + try { |
| 150 | + await fs.access(absolute, fs.constants.F_OK) |
| 151 | + |
| 152 | + return true |
| 153 | + } catch (e) { |
| 154 | + return false |
| 155 | + } |
| 156 | +} |
0 commit comments