mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-17 17:28:09 +00:00
feat: add --before-write cli option (#89)
* feat: add `--before-write` cli option * refactor: cleanup before write wrapper * docs: add before-write docs
This commit is contained in:
@@ -11,24 +11,57 @@ Usage: simple-scaffold [options]
|
||||
To see this and more information anytime, add the `-h` or `--help` flag to your call, e.g.
|
||||
`npx simple-scaffold@latest -h`.
|
||||
|
||||
| Command \| alias | |
|
||||
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--name` \| `-n` | Name to be passed to the generated files. `{{name}}` and other data parameters inside contents and file names will be replaced accordingly. You may omit the `--name` or `-n` for this specific option. |
|
||||
| `--config`\|`-c` | Filename or directory to load config from |
|
||||
| `--git`\|`-g` | Git URL or GitHub path to load a template from. |
|
||||
| `--key` \| `-k` | Key to load inside the config file. This overwrites the config key provided after the colon in `--config` (e.g. `--config scaffold.cmd.js:component`) |
|
||||
| `--output` \| `-o` | Path to output to. If `--create-sub-folder` is enabled, the subfolder will be created inside this path. Default is current working directory. |
|
||||
| `--templates` \| `-t` | Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path, or a glob pattern for multiple file matching easily. |
|
||||
| `--overwrite` \| `-w` | Enable to override output files, even if they already exist. |
|
||||
| `--data` \| `-d` | Add custom data to the templates. By default, only your app name is included. |
|
||||
| `--append-data` \| `-D` | Append additional custom data to the templates, which will overwrite `--data`, using an alternate syntax, which is easier to use with CLI: `-D key1=string -D key2:=raw` |
|
||||
| `--create-sub-folder` \| `-s` | Create subfolder with the input name |
|
||||
| `--sub-folder-name-helper` \| `-sh` | Default helper to apply to subfolder name when using `--create-sub-folder true`. |
|
||||
| `--quiet` \| `-q` | Suppress output logs (Same as `--log-level none`) |
|
||||
| `--log-level` \| `-l` | Determine amount of logs to display. The values are: `none \| debug \| info \| warn \| error`. The provided level will display messages of the same level or higher. |
|
||||
| `--dry-run` \| `-dr` | Don't emit files. This is good for testing your scaffolds and making sure they don't fail, without having to write actual file contents or create directories. |
|
||||
| `--help` \| `-h` | Show this help message |
|
||||
| `--version` \| `-v` | Display version. |
|
||||
| Command \| alias | |
|
||||
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--name` \| `-n` | Name to be passed to the generated files. `{{name}}` and other data parameters inside contents and file names will be replaced accordingly. You may omit the `--name` or `-n` for this specific option. |
|
||||
| `--config`\|`-c` | Filename or directory to load config from |
|
||||
| `--git`\|`-g` | Git URL or GitHub path to load a template from. |
|
||||
| `--key` \| `-k` | Key to load inside the config file. This overwrites the config key provided after the colon in `--config` (e.g. `--config scaffold.cmd.js:component`) |
|
||||
| `--output` \| `-o` | Path to output to. If `--create-sub-folder` is enabled, the subfolder will be created inside this path. Default is current working directory. |
|
||||
| `--templates` \| `-t` | Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path, or a glob pattern for multiple file matching easily. |
|
||||
| `--overwrite` \| `-w` | Enable to override output files, even if they already exist. |
|
||||
| `--data` \| `-d` | Add custom data to the templates. By default, only your app name is included. |
|
||||
| `--append-data` \| `-D` | Append additional custom data to the templates, which will overwrite `--data`, using an alternate syntax, which is easier to use with CLI: `-D key1=string -D key2:=raw` |
|
||||
| `--create-sub-folder` \| `-s` | Create subfolder with the input name |
|
||||
| `--sub-folder-name-helper` \| `-sh` | Default helper to apply to subfolder name when using `--create-sub-folder true`. |
|
||||
| `--quiet` \| `-q` | Suppress output logs (Same as `--log-level none`) |
|
||||
| `--log-level` \| `-l` | Determine amount of logs to display. The values are: `none \| debug \| info \| warn \| error`. The provided level will display messages of the same level or higher. |
|
||||
| `--before-write` \| `-B` | Run a script before writing the files. This can be a command or a path to a file. A temporary file path will be passed to the given command and the command should return a string for the final output. |
|
||||
| `--dry-run` \| `-dr` | Don't emit files. This is good for testing your scaffolds and making sure they don't fail, without having to write actual file contents or create directories. |
|
||||
| `--help` \| `-h` | Show this help message |
|
||||
| `--version` \| `-v` | Display version. |
|
||||
|
||||
### Before Write option
|
||||
|
||||
This option allows you to preprocess a file before it is being written, such as running a formatter,
|
||||
linter or other commands.
|
||||
|
||||
To use this option, pass it the command you would like to run. The following tokens will be replaced
|
||||
in your string:
|
||||
|
||||
- `{{path}}` - the temporary file path for you to read from
|
||||
- `{{rawpath}}` - a different file path containing the raw file contents **before** they were
|
||||
handled by Handlebars.js.
|
||||
|
||||
If none of these tokens are found, the regular (non-raw) path will be appended to the end of the
|
||||
command.
|
||||
|
||||
```shell
|
||||
simple-scaffold -c . --before-write prettier
|
||||
# command: prettier /tmp/somefile
|
||||
|
||||
simple-scaffold -c . --before-write 'cat {{path}} | my-linter'
|
||||
# command: cat /tmp/somefile | my-linter
|
||||
```
|
||||
|
||||
The command should return the string to write to the file through standard output (stdout), and not
|
||||
re-write the tmp file as it is not used for writing. Returning an empty string (after trimming) will
|
||||
discard the result and write the original file contents.
|
||||
|
||||
See
|
||||
[beforeWrite](https://chenasraf.github.io/simple-scaffold/docs/api/interfaces/ScaffoldConfig#beforewrite)
|
||||
Node.js API for more details. Instead of returning `undefined` to keep the default behavior, you can
|
||||
output `''` for the same effect.
|
||||
|
||||
## Examples:
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
title: Node.js Usage
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
You can build the scaffold yourself, if you want to create more complex arguments, scaffold groups,
|
||||
etc - simply pass a config object to the Scaffold function when you are ready to start.
|
||||
|
||||
@@ -33,6 +35,19 @@ interface ScaffoldConfig {
|
||||
}
|
||||
```
|
||||
|
||||
### Before Write option
|
||||
|
||||
This option allows you to preprocess a file before it is being written, such as running a formatter,
|
||||
linter or other commands.
|
||||
|
||||
To use this option, you can run any async/blocking command, and return a string as the final output
|
||||
to be used as the file contents.
|
||||
|
||||
Returning `undefined` will keep the file contents as-is, after normal Handlebars.js procesing by
|
||||
Simple Scaffold.
|
||||
|
||||
## Example
|
||||
|
||||
This is an example of loading a complete scaffold via Node.js:
|
||||
|
||||
```typescript
|
||||
@@ -50,6 +65,8 @@ const config = {
|
||||
helpers: {
|
||||
twice: (text) => [text, text].join(" ")
|
||||
},
|
||||
// return a string to replace the final file contents after pre-processing, or `undefined`
|
||||
// to keep it as-is
|
||||
beforeWrite: (content, rawContent, outputPath) => content.toString().toUpperCase()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import * as css from "./{{pascalCae name}}.css"
|
||||
import * as css from "./{{pascalCase name}}.css"
|
||||
|
||||
class {{pascalCae name}} extends React.Component<any> {
|
||||
class {{pascalCase name}} extends React.Component<any> {
|
||||
private {{ property }}
|
||||
|
||||
constructor(props: any) {
|
||||
@@ -10,8 +10,8 @@ class {{pascalCae name}} extends React.Component<any> {
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <div className={ css.{{pascalCae name}} } />
|
||||
return <div className={ css.{{pascalCase name}} } />
|
||||
}
|
||||
}
|
||||
|
||||
export default {pascalCae nName}}
|
||||
export default {{pascalCase name}}
|
||||
|
||||
18
src/cmd.ts
18
src/cmd.ts
@@ -1,15 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import fs from "node:fs/promises"
|
||||
import { massarg } from "massarg"
|
||||
import chalk from "chalk"
|
||||
import { ListCommandCliOptions, LogLevel, ScaffoldCmdConfig } from "./types"
|
||||
import { Scaffold } from "./scaffold"
|
||||
import path from "node:path"
|
||||
import fs from "node:fs/promises"
|
||||
import { getConfigFile, parseAppendData, parseConfigFile } from "./config"
|
||||
import { log } from "./logger"
|
||||
import { MassargCommand } from "massarg/command"
|
||||
import { getUniqueTmpPath as generateUniqueTmpPath } from "./file"
|
||||
|
||||
export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
const isProjectRoot = Boolean(await fs.stat(path.join(__dirname, "package.json")).catch(() => false))
|
||||
@@ -30,7 +30,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
return
|
||||
}
|
||||
log(config, LogLevel.info, `Simple Scaffold v${pkg.version}`)
|
||||
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
|
||||
const tmpPath = generateUniqueTmpPath()
|
||||
try {
|
||||
log(config, LogLevel.debug, "Parsing config file...", config)
|
||||
const parsed = await parseConfigFile(config, tmpPath)
|
||||
@@ -144,6 +144,14 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
return val
|
||||
},
|
||||
})
|
||||
.option({
|
||||
name: "before-write",
|
||||
aliases: ["B"],
|
||||
description:
|
||||
"Run a script before writing the files. This can be a command or a path to a" +
|
||||
" file. A temporary file path will be passed to the given command and the command should " +
|
||||
"return a string for the final output.",
|
||||
})
|
||||
.flag({
|
||||
name: "dry-run",
|
||||
aliases: ["dr"],
|
||||
@@ -163,7 +171,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
aliases: ["ls"],
|
||||
description: "List all available templates for a given config. See `list -h` for more information.",
|
||||
run: async (_config) => {
|
||||
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
|
||||
const tmpPath = generateUniqueTmpPath()
|
||||
const config = {
|
||||
templates: [],
|
||||
name: "",
|
||||
|
||||
@@ -16,7 +16,8 @@ import { handlebarsParse } from "./parser"
|
||||
import { log } from "./logger"
|
||||
import { resolve, wrapNoopResolver } from "./utils"
|
||||
import { getGitConfig } from "./git"
|
||||
import { isDir, pathExists } from "./file"
|
||||
import { createDirIfNotExists, getUniqueTmpPath, isDir, pathExists } from "./file"
|
||||
import { exec, spawn } from "node:child_process"
|
||||
|
||||
/** @internal */
|
||||
export function getOptionValueForFile<T>(
|
||||
@@ -80,7 +81,7 @@ export async function getConfigFile(config: ScaffoldCmdConfig, tmpPath: string):
|
||||
|
||||
/** @internal */
|
||||
export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string): Promise<ScaffoldConfig> {
|
||||
let output: ScaffoldConfig = config
|
||||
let output: ScaffoldConfig = { ...config, beforeWrite: undefined }
|
||||
|
||||
if (config.quiet) {
|
||||
config.logLevel = LogLevel.none
|
||||
@@ -101,6 +102,7 @@ export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string
|
||||
output = {
|
||||
...config,
|
||||
...imported,
|
||||
beforeWrite: undefined,
|
||||
data: {
|
||||
...(imported as any).data,
|
||||
...config.data,
|
||||
@@ -109,9 +111,12 @@ export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string
|
||||
}
|
||||
|
||||
output.data = { ...output.data, ...config.appendData }
|
||||
output.beforeWrite = config.beforeWrite ? wrapBeforeWrite(config, config.beforeWrite) : undefined
|
||||
|
||||
if (!output.name) {
|
||||
throw new Error("simple-scaffold: Missing required option: name")
|
||||
}
|
||||
|
||||
log(output, LogLevel.debug, "Parsed config", output)
|
||||
return output
|
||||
}
|
||||
@@ -182,3 +187,72 @@ export async function findConfigFile(root: string): Promise<string> {
|
||||
}
|
||||
throw new Error(`Could not find config file in git repo`)
|
||||
}
|
||||
|
||||
function wrapBeforeWrite(
|
||||
config: LogConfig & Pick<ScaffoldConfig, "dryRun">,
|
||||
beforeWrite: string,
|
||||
): ScaffoldConfig["beforeWrite"] {
|
||||
return async (content, rawContent, outputFile) => {
|
||||
const tmpPath = path.join(getUniqueTmpPath(), path.basename(outputFile))
|
||||
await createDirIfNotExists(path.dirname(tmpPath), config)
|
||||
const ext = path.extname(outputFile)
|
||||
const rawTmpPath = tmpPath.replace(ext, ".raw" + ext)
|
||||
try {
|
||||
log(config, LogLevel.debug, "Parsing beforeWrite command", beforeWrite)
|
||||
let cmd = await prepareBeforeWriteCmd({ beforeWrite, tmpPath, content, rawTmpPath, rawContent })
|
||||
const result = await new Promise<string | undefined>((resolve, reject) => {
|
||||
log(config, LogLevel.debug, "Running parsed beforeWrite command:", cmd)
|
||||
const proc = exec(cmd)
|
||||
proc.stdout!.on("data", (data) => {
|
||||
if (data.trim()) {
|
||||
resolve(data.toString())
|
||||
} else {
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
proc.stderr!.on("data", (data) => {
|
||||
reject(data.toString())
|
||||
})
|
||||
})
|
||||
return result
|
||||
} catch (e) {
|
||||
log(config, LogLevel.debug, e)
|
||||
log(config, LogLevel.warning, "Error running beforeWrite command, returning original content")
|
||||
return undefined
|
||||
} finally {
|
||||
await fs.rm(tmpPath, { force: true })
|
||||
await fs.rm(rawTmpPath, { force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareBeforeWriteCmd({
|
||||
beforeWrite,
|
||||
tmpPath,
|
||||
content,
|
||||
rawTmpPath,
|
||||
rawContent,
|
||||
}: {
|
||||
beforeWrite: string
|
||||
tmpPath: string
|
||||
content: Buffer
|
||||
rawTmpPath: string
|
||||
rawContent: Buffer
|
||||
}): Promise<string> {
|
||||
let cmd: string = ""
|
||||
const pathReg = /\{\{\s*path\s*\}\}/gi
|
||||
const rawPathReg = /\{\{\s*rawpath\s*\}\}/gi
|
||||
if (pathReg.test(beforeWrite)) {
|
||||
await fs.writeFile(tmpPath, content)
|
||||
cmd = beforeWrite.replaceAll(pathReg, tmpPath)
|
||||
}
|
||||
if (rawPathReg.test(beforeWrite)) {
|
||||
await fs.writeFile(rawTmpPath, rawContent)
|
||||
cmd = beforeWrite.replaceAll(rawPathReg, rawTmpPath)
|
||||
}
|
||||
if (!cmd) {
|
||||
await fs.writeFile(tmpPath, content)
|
||||
cmd = [beforeWrite, tmpPath].join(" ")
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
18
src/file.ts
18
src/file.ts
@@ -1,7 +1,8 @@
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { F_OK } from "node:constants"
|
||||
import { LogLevel, ScaffoldConfig } from "./types"
|
||||
import fs from "node:fs/promises"
|
||||
import { F_OK } from "node:constants"
|
||||
import { LogConfig, LogLevel, ScaffoldConfig } from "./types"
|
||||
import { glob, hasMagic } from "glob"
|
||||
import { log } from "./logger"
|
||||
import { getOptionValueForFile } from "./config"
|
||||
@@ -10,7 +11,10 @@ import { handleErr } from "./utils"
|
||||
|
||||
const { stat, access, mkdir, readFile, writeFile } = fs
|
||||
|
||||
export async function createDirIfNotExists(dir: string, config: ScaffoldConfig): Promise<void> {
|
||||
export async function createDirIfNotExists(
|
||||
dir: string,
|
||||
config: LogConfig & Pick<ScaffoldConfig, "dryRun">,
|
||||
): Promise<void> {
|
||||
if (config.dryRun) {
|
||||
log(config, LogLevel.info, `Dry Run. Not creating dir ${dir}`)
|
||||
return
|
||||
@@ -142,6 +146,7 @@ export async function copyFileTransformed(
|
||||
if (exists && overwrite) {
|
||||
log(config, LogLevel.info, `File ${outputPath} exists, overwriting`)
|
||||
}
|
||||
log(config, LogLevel.debug, `Processing file ${inputPath}`)
|
||||
const templateBuffer = await readFile(inputPath)
|
||||
const unprocessedOutputContents = handlebarsParse(config, templateBuffer)
|
||||
const finalOutputContents =
|
||||
@@ -149,7 +154,6 @@ export async function copyFileTransformed(
|
||||
|
||||
if (!config.dryRun) {
|
||||
await writeFile(outputPath, finalOutputContents)
|
||||
log(config, LogLevel.info, "Done.")
|
||||
} else {
|
||||
log(config, LogLevel.info, "Dry Run. Output should be:")
|
||||
log(config, LogLevel.info, finalOutputContents.toString())
|
||||
@@ -157,6 +161,7 @@ export async function copyFileTransformed(
|
||||
} else if (exists) {
|
||||
log(config, LogLevel.info, `File ${outputPath} already exists, skipping`)
|
||||
}
|
||||
log(config, LogLevel.info, "Done.")
|
||||
}
|
||||
|
||||
export function getOutputDir(config: ScaffoldConfig, outputPathOpt: string, basePath: string): string {
|
||||
@@ -209,3 +214,8 @@ export async function handleTemplateFile(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function getUniqueTmpPath(): string {
|
||||
return path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import util from "util"
|
||||
import { LogConfig, LogLevel, ScaffoldConfig } from "./types"
|
||||
import chalk from "chalk"
|
||||
|
||||
@@ -30,7 +31,7 @@ export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
|
||||
i instanceof Error
|
||||
? chalkFn(i, JSON.stringify(i, undefined, 1), i.stack)
|
||||
: typeof i === "object"
|
||||
? chalkFn(JSON.stringify(i, undefined, 1))
|
||||
? util.inspect(i, { depth: null, colors: true })
|
||||
: chalkFn(i),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -121,7 +121,7 @@ export function handlebarsParse(
|
||||
return Buffer.from(outputContents)
|
||||
} catch (e) {
|
||||
log(config, LogLevel.debug, e)
|
||||
log(config, LogLevel.info, "Couldn't parse file with handlebars, returning original content")
|
||||
log(config, LogLevel.warning, "Couldn't parse file with handlebars, returning original content")
|
||||
return Buffer.from(templateBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,6 +368,8 @@ export type ScaffoldCmdConfig = {
|
||||
git?: string
|
||||
/** Display version */
|
||||
version: boolean
|
||||
/** Run a script before writing the files. This can be a command or a path to a file. The file contents will be passed to the given command. */
|
||||
beforeWrite?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user