CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-plop

Micro-generator framework that makes it easy for an entire team to create files with a level of uniformity

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

action-system.mddocs/

Action System

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.

Capabilities

Built-in Action Types

Plop includes four core action types for common file operations.

// Built-in action types
type BuiltInActionType = "add" | "addMany" | "modify" | "append";

Add Action

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>;
} & TemplateStrOrFile

Usage 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'}`);
  }
}

Add Many Action

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 Action

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>;
} & TemplateStrOrFile

Usage 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 Action

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)
} & TemplateStrOrFile

Usage 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'
}

Custom Action Types

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}}'
    }
  ]
});

Action Configuration

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
}

Dynamic Actions

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;
  }
});

Action Execution

Execution Order

Actions execute sequentially in the order they are defined, with each action waiting for the previous to complete.

Error Handling

Actions can handle errors in several ways:

  • Continue on error (default): Log error and continue with next action
  • Abort on fail: Stop execution if action fails (abortOnFail: true)
  • Skip action: Conditionally skip actions with skip function

Progress Feedback

Actions provide visual feedback during execution with customizable progress indicators and success/failure messages.

Install with Tessl CLI

npx tessl i tessl/npm-plop

docs

action-system.md

cli.md

console.md

generator-api.md

index.md

programmatic.md

template-system.md

tile.json