mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-18 01:29:09 +00:00
feat: predefined data inputs
This commit is contained in:
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
docs/docs/api/
|
||||
examples/
|
||||
.github/
|
||||
CHANGELOG.md
|
||||
34
README.md
34
README.md
@@ -13,9 +13,12 @@
|
||||
|
||||
</h2>
|
||||
|
||||
Simple Scaffold is a file scaffolding tool. You define templates once, then generate files from them whenever you need — whether it's a single component or an entire app boilerplate.
|
||||
Simple Scaffold is a file scaffolding tool. You define templates once, then generate files from them
|
||||
whenever you need — whether it's a single component or an entire app boilerplate.
|
||||
|
||||
Templates use **Handlebars.js** syntax, so you can inject data, loop over lists, use conditionals, and write custom helpers. It works as a CLI or as a Node.js library, and it doesn't care what kind of files you're generating.
|
||||
Templates use **Handlebars.js** syntax, so you can inject data, loop over lists, use conditionals,
|
||||
and write custom helpers. It works as a CLI or as a Node.js library, and it doesn't care what kind
|
||||
of files you're generating.
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -92,6 +95,33 @@ See information about each option and flag using the `--help` flag, or read the
|
||||
[CLI documentation](https://chenasraf.github.io/simple-scaffold/docs/usage/cli). For information
|
||||
about how configuration files work, [see below](#configuration-files).
|
||||
|
||||
### Interactive Mode
|
||||
|
||||
When running in a terminal, Simple Scaffold will interactively prompt for any missing required
|
||||
values — name, output directory, template paths, and template key (if multiple are available).
|
||||
|
||||
Config files can also define **inputs** — custom fields that are prompted interactively and become
|
||||
template data:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
inputs: {
|
||||
author: { message: "Author name", required: true },
|
||||
license: { message: "License", default: "MIT" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Inputs can be pre-provided via `--data` or `-D` to skip the prompt:
|
||||
|
||||
```sh
|
||||
npx simple-scaffold -c scaffold.config.js -k component -D author=John MyComponent
|
||||
```
|
||||
|
||||
### Configuration Files
|
||||
|
||||
You can use a config file to more easily maintain all your scaffold definitions.
|
||||
|
||||
@@ -23,7 +23,8 @@ module.exports = {
|
||||
}
|
||||
```
|
||||
|
||||
For the full configuration options, see [ScaffoldConfigFile](../api/type-aliases/ScaffoldConfigFile).
|
||||
For the full configuration options, see
|
||||
[ScaffoldConfigFile](../api/type-aliases/ScaffoldConfigFile).
|
||||
|
||||
If you want to supply functions inside the configurations, you must use a `.js`/`.cjs`/`.mjs` file
|
||||
as JSON does not support non-primitives.
|
||||
@@ -45,6 +46,31 @@ module.exports = (config) => {
|
||||
}
|
||||
```
|
||||
|
||||
### Template Inputs
|
||||
|
||||
You can define **inputs** in your config to prompt users for custom values when scaffolding. Each
|
||||
input becomes a template data variable:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
inputs: {
|
||||
author: { message: "Author name", required: true },
|
||||
license: { message: "License type", default: "MIT" },
|
||||
description: { message: "Component description" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
In your templates, use these as `{{ author }}`, `{{ license }}`, `{{ description }}`.
|
||||
|
||||
- **Required** inputs are prompted interactively if not provided via `--data` or `-D`
|
||||
- **Optional** inputs with a `default` use that value silently if not provided
|
||||
- In non-interactive environments, only defaults are applied
|
||||
|
||||
If you want to provide templates that need no name (such as common config files which are easily
|
||||
portable between projects), you may provide the `name` property in the config object.
|
||||
|
||||
@@ -86,7 +112,6 @@ simple-scaffold -c scaffold.json MyComponentName
|
||||
```
|
||||
|
||||
- When the a directory is given, the following files in the given directory will be tried in order:
|
||||
|
||||
- `scaffold.config.*`
|
||||
- `scaffold.*`
|
||||
|
||||
|
||||
@@ -13,24 +13,68 @@ To see this and more information anytime, add the `-h` or `--help` flag to your
|
||||
|
||||
Options:
|
||||
|
||||
| Option/flag \| Alias | Description |
|
||||
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--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 `--subdir` is enabled, the subdir 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` \| `--no-overwrite` \| `-W` | Enable to override output files, even if they already exist. (default: false) |
|
||||
| `--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` |
|
||||
| `--subdir` \| `-s` \| `--no-subdir` \| `-S` | Create a parent directory with the input name (and possibly `--subdir-helper` (default: false) |
|
||||
| `--subdir-helper` \| `-H` | Default helper to apply to subdir name when using `--subdir`. |
|
||||
| `--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) |
|
||||
| `--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. (default: false) |
|
||||
| `--version` \| `-v` | Display version. |
|
||||
| Option/flag \| Alias | Description |
|
||||
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--name` \| `-n` | Name to be passed to the generated files. `{{name}}` and other data parameters inside contents and file names will be replaced accordingly. If omitted in an interactive terminal, you will be prompted. |
|
||||
| `--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. If omitted and multiple templates are available, you will be prompted to select one. |
|
||||
| `--output` \| `-o` | Path to output to. If `--subdir` is enabled, the subdir will be created inside this path. If omitted in an interactive terminal, you will be prompted. |
|
||||
| `--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. If omitted in an interactive terminal, you will be prompted for a comma-separated list. |
|
||||
| `--overwrite` \| `-w` \| `--no-overwrite` \| `-W` | Enable to override output files, even if they already exist. (default: false) |
|
||||
| `--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` |
|
||||
| `--subdir` \| `-s` \| `--no-subdir` \| `-S` | Create a parent directory with the input name (and possibly `--subdir-helper` (default: false) |
|
||||
| `--subdir-helper` \| `-H` | Default helper to apply to subdir name when using `--subdir`. |
|
||||
| `--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) |
|
||||
| `--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. (default: false) |
|
||||
| `--version` \| `-v` | Display version. |
|
||||
|
||||
### Interactive Mode
|
||||
|
||||
When running in a terminal (TTY), Simple Scaffold will prompt for any missing required values:
|
||||
|
||||
- **Name** — text input if `--name` is not provided
|
||||
- **Template key** — selectable list if `--key` is not provided and the config file has multiple
|
||||
templates
|
||||
- **Output directory** — text input if `--output` is not provided
|
||||
- **Template paths** — comma-separated text input if `--templates` is not provided
|
||||
|
||||
In non-interactive environments (CI, piped input), missing values will cause an error instead of
|
||||
prompting.
|
||||
|
||||
### Template Inputs
|
||||
|
||||
Config files can define **inputs** — custom fields that are prompted interactively and injected as
|
||||
template data. This is useful for templates that need user-specific values like author name,
|
||||
license, or description.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
inputs: {
|
||||
author: { message: "Author name", required: true },
|
||||
license: { message: "License type", default: "MIT" },
|
||||
description: { message: "Description" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Each input becomes available as a Handlebars variable in your templates (e.g., `{{ author }}`,
|
||||
`{{ license }}`).
|
||||
|
||||
- **Required inputs** without a value will be prompted interactively
|
||||
- **Optional inputs** with a `default` will use that value if not provided
|
||||
- All inputs can be pre-provided via `--data` or `-D` to skip the prompt:
|
||||
|
||||
```shell
|
||||
simple-scaffold -c scaffold.config.js -k component -D author=John -D license=Apache-2.0 MyComponent
|
||||
```
|
||||
|
||||
### Before Write option
|
||||
|
||||
|
||||
@@ -7,12 +7,9 @@ title: Node.js Usage
|
||||
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.
|
||||
|
||||
The config takes similar arguments to the command line. The full type definitions can be found in
|
||||
[src/types.ts](https://github.com/chenasraf/simple-scaffold/blob/develop/src/types.ts#L13).
|
||||
|
||||
See the full
|
||||
[documentation](https://chenasraf.github.io/simple-scaffold/interfaces/ScaffoldConfig.html) for the
|
||||
configuration options and their behavior.
|
||||
The config takes similar arguments to the command line. See the full
|
||||
[API documentation](https://chenasraf.github.io/simple-scaffold/docs/api/interfaces/ScaffoldConfig)
|
||||
for all configuration options and their behavior.
|
||||
|
||||
```ts
|
||||
interface ScaffoldConfig {
|
||||
@@ -20,19 +17,25 @@ interface ScaffoldConfig {
|
||||
templates: string[]
|
||||
output: FileResponse<string>
|
||||
subdir?: boolean
|
||||
data?: Record<string, any>
|
||||
data?: Record<string, unknown>
|
||||
overwrite?: FileResponse<boolean>
|
||||
quiet?: boolean
|
||||
verbose?: LogLevel
|
||||
logLevel?: LogLevel
|
||||
dryRun?: boolean
|
||||
helpers?: Record<string, Helper>
|
||||
subdirHelper?: DefaultHelpers | string
|
||||
inputs?: Record<string, ScaffoldInput>
|
||||
beforeWrite?(
|
||||
content: Buffer,
|
||||
rawContent: Buffer,
|
||||
outputPath: string,
|
||||
): string | Buffer | undefined | Promise<string | Buffer | undefined>
|
||||
}
|
||||
|
||||
interface ScaffoldInput {
|
||||
message?: string
|
||||
required?: boolean
|
||||
default?: string
|
||||
}
|
||||
```
|
||||
|
||||
### Before Write option
|
||||
@@ -46,6 +49,31 @@ to be used as the file contents.
|
||||
Returning `undefined` will keep the file contents as-is, after normal Handlebars.js procesing by
|
||||
Simple Scaffold.
|
||||
|
||||
### Inputs
|
||||
|
||||
The `inputs` option lets you define fields that will be prompted interactively (when running in a
|
||||
TTY) and merged into the template data. This is useful when your templates need user-specific
|
||||
values.
|
||||
|
||||
```typescript
|
||||
import Scaffold from "simple-scaffold"
|
||||
|
||||
await Scaffold({
|
||||
name: "component",
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
inputs: {
|
||||
author: { message: "Author name", required: true },
|
||||
license: { message: "License", default: "MIT" },
|
||||
},
|
||||
})
|
||||
// In templates: {{ author }}, {{ license }}
|
||||
```
|
||||
|
||||
- **Required** inputs are prompted if not already in `data`
|
||||
- **Optional** inputs with a `default` are applied silently
|
||||
- Pre-providing values in `data` skips the prompt for that input
|
||||
|
||||
## Example
|
||||
|
||||
This is an example of loading a complete scaffold via Node.js:
|
||||
@@ -53,7 +81,7 @@ This is an example of loading a complete scaffold via Node.js:
|
||||
```typescript
|
||||
import Scaffold from "simple-scaffold"
|
||||
|
||||
const config = {
|
||||
await Scaffold({
|
||||
name: "component",
|
||||
templates: [path.join(__dirname, "scaffolds", "component")],
|
||||
output: path.join(__dirname, "src", "components"),
|
||||
@@ -65,10 +93,12 @@ const config = {
|
||||
helpers: {
|
||||
twice: (text) => [text, text].join(" "),
|
||||
},
|
||||
inputs: {
|
||||
author: { message: "Author name", required: true },
|
||||
license: { message: "License", default: "MIT" },
|
||||
},
|
||||
// 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(),
|
||||
}
|
||||
|
||||
const scaffold = Scaffold(config)
|
||||
})
|
||||
```
|
||||
|
||||
@@ -31,7 +31,6 @@ title: Examples
|
||||
### Output
|
||||
|
||||
- Output file path:
|
||||
|
||||
- With `subdir = false` (default):
|
||||
|
||||
```text
|
||||
|
||||
@@ -3,9 +3,9 @@ title: Usage
|
||||
sidebar_position: 0
|
||||
---
|
||||
|
||||
- [CLI Usage](cli)
|
||||
- [Template Files](templates)
|
||||
- [Configuration Files](configuration_files)
|
||||
- [CLI Usage](cli)
|
||||
- [Node.js Usage](node)
|
||||
- [Examples](examples)
|
||||
- [Migration](migration)
|
||||
- [Node.js Usage](node)
|
||||
- [Template Files](templates)
|
||||
|
||||
@@ -10,7 +10,7 @@ import { log } from "./logger"
|
||||
import { MassargCommand } from "massarg/command"
|
||||
import { getUniqueTmpPath as generateUniqueTmpPath } from "./file"
|
||||
import { colorize } from "./colors"
|
||||
import { isInteractive, promptForMissingConfig } from "./prompts"
|
||||
import { promptForMissingConfig, resolveInputs } from "./prompts"
|
||||
|
||||
export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
const isProjectRoot = Boolean(await fs.stat(path.join(__dirname, "package.json")).catch(() => false))
|
||||
@@ -45,7 +45,8 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
|
||||
log(config, LogLevel.debug, "Parsing config file...", config)
|
||||
const parsed = await parseConfigFile(config)
|
||||
await Scaffold(parsed)
|
||||
const resolved = await resolveInputs(parsed)
|
||||
await Scaffold(resolved)
|
||||
} catch (e) {
|
||||
const message = "message" in (e as object) ? (e as Error).message : e?.toString()
|
||||
log(config, LogLevel.error, message)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import input from "@inquirer/input"
|
||||
import select from "@inquirer/select"
|
||||
import { colorize } from "./colors"
|
||||
import { ScaffoldCmdConfig, ScaffoldConfigMap } from "./types"
|
||||
import { ScaffoldCmdConfig, ScaffoldConfig, ScaffoldConfigMap, ScaffoldInput } from "./types"
|
||||
|
||||
/** Prompts the user for a scaffold name. */
|
||||
export async function promptForName(): Promise<string> {
|
||||
@@ -59,6 +59,41 @@ export async function promptForTemplates(): Promise<string[]> {
|
||||
return value.split(",").map((t) => t.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user for any required scaffold inputs that are not already provided in data.
|
||||
* Also applies default values for optional inputs that have one.
|
||||
* Returns the merged data object.
|
||||
*/
|
||||
export async function promptForInputs(
|
||||
inputs: Record<string, ScaffoldInput>,
|
||||
existingData: Record<string, unknown> = {},
|
||||
): Promise<Record<string, unknown>> {
|
||||
const data = { ...existingData }
|
||||
|
||||
for (const [key, def] of Object.entries(inputs)) {
|
||||
// Skip if already provided via data/CLI
|
||||
if (key in data && data[key] !== undefined && data[key] !== "") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (def.required) {
|
||||
data[key] = await input({
|
||||
message: colorize.cyan(def.message ?? `${key}:`),
|
||||
required: true,
|
||||
default: def.default,
|
||||
validate: (value) => {
|
||||
if (!value.trim()) return `${key} is required`
|
||||
return true
|
||||
},
|
||||
})
|
||||
} else if (def.default !== undefined && !(key in data)) {
|
||||
data[key] = def.default
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/** Returns true if the process is running in an interactive terminal. */
|
||||
export function isInteractive(): boolean {
|
||||
return Boolean(process.stdin.isTTY)
|
||||
@@ -97,3 +132,30 @@ export async function promptForMissingConfig(
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts for any required inputs defined in the scaffold config and merges them into data.
|
||||
* Only prompts in interactive mode; in non-interactive mode, only applies defaults.
|
||||
*/
|
||||
export async function resolveInputs(config: ScaffoldConfig): Promise<ScaffoldConfig> {
|
||||
if (!config.inputs) {
|
||||
return config
|
||||
}
|
||||
|
||||
const interactive = isInteractive()
|
||||
|
||||
if (interactive) {
|
||||
config.data = await promptForInputs(config.inputs, config.data)
|
||||
} else {
|
||||
// Non-interactive: only apply defaults
|
||||
const data = { ...config.data }
|
||||
for (const [key, def] of Object.entries(config.inputs)) {
|
||||
if (def.default !== undefined && !(key in data)) {
|
||||
data[key] = def.default
|
||||
}
|
||||
}
|
||||
config.data = data
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { LogLevel, MinimalConfig, Resolver, ScaffoldCmdConfig, ScaffoldConfig }
|
||||
import { registerHelpers } from "./parser"
|
||||
import { log, logInitStep } from "./logger"
|
||||
import { parseConfigFile } from "./config"
|
||||
import { resolveInputs } from "./prompts"
|
||||
|
||||
/**
|
||||
* Create a scaffold using given `options`.
|
||||
@@ -50,6 +51,7 @@ import { parseConfigFile } from "./config"
|
||||
export async function Scaffold(config: ScaffoldConfig): Promise<void> {
|
||||
config.output ??= process.cwd()
|
||||
|
||||
config = await resolveInputs(config)
|
||||
registerHelpers(config)
|
||||
try {
|
||||
config.data = { name: config.name, ...config.data }
|
||||
|
||||
37
src/types.ts
37
src/types.ts
@@ -166,10 +166,47 @@ export interface ScaffoldConfig {
|
||||
outputPath: string,
|
||||
): string | Buffer | undefined | Promise<string | Buffer | undefined>
|
||||
|
||||
/**
|
||||
* Defines interactive inputs for the template. Each input becomes a template data variable.
|
||||
*
|
||||
* When running interactively, required inputs that are not already provided via `data` or CLI args
|
||||
* will be prompted for. Optional inputs without a value will use their `default` if defined.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* Scaffold({
|
||||
* // ...
|
||||
* inputs: {
|
||||
* author: { message: "Author name", required: true },
|
||||
* license: { message: "License", default: "MIT" },
|
||||
* },
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* In templates: `{{ author }}`, `{{ license }}`
|
||||
*
|
||||
* @see {@link ScaffoldInput}
|
||||
*/
|
||||
inputs?: Record<string, ScaffoldInput>
|
||||
|
||||
/** @internal */
|
||||
tmpDir?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a single interactive input for a scaffold template.
|
||||
*
|
||||
* @category Config
|
||||
*/
|
||||
export interface ScaffoldInput {
|
||||
/** The prompt message shown to the user. Defaults to the input key name if omitted. */
|
||||
message?: string
|
||||
/** Whether this input must be provided. If true and missing, the user will be prompted interactively. */
|
||||
required?: boolean
|
||||
/** Default value used when the user doesn't provide one. */
|
||||
default?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The names of the available helper functions that relate to text capitalization.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect, vi, beforeEach } from "vitest"
|
||||
import { LogLevel, ScaffoldCmdConfig } from "../src/types"
|
||||
import { LogLevel, ScaffoldCmdConfig, ScaffoldConfig } from "../src/types"
|
||||
|
||||
vi.mock("@inquirer/input", () => ({
|
||||
default: vi.fn(),
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
promptForOutput,
|
||||
promptForTemplates,
|
||||
promptForMissingConfig,
|
||||
promptForInputs,
|
||||
resolveInputs,
|
||||
isInteractive,
|
||||
} from "../src/prompts"
|
||||
|
||||
@@ -235,4 +237,163 @@ describe("prompts", () => {
|
||||
expect(inputMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
describe("promptForInputs", () => {
|
||||
test("prompts for required inputs not in existing data", async () => {
|
||||
vi.mocked(inputMock).mockResolvedValueOnce("John")
|
||||
const result = await promptForInputs(
|
||||
{ author: { message: "Author name", required: true } },
|
||||
{},
|
||||
)
|
||||
expect(result.author).toEqual("John")
|
||||
expect(inputMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
test("skips inputs already provided in data", async () => {
|
||||
const result = await promptForInputs(
|
||||
{ author: { message: "Author name", required: true } },
|
||||
{ author: "Jane" },
|
||||
)
|
||||
expect(result.author).toEqual("Jane")
|
||||
expect(inputMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("applies default value for optional inputs not in data", async () => {
|
||||
const result = await promptForInputs(
|
||||
{ license: { default: "MIT" } },
|
||||
{},
|
||||
)
|
||||
expect(result.license).toEqual("MIT")
|
||||
expect(inputMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("does not apply default when value already exists", async () => {
|
||||
const result = await promptForInputs(
|
||||
{ license: { default: "MIT" } },
|
||||
{ license: "Apache-2.0" },
|
||||
)
|
||||
expect(result.license).toEqual("Apache-2.0")
|
||||
})
|
||||
|
||||
test("uses input key as message fallback", async () => {
|
||||
vi.mocked(inputMock).mockResolvedValueOnce("val")
|
||||
await promptForInputs(
|
||||
{ myField: { required: true } },
|
||||
{},
|
||||
)
|
||||
const call = vi.mocked(inputMock).mock.calls[0][0] as { message: string }
|
||||
expect(call.message).toContain("myField")
|
||||
})
|
||||
|
||||
test("prompts multiple required inputs in order", async () => {
|
||||
vi.mocked(inputMock)
|
||||
.mockResolvedValueOnce("John")
|
||||
.mockResolvedValueOnce("2.0")
|
||||
const result = await promptForInputs(
|
||||
{
|
||||
author: { message: "Author", required: true },
|
||||
version: { message: "Version", required: true },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.author).toEqual("John")
|
||||
expect(result.version).toEqual("2.0")
|
||||
expect(inputMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test("mixes prompts, defaults, and existing data", async () => {
|
||||
vi.mocked(inputMock).mockResolvedValueOnce("John")
|
||||
const result = await promptForInputs(
|
||||
{
|
||||
author: { message: "Author", required: true },
|
||||
license: { default: "MIT" },
|
||||
description: { message: "Desc", required: true },
|
||||
},
|
||||
{ description: "My project" },
|
||||
)
|
||||
expect(result.author).toEqual("John")
|
||||
expect(result.license).toEqual("MIT")
|
||||
expect(result.description).toEqual("My project")
|
||||
expect(inputMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
test("preserves existing data keys not in inputs", async () => {
|
||||
const result = await promptForInputs(
|
||||
{ license: { default: "MIT" } },
|
||||
{ extra: "value" },
|
||||
)
|
||||
expect(result.extra).toEqual("value")
|
||||
expect(result.license).toEqual("MIT")
|
||||
})
|
||||
|
||||
test("required input with default pre-fills prompt", async () => {
|
||||
vi.mocked(inputMock).mockResolvedValueOnce("custom")
|
||||
await promptForInputs(
|
||||
{ author: { required: true, default: "Anonymous" } },
|
||||
{},
|
||||
)
|
||||
const call = vi.mocked(inputMock).mock.calls[0][0] as { default?: string }
|
||||
expect(call.default).toEqual("Anonymous")
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveInputs", () => {
|
||||
test("returns config unchanged when no inputs defined", async () => {
|
||||
const config: ScaffoldConfig = {
|
||||
name: "test",
|
||||
output: "out",
|
||||
templates: [],
|
||||
data: { foo: "bar" },
|
||||
}
|
||||
const result = await resolveInputs(config)
|
||||
expect(result.data).toEqual({ foo: "bar" })
|
||||
})
|
||||
|
||||
test("applies defaults in non-interactive mode", async () => {
|
||||
mockTTY(false)
|
||||
const config: ScaffoldConfig = {
|
||||
name: "test",
|
||||
output: "out",
|
||||
templates: [],
|
||||
data: {},
|
||||
inputs: {
|
||||
license: { default: "MIT" },
|
||||
},
|
||||
}
|
||||
const result = await resolveInputs(config)
|
||||
expect(result.data?.license).toEqual("MIT")
|
||||
expect(inputMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("does not overwrite existing data with defaults in non-interactive mode", async () => {
|
||||
mockTTY(false)
|
||||
const config: ScaffoldConfig = {
|
||||
name: "test",
|
||||
output: "out",
|
||||
templates: [],
|
||||
data: { license: "Apache-2.0" },
|
||||
inputs: {
|
||||
license: { default: "MIT" },
|
||||
},
|
||||
}
|
||||
const result = await resolveInputs(config)
|
||||
expect(result.data?.license).toEqual("Apache-2.0")
|
||||
})
|
||||
|
||||
test("prompts for required inputs in interactive mode", async () => {
|
||||
mockTTY(true)
|
||||
vi.mocked(inputMock).mockResolvedValueOnce("John")
|
||||
const config: ScaffoldConfig = {
|
||||
name: "test",
|
||||
output: "out",
|
||||
templates: [],
|
||||
data: {},
|
||||
inputs: {
|
||||
author: { message: "Author", required: true },
|
||||
},
|
||||
}
|
||||
const result = await resolveInputs(config)
|
||||
expect(result.data?.author).toEqual("John")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user