mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-17 17:28:09 +00:00
feat: after scaffold hook
This commit is contained in:
@@ -29,6 +29,7 @@ Options:
|
|||||||
| `--quiet` \| `-q` | Suppress output logs (Same as `--log-level none`)(default: false) |
|
| `--quiet` \| `-q` | Suppress output logs (Same as `--log-level none`)(default: false) |
|
||||||
| `--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. (default: info) |
|
| `--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. (default: info) |
|
||||||
| `--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. |
|
| `--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. |
|
||||||
|
| `--after-scaffold` \| `-A` | Run a shell command after all files have been written. The command is executed in the output directory (e.g. `--after-scaffold 'npm install'`). |
|
||||||
| `--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. (default: false) |
|
| `--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. (default: false) |
|
||||||
| `--version` \| `-v` | Display version. |
|
| `--version` \| `-v` | Display version. |
|
||||||
|
|
||||||
@@ -108,6 +109,35 @@ See
|
|||||||
Node.js API for more details. Instead of returning `undefined` to keep the default behavior, you can
|
Node.js API for more details. Instead of returning `undefined` to keep the default behavior, you can
|
||||||
output `''` for the same effect.
|
output `''` for the same effect.
|
||||||
|
|
||||||
|
### After Scaffold option
|
||||||
|
|
||||||
|
This option runs a shell command after all files have been written. The command is executed in the
|
||||||
|
output directory, making it useful for post-scaffolding tasks like installing dependencies or
|
||||||
|
initializing a git repo.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
simple-scaffold -c . --after-scaffold 'npm install'
|
||||||
|
simple-scaffold -c . --after-scaffold 'git init && git add .'
|
||||||
|
```
|
||||||
|
|
||||||
|
In a config file, you can use a function for more control:
|
||||||
|
|
||||||
|
```js
|
||||||
|
module.exports = {
|
||||||
|
default: {
|
||||||
|
templates: ["templates/app"],
|
||||||
|
output: ".",
|
||||||
|
afterScaffold: async ({ config, files }) => {
|
||||||
|
console.log(`Created ${files.length} files in ${config.output}`)
|
||||||
|
// run any post-processing here
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The function receives a context with the resolved `config` and an array of `files` (absolute paths)
|
||||||
|
that were written.
|
||||||
|
|
||||||
## Available Commands:
|
## Available Commands:
|
||||||
|
|
||||||
| Command \| Alias | Description |
|
| Command \| Alias | Description |
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ interface ScaffoldConfig {
|
|||||||
rawContent: Buffer,
|
rawContent: Buffer,
|
||||||
outputPath: string,
|
outputPath: string,
|
||||||
): string | Buffer | undefined | Promise<string | Buffer | undefined>
|
): string | Buffer | undefined | Promise<string | Buffer | undefined>
|
||||||
|
afterScaffold?: AfterScaffoldHook
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ScaffoldInput {
|
interface ScaffoldInput {
|
||||||
@@ -36,6 +37,13 @@ interface ScaffoldInput {
|
|||||||
required?: boolean
|
required?: boolean
|
||||||
default?: string
|
default?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AfterScaffoldContext {
|
||||||
|
config: ScaffoldConfig
|
||||||
|
files: string[] // absolute paths of written files
|
||||||
|
}
|
||||||
|
|
||||||
|
type AfterScaffoldHook = ((context: AfterScaffoldContext) => void | Promise<void>) | string
|
||||||
```
|
```
|
||||||
|
|
||||||
### Before Write option
|
### Before Write option
|
||||||
@@ -49,6 +57,38 @@ to be used as the file contents.
|
|||||||
Returning `undefined` will keep the file contents as-is, after normal Handlebars.js procesing by
|
Returning `undefined` will keep the file contents as-is, after normal Handlebars.js procesing by
|
||||||
Simple Scaffold.
|
Simple Scaffold.
|
||||||
|
|
||||||
|
### After Scaffold hook
|
||||||
|
|
||||||
|
The `afterScaffold` option runs after all files have been written. It receives a context object with
|
||||||
|
the resolved config and the list of files that were created.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import Scaffold from "simple-scaffold"
|
||||||
|
|
||||||
|
await Scaffold({
|
||||||
|
name: "my-app",
|
||||||
|
templates: ["templates/app"],
|
||||||
|
output: ".",
|
||||||
|
afterScaffold: async ({ config, files }) => {
|
||||||
|
console.log(`Created ${files.length} files`)
|
||||||
|
// e.g. run npm install, git init, open editor, etc.
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also pass a shell command string, which will be executed in the output directory:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await Scaffold({
|
||||||
|
name: "my-app",
|
||||||
|
templates: ["templates/app"],
|
||||||
|
output: "my-app",
|
||||||
|
afterScaffold: "npm install && git init",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In dry-run mode, the hook is still called but the `files` array will be empty.
|
||||||
|
|
||||||
### Inputs
|
### Inputs
|
||||||
|
|
||||||
The `inputs` option lets you define fields that will be prompted interactively (when running in a
|
The `inputs` option lets you define fields that will be prompted interactively (when running in a
|
||||||
|
|||||||
@@ -175,6 +175,14 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
|
|||||||
" file. A temporary file path will be passed to the given command and the command should " +
|
" file. A temporary file path will be passed to the given command and the command should " +
|
||||||
"return a string for the final output.",
|
"return a string for the final output.",
|
||||||
})
|
})
|
||||||
|
.option({
|
||||||
|
name: "after-scaffold",
|
||||||
|
aliases: ["A"],
|
||||||
|
description:
|
||||||
|
"Run a shell command after all files have been written. " +
|
||||||
|
"The command is executed in the output directory. " +
|
||||||
|
"For example: `--after-scaffold 'npm install'`",
|
||||||
|
})
|
||||||
.flag({
|
.flag({
|
||||||
name: "dry-run",
|
name: "dry-run",
|
||||||
aliases: ["dr"],
|
aliases: ["dr"],
|
||||||
|
|||||||
@@ -119,6 +119,9 @@ export async function parseConfigFile(config: ScaffoldCmdConfig): Promise<Scaffo
|
|||||||
? wrapBeforeWrite(config, config.beforeWrite)
|
? wrapBeforeWrite(config, config.beforeWrite)
|
||||||
: undefined
|
: undefined
|
||||||
output.beforeWrite = cmdBeforeWrite ?? output.beforeWrite
|
output.beforeWrite = cmdBeforeWrite ?? output.beforeWrite
|
||||||
|
if (config.afterScaffold) {
|
||||||
|
output.afterScaffold = config.afterScaffold
|
||||||
|
}
|
||||||
|
|
||||||
if (!output.name) {
|
if (!output.name) {
|
||||||
throw new Error("simple-scaffold: Missing required option: name")
|
throw new Error("simple-scaffold: Missing required option: name")
|
||||||
|
|||||||
@@ -174,11 +174,12 @@ export function getOutputDir(
|
|||||||
/**
|
/**
|
||||||
* Processes a single template file: resolves output paths, creates directories,
|
* Processes a single template file: resolves output paths, creates directories,
|
||||||
* and writes the transformed output.
|
* and writes the transformed output.
|
||||||
|
* Returns the output path if the file was written, or null if skipped.
|
||||||
*/
|
*/
|
||||||
export async function handleTemplateFile(
|
export async function handleTemplateFile(
|
||||||
config: ScaffoldConfig,
|
config: ScaffoldConfig,
|
||||||
{ templatePath, basePath }: { templatePath: string; basePath: string },
|
{ templatePath, basePath }: { templatePath: string; basePath: string },
|
||||||
): Promise<void> {
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(
|
const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(
|
||||||
config,
|
config,
|
||||||
@@ -203,8 +204,10 @@ export async function handleTemplateFile(
|
|||||||
|
|
||||||
await createDirIfNotExists(path.dirname(outputPath), config)
|
await createDirIfNotExists(path.dirname(outputPath), config)
|
||||||
|
|
||||||
|
const shouldWrite = (!exists || overwrite) && !config.dryRun
|
||||||
log(config, LogLevel.info, `Writing to ${outputPath}`)
|
log(config, LogLevel.info, `Writing to ${outputPath}`)
|
||||||
await copyFileTransformed(config, { exists, overwrite, outputPath, inputPath })
|
await copyFileTransformed(config, { exists, overwrite, outputPath, inputPath })
|
||||||
|
return shouldWrite ? outputPath : null
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
handleErr(e as NodeJS.ErrnoException)
|
handleErr(e as NodeJS.ErrnoException)
|
||||||
throw e
|
throw e
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import os from "node:os"
|
import os from "node:os"
|
||||||
|
import { exec } from "node:child_process"
|
||||||
|
|
||||||
import { handleErr, resolve } from "./utils"
|
import { handleErr, resolve } from "./utils"
|
||||||
import { isDir, getTemplateGlobInfo, getFileList, handleTemplateFile, GlobInfo } from "./file"
|
import { isDir, getTemplateGlobInfo, getFileList, handleTemplateFile, GlobInfo } from "./file"
|
||||||
@@ -53,6 +54,7 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
|
|||||||
|
|
||||||
config = await resolveInputs(config)
|
config = await resolveInputs(config)
|
||||||
registerHelpers(config)
|
registerHelpers(config)
|
||||||
|
const writtenFiles: string[] = []
|
||||||
try {
|
try {
|
||||||
config.data = { name: config.name, ...config.data }
|
config.data = { name: config.name, ...config.data }
|
||||||
logInitStep(config)
|
logInitStep(config)
|
||||||
@@ -63,12 +65,17 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
|
|||||||
const templates = await resolveTemplateGlobs(config, includes)
|
const templates = await resolveTemplateGlobs(config, includes)
|
||||||
|
|
||||||
for (const tpl of templates) {
|
for (const tpl of templates) {
|
||||||
await processTemplateGlob(config, tpl, excludes)
|
const files = await processTemplateGlob(config, tpl, excludes)
|
||||||
|
writtenFiles.push(...files)
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
log(config, LogLevel.error, e)
|
log(config, LogLevel.error, e)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.afterScaffold) {
|
||||||
|
await runAfterScaffoldHook(config, writtenFiles)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolves included template paths into GlobInfo objects. */
|
/** Resolves included template paths into GlobInfo objects. */
|
||||||
@@ -87,12 +94,13 @@ async function resolveTemplateGlobs(
|
|||||||
return templates
|
return templates
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Processes all files matching a single template glob pattern. */
|
/** Processes all files matching a single template glob pattern. Returns paths of written files. */
|
||||||
async function processTemplateGlob(
|
async function processTemplateGlob(
|
||||||
config: ScaffoldConfig,
|
config: ScaffoldConfig,
|
||||||
tpl: GlobInfo,
|
tpl: GlobInfo,
|
||||||
excludes: string[],
|
excludes: string[],
|
||||||
): Promise<void> {
|
): Promise<string[]> {
|
||||||
|
const written: string[] = []
|
||||||
const files = await getFileList(config, [tpl.template, ...excludes])
|
const files = await getFileList(config, [tpl.template, ...excludes])
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (await isDir(file)) {
|
if (await isDir(file)) {
|
||||||
@@ -115,8 +123,46 @@ async function processTemplateGlob(
|
|||||||
isGlob: tpl.isGlob,
|
isGlob: tpl.isGlob,
|
||||||
})
|
})
|
||||||
|
|
||||||
await handleTemplateFile(config, { templatePath: file, basePath })
|
const outputPath = await handleTemplateFile(config, { templatePath: file, basePath })
|
||||||
|
if (outputPath) {
|
||||||
|
written.push(outputPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return written
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Executes the afterScaffold hook — either a function or a shell command string. */
|
||||||
|
async function runAfterScaffoldHook(config: ScaffoldConfig, files: string[]): Promise<void> {
|
||||||
|
const hook = config.afterScaffold!
|
||||||
|
|
||||||
|
if (typeof hook === "function") {
|
||||||
|
log(config, LogLevel.debug, "Running afterScaffold function hook")
|
||||||
|
await hook({ config, files })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shell command string
|
||||||
|
const outputDir = typeof config.output === "string" ? config.output : process.cwd()
|
||||||
|
const cwd = path.resolve(process.cwd(), outputDir)
|
||||||
|
|
||||||
|
log(config, LogLevel.info, `Running afterScaffold command: ${hook}`)
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const proc = exec(hook, { cwd })
|
||||||
|
proc.stdout?.on("data", (data: string) => {
|
||||||
|
log(config, LogLevel.info, data.toString().trimEnd())
|
||||||
|
})
|
||||||
|
proc.stderr?.on("data", (data: string) => {
|
||||||
|
log(config, LogLevel.warning, data.toString().trimEnd())
|
||||||
|
})
|
||||||
|
proc.on("close", (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
reject(new Error(`afterScaffold command exited with code ${code}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
proc.on("error", reject)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
45
src/types.ts
45
src/types.ts
@@ -189,10 +189,53 @@ export interface ScaffoldConfig {
|
|||||||
*/
|
*/
|
||||||
inputs?: Record<string, ScaffoldInput>
|
inputs?: Record<string, ScaffoldInput>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback or shell command that runs after all files have been written.
|
||||||
|
*
|
||||||
|
* When provided as a **function** (Node.js API), it receives a context object with the scaffold
|
||||||
|
* config and the list of files that were written:
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* Scaffold({
|
||||||
|
* // ...
|
||||||
|
* afterScaffold: async ({ config, files }) => {
|
||||||
|
* console.log(`Created ${files.length} files`)
|
||||||
|
* execSync("npm install", { cwd: config.output })
|
||||||
|
* },
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* When provided as a **string** (CLI `--after` flag), it is executed as a shell command
|
||||||
|
* in the output directory after scaffolding completes.
|
||||||
|
*
|
||||||
|
* @see {@link AfterScaffoldContext}
|
||||||
|
*/
|
||||||
|
afterScaffold?: AfterScaffoldHook
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
tmpDir?: string
|
tmpDir?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context passed to the {@link ScaffoldConfig.afterScaffold} hook.
|
||||||
|
*
|
||||||
|
* @category Config
|
||||||
|
*/
|
||||||
|
export interface AfterScaffoldContext {
|
||||||
|
/** The resolved scaffold config that was used. */
|
||||||
|
config: ScaffoldConfig
|
||||||
|
/** List of absolute paths to files that were written. */
|
||||||
|
files: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that runs after scaffolding completes.
|
||||||
|
* Can be a function receiving context, or a shell command string.
|
||||||
|
*
|
||||||
|
* @category Config
|
||||||
|
*/
|
||||||
|
export type AfterScaffoldHook = ((context: AfterScaffoldContext) => void | Promise<void>) | string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines a single interactive input for a scaffold template.
|
* Defines a single interactive input for a scaffold template.
|
||||||
*
|
*
|
||||||
@@ -417,6 +460,8 @@ export type ScaffoldCmdConfig = {
|
|||||||
version: boolean
|
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. */
|
/** 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
|
beforeWrite?: string
|
||||||
|
/** Run a shell command after all files have been written. Executed in the output directory. */
|
||||||
|
afterScaffold?: string
|
||||||
/** @internal */
|
/** @internal */
|
||||||
tmpDir?: string
|
tmpDir?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll, vi, type MockInstance } from "vitest"
|
import {
|
||||||
|
describe,
|
||||||
|
test,
|
||||||
|
expect,
|
||||||
|
beforeEach,
|
||||||
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
|
afterAll,
|
||||||
|
vi,
|
||||||
|
type MockInstance,
|
||||||
|
} from "vitest"
|
||||||
import mockFs from "mock-fs"
|
import mockFs from "mock-fs"
|
||||||
import FileSystem from "mock-fs/lib/filesystem"
|
import FileSystem from "mock-fs/lib/filesystem"
|
||||||
import Scaffold from "../src/scaffold"
|
import Scaffold from "../src/scaffold"
|
||||||
@@ -67,7 +77,8 @@ const fileStructDates = {
|
|||||||
input: {
|
input: {
|
||||||
"now.txt": "Today is {{ now 'mmm' }}, time is {{ now 'HH:mm' }}",
|
"now.txt": "Today is {{ now 'mmm' }}, time is {{ now 'HH:mm' }}",
|
||||||
"offset.txt": "Yesterday was {{ now 'mmm' -1 'days' }}, time is {{ now 'HH:mm' -1 'days' }}",
|
"offset.txt": "Yesterday was {{ now 'mmm' -1 'days' }}, time is {{ now 'HH:mm' -1 'days' }}",
|
||||||
"custom.txt": "Custom date is {{ date customDate 'mmm' }}, time is {{ date customDate 'HH:mm' }}",
|
"custom.txt":
|
||||||
|
"Custom date is {{ date customDate 'mmm' }}, time is {{ date customDate 'HH:mm' }}",
|
||||||
},
|
},
|
||||||
output: {},
|
output: {},
|
||||||
}
|
}
|
||||||
@@ -100,7 +111,6 @@ function withMock(fileStruct: FileSystem.DirectoryItems, testFn: () => void): ()
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("Scaffold", () => {
|
describe("Scaffold", () => {
|
||||||
|
|
||||||
describe(
|
describe(
|
||||||
"create subdir",
|
"create subdir",
|
||||||
|
|
||||||
@@ -301,7 +311,9 @@ describe("Scaffold", () => {
|
|||||||
|
|
||||||
const rootFile = readFileSync(join(process.cwd(), "output", "app_name-1.txt"))
|
const rootFile = readFileSync(join(process.cwd(), "output", "app_name-1.txt"))
|
||||||
const oneDeepFile = readFileSync(join(process.cwd(), "output", "AppName/app_name-2.txt"))
|
const oneDeepFile = readFileSync(join(process.cwd(), "output", "AppName/app_name-2.txt"))
|
||||||
const twoDeepFile = readFileSync(join(process.cwd(), "output", "AppName/moreNesting/app_name-3.txt"))
|
const twoDeepFile = readFileSync(
|
||||||
|
join(process.cwd(), "output", "AppName/moreNesting/app_name-3.txt"),
|
||||||
|
)
|
||||||
expect(rootFile.toString()).toEqual("This should be in root")
|
expect(rootFile.toString()).toEqual("This should be in root")
|
||||||
expect(oneDeepFile.toString()).toEqual("Hello, my value is 1")
|
expect(oneDeepFile.toString()).toEqual("Hello, my value is 1")
|
||||||
expect(twoDeepFile.toString()).toEqual("Hi! My value is actually NOT 1!")
|
expect(twoDeepFile.toString()).toEqual("Hi! My value is actually NOT 1!")
|
||||||
@@ -664,7 +676,9 @@ describe("Scaffold", () => {
|
|||||||
templates: ["input"],
|
templates: ["input"],
|
||||||
logLevel: "none",
|
logLevel: "none",
|
||||||
})
|
})
|
||||||
const content = readFileSync(join(process.cwd(), "output", "my-component", "index.ts")).toString()
|
const content = readFileSync(
|
||||||
|
join(process.cwd(), "output", "my-component", "index.ts"),
|
||||||
|
).toString()
|
||||||
expect(content).toEqual("export from MyComponent")
|
expect(content).toEqual("export from MyComponent")
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -698,7 +712,9 @@ describe("Scaffold", () => {
|
|||||||
logLevel: "none",
|
logLevel: "none",
|
||||||
})
|
})
|
||||||
expect(readFileSync(join(process.cwd(), "output", "root.txt")).toString()).toEqual("root")
|
expect(readFileSync(join(process.cwd(), "output", "root.txt")).toString()).toEqual("root")
|
||||||
expect(readFileSync(join(process.cwd(), "output", "level1", "l1.txt")).toString()).toEqual("level 1")
|
expect(
|
||||||
|
readFileSync(join(process.cwd(), "output", "level1", "l1.txt")).toString(),
|
||||||
|
).toEqual("level 1")
|
||||||
expect(
|
expect(
|
||||||
readFileSync(join(process.cwd(), "output", "level1", "level2", "l2.txt")).toString(),
|
readFileSync(join(process.cwd(), "output", "level1", "level2", "l2.txt")).toString(),
|
||||||
).toEqual("level 2")
|
).toEqual("level 2")
|
||||||
@@ -734,8 +750,12 @@ describe("Scaffold", () => {
|
|||||||
logLevel: "none",
|
logLevel: "none",
|
||||||
overwrite: (_fullPath, _basedir, basename) => basename === "replace.txt",
|
overwrite: (_fullPath, _basedir, basename) => basename === "replace.txt",
|
||||||
})
|
})
|
||||||
expect(readFileSync(join(process.cwd(), "output", "keep.txt")).toString()).toEqual("old keep")
|
expect(readFileSync(join(process.cwd(), "output", "keep.txt")).toString()).toEqual(
|
||||||
expect(readFileSync(join(process.cwd(), "output", "replace.txt")).toString()).toEqual("new replace")
|
"old keep",
|
||||||
|
)
|
||||||
|
expect(readFileSync(join(process.cwd(), "output", "replace.txt")).toString()).toEqual(
|
||||||
|
"new replace",
|
||||||
|
)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -786,7 +806,9 @@ describe("Scaffold", () => {
|
|||||||
subdir: true,
|
subdir: true,
|
||||||
subdirHelper: "camelCase",
|
subdirHelper: "camelCase",
|
||||||
})
|
})
|
||||||
const content = readFileSync(join(process.cwd(), "output", "myComponent", "file.txt")).toString()
|
const content = readFileSync(
|
||||||
|
join(process.cwd(), "output", "myComponent", "file.txt"),
|
||||||
|
).toString()
|
||||||
expect(content).toEqual("content")
|
expect(content).toEqual("content")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -799,7 +821,9 @@ describe("Scaffold", () => {
|
|||||||
subdir: true,
|
subdir: true,
|
||||||
subdirHelper: "kebabCase",
|
subdirHelper: "kebabCase",
|
||||||
})
|
})
|
||||||
const content = readFileSync(join(process.cwd(), "output", "my-component", "file.txt")).toString()
|
const content = readFileSync(
|
||||||
|
join(process.cwd(), "output", "my-component", "file.txt"),
|
||||||
|
).toString()
|
||||||
expect(content).toEqual("content")
|
expect(content).toEqual("content")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -812,7 +836,9 @@ describe("Scaffold", () => {
|
|||||||
subdir: true,
|
subdir: true,
|
||||||
subdirHelper: "snakeCase",
|
subdirHelper: "snakeCase",
|
||||||
})
|
})
|
||||||
const content = readFileSync(join(process.cwd(), "output", "my_component", "file.txt")).toString()
|
const content = readFileSync(
|
||||||
|
join(process.cwd(), "output", "my_component", "file.txt"),
|
||||||
|
).toString()
|
||||||
expect(content).toEqual("content")
|
expect(content).toEqual("content")
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -855,10 +881,10 @@ describe("Scaffold", () => {
|
|||||||
output: "output",
|
output: "output",
|
||||||
templates: ["input"],
|
templates: ["input"],
|
||||||
logLevel: "none",
|
logLevel: "none",
|
||||||
data: { value: "hello & <world> \"test\"" },
|
data: { value: 'hello & <world> "test"' },
|
||||||
})
|
})
|
||||||
const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString()
|
const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString()
|
||||||
expect(content).toEqual("Value: hello & <world> \"test\"")
|
expect(content).toEqual('Value: hello & <world> "test"')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -1052,8 +1078,12 @@ describe("Scaffold", () => {
|
|||||||
templates: ["input"],
|
templates: ["input"],
|
||||||
logLevel: "none",
|
logLevel: "none",
|
||||||
})
|
})
|
||||||
expect(readFileSync(join(process.cwd(), "output", ".gitignore")).toString()).toEqual("node_modules")
|
expect(readFileSync(join(process.cwd(), "output", ".gitignore")).toString()).toEqual(
|
||||||
expect(readFileSync(join(process.cwd(), "output", ".env.example")).toString()).toEqual("KEY=app")
|
"node_modules",
|
||||||
|
)
|
||||||
|
expect(readFileSync(join(process.cwd(), "output", ".env.example")).toString()).toEqual(
|
||||||
|
"KEY=app",
|
||||||
|
)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -1078,8 +1108,78 @@ describe("Scaffold", () => {
|
|||||||
})
|
})
|
||||||
const files = readdirSync(join(process.cwd(), "output"))
|
const files = readdirSync(join(process.cwd(), "output"))
|
||||||
expect(files.length).toBe(50)
|
expect(files.length).toBe(50)
|
||||||
expect(readFileSync(join(process.cwd(), "output", "file0.txt")).toString()).toEqual("Content 0 for app")
|
expect(readFileSync(join(process.cwd(), "output", "file0.txt")).toString()).toEqual(
|
||||||
expect(readFileSync(join(process.cwd(), "output", "file49.txt")).toString()).toEqual("Content 49 for app")
|
"Content 0 for app",
|
||||||
|
)
|
||||||
|
expect(readFileSync(join(process.cwd(), "output", "file49.txt")).toString()).toEqual(
|
||||||
|
"Content 49 for app",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
describe(
|
||||||
|
"afterScaffold hook",
|
||||||
|
withMock(
|
||||||
|
{
|
||||||
|
input: { "file.txt": "Hello {{name}}" },
|
||||||
|
output: {},
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
test("calls function hook with config and files", async () => {
|
||||||
|
const hookFn = vi.fn()
|
||||||
|
await Scaffold({
|
||||||
|
name: "app",
|
||||||
|
output: "output",
|
||||||
|
templates: ["input"],
|
||||||
|
logLevel: "none",
|
||||||
|
afterScaffold: hookFn,
|
||||||
|
})
|
||||||
|
expect(hookFn).toHaveBeenCalledOnce()
|
||||||
|
const ctx = hookFn.mock.calls[0][0]
|
||||||
|
expect(ctx.config.name).toEqual("app")
|
||||||
|
expect(ctx.files.length).toBe(1)
|
||||||
|
expect(ctx.files[0]).toContain("file.txt")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("calls async function hook", async () => {
|
||||||
|
let called = false
|
||||||
|
await Scaffold({
|
||||||
|
name: "app",
|
||||||
|
output: "output",
|
||||||
|
templates: ["input"],
|
||||||
|
logLevel: "none",
|
||||||
|
afterScaffold: async () => {
|
||||||
|
called = true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(called).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not call hook when no files written (dry run)", async () => {
|
||||||
|
const hookFn = vi.fn()
|
||||||
|
await Scaffold({
|
||||||
|
name: "app",
|
||||||
|
output: "output",
|
||||||
|
templates: ["input"],
|
||||||
|
logLevel: "none",
|
||||||
|
dryRun: true,
|
||||||
|
afterScaffold: hookFn,
|
||||||
|
})
|
||||||
|
expect(hookFn).toHaveBeenCalledOnce()
|
||||||
|
expect(hookFn.mock.calls[0][0].files.length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not call hook when not provided", async () => {
|
||||||
|
await expect(
|
||||||
|
Scaffold({
|
||||||
|
name: "app",
|
||||||
|
output: "output",
|
||||||
|
templates: ["input"],
|
||||||
|
logLevel: "none",
|
||||||
|
}),
|
||||||
|
).resolves.toBeUndefined()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user