Micro-generator framework that makes it easy for an entire team to create files with a level of uniformity
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Plop's action system provides built-in file operations and supports custom actions for flexible code generation workflows. Actions are executed after prompts are answered and define what files to create, modify, or manipulate.
Plop includes four core action types for common file operations.
// Built-in action types
type BuiltInActionType = "add" | "addMany" | "modify" | "append";Create a single file from a template.
interface AddActionConfig extends ActionConfig {
type: "add";
path: string; // Destination file path (supports templates)
skipIfExists?: boolean; // Skip if file already exists (default: false)
transform?: TransformFn<AddActionConfig>;
} & TemplateStrOrFileUsage Examples:
// Basic add action with inline template
{
type: 'add',
path: 'src/components/{{pascalCase name}}.jsx',
template: `import React from 'react';
export const {{pascalCase name}} = () => {
return <div>{{name}}</div>;
};`
}
// Add action with template file
{
type: 'add',
path: 'src/{{type}}/{{kebabCase name}}.js',
templateFile: 'templates/{{type}}.hbs',
skipIfExists: true
}
// Add with transform function
{
type: 'add',
path: 'src/{{name}}.ts',
templateFile: 'templates/service.hbs',
transform: (template, data) => {
// Add type annotations for TypeScript
return template.replace(/: any/g, `: ${data.returnType || 'unknown'}`);
}
}Create multiple files from a template pattern or directory.
interface AddManyActionConfig extends ActionConfig {
type: "addMany";
destination: string; // Base destination directory
base: string; // Base template directory
templateFiles: string | string[]; // Glob pattern(s) for template files
stripExtensions?: string[]; // Extensions to remove from filenames
globOptions?: GlobOptions; // Options for glob matching
verbose?: boolean; // Log each file operation
transform?: TransformFn<AddManyActionConfig>;
}Usage Examples:
// Copy entire template directory
{
type: 'addMany',
destination: 'src/components/{{pascalCase name}}/',
base: 'templates/component/',
templateFiles: '**/*',
stripExtensions: ['hbs']
}
// Copy specific file patterns
{
type: 'addMany',
destination: 'src/modules/{{kebabCase name}}/',
base: 'templates/module/',
templateFiles: ['*.js.hbs', 'tests/*.test.js.hbs'],
stripExtensions: ['hbs'],
verbose: true
}
// With glob options
{
type: 'addMany',
destination: 'generated/{{name}}/',
base: 'templates/',
templateFiles: '**/*.{js,ts,json}',
globOptions: {
ignore: ['**/*.spec.*', '**/node_modules/**']
}
}Modify existing files using pattern matching and replacement.
interface ModifyActionConfig extends ActionConfig {
type: "modify";
path: string; // Target file path (supports templates)
pattern: string | RegExp; // Pattern to find in file
transform?: TransformFn<ModifyActionConfig>;
} & TemplateStrOrFileUsage Examples:
// Add import statement to existing file
{
type: 'modify',
path: 'src/index.js',
pattern: /(\/\/ -- PLOP IMPORTS --)/gi,
template: "import { {{pascalCase name}} } from './components/{{pascalCase name}}';\n$1"
}
// Replace section in config file
{
type: 'modify',
path: 'config/routes.js',
pattern: /(\s*)(\/\/ -- ROUTES --)/gi,
templateFile: 'templates/route-entry.hbs'
}
// Modify with regex and transform
{
type: 'modify',
path: 'src/{{name}}.js',
pattern: /class\s+(\w+)/,
template: 'class Enhanced{{pascalCase name}}',
transform: (template, data) => {
return template.replace(/Enhanced/, data.prefix || 'Enhanced');
}
}Append content to existing files with pattern matching.
interface AppendActionConfig extends ActionConfig {
type: "append";
path: string; // Target file path (supports templates)
pattern: string | RegExp; // Pattern to find insertion point
unique?: boolean; // Prevent duplicate entries (default: true)
separator?: string; // Separator between entries (default: newline)
} & TemplateStrOrFileUsage Examples:
// Append to array in JavaScript file
{
type: 'append',
path: 'src/config.js',
pattern: /(const routes = \[)/gi,
template: " '{{kebabCase name}}',",
separator: '\n'
}
// Append import with uniqueness check
{
type: 'append',
path: 'src/components/index.js',
pattern: /(\/\/ -- EXPORTS --)/gi,
template: "export { {{pascalCase name}} } from './{{pascalCase name}}';",
unique: true
}
// Append to end of file
{
type: 'append',
path: 'README.md',
pattern: /$/,
templateFile: 'templates/readme-section.hbs',
separator: '\n\n'
}Register custom action types for specialized operations.
/**
* Register a custom action type
* @param name - Action type name
* @param fn - Action function
*/
setActionType(name: string, fn: CustomActionFunction): void;
/**
* Get a registered action type function
* @param name - Action type name
* @returns Action type function
*/
getActionType(name: string): ActionType;
/**
* Get list of all registered action type names
* @returns Array of action type names
*/
getActionTypeList(): string[];
/**
* Custom action function signature
* @param answers - User answers from prompts
* @param config - Action configuration object
* @param plopfileApi - Plop API instance
* @returns Promise resolving to success message or string result
*/
type CustomActionFunction = (
answers: Answers,
config: CustomActionConfig<string>,
plopfileApi: NodePlopAPI,
) => Promise<string> | string;
interface CustomActionConfig<T extends string> extends ActionConfig {
type: T;
[key: string]: any; // Custom properties for action
}Usage Examples:
import { nodePlop } from "plop";
import fs from 'fs/promises';
import path from 'path';
const plop = await nodePlop();
// Register custom action for API documentation
plop.setActionType('generateDocs', async (answers, config, plop) => {
const apiPath = path.join(config.basePath, answers.name, 'api.json');
// Generate API documentation
const apiSpec = {
name: answers.name,
version: '1.0.0',
endpoints: answers.endpoints || []
};
await fs.writeFile(apiPath, JSON.stringify(apiSpec, null, 2));
return `API documentation generated at ${apiPath}`;
});
// Register custom action for database operations
plop.setActionType('createMigration', async (answers, config) => {
const timestamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0];
const filename = `${timestamp}_create_${answers.tableName}.sql`;
const migrationPath = path.join('migrations', filename);
const migration = `
CREATE TABLE ${answers.tableName} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
${answers.columns.map(col => `${col.name} ${col.type}`).join(',\n ')},
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`;
await fs.writeFile(migrationPath, migration.trim());
return `Migration created: ${filename}`;
});
// Use custom actions in generator
plop.setGenerator('api-module', {
description: 'Generate API module with documentation',
prompts: [
{ type: 'input', name: 'name', message: 'Module name:' },
{ type: 'input', name: 'tableName', message: 'Database table:' }
],
actions: [
{
type: 'add',
path: 'src/{{name}}/index.js',
templateFile: 'templates/api-module.hbs'
},
{
type: 'generateDocs',
basePath: 'src'
},
{
type: 'createMigration',
tableName: '{{tableName}}'
}
]
});Common configuration options for all action types.
interface ActionConfig {
type: string; // Action type name
force?: boolean; // Force overwrite (overrides global setting)
data?: object; // Additional data for template rendering
abortOnFail?: boolean; // Abort generation if action fails
skip?: Function; // Function to conditionally skip action
}Usage Examples:
// Action with conditional execution
{
type: 'add',
path: 'src/{{name}}.test.js',
templateFile: 'templates/test.hbs',
skip: (data) => {
if (!data.includeTests) {
return 'Tests disabled by user';
}
return false; // Don't skip
}
}
// Action with additional template data
{
type: 'add',
path: 'src/{{name}}.js',
templateFile: 'templates/component.hbs',
data: {
timestamp: new Date().toISOString(),
author: 'Plop Generator',
version: '1.0.0'
}
}
// Action that aborts on failure
{
type: 'modify',
path: 'package.json',
pattern: /"dependencies":\s*{/,
template: '"dependencies": {\n "{{name}}": "^1.0.0",',
abortOnFail: true
}Use functions to dynamically generate actions based on user input.
/**
* Function that returns actions array based on user answers
* @param data - User answers object
* @returns Array of actions to execute
*/
type DynamicActionsFunction = (data?: Answers) => ActionType[];Usage Examples:
plop.setGenerator('flexible', {
description: 'Flexible generator with dynamic actions',
prompts: [
{ type: 'input', name: 'name', message: 'Component name:' },
{ type: 'confirm', name: 'includeStyles', message: 'Include CSS?' },
{ type: 'confirm', name: 'includeTests', message: 'Include tests?' },
{ type: 'list', name: 'framework', choices: ['react', 'vue', 'angular'] }
],
// Dynamic actions based on user choices
actions: (data) => {
const actions = [];
// Always add main component
actions.push({
type: 'add',
path: `src/components/{{pascalCase name}}.${data.framework === 'react' ? 'jsx' : 'js'}`,
templateFile: `templates/${data.framework}-component.hbs`
});
// Conditionally add styles
if (data.includeStyles) {
actions.push({
type: 'add',
path: 'src/components/{{pascalCase name}}.css',
templateFile: 'templates/component-styles.hbs'
});
}
// Conditionally add tests
if (data.includeTests) {
actions.push({
type: 'add',
path: 'src/components/__tests__/{{pascalCase name}}.test.js',
templateFile: `templates/${data.framework}-test.hbs`
});
}
// Framework-specific additions
if (data.framework === 'react') {
actions.push({
type: 'modify',
path: 'src/components/index.js',
pattern: /(\/\/ -- REACT EXPORTS --)/gi,
template: "export { {{pascalCase name}} } from './{{pascalCase name}}';\n$1"
});
}
return actions;
}
});Actions execute sequentially in the order they are defined, with each action waiting for the previous to complete.
Actions can handle errors in several ways:
abortOnFail: true)skip functionActions provide visual feedback during execution with customizable progress indicators and success/failure messages.
Install with Tessl CLI
npx tessl i tessl/npm-plop