@corrbo/plop-kit
v1.0.0
Published
A structured plop framework — defineCommand, auto-discovery, dry-run, meta generators
Maintainers
Readme
plop-kit
A structured plop framework for projects that need multiple generators organized by category. Adds auto-discovery, dry-run preview, command banners, bypass args, and meta generators on top of raw plop.
Requirements
- Node.js ≥ 18
- plop ≥ 4 (peer dependency)
Installation
npm install plop-kit plop
# or copy the package into packages/plop-kit and add NODE_PATH=packages to your scriptsBootstrap a new project
If you're setting up from scratch, run the gen-cli meta generator once — it scaffolds the entire CLI folder with auto-discovery, constants, and tsconfig:
# no plop-kit.config.js needed for this first run
NODE_PATH=packages npx plop --plopfile packages/plop-kit/plopfile.js
# → select "gen-cli", answer three questionsThis creates packages/configs/cli/ and adds gen + gen:meta to package.json.
Core API
defineCommand(plop, config)
Registers a plop generator with a category prefix in the menu, a help banner shown before prompts, and optional dry-run wrapping.
import { defineCommand } from 'plop-kit'
import type { NodePlopAPI } from 'node-plop'
export = (plop: NodePlopAPI) => defineCommand(plop, {
name: 'feature', // generator name shown in menu and used as CLI arg
category: 'blm', // must match a key registered via configure()
description: 'create a new feature',
help: 'Creates actions.ts, types.ts, index.ts in src/blm/{module}/features/{name}/',
prompts: featurePrompts,
actions: getFeatureActions, // ActionType[] or (data) => ActionType[]
})Banner behavior: in interactive mode, plop-kit prints the description + help text before the first prompt, plus a tip about -n when not already in dry-run mode. When bypass args are present (e.g. --name auth) the banner is suppressed entirely — safe for CI and scripts.
configure(plop, categories)
Registers the category menu labels. Call once in your root plopfile before any defineCommand.
configure(plop, {
blm: '⚙️ BLM',
ui: '🖼 UI',
tools: '🔧 Tools',
})Action helpers
All helpers validate template files at call time and return plain ActionType[] arrays that plop can consume.
createAddActions(items)
Creates type: 'add' actions. Each item needs dest (absolute path), template (absolute path to .hbs file), and optionally data and force.
import { createAddActions, withTemplates } from 'plop-kit'
const T = withTemplates(__dirname) // resolves to __dirname/templates/<name>
createAddActions([
{ dest: path.join(dir, 'index.ts'), template: T('index.hbs'), data: { name } },
{ dest: path.join(dir, 'actions.ts'), template: T('actions.hbs'), data: { name } },
withData && { dest: path.join(dir, 'data.ts'), template: T('data.hbs') },
])
// falsy items (false | null | undefined) are filtered out automaticallycreateModifyActions(items)
Creates type: 'modify' actions (regex replace inside an existing file).
createModifyActions([
{
path: path.join(dir, 'store/index.ts'),
pattern: /^/,
template: "export * from './{{name}}'\n",
},
])createAppendActions(items)
Creates type: 'append' actions (insert after a matched pattern).
createAppendActions([
{
path: path.join(dir, 'index.ts'),
pattern: /\/\/ exports/,
template: "export { {{name}} } from './{{name}}'",
},
])withTemplates(dir)
Returns a helper that resolves template names to dir/templates/<name>.
const T = withTemplates(__dirname)
T('index.hbs') // → /absolute/path/to/templates/index.hbsName utilities
Pure functions for transforming names — also registered as plop handlebars helpers via registerHelpers.
| Function | Input | Output |
|---|---|---|
| toPascal(name, suffix?) | 'auth-list' | 'AuthList' |
| toCamel(name, suffix?) | 'auth-list' | 'authList' |
| toKebab(name) | 'AuthList' | 'auth-list' |
| toSnake(name) | 'AuthList' | 'auth_list' |
| parseList(str) | 'a, b, c' | ['a', 'b', 'c'] |
Optional suffix strips the suffix before casing and re-appends it:
toPascal('auth', 'Store') // → 'AuthStore'
toPascal('authStore', 'Store') // → 'AuthStore' (idempotent)registerHelpers(plop)
Registers the name utils as handlebars helpers plus two extras:
{{pascalCase name}},{{camelCase name}},{{kebabCase name}},{{snakeCase name}}{{toJS obj}}— serializes an object as JS literal (unquoted keys){{toProps obj}}— serializes as unquoted keys + unquoted string values
Safe to call multiple times — registers only once per plop instance.
registerMeta(plop, options?)
Registers the meta generators (gen-cli, gen-command) under the 🔧 Meta category. Call once in your root plopfile.
// plop-kit.config.js
const path = require('path')
module.exports = {
commandsPath: path.join(__dirname, 'packages/configs/cli/commands'), // where category folders live
configsPath: path.join(__dirname, 'packages/configs/cli'), // root of the CLI folder (used by gen-cli)
rootPath: __dirname, // project root (where package.json is)
nodePath: 'packages', // NODE_PATH value (for require('plop-kit'))
anchorScript: 'gen:meta', // new gen:{category} scripts are inserted after this key in package.json
}// packages/plop-kit/plopfile.js (or your custom plopfile)
const options = require('./plop-kit.config.js')
module.exports = plop => {
registerHelpers(plop)
registerMeta(plop, options)
}isDryRun()
Returns true when the process was started with --dry-run, -n, or --preview.
import { isDryRun } from 'plop-kit'
if (isDryRun()) {
// skip expensive side effects
}Dry-run mode
Every generator registered via defineCommand automatically supports dry-run. Pass the flag after --:
npm run gen -- --dry-run # preview all files
npm run gen:blm -- -n # short form
npm run gen -- --preview # aliasOutput:
📋 Dry run — no files will be written
+ src/blm/auth/features/login/actions.ts
+ src/blm/auth/features/login/index.ts
~ src/blm/auth/store/index.tsNo files are written. Prompts are still interactive so you see real resolved paths.
Bypass args (non-interactive)
Pass prompt answers directly on the CLI to skip interactive input — useful for scripts and CI:
npm run gen:blm -- feature --blmName auth --features "login,logout"--name value syntax, or --name=value. Boolean prompts: --withForm true. Checkbox prompts: --features "a,b,c".
Unrecognised answers fall back to interactive mode for that question only.
Auto-discovery conventions
plop-kit assumes this folder layout. Adding a new category or command is just creating a folder — no manual registration needed.
packages/configs/cli/
├── bootstrap.cjs ← require('tsx/cjs') + require('./index.ts')
├── index.ts ← scans commands/*/plopfile.ts
└── commands/
└── {category}/
├── plopfile.ts ← exports { key, label, register }
└── {command}/
└── index.ts ← export = (plop) => defineCommand(plop, {...})Category plopfile (commands/blm/plopfile.ts):
import fs from 'fs'
import path from 'path'
import { registerHelpers } from 'plop-kit'
import type { NodePlopAPI } from 'node-plop'
export const key = 'blm'
export const label = '⚙️ BLM'
export function register(plop: NodePlopAPI) {
registerHelpers(plop)
for (const entry of fs.readdirSync(__dirname, { withFileTypes: true })) {
if (!entry.isDirectory()) continue
const idx = path.join(__dirname, entry.name, 'index.ts')
if (fs.existsSync(idx)) require(idx)(plop)
}
}Meta generators
Run via npm run gen:meta.
gen-cli
Bootstraps a full CLI folder in a new project (one-time setup). Creates bootstrap.cjs, index.ts, tsconfig.json, constants/, plop-kit.config.js, and adds gen + gen:meta to package.json.
gen-command
Scaffolds a new command inside an existing category. Detects whether the category uses TypeScript or JavaScript and generates index.ts/js, actions.ts/js, prompts.ts/js, and starter .hbs template files accordingly.
When creating a new category, also generates plopfile.ts, bootstrap.cjs, and inserts a gen:{category} npm script into package.json after anchorScript.
Custom commands (beyond scaffolding)
defineCommand isn't limited to file generation. Any logic that belongs in the developer toolbox — running scripts, parsing existing files, orchestrating multiple generators — can be registered as a command and appear in the same unified menu.
Shell command wrapper
Run an existing npm script (or any shell command) as a plop action:
export function iconsActions(): ActionType[] {
return [
{
type: 'custom',
description: 'Generate icons via SVGR',
async action() {
const { execSync } = require('child_process')
execSync('npm run gen:icons', { stdio: 'inherit' })
return 'done'
},
},
]
}
// index.ts
export = (plop: NodePlopAPI) => defineCommand(plop, {
name: 'icons',
category: 'tools',
description: 'Regenerate icon components from SVG sources',
help: 'Runs SVGR on svg-icons/files/ → src/components/icons/.',
prompts: [],
actions: iconsActions,
})The developer runs npm run gen:tools, picks icons from the menu, and the script executes — no need to remember the underlying command.
Orchestrator (compose multiple generators)
A command can read existing files and call other commands' action functions directly, assembling a composite action list at runtime:
import { getBlockCreateActions } from '../ui/screen-block/actions'
import { getScreenComponentActions } from '../ui/screen-component/actions'
export function componentParserActions(data: { file: string }): ActionType[] {
const content = fs.readFileSync(screenPath(data.file), 'utf8')
const parsed = extractComponentsFromComments(content)
return parsed.flatMap(item => {
if (item.type === 'block') return getBlockCreateActions(item)
if (item.type === 'screen-component') return getScreenComponentActions(item)
return []
})
}This lets a single command parse a file annotated with special comments and scaffold everything it finds — blocks, components, stores — in one run, reusing the same action functions used by individual generators.
Testing
npm test # vitest run (single pass)
npm run test:watch # watch modeTests live in tests/ and cover name-utils, dry-run, bypass args, action helpers, and defineCommand / configure behavior. All tests use vitest with a mock plop object — no disk writes except create-actions.test.js which writes to os.tmpdir().
CHANGELOG
See CHANGELOG.md.
