mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-17 17:28:09 +00:00
542 lines
18 KiB
TypeScript
542 lines
18 KiB
TypeScript
import { HelperDelegate } from "handlebars/runtime"
|
|
|
|
/**
|
|
* The config object for defining a scaffolding group.
|
|
*
|
|
* @see {@link https://chenasraf.github.io/simple-scaffold/docs/usage/node| Node.js usage}
|
|
* @see {@link https://chenasraf.github.io/simple-scaffold/docs/usage/cli| CLI usage}
|
|
* @see {@link DefaultHelpers}
|
|
* @see {@link CaseHelpers}
|
|
* @see {@link DateHelpers}
|
|
*
|
|
* @category Config
|
|
*/
|
|
export interface ScaffoldConfig {
|
|
/**
|
|
* Name to be passed to the generated files. `{{name}}` and `{{Name}}` inside contents and file names will be replaced
|
|
* accordingly.
|
|
*/
|
|
name: string
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* You may omit files from output by prepending a `!` to their glob pattern.
|
|
*
|
|
* For example, `["components/**", "!components/README.md"]` will include everything in the directory `components`
|
|
* except the `README.md` file inside.
|
|
*
|
|
* @default Current working directory
|
|
*/
|
|
templates: string[]
|
|
|
|
/**
|
|
* Path to output to. If `subdir` is `true`, the subdir will be created inside this path.
|
|
*
|
|
* May also be a {@link FileResponseHandler} which returns a new output path to override the default one.
|
|
*
|
|
* @see {@link FileResponse}
|
|
* @see {@link FileResponseHandler}
|
|
*/
|
|
output: FileResponse<string>
|
|
|
|
/**
|
|
* Whether to create subdir with the input name.
|
|
*
|
|
* When `true`, you may also use {@link subdirHelper} to determine a pre-process helper on
|
|
* the directory name.
|
|
*
|
|
* @default `false`
|
|
*/
|
|
subdir?: boolean
|
|
|
|
/**
|
|
* Add custom data to the templates. By default, only your app name is included as `{{name}}` and `{{Name}}`.
|
|
*
|
|
* This can be any object that will be usable by Handlebars.
|
|
*/
|
|
data?: Record<string, unknown>
|
|
|
|
/**
|
|
* Enable to override output files, even if they already exist.
|
|
*
|
|
* You may supply a function to this option, which can take the arguments `(fullPath, baseDir, baseName)` and returns
|
|
* a boolean for each file.
|
|
*
|
|
* May also be a {@link FileResponseHandler} which returns a boolean value per file.
|
|
*
|
|
* @see {@link FileResponse}
|
|
* @see {@link FileResponseHandler}
|
|
*
|
|
* @default `false`
|
|
*/
|
|
overwrite?: FileResponse<boolean>
|
|
|
|
/**
|
|
* Determine amount of logs to display.
|
|
*
|
|
* The values are: `0 (none) | 1 (debug) | 2 (info) | 3 (warn) | 4 (error)`. The provided level will display messages
|
|
* of the same level or higher.
|
|
*
|
|
* @see {@link LogLevel}
|
|
*
|
|
* @default `2 (info)`
|
|
*/
|
|
logLevel?: LogLevel
|
|
|
|
/**
|
|
* 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`
|
|
*/
|
|
dryRun?: boolean
|
|
|
|
/**
|
|
* Additional helpers to add to the template parser. Provide an object whose keys are the name of the function to add,
|
|
* and the value is the helper function itself. The signature of helpers is as follows:
|
|
* ```typescript
|
|
* (text: string, ...args: any[]) => string
|
|
* ```
|
|
*
|
|
* A full example might be:
|
|
*
|
|
* ```typescript
|
|
* Scaffold({
|
|
* //...
|
|
* helpers: {
|
|
* upperKebabCase: (text) => kebabCase(text).toUpperCase()
|
|
* }
|
|
* })
|
|
* ```
|
|
*
|
|
* Which will allow:
|
|
*
|
|
* ```
|
|
* {{ upperKebabCase "my value" }}
|
|
* ```
|
|
*
|
|
* To transform to:
|
|
*
|
|
* ```
|
|
* MY-VALUE
|
|
* ```
|
|
*
|
|
* See {@link DefaultHelpers} for a list of all the built-in available helpers.
|
|
*
|
|
* Simple Scaffold uses Handlebars.js, so all the syntax from there is supported. See
|
|
* [their docs](https://handlebarsjs.com/guide/#custom-helpers) for more information.
|
|
*
|
|
* @see {@link DefaultHelpers}
|
|
* @see {@link CaseHelpers}
|
|
* @see {@link DateHelpers}
|
|
* @see {@link https://chenasraf.github.io/simple-scaffold/docs/usage/templates| Templates}
|
|
* */
|
|
helpers?: Record<string, Helper>
|
|
|
|
/**
|
|
* Default transformer to apply to subdir name when using `subdir: true`. Can be one of the default
|
|
* capitalization helpers, or a custom one you provide to `helpers`. Defaults to `undefined`, which means no
|
|
* transformation is done.
|
|
*
|
|
* @see {@link subdir}
|
|
* @see {@link CaseHelpers}
|
|
* @see {@link DefaultHelpers}
|
|
*/
|
|
subdirHelper?: DefaultHelpers | string
|
|
|
|
/**
|
|
* This callback runs right before content is being written to the disk. If you supply this function, you may return
|
|
* a string that represents the final content of your file, you may process the content as you see fit. For example,
|
|
* you may run formatters on a file, fix output in edge-cases not supported by helpers or data, etc.
|
|
*
|
|
* If the return value of this function is `undefined`, the original content will be used.
|
|
*
|
|
* @param content The original template after token replacement
|
|
* @param rawContent The original template before token replacement
|
|
* @param outputPath The final output path of the processed file
|
|
*
|
|
* @returns {Promise<String | Buffer | undefined> | String | Buffer | undefined} The final output of the file
|
|
* contents-only, after further modifications - or `undefined` to use the original content (i.e. `content.toString()`)
|
|
*/
|
|
beforeWrite?(
|
|
content: Buffer,
|
|
rawContent: Buffer,
|
|
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>
|
|
|
|
/**
|
|
* 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 */
|
|
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
|
|
|
|
/**
|
|
* The type of an interactive input prompt.
|
|
*
|
|
* - `"text"` — free-form text input (default)
|
|
* - `"select"` — choose from a list of options
|
|
* - `"confirm"` — yes/no boolean prompt
|
|
* - `"number"` — numeric input
|
|
*
|
|
* @category Config
|
|
*/
|
|
export type ScaffoldInputType = "text" | "select" | "confirm" | "number"
|
|
|
|
/**
|
|
* Defines a single interactive input for a scaffold template.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* inputs: {
|
|
* author: { message: "Author name", required: true },
|
|
* license: { type: "select", message: "License", options: ["MIT", "Apache-2.0", "GPL-3.0"] },
|
|
* private: { type: "confirm", message: "Private package?", default: false },
|
|
* port: { type: "number", message: "Dev server port", default: 3000 },
|
|
* }
|
|
* ```
|
|
*
|
|
* @category Config
|
|
*/
|
|
export interface ScaffoldInput {
|
|
/** The type of prompt. Defaults to `"text"`. */
|
|
type?: ScaffoldInputType
|
|
/** 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. Type depends on the input type: string for text/select, boolean for confirm, number for number. */
|
|
default?: string | boolean | number
|
|
/** List of options for `type: "select"`. Each can be a string or `{ name, value }`. */
|
|
options?: (string | { name: string; value: string })[]
|
|
}
|
|
|
|
/**
|
|
* The names of the available helper functions that relate to text capitalization.
|
|
*
|
|
* These are available for `subdirHelper`.
|
|
*
|
|
* | Helper name | Example code | Example output |
|
|
* | ------------ | ----------------------- | -------------- |
|
|
* | [None] | `{{ name }}` | my name |
|
|
* | `camelCase` | `{{ camelCase name }}` | myName |
|
|
* | `snakeCase` | `{{ snakeCase name }}` | my_name |
|
|
* | `startCase` | `{{ startCase name }}` | My Name |
|
|
* | `kebabCase` | `{{ kebabCase name }}` | my-name |
|
|
* | `hyphenCase` | `{{ hyphenCase name }}` | my-name |
|
|
* | `pascalCase` | `{{ pascalCase name }}` | MyName |
|
|
* | `upperCase` | `{{ upperCase name }}` | MY NAME |
|
|
* | `lowerCase` | `{{ lowerCase name }}` | my name |
|
|
*
|
|
* @see {@link DefaultHelpers}
|
|
* @see {@link DateHelpers}
|
|
* @see {@link ScaffoldConfig}
|
|
* @see {@link ScaffoldConfig.subdirHelper}
|
|
*
|
|
* @category Helpers
|
|
*/
|
|
export type CaseHelpers =
|
|
| "camelCase"
|
|
| "hyphenCase"
|
|
| "kebabCase"
|
|
| "lowerCase"
|
|
| "pascalCase"
|
|
| "snakeCase"
|
|
| "startCase"
|
|
| "upperCase"
|
|
|
|
/**
|
|
* The names of the available helper functions that relate to dates.
|
|
*
|
|
* | Helper name | Description | Example code | Example output |
|
|
* | -------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------ |
|
|
* | `now` | Current date with format | `{{ now "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
|
|
* | `now` (with offset) | Current date with format, and with offset | `{{ now "yyyy-MM-dd HH:mm" -1 "hours" }}` | `2042-01-01 14:00` |
|
|
* | `date` | Custom date with format | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
|
|
* | `date` (with offset) | Custom date with format, and with offset | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" -1 "days" }}` | `2041-31-12 15:00` |
|
|
* | `date` (with date from `--data`) | Custom date with format, with data from the `data` config option | `{{ date myCustomDate "yyyy-MM-dd HH:mm" }}` | `2042-01-01 12:00` |
|
|
*
|
|
* Further details:
|
|
*
|
|
* - We use [`date-fns`](https://date-fns.org/docs/) for parsing/manipulating the dates. If you want
|
|
* more information on the date tokens to use, refer to
|
|
* [their format documentation](https://date-fns.org/docs/format).
|
|
*
|
|
* - The date helper format takes the following arguments:
|
|
*
|
|
* ```typescript
|
|
* (
|
|
* date: string,
|
|
* format: string,
|
|
* offsetAmount?: number,
|
|
* offsetType?: "years" | "months" | "weeks" | "days" | "hours" | "minutes" | "seconds"
|
|
* )
|
|
* ```
|
|
*
|
|
* - **The now helper** (for current time) takes the same arguments, minus the first one (`date`) as it is implicitly
|
|
* the current date.
|
|
*
|
|
* @see {@link DefaultHelpers}
|
|
* @see {@link CaseHelpers}
|
|
* @see {@link ScaffoldConfig}
|
|
*
|
|
* @category Helpers
|
|
*/
|
|
export type DateHelpers = "date" | "now"
|
|
|
|
/**
|
|
* The names of all the available helper functions in templates.
|
|
* Simple-Scaffold provides some built-in text transformation filters usable by Handlebars.js.
|
|
*
|
|
* For example, you may use `{{ snakeCase name }}` inside a template file or filename, and it will
|
|
* replace `My Name` with `my_name` when producing the final value.
|
|
*
|
|
* @see {@link CaseHelpers}
|
|
* @see {@link DateHelpers}
|
|
* @see {@link ScaffoldConfig}
|
|
*
|
|
* @category Helpers
|
|
*/
|
|
export type DefaultHelpers = CaseHelpers | DateHelpers
|
|
|
|
/**
|
|
* Helper function, see https://handlebarsjs.com/guide/#custom-helpers
|
|
*
|
|
* @category Helpers
|
|
*/
|
|
export type Helper = HelperDelegate
|
|
|
|
/**
|
|
* The amount of information to log when generating scaffold.
|
|
* When not `none`, the selected level will be the lowest level included.
|
|
*
|
|
* For example, level `info` will include `info`, `warning` and `error`, but not `debug`; and `warning` will only
|
|
* show `warning` and `error`, but not `info` or `debug`.
|
|
*
|
|
* @default `info`
|
|
*
|
|
* @category Logging (const)
|
|
*/
|
|
|
|
export const LogLevel = {
|
|
/** Silent output */
|
|
none: "none",
|
|
/** Debugging information. Very verbose and only recommended for troubleshooting. */
|
|
debug: "debug",
|
|
/**
|
|
* The regular level of logging. Major actions are logged to show the scaffold progress.
|
|
*
|
|
* @default
|
|
*/
|
|
info: "info",
|
|
/** Warnings such as when file fails to replace token values properly in template. */
|
|
warning: "warning",
|
|
/** Errors, such as missing files, bad replacement token syntax, or un-writable directories. */
|
|
error: "error",
|
|
} as const
|
|
|
|
/**
|
|
* The amount of information to log when generating scaffold.
|
|
* When not `none`, the selected level will be the lowest level included.
|
|
*
|
|
* For example, level `info` will include `info`, `warning` and `error`, but not `debug`; and `warning` will only
|
|
* show `warning` and `error`, but not `info` or `debug`.
|
|
*
|
|
* @default `info`
|
|
*
|
|
* @category Logging (type)
|
|
*/
|
|
export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]
|
|
|
|
/**
|
|
* A function that takes path information about file, and returns a value of type `T`
|
|
*
|
|
* @template T The return type for the function
|
|
* @param {string} fullPath The full path of the current file
|
|
* @param {string} basedir The directory containing the current file
|
|
* @param {string} basename The name of the file
|
|
*
|
|
* @returns {T} A return value
|
|
*
|
|
* @category Config
|
|
*/
|
|
export type FileResponseHandler<T> = (fullPath: string, basedir: string, basename: string) => T
|
|
|
|
/**
|
|
* Represents a response for file path information.
|
|
* Can either be:
|
|
*
|
|
* 1. `T` - static value
|
|
* 2. A function with the following signature which returns `T`:
|
|
* ```typescript
|
|
* (fullPath: string, basedir: string, basename: string) => T
|
|
* ```
|
|
*
|
|
* @see {@link FileResponseHandler}
|
|
*
|
|
* @category Config
|
|
* */
|
|
export type FileResponse<T> = T | FileResponseHandler<T>
|
|
|
|
/**
|
|
* The Scaffold config for CLI
|
|
* Contains less and more specific options than {@link ScaffoldConfig}.
|
|
*
|
|
* For more information about each option, see {@link ScaffoldConfig}.
|
|
*/
|
|
export type ScaffoldCmdConfig = {
|
|
/** The name of the scaffold template to use. */
|
|
name: string
|
|
/** The templates to use for generation */
|
|
templates: string[]
|
|
/** The output path to write to */
|
|
output: string
|
|
/** Whether to create subdir with the input name */
|
|
subdir: boolean
|
|
/** Default transformer to apply to subdir name when using `subdir: true` */
|
|
subdirHelper?: string
|
|
/** Add custom data to the templates */
|
|
data?: Record<string, string>
|
|
/** Add custom data to the template in a CLI-friendly syntax (and not JSON) */
|
|
appendData?: Record<string, string>
|
|
/** Enable to override output files, even if they already exist */
|
|
overwrite: boolean
|
|
/** Silence logs, same as `logLevel: "none"` */
|
|
quiet: boolean
|
|
/**
|
|
* Determine amount of logs to display.
|
|
*
|
|
* @see {@link LogLevel}
|
|
*/
|
|
logLevel: LogLevel
|
|
/** 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. */
|
|
dryRun: boolean
|
|
/** Config file path to use */
|
|
config?: string
|
|
/** The key of the template to use */
|
|
key?: string
|
|
/** The git repository to use to fetch the config file */
|
|
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
|
|
/** Run a shell command after all files have been written. Executed in the output directory. */
|
|
afterScaffold?: string
|
|
/** @internal */
|
|
tmpDir?: string
|
|
}
|
|
|
|
/**
|
|
* A mapping of scaffold template keys to their configurations.
|
|
*
|
|
* Each configuration is a {@link ScaffoldConfig} object.
|
|
*
|
|
* The key is the name of the template, and the value is the configuration for that template.
|
|
*
|
|
* When no template key is provided to the scaffold command, the "default" template is used.
|
|
*
|
|
* @see {@link ScaffoldConfig}
|
|
*
|
|
* @category Config
|
|
*/
|
|
export type ScaffoldConfigMap = Record<string, ScaffoldConfig>
|
|
|
|
/**
|
|
* The scaffold config file is either:
|
|
* - A {@link ScaffoldConfigMap} object
|
|
* - A function that returns a {@link ScaffoldConfigMap} object
|
|
* - A promise that resolves to a {@link ScaffoldConfigMap} object
|
|
* - A function that returns a promise that resolves to a {@link ScaffoldConfigMap} object
|
|
*
|
|
* @category Config
|
|
*/
|
|
export type ScaffoldConfigFile = AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>
|
|
|
|
/** @internal */
|
|
export type Resolver<T, R = T> = R | ((_value: T) => R)
|
|
|
|
/** @internal */
|
|
export type AsyncResolver<T, R = T> = Resolver<T, Promise<R> | R>
|
|
|
|
/** @internal */
|
|
export type LogConfig = Pick<ScaffoldConfig, "logLevel">
|
|
|
|
/** @internal */
|
|
export type ConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config">
|
|
|
|
/** @internal */
|
|
export type RemoteConfigLoadConfig = LogConfig &
|
|
Pick<ScaffoldCmdConfig, "config" | "git" | "tmpDir">
|
|
|
|
/** @internal */
|
|
export type MinimalConfig = Pick<ScaffoldCmdConfig, "name" | "key">
|
|
|
|
/** @internal */
|
|
export type ListCommandCliOptions = Pick<ScaffoldCmdConfig, "config" | "git" | "logLevel" | "quiet">
|