Skip to content

Commit 5f810e2

Browse files
committed
feat: add --before-write cli option (#89)
* feat: add `--before-write` cli option * refactor: cleanup before write wrapper * docs: add before-write docs
1 parent d579c09 commit 5f810e2

File tree

9 files changed

+180
-35
lines changed

9 files changed

+180
-35
lines changed

docs/docs/usage/03-cli.md

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,57 @@ Usage: simple-scaffold [options]
1111
To see this and more information anytime, add the `-h` or `--help` flag to your call, e.g.
1212
`npx simple-scaffold@latest -h`.
1313

14-
| Command \| alias | |
15-
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16-
| `--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. |
17-
| `--config`\|`-c` | Filename or directory to load config from |
18-
| `--git`\|`-g` | Git URL or GitHub path to load a template from. |
19-
| `--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`) |
20-
| `--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. |
21-
| `--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. |
22-
| `--overwrite` \| `-w` | Enable to override output files, even if they already exist. |
23-
| `--data` \| `-d` | Add custom data to the templates. By default, only your app name is included. |
24-
| `--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` |
25-
| `--create-sub-folder` \| `-s` | Create subfolder with the input name |
26-
| `--sub-folder-name-helper` \| `-sh` | Default helper to apply to subfolder name when using `--create-sub-folder true`. |
27-
| `--quiet` \| `-q` | Suppress output logs (Same as `--log-level none`) |
28-
| `--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. |
29-
| `--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. |
30-
| `--help` \| `-h` | Show this help message |
31-
| `--version` \| `-v` | Display version. |
14+
| Command \| alias | |
15+
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16+
| `--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. |
17+
| `--config`\|`-c` | Filename or directory to load config from |
18+
| `--git`\|`-g` | Git URL or GitHub path to load a template from. |
19+
| `--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`) |
20+
| `--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. |
21+
| `--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. |
22+
| `--overwrite` \| `-w` | Enable to override output files, even if they already exist. |
23+
| `--data` \| `-d` | Add custom data to the templates. By default, only your app name is included. |
24+
| `--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` |
25+
| `--create-sub-folder` \| `-s` | Create subfolder with the input name |
26+
| `--sub-folder-name-helper` \| `-sh` | Default helper to apply to subfolder name when using `--create-sub-folder true`. |
27+
| `--quiet` \| `-q` | Suppress output logs (Same as `--log-level none`) |
28+
| `--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. |
29+
| `--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. |
30+
| `--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. |
31+
| `--help` \| `-h` | Show this help message |
32+
| `--version` \| `-v` | Display version. |
33+
34+
### Before Write option
35+
36+
This option allows you to preprocess a file before it is being written, such as running a formatter,
37+
linter or other commands.
38+
39+
To use this option, pass it the command you would like to run. The following tokens will be replaced
40+
in your string:
41+
42+
- `{{path}}` - the temporary file path for you to read from
43+
- `{{rawpath}}` - a different file path containing the raw file contents **before** they were
44+
handled by Handlebars.js.
45+
46+
If none of these tokens are found, the regular (non-raw) path will be appended to the end of the
47+
command.
48+
49+
```shell
50+
simple-scaffold -c . --before-write prettier
51+
# command: prettier /tmp/somefile
52+
53+
simple-scaffold -c . --before-write 'cat {{path}} | my-linter'
54+
# command: cat /tmp/somefile | my-linter
55+
```
56+
57+
The command should return the string to write to the file through standard output (stdout), and not
58+
re-write the tmp file as it is not used for writing. Returning an empty string (after trimming) will
59+
discard the result and write the original file contents.
60+
61+
See
62+
[beforeWrite](https://chenasraf.github.io/simple-scaffold/docs/api/interfaces/ScaffoldConfig#beforewrite)
63+
Node.js API for more details. Instead of returning `undefined` to keep the default behavior, you can
64+
output `''` for the same effect.
3265

3366
## Examples:
3467

docs/docs/usage/04-node.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
title: Node.js Usage
33
---
44

5+
## Overview
6+
57
You can build the scaffold yourself, if you want to create more complex arguments, scaffold groups,
68
etc - simply pass a config object to the Scaffold function when you are ready to start.
79

@@ -33,6 +35,19 @@ interface ScaffoldConfig {
3335
}
3436
```
3537

38+
### Before Write option
39+
40+
This option allows you to preprocess a file before it is being written, such as running a formatter,
41+
linter or other commands.
42+
43+
To use this option, you can run any async/blocking command, and return a string as the final output
44+
to be used as the file contents.
45+
46+
Returning `undefined` will keep the file contents as-is, after normal Handlebars.js procesing by
47+
Simple Scaffold.
48+
49+
## Example
50+
3651
This is an example of loading a complete scaffold via Node.js:
3752

3853
```typescript
@@ -50,6 +65,8 @@ const config = {
5065
helpers: {
5166
twice: (text) => [text, text].join(" ")
5267
},
68+
// return a string to replace the final file contents after pre-processing, or `undefined`
69+
// to keep it as-is
5370
beforeWrite: (content, rawContent, outputPath) => content.toString().toUpperCase()
5471
}
5572

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from "react"
2-
import * as css from "./{{pascalCae name}}.css"
2+
import * as css from "./{{pascalCase name}}.css"
33

4-
class {{pascalCae name}} extends React.Component<any> {
4+
class {{pascalCase name}} extends React.Component<any> {
55
private {{ property }}
66

77
constructor(props: any) {
@@ -10,8 +10,8 @@ class {{pascalCae name}} extends React.Component<any> {
1010
}
1111

1212
public render() {
13-
return <div className={ css.{{pascalCae name}} } />
13+
return <div className={ css.{{pascalCase name}} } />
1414
}
1515
}
1616

17-
export default {pascalCae nName}}
17+
export default {{pascalCase name}}

src/cmd.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
#!/usr/bin/env node
22

3-
import os from "node:os"
3+
import path from "node:path"
4+
import fs from "node:fs/promises"
45
import { massarg } from "massarg"
56
import chalk from "chalk"
67
import { ListCommandCliOptions, LogLevel, ScaffoldCmdConfig } from "./types"
78
import { Scaffold } from "./scaffold"
8-
import path from "node:path"
9-
import fs from "node:fs/promises"
109
import { getConfigFile, parseAppendData, parseConfigFile } from "./config"
1110
import { log } from "./logger"
1211
import { MassargCommand } from "massarg/command"
12+
import { getUniqueTmpPath as generateUniqueTmpPath } from "./file"
1313

1414
export async function parseCliArgs(args = process.argv.slice(2)) {
1515
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)) {
3030
return
3131
}
3232
log(config, LogLevel.info, `Simple Scaffold v${pkg.version}`)
33-
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
33+
const tmpPath = generateUniqueTmpPath()
3434
try {
3535
log(config, LogLevel.debug, "Parsing config file...", config)
3636
const parsed = await parseConfigFile(config, tmpPath)
@@ -144,6 +144,14 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
144144
return val
145145
},
146146
})
147+
.option({
148+
name: "before-write",
149+
aliases: ["B"],
150+
description:
151+
"Run a script before writing the files. This can be a command or a path to a" +
152+
" file. A temporary file path will be passed to the given command and the command should " +
153+
"return a string for the final output.",
154+
})
147155
.flag({
148156
name: "dry-run",
149157
aliases: ["dr"],
@@ -163,7 +171,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
163171
aliases: ["ls"],
164172
description: "List all available templates for a given config. See `list -h` for more information.",
165173
run: async (_config) => {
166-
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
174+
const tmpPath = generateUniqueTmpPath()
167175
const config = {
168176
templates: [],
169177
name: "",

src/config.ts

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import { handlebarsParse } from "./parser"
1616
import { log } from "./logger"
1717
import { resolve, wrapNoopResolver } from "./utils"
1818
import { getGitConfig } from "./git"
19-
import { isDir, pathExists } from "./file"
19+
import { createDirIfNotExists, getUniqueTmpPath, isDir, pathExists } from "./file"
20+
import { exec, spawn } from "node:child_process"
2021

2122
/** @internal */
2223
export function getOptionValueForFile<T>(
@@ -80,7 +81,7 @@ export async function getConfigFile(config: ScaffoldCmdConfig, tmpPath: string):
8081

8182
/** @internal */
8283
export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string): Promise<ScaffoldConfig> {
83-
let output: ScaffoldConfig = config
84+
let output: ScaffoldConfig = { ...config, beforeWrite: undefined }
8485

8586
if (config.quiet) {
8687
config.logLevel = LogLevel.none
@@ -101,6 +102,7 @@ export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string
101102
output = {
102103
...config,
103104
...imported,
105+
beforeWrite: undefined,
104106
data: {
105107
...(imported as any).data,
106108
...config.data,
@@ -109,9 +111,12 @@ export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string
109111
}
110112

111113
output.data = { ...output.data, ...config.appendData }
114+
output.beforeWrite = config.beforeWrite ? wrapBeforeWrite(config, config.beforeWrite) : undefined
115+
112116
if (!output.name) {
113117
throw new Error("simple-scaffold: Missing required option: name")
114118
}
119+
115120
log(output, LogLevel.debug, "Parsed config", output)
116121
return output
117122
}
@@ -182,3 +187,72 @@ export async function findConfigFile(root: string): Promise<string> {
182187
}
183188
throw new Error(`Could not find config file in git repo`)
184189
}
190+
191+
function wrapBeforeWrite(
192+
config: LogConfig & Pick<ScaffoldConfig, "dryRun">,
193+
beforeWrite: string,
194+
): ScaffoldConfig["beforeWrite"] {
195+
return async (content, rawContent, outputFile) => {
196+
const tmpPath = path.join(getUniqueTmpPath(), path.basename(outputFile))
197+
await createDirIfNotExists(path.dirname(tmpPath), config)
198+
const ext = path.extname(outputFile)
199+
const rawTmpPath = tmpPath.replace(ext, ".raw" + ext)
200+
try {
201+
log(config, LogLevel.debug, "Parsing beforeWrite command", beforeWrite)
202+
let cmd = await prepareBeforeWriteCmd({ beforeWrite, tmpPath, content, rawTmpPath, rawContent })
203+
const result = await new Promise<string | undefined>((resolve, reject) => {
204+
log(config, LogLevel.debug, "Running parsed beforeWrite command:", cmd)
205+
const proc = exec(cmd)
206+
proc.stdout!.on("data", (data) => {
207+
if (data.trim()) {
208+
resolve(data.toString())
209+
} else {
210+
resolve(undefined)
211+
}
212+
})
213+
proc.stderr!.on("data", (data) => {
214+
reject(data.toString())
215+
})
216+
})
217+
return result
218+
} catch (e) {
219+
log(config, LogLevel.debug, e)
220+
log(config, LogLevel.warning, "Error running beforeWrite command, returning original content")
221+
return undefined
222+
} finally {
223+
await fs.rm(tmpPath, { force: true })
224+
await fs.rm(rawTmpPath, { force: true })
225+
}
226+
}
227+
}
228+
229+
async function prepareBeforeWriteCmd({
230+
beforeWrite,
231+
tmpPath,
232+
content,
233+
rawTmpPath,
234+
rawContent,
235+
}: {
236+
beforeWrite: string
237+
tmpPath: string
238+
content: Buffer
239+
rawTmpPath: string
240+
rawContent: Buffer
241+
}): Promise<string> {
242+
let cmd: string = ""
243+
const pathReg = /\{\{\s*path\s*\}\}/gi
244+
const rawPathReg = /\{\{\s*rawpath\s*\}\}/gi
245+
if (pathReg.test(beforeWrite)) {
246+
await fs.writeFile(tmpPath, content)
247+
cmd = beforeWrite.replaceAll(pathReg, tmpPath)
248+
}
249+
if (rawPathReg.test(beforeWrite)) {
250+
await fs.writeFile(rawTmpPath, rawContent)
251+
cmd = beforeWrite.replaceAll(rawPathReg, rawTmpPath)
252+
}
253+
if (!cmd) {
254+
await fs.writeFile(tmpPath, content)
255+
cmd = [beforeWrite, tmpPath].join(" ")
256+
}
257+
return cmd
258+
}

0 commit comments

Comments
 (0)