Code Generators
Overview¶
This document provides comprehensive templates for code generation tools including Plop, Copier, and Yeoman. These generators enable consistent code scaffolding across projects, reducing boilerplate and enforcing best practices.
Plop Generator Templates¶
Plop is a micro-generator framework that makes it easy to create files with a consistent structure.
Project Setup¶
// plopfile.js
module.exports = function (plop) {
// Load all generators
plop.load('./plop/generators/component.js');
plop.load('./plop/generators/api-endpoint.js');
plop.load('./plop/generators/database-migration.js');
plop.load('./plop/generators/test.js');
plop.load('./plop/generators/hook.js');
plop.load('./plop/generators/service.js');
// Custom helpers
plop.setHelper('upperCase', (text) => text.toUpperCase());
plop.setHelper('lowerCase', (text) => text.toLowerCase());
plop.setHelper('camelCase', (text) => plop.getHelper('camelCase')(text));
plop.setHelper('pascalCase', (text) => plop.getHelper('pascalCase')(text));
plop.setHelper('kebabCase', (text) => plop.getHelper('kebabCase')(text));
plop.setHelper('snakeCase', (text) => plop.getHelper('snakeCase')(text));
// Date helpers
plop.setHelper('currentDate', () => new Date().toISOString().split('T')[0]);
plop.setHelper('currentYear', () => new Date().getFullYear().toString());
// Conditional helper
plop.setHelper('ifEquals', function (arg1, arg2, options) {
return arg1 === arg2 ? options.fn(this) : options.inverse(this);
});
};
React Component Generator¶
// plop/generators/component.js
module.exports = function (plop) {
plop.setGenerator('component', {
description: 'Create a React component',
prompts: [
{
type: 'input',
name: 'name',
message: 'Component name:',
validate: (value) => {
if (!value) return 'Component name is required';
if (!/^[A-Z][a-zA-Z0-9]*$/.test(value)) {
return 'Component name must be PascalCase';
}
return true;
},
},
{
type: 'list',
name: 'type',
message: 'Component type:',
choices: [
{ name: 'Functional Component', value: 'functional' },
{ name: 'Page Component', value: 'page' },
{ name: 'Layout Component', value: 'layout' },
{ name: 'UI Component', value: 'ui' },
],
default: 'functional',
},
{
type: 'confirm',
name: 'hasTests',
message: 'Include tests?',
default: true,
},
{
type: 'confirm',
name: 'hasStyles',
message: 'Include styles (CSS module)?',
default: true,
},
{
type: 'confirm',
name: 'hasStorybook',
message: 'Include Storybook story?',
default: true,
},
],
actions: (data) => {
const basePath = data.type === 'page'
? 'src/pages/{{pascalCase name}}'
: data.type === 'layout'
? 'src/layouts/{{pascalCase name}}'
: data.type === 'ui'
? 'src/components/ui/{{pascalCase name}}'
: 'src/components/{{pascalCase name}}';
const actions = [
{
type: 'add',
path: `${basePath}/{{pascalCase name}}.tsx`,
templateFile: 'plop/templates/component/component.tsx.hbs',
},
{
type: 'add',
path: `${basePath}/index.ts`,
templateFile: 'plop/templates/component/index.ts.hbs',
},
];
if (data.hasStyles) {
actions.push({
type: 'add',
path: `${basePath}/{{pascalCase name}}.module.css`,
templateFile: 'plop/templates/component/styles.module.css.hbs',
});
}
if (data.hasTests) {
actions.push({
type: 'add',
path: `${basePath}/{{pascalCase name}}.test.tsx`,
templateFile: 'plop/templates/component/component.test.tsx.hbs',
});
}
if (data.hasStorybook) {
actions.push({
type: 'add',
path: `${basePath}/{{pascalCase name}}.stories.tsx`,
templateFile: 'plop/templates/component/component.stories.tsx.hbs',
});
}
return actions;
},
});
};
Component Templates¶
{{!-- plop/templates/component/component.tsx.hbs --}}
import React from 'react';
{{#if hasStyles}}
import styles from './{{pascalCase name}}.module.css';
{{/if}}
export interface {{pascalCase name}}Props {
/** Optional className for styling */
className?: string;
/** Content to render */
children?: React.ReactNode;
}
/**
* {{pascalCase name}} component
*
* @example
* ```tsx
* <{{pascalCase name}}>
* Content here
* </{{pascalCase name}}>
* ```
*/
export const {{pascalCase name}}: React.FC<{{pascalCase name}}Props> = ({
className,
children,
}) => {
return (
<div
className={`{{#if hasStyles}}${styles.container}{{/if}}${className ? ` ${className}` : ''}`}
data-testid="{{kebabCase name}}"
>
{children}
</div>
);
};
{{pascalCase name}}.displayName = '{{pascalCase name}}';
{{!-- plop/templates/component/component.test.tsx.hbs --}}
import { render, screen } from '@testing-library/react';
import { {{pascalCase name}} } from './{{pascalCase name}}';
describe('{{pascalCase name}}', () => {
it('renders without crashing', () => {
render(<{{pascalCase name}} />);
expect(screen.getByTestId('{{kebabCase name}}')).toBeInTheDocument();
});
it('renders children correctly', () => {
render(<{{pascalCase name}}>Test content</{{pascalCase name}}>);
expect(screen.getByText('Test content')).toBeInTheDocument();
});
it('applies custom className', () => {
render(<{{pascalCase name}} className="custom-class" />);
expect(screen.getByTestId('{{kebabCase name}}')).toHaveClass('custom-class');
});
});
{{!-- plop/templates/component/component.stories.tsx.hbs --}}
import type { Meta, StoryObj } from '@storybook/react';
import { {{pascalCase name}} } from './{{pascalCase name}}';
const meta: Meta<typeof {{pascalCase name}}> = {
title: 'Components/{{pascalCase name}}',
component: {{pascalCase name}},
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
className: {
control: 'text',
description: 'Additional CSS class',
},
},
};
export default meta;
type Story = StoryObj<typeof {{pascalCase name}}>;
export const Default: Story = {
args: {
children: 'Default content',
},
};
export const WithCustomClass: Story = {
args: {
children: 'Custom styled content',
className: 'custom-class',
},
};
{{!-- plop/templates/component/styles.module.css.hbs --}}
.container {
/* Base styles */
}
{{!-- plop/templates/component/index.ts.hbs --}}
export { {{pascalCase name}} } from './{{pascalCase name}}';
export type { {{pascalCase name}}Props } from './{{pascalCase name}}';
API Endpoint Generator¶
// plop/generators/api-endpoint.js
module.exports = function (plop) {
plop.setGenerator('api-endpoint', {
description: 'Create an API endpoint',
prompts: [
{
type: 'input',
name: 'name',
message: 'Endpoint name (e.g., users, products):',
validate: (value) => {
if (!value) return 'Endpoint name is required';
if (!/^[a-z][a-z0-9-]*$/.test(value)) {
return 'Endpoint name must be lowercase with hyphens';
}
return true;
},
},
{
type: 'list',
name: 'framework',
message: 'API framework:',
choices: [
{ name: 'Express.js', value: 'express' },
{ name: 'Fastify', value: 'fastify' },
{ name: 'Next.js API Routes', value: 'nextjs' },
{ name: 'NestJS', value: 'nestjs' },
],
default: 'express',
},
{
type: 'checkbox',
name: 'methods',
message: 'HTTP methods to generate:',
choices: [
{ name: 'GET (list)', value: 'list', checked: true },
{ name: 'GET (single)', value: 'get', checked: true },
{ name: 'POST (create)', value: 'create', checked: true },
{ name: 'PUT (update)', value: 'update', checked: true },
{ name: 'DELETE', value: 'delete', checked: true },
],
},
{
type: 'confirm',
name: 'hasAuth',
message: 'Require authentication?',
default: true,
},
{
type: 'confirm',
name: 'hasValidation',
message: 'Include request validation?',
default: true,
},
],
actions: (data) => {
const actions = [];
if (data.framework === 'express') {
actions.push(
{
type: 'add',
path: 'src/routes/{{kebabCase name}}.routes.ts',
templateFile: 'plop/templates/api/express/routes.ts.hbs',
},
{
type: 'add',
path: 'src/controllers/{{kebabCase name}}.controller.ts',
templateFile: 'plop/templates/api/express/controller.ts.hbs',
},
{
type: 'add',
path: 'src/services/{{kebabCase name}}.service.ts',
templateFile: 'plop/templates/api/express/service.ts.hbs',
}
);
if (data.hasValidation) {
actions.push({
type: 'add',
path: 'src/validators/{{kebabCase name}}.validator.ts',
templateFile: 'plop/templates/api/express/validator.ts.hbs',
});
}
}
if (data.framework === 'nextjs') {
actions.push({
type: 'add',
path: 'src/app/api/{{kebabCase name}}/route.ts',
templateFile: 'plop/templates/api/nextjs/route.ts.hbs',
});
if (data.methods.includes('get') || data.methods.includes('update') || data.methods.includes('delete')) {
actions.push({
type: 'add',
path: 'src/app/api/{{kebabCase name}}/[id]/route.ts',
templateFile: 'plop/templates/api/nextjs/route-id.ts.hbs',
});
}
}
if (data.framework === 'nestjs') {
actions.push(
{
type: 'add',
path: 'src/{{kebabCase name}}/{{kebabCase name}}.module.ts',
templateFile: 'plop/templates/api/nestjs/module.ts.hbs',
},
{
type: 'add',
path: 'src/{{kebabCase name}}/{{kebabCase name}}.controller.ts',
templateFile: 'plop/templates/api/nestjs/controller.ts.hbs',
},
{
type: 'add',
path: 'src/{{kebabCase name}}/{{kebabCase name}}.service.ts',
templateFile: 'plop/templates/api/nestjs/service.ts.hbs',
},
{
type: 'add',
path: 'src/{{kebabCase name}}/dto/create-{{kebabCase name}}.dto.ts',
templateFile: 'plop/templates/api/nestjs/create-dto.ts.hbs',
},
{
type: 'add',
path: 'src/{{kebabCase name}}/dto/update-{{kebabCase name}}.dto.ts',
templateFile: 'plop/templates/api/nestjs/update-dto.ts.hbs',
}
);
}
// Add tests
actions.push({
type: 'add',
path: 'src/__tests__/{{kebabCase name}}.test.ts',
templateFile: `plop/templates/api/${data.framework}/test.ts.hbs`,
});
return actions;
},
});
};
Express API Templates¶
{{!-- plop/templates/api/express/routes.ts.hbs --}}
import { Router } from 'express';
import * as controller from '../controllers/{{kebabCase name}}.controller';
{{#if hasAuth}}
import { authenticate } from '../middleware/auth';
{{/if}}
{{#if hasValidation}}
import { validate } from '../middleware/validate';
import * as validators from '../validators/{{kebabCase name}}.validator';
{{/if}}
const router = Router();
{{#if hasAuth}}
// Apply authentication to all routes
router.use(authenticate);
{{/if}}
{{#each methods}}
{{#ifEquals this "list"}}
// GET /{{kebabCase ../name}} - List all {{kebabCase ../name}}
router.get('/', controller.list);
{{/ifEquals}}
{{#ifEquals this "get"}}
// GET /{{kebabCase ../name}}/:id - Get single {{kebabCase ../name}}
router.get('/:id', controller.getById);
{{/ifEquals}}
{{#ifEquals this "create"}}
// POST /{{kebabCase ../name}} - Create new {{kebabCase ../name}}
router.post(
'/',
{{#if ../hasValidation}}
validate(validators.create{{pascalCase ../name}}Schema),
{{/if}}
controller.create
);
{{/ifEquals}}
{{#ifEquals this "update"}}
// PUT /{{kebabCase ../name}}/:id - Update {{kebabCase ../name}}
router.put(
'/:id',
{{#if ../hasValidation}}
validate(validators.update{{pascalCase ../name}}Schema),
{{/if}}
controller.update
);
{{/ifEquals}}
{{#ifEquals this "delete"}}
// DELETE /{{kebabCase ../name}}/:id - Delete {{kebabCase ../name}}
router.delete('/:id', controller.remove);
{{/ifEquals}}
{{/each}}
export default router;
{{!-- plop/templates/api/express/controller.ts.hbs --}}
import { Request, Response, NextFunction } from 'express';
import * as {{camelCase name}}Service from '../services/{{kebabCase name}}.service';
import { AppError } from '../utils/errors';
{{#each methods}}
{{#ifEquals this "list"}}
export const list = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const { page = 1, limit = 10, ...filters } = req.query;
const result = await {{camelCase ../name}}Service.findAll({
page: Number(page),
limit: Number(limit),
filters,
});
res.json(result);
} catch (error) {
next(error);
}
};
{{/ifEquals}}
{{#ifEquals this "get"}}
export const getById = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const { id } = req.params;
const item = await {{camelCase ../name}}Service.findById(id);
if (!item) {
throw new AppError('{{pascalCase ../name}} not found', 404);
}
res.json(item);
} catch (error) {
next(error);
}
};
{{/ifEquals}}
{{#ifEquals this "create"}}
export const create = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const item = await {{camelCase ../name}}Service.create(req.body);
res.status(201).json(item);
} catch (error) {
next(error);
}
};
{{/ifEquals}}
{{#ifEquals this "update"}}
export const update = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const { id } = req.params;
const item = await {{camelCase ../name}}Service.update(id, req.body);
if (!item) {
throw new AppError('{{pascalCase ../name}} not found', 404);
}
res.json(item);
} catch (error) {
next(error);
}
};
{{/ifEquals}}
{{#ifEquals this "delete"}}
export const remove = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const { id } = req.params;
await {{camelCase ../name}}Service.remove(id);
res.status(204).send();
} catch (error) {
next(error);
}
};
{{/ifEquals}}
{{/each}}
{{!-- plop/templates/api/express/service.ts.hbs --}}
import { db } from '../db';
export interface {{pascalCase name}} {
id: string;
createdAt: Date;
updatedAt: Date;
// Add your fields here
}
export interface Create{{pascalCase name}}Input {
// Add your input fields here
}
export interface Update{{pascalCase name}}Input {
// Add your update fields here
}
export interface FindAllOptions {
page: number;
limit: number;
filters: Record<string, unknown>;
}
export interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export const findAll = async (
options: FindAllOptions
): Promise<PaginatedResult<{{pascalCase name}}>> => {
const { page, limit, filters } = options;
const offset = (page - 1) * limit;
// Replace with your database query
const [data, total] = await Promise.all([
db.{{camelCase name}}.findMany({
where: filters,
skip: offset,
take: limit,
orderBy: { createdAt: 'desc' },
}),
db.{{camelCase name}}.count({ where: filters }),
]);
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
};
export const findById = async (id: string): Promise<{{pascalCase name}} | null> => {
return db.{{camelCase name}}.findUnique({ where: { id } });
};
export const create = async (
input: Create{{pascalCase name}}Input
): Promise<{{pascalCase name}}> => {
return db.{{camelCase name}}.create({ data: input });
};
export const update = async (
id: string,
input: Update{{pascalCase name}}Input
): Promise<{{pascalCase name}} | null> => {
return db.{{camelCase name}}.update({
where: { id },
data: input,
});
};
export const remove = async (id: string): Promise<void> => {
await db.{{camelCase name}}.delete({ where: { id } });
};
{{!-- plop/templates/api/express/validator.ts.hbs --}}
import { z } from 'zod';
export const create{{pascalCase name}}Schema = z.object({
body: z.object({
// Add your validation rules here
// name: z.string().min(1).max(255),
// email: z.string().email(),
}),
});
export const update{{pascalCase name}}Schema = z.object({
params: z.object({
id: z.string().uuid(),
}),
body: z.object({
// Add your validation rules here (all optional for updates)
// name: z.string().min(1).max(255).optional(),
// email: z.string().email().optional(),
}),
});
export type Create{{pascalCase name}}Input = z.infer<typeof create{{pascalCase name}}Schema>['body'];
export type Update{{pascalCase name}}Input = z.infer<typeof update{{pascalCase name}}Schema>['body'];
Database Migration Generator¶
// plop/generators/database-migration.js
module.exports = function (plop) {
plop.setGenerator('migration', {
description: 'Create a database migration',
prompts: [
{
type: 'input',
name: 'name',
message: 'Migration name (e.g., add-users-table, add-email-index):',
validate: (value) => {
if (!value) return 'Migration name is required';
if (!/^[a-z][a-z0-9-]*$/.test(value)) {
return 'Migration name must be lowercase with hyphens';
}
return true;
},
},
{
type: 'list',
name: 'type',
message: 'Migration type:',
choices: [
{ name: 'Create Table', value: 'create-table' },
{ name: 'Alter Table', value: 'alter-table' },
{ name: 'Add Index', value: 'add-index' },
{ name: 'Add Foreign Key', value: 'add-fk' },
{ name: 'Custom SQL', value: 'custom' },
],
},
{
type: 'input',
name: 'tableName',
message: 'Table name:',
when: (answers) => ['create-table', 'alter-table', 'add-index', 'add-fk'].includes(answers.type),
},
{
type: 'list',
name: 'tool',
message: 'Migration tool:',
choices: [
{ name: 'Prisma', value: 'prisma' },
{ name: 'TypeORM', value: 'typeorm' },
{ name: 'Knex.js', value: 'knex' },
{ name: 'Sequelize', value: 'sequelize' },
{ name: 'Raw SQL', value: 'sql' },
],
default: 'prisma',
},
],
actions: (data) => {
const timestamp = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
const actions = [];
if (data.tool === 'prisma') {
actions.push({
type: 'add',
path: 'prisma/migrations/{{timestamp}}_{{kebabCase name}}/migration.sql',
templateFile: 'plop/templates/migration/prisma.sql.hbs',
data: { timestamp },
});
} else if (data.tool === 'typeorm') {
actions.push({
type: 'add',
path: 'src/migrations/{{timestamp}}-{{pascalCase name}}.ts',
templateFile: 'plop/templates/migration/typeorm.ts.hbs',
data: { timestamp },
});
} else if (data.tool === 'knex') {
actions.push({
type: 'add',
path: 'migrations/{{timestamp}}_{{snakeCase name}}.ts',
templateFile: 'plop/templates/migration/knex.ts.hbs',
data: { timestamp },
});
} else if (data.tool === 'sql') {
actions.push(
{
type: 'add',
path: 'migrations/{{timestamp}}_{{snakeCase name}}_up.sql',
templateFile: 'plop/templates/migration/sql-up.sql.hbs',
data: { timestamp },
},
{
type: 'add',
path: 'migrations/{{timestamp}}_{{snakeCase name}}_down.sql',
templateFile: 'plop/templates/migration/sql-down.sql.hbs',
data: { timestamp },
}
);
}
return actions;
},
});
};
Migration Templates¶
{{!-- plop/templates/migration/typeorm.ts.hbs --}}
import { MigrationInterface, QueryRunner, Table, TableIndex, TableForeignKey } from 'typeorm';
export class {{pascalCase name}}{{timestamp}} implements MigrationInterface {
name = '{{pascalCase name}}{{timestamp}}';
public async up(queryRunner: QueryRunner): Promise<void> {
{{#ifEquals type "create-table"}}
await queryRunner.createTable(
new Table({
name: '{{snakeCase tableName}}',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
generationStrategy: 'uuid',
default: 'uuid_generate_v4()',
},
// Add your columns here
{
name: 'created_at',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updated_at',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP',
},
],
}),
true
);
{{/ifEquals}}
{{#ifEquals type "alter-table"}}
await queryRunner.query(`
ALTER TABLE "{{snakeCase tableName}}"
-- Add your alterations here
`);
{{/ifEquals}}
{{#ifEquals type "add-index"}}
await queryRunner.createIndex(
'{{snakeCase tableName}}',
new TableIndex({
name: 'IDX_{{upperCase (snakeCase tableName)}}_COLUMN',
columnNames: ['column_name'],
})
);
{{/ifEquals}}
{{#ifEquals type "add-fk"}}
await queryRunner.createForeignKey(
'{{snakeCase tableName}}',
new TableForeignKey({
columnNames: ['foreign_id'],
referencedColumnNames: ['id'],
referencedTableName: 'referenced_table',
onDelete: 'CASCADE',
})
);
{{/ifEquals}}
{{#ifEquals type "custom"}}
await queryRunner.query(`
-- Add your custom SQL here
`);
{{/ifEquals}}
}
public async down(queryRunner: QueryRunner): Promise<void> {
{{#ifEquals type "create-table"}}
await queryRunner.dropTable('{{snakeCase tableName}}');
{{/ifEquals}}
{{#ifEquals type "alter-table"}}
await queryRunner.query(`
ALTER TABLE "{{snakeCase tableName}}"
-- Reverse your alterations here
`);
{{/ifEquals}}
{{#ifEquals type "add-index"}}
await queryRunner.dropIndex('{{snakeCase tableName}}', 'IDX_{{upperCase (snakeCase tableName)}}_COLUMN');
{{/ifEquals}}
{{#ifEquals type "add-fk"}}
const table = await queryRunner.getTable('{{snakeCase tableName}}');
const foreignKey = table.foreignKeys.find(
(fk) => fk.columnNames.indexOf('foreign_id') !== -1
);
await queryRunner.dropForeignKey('{{snakeCase tableName}}', foreignKey);
{{/ifEquals}}
{{#ifEquals type "custom"}}
await queryRunner.query(`
-- Reverse your custom SQL here
`);
{{/ifEquals}}
}
}
{{!-- plop/templates/migration/knex.ts.hbs --}}
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
{{#ifEquals type "create-table"}}
return knex.schema.createTable('{{snakeCase tableName}}', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
// Add your columns here
table.timestamps(true, true);
});
{{/ifEquals}}
{{#ifEquals type "alter-table"}}
return knex.schema.alterTable('{{snakeCase tableName}}', (table) => {
// Add your alterations here
});
{{/ifEquals}}
{{#ifEquals type "add-index"}}
return knex.schema.alterTable('{{snakeCase tableName}}', (table) => {
table.index(['column_name'], 'idx_{{snakeCase tableName}}_column');
});
{{/ifEquals}}
{{#ifEquals type "add-fk"}}
return knex.schema.alterTable('{{snakeCase tableName}}', (table) => {
table.foreign('foreign_id').references('id').inTable('referenced_table').onDelete('CASCADE');
});
{{/ifEquals}}
{{#ifEquals type "custom"}}
return knex.raw(`
-- Add your custom SQL here
`);
{{/ifEquals}}
}
export async function down(knex: Knex): Promise<void> {
{{#ifEquals type "create-table"}}
return knex.schema.dropTable('{{snakeCase tableName}}');
{{/ifEquals}}
{{#ifEquals type "alter-table"}}
return knex.schema.alterTable('{{snakeCase tableName}}', (table) => {
// Reverse your alterations here
});
{{/ifEquals}}
{{#ifEquals type "add-index"}}
return knex.schema.alterTable('{{snakeCase tableName}}', (table) => {
table.dropIndex(['column_name'], 'idx_{{snakeCase tableName}}_column');
});
{{/ifEquals}}
{{#ifEquals type "add-fk"}}
return knex.schema.alterTable('{{snakeCase tableName}}', (table) => {
table.dropForeign(['foreign_id']);
});
{{/ifEquals}}
{{#ifEquals type "custom"}}
return knex.raw(`
-- Reverse your custom SQL here
`);
{{/ifEquals}}
}
React Hook Generator¶
// plop/generators/hook.js
module.exports = function (plop) {
plop.setGenerator('hook', {
description: 'Create a React hook',
prompts: [
{
type: 'input',
name: 'name',
message: 'Hook name (without "use" prefix):',
validate: (value) => {
if (!value) return 'Hook name is required';
if (!/^[A-Z][a-zA-Z0-9]*$/.test(value)) {
return 'Hook name must be PascalCase';
}
return true;
},
},
{
type: 'list',
name: 'type',
message: 'Hook type:',
choices: [
{ name: 'State Hook', value: 'state' },
{ name: 'Effect Hook', value: 'effect' },
{ name: 'API/Fetch Hook', value: 'api' },
{ name: 'Form Hook', value: 'form' },
{ name: 'Custom Logic Hook', value: 'custom' },
],
default: 'state',
},
{
type: 'confirm',
name: 'hasTests',
message: 'Include tests?',
default: true,
},
],
actions: (data) => {
const actions = [
{
type: 'add',
path: 'src/hooks/use{{pascalCase name}}/use{{pascalCase name}}.ts',
templateFile: `plop/templates/hook/${data.type}.ts.hbs`,
},
{
type: 'add',
path: 'src/hooks/use{{pascalCase name}}/index.ts',
templateFile: 'plop/templates/hook/index.ts.hbs',
},
];
if (data.hasTests) {
actions.push({
type: 'add',
path: 'src/hooks/use{{pascalCase name}}/use{{pascalCase name}}.test.ts',
templateFile: `plop/templates/hook/${data.type}.test.ts.hbs`,
});
}
return actions;
},
});
};
{{!-- plop/templates/hook/api.ts.hbs --}}
import { useState, useEffect, useCallback } from 'react';
interface Use{{pascalCase name}}Options<T> {
/** Initial data */
initialData?: T;
/** Enable automatic fetching */
enabled?: boolean;
/** Refetch interval in milliseconds */
refetchInterval?: number;
}
interface Use{{pascalCase name}}Result<T> {
data: T | undefined;
error: Error | null;
isLoading: boolean;
isError: boolean;
isSuccess: boolean;
refetch: () => Promise<void>;
}
/**
* Hook for fetching and managing {{name}} data
*
* @example
* ```tsx
* const { data, isLoading, error, refetch } = use{{pascalCase name}}({
* enabled: true,
* });
* ```
*/
export function use{{pascalCase name}}<T = unknown>(
options: Use{{pascalCase name}}Options<T> = {}
): Use{{pascalCase name}}Result<T> {
const { initialData, enabled = true, refetchInterval } = options;
const [data, setData] = useState<T | undefined>(initialData);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false);
const fetchData = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
// Replace with your API call
const response = await fetch('/api/{{kebabCase name}}');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
if (enabled) {
fetchData();
}
}, [enabled, fetchData]);
useEffect(() => {
if (refetchInterval && enabled) {
const interval = setInterval(fetchData, refetchInterval);
return () => clearInterval(interval);
}
}, [refetchInterval, enabled, fetchData]);
return {
data,
error,
isLoading,
isError: error !== null,
isSuccess: data !== undefined && error === null,
refetch: fetchData,
};
}
{{!-- plop/templates/hook/api.test.ts.hbs --}}
import { renderHook, waitFor, act } from '@testing-library/react';
import { use{{pascalCase name}} } from './use{{pascalCase name}}';
// Mock fetch
global.fetch = jest.fn();
describe('use{{pascalCase name}}', () => {
beforeEach(() => {
(fetch as jest.Mock).mockClear();
});
it('should fetch data successfully', async () => {
const mockData = { id: 1, name: 'Test' };
(fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockData),
});
const { result } = renderHook(() => use{{pascalCase name}}());
expect(result.current.isLoading).toBe(true);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.data).toEqual(mockData);
expect(result.current.isSuccess).toBe(true);
expect(result.current.isError).toBe(false);
});
it('should handle errors', async () => {
(fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'));
const { result } = renderHook(() => use{{pascalCase name}}());
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.error?.message).toBe('Network error');
expect(result.current.isError).toBe(true);
expect(result.current.isSuccess).toBe(false);
});
it('should not fetch when disabled', () => {
renderHook(() => use{{pascalCase name}}({ enabled: false }));
expect(fetch).not.toHaveBeenCalled();
});
it('should refetch on demand', async () => {
(fetch as jest.Mock).mockResolvedValue({
ok: true,
json: () => Promise.resolve({ id: 1 }),
});
const { result } = renderHook(() => use{{pascalCase name}}());
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
await act(async () => {
await result.current.refetch();
});
expect(fetch).toHaveBeenCalledTimes(2);
});
});
Copier Templates¶
Copier is a Python-based project templating tool that supports advanced features like conditional files and post-generation hooks.
Project Structure¶
my-copier-template/
copier.yml # Template configuration
template/ # Template files
{{project_slug}}/
pyproject.toml.jinja
README.md.jinja
src/
{{project_slug}}/
__init__.py.jinja
main.py.jinja
tests/
__init__.py
test_main.py.jinja
hooks/
post_gen_project.py # Post-generation hook
Copier Configuration¶
# copier.yml
_min_copier_version: "9.0.0"
_subdirectory: template
_skip_if_exists:
- "*.lock"
- ".env"
# Project metadata
project_name:
type: str
help: "Project name"
placeholder: "My Awesome Project"
validator: >-
{% if not project_name %}
Project name is required
{% endif %}
project_slug:
type: str
help: "Project slug (used for package name)"
default: "{{ project_name | lower | replace(' ', '-') | replace('_', '-') }}"
validator: >-
{% if not project_slug | regex_search('^[a-z][a-z0-9-]*$') %}
Project slug must be lowercase with hyphens
{% endif %}
project_description:
type: str
help: "Short project description"
default: "A new project"
author_name:
type: str
help: "Author name"
default: "{{ 'git config user.name' | shell | trim }}"
author_email:
type: str
help: "Author email"
default: "{{ 'git config user.email' | shell | trim }}"
python_version:
type: str
help: "Minimum Python version"
choices:
- "3.10"
- "3.11"
- "3.12"
default: "3.11"
# Features
use_pytest:
type: bool
help: "Use pytest for testing?"
default: true
use_mypy:
type: bool
help: "Use mypy for type checking?"
default: true
use_ruff:
type: bool
help: "Use ruff for linting?"
default: true
use_pre_commit:
type: bool
help: "Use pre-commit hooks?"
default: true
use_docker:
type: bool
help: "Include Docker configuration?"
default: false
use_github_actions:
type: bool
help: "Include GitHub Actions CI?"
default: true
# License
license:
type: str
help: "License type"
choices:
MIT: "MIT License"
Apache-2.0: "Apache License 2.0"
GPL-3.0: "GNU General Public License v3"
BSD-3-Clause: "BSD 3-Clause License"
None: "No license"
default: "MIT"
Copier Template Files¶
{#- template/{{project_slug}}/pyproject.toml.jinja -#}
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "{{ project_slug }}"
version = "0.1.0"
description = "{{ project_description }}"
readme = "README.md"
requires-python = ">={{ python_version }}"
{%- if license != "None" %}
license = "{{ license }}"
{%- endif %}
authors = [
{ name = "{{ author_name }}", email = "{{ author_email }}" },
]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
{%- if license != "None" %}
"License :: OSI Approved :: {{ license }} License",
{%- endif %}
"Programming Language :: Python :: {{ python_version }}",
]
[project.optional-dependencies]
dev = [
{%- if use_pytest %}
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
{%- endif %}
{%- if use_mypy %}
"mypy>=1.0.0",
{%- endif %}
{%- if use_ruff %}
"ruff>=0.1.0",
{%- endif %}
{%- if use_pre_commit %}
"pre-commit>=3.0.0",
{%- endif %}
]
{%- if use_ruff %}
[tool.ruff]
target-version = "py{{ python_version | replace('.', '') }}"
line-length = 100
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = [
"E501", # line too long (handled by formatter)
]
[tool.ruff.isort]
known-first-party = ["{{ project_slug | replace('-', '_') }}"]
{%- endif %}
{%- if use_mypy %}
[tool.mypy]
python_version = "{{ python_version }}"
strict = true
warn_return_any = true
warn_unused_ignores = true
{%- endif %}
{%- if use_pytest %}
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --cov={{ project_slug | replace('-', '_') }} --cov-report=term-missing"
{%- endif %}
[tool.coverage.run]
source = ["src"]
branch = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]
{#- template/{{project_slug}}/README.md.jinja -#}
# {{ project_name }}
{{ project_description }}
## Installation
```bash
pip install {{ project_slug }}
```
## Development
### Setup
```bash
# Clone repository
git clone https://github.com/{{ author_name | lower | replace(' ', '') }}/{{ project_slug }}.git
cd {{ project_slug }}
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -e ".[dev]"
{%- if use_pre_commit %}
# Install pre-commit hooks
pre-commit install
{%- endif %}
```
### Commands
```bash
{%- if use_pytest %}
# Run tests
pytest
# Run tests with coverage
pytest --cov
{%- endif %}
{%- if use_ruff %}
# Lint code
ruff check .
# Format code
ruff format .
{%- endif %}
{%- if use_mypy %}
# Type check
mypy src
{%- endif %}
```
## License
{%- if license != "None" %}
This project is licensed under the {{ license }} License - see the [LICENSE](LICENSE) file for details.
{%- else %}
This project is proprietary.
{%- endif %}
{#- template/{{project_slug}}/src/{{project_slug|replace('-', '_')}}/__init__.py.jinja -#}
"""{{ project_name }}
{{ project_description }}
"""
__version__ = "0.1.0"
__author__ = "{{ author_name }}"
__email__ = "{{ author_email }}"
{#- template/{{project_slug}}/src/{{project_slug|replace('-', '_')}}/main.py.jinja -#}
"""Main module for {{ project_name }}."""
from __future__ import annotations
def main() -> int:
"""Entry point for the application."""
print("Hello from {{ project_name }}!")
return 0
if __name__ == "__main__":
raise SystemExit(main())
{#- template/{{project_slug}}/tests/test_main.py.jinja -#}
"""Tests for main module."""
from {{ project_slug | replace('-', '_') }}.main import main
def test_main() -> None:
"""Test main function returns 0."""
assert main() == 0
Conditional Docker Files¶
{#- template/{{project_slug}}/Dockerfile.jinja -#}
{%- if use_docker %}
# Build stage
FROM python:{{ python_version }}-slim as builder
WORKDIR /app
# Install build dependencies
RUN pip install --no-cache-dir build
# Copy source
COPY pyproject.toml README.md ./
COPY src ./src
# Build wheel
RUN python -m build --wheel
# Runtime stage
FROM python:{{ python_version }}-slim
WORKDIR /app
# Copy wheel from builder
COPY --from=builder /app/dist/*.whl ./
# Install package
RUN pip install --no-cache-dir *.whl && rm *.whl
# Create non-root user
RUN useradd --create-home appuser
USER appuser
ENTRYPOINT ["python", "-m", "{{ project_slug | replace('-', '_') }}"]
{%- endif %}
GitHub Actions Conditional¶
{#- template/{{project_slug}}/.github/workflows/ci.yml.jinja -#}
{%- if use_github_actions %}
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["{{ python_version }}", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ '{{' }} matrix.python-version {{ '}}' }}
uses: actions/setup-python@v5
with:
python-version: ${{ '{{' }} matrix.python-version {{ '}}' }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
{%- if use_ruff %}
- name: Lint with ruff
run: ruff check .
{%- endif %}
{%- if use_mypy %}
- name: Type check with mypy
run: mypy src
{%- endif %}
{%- if use_pytest %}
- name: Test with pytest
run: pytest --cov --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v4
{%- endif %}
{%- endif %}
Post-Generation Hook¶
# hooks/post_gen_project.py
"""Post-generation hook for Copier template."""
import subprocess
import sys
from pathlib import Path
def run_command(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess:
"""Run a command and return the result."""
return subprocess.run(cmd, check=check, capture_output=True, text=True)
def main() -> int:
"""Main hook function."""
project_dir = Path.cwd()
print("Setting up project...")
# Initialize git repository
if not (project_dir / ".git").exists():
print("Initializing git repository...")
run_command(["git", "init"])
run_command(["git", "add", "."])
run_command(["git", "commit", "-m", "Initial commit from template"])
# Create virtual environment
print("Creating virtual environment...")
run_command([sys.executable, "-m", "venv", "venv"])
# Install dependencies
print("Installing dependencies...")
pip_path = project_dir / "venv" / "bin" / "pip"
if sys.platform == "win32":
pip_path = project_dir / "venv" / "Scripts" / "pip.exe"
run_command([str(pip_path), "install", "-e", ".[dev]"])
# Install pre-commit hooks if enabled
if (project_dir / ".pre-commit-config.yaml").exists():
print("Installing pre-commit hooks...")
precommit_path = project_dir / "venv" / "bin" / "pre-commit"
if sys.platform == "win32":
precommit_path = project_dir / "venv" / "Scripts" / "pre-commit.exe"
run_command([str(precommit_path), "install"])
print("\nProject setup complete!")
print(f"\nNext steps:")
print(f" cd {project_dir.name}")
print(f" source venv/bin/activate # On Windows: venv\\Scripts\\activate")
print(f" pytest")
return 0
if __name__ == "__main__":
sys.exit(main())
Using Copier¶
# Generate new project
copier copy https://github.com/org/my-template ./my-new-project
# Generate with answers file
copier copy --answers-file .copier-answers.yml https://github.com/org/my-template .
# Update existing project
copier update
# Update with specific version
copier update --vcs-ref v2.0.0
Yeoman Generators¶
Yeoman is a scaffolding tool with a rich ecosystem of generators.
Generator Structure¶
generator-my-project/
package.json
generators/
app/
index.js
templates/
package.json.ejs
src/
index.ts.ejs
component/
index.js
templates/
component.tsx.ejs
Yeoman Generator Package¶
{
"name": "generator-my-project",
"version": "1.0.0",
"description": "Yeoman generator for my project",
"files": ["generators"],
"keywords": ["yeoman-generator"],
"dependencies": {
"yeoman-generator": "^6.0.0",
"chalk": "^5.0.0",
"yosay": "^2.0.2"
},
"devDependencies": {
"yeoman-test": "^8.0.0"
}
}
Main App Generator¶
// generators/app/index.js
import Generator from 'yeoman-generator';
import chalk from 'chalk';
import yosay from 'yosay';
export default class extends Generator {
constructor(args, opts) {
super(args, opts);
// Register options
this.option('typescript', {
type: Boolean,
description: 'Use TypeScript',
default: true,
});
this.option('docker', {
type: Boolean,
description: 'Include Docker configuration',
default: false,
});
}
async prompting() {
this.log(yosay(`Welcome to the ${chalk.red('my-project')} generator!`));
this.answers = await this.prompt([
{
type: 'input',
name: 'projectName',
message: 'Project name:',
default: this.appname,
validate: (input) => {
if (!input) return 'Project name is required';
if (!/^[a-z][a-z0-9-]*$/.test(input)) {
return 'Project name must be lowercase with hyphens';
}
return true;
},
},
{
type: 'input',
name: 'description',
message: 'Project description:',
default: 'A new project',
},
{
type: 'list',
name: 'packageManager',
message: 'Package manager:',
choices: ['npm', 'yarn', 'pnpm'],
default: 'npm',
},
{
type: 'checkbox',
name: 'features',
message: 'Select features:',
choices: [
{ name: 'ESLint', value: 'eslint', checked: true },
{ name: 'Prettier', value: 'prettier', checked: true },
{ name: 'Jest', value: 'jest', checked: true },
{ name: 'Husky (git hooks)', value: 'husky', checked: true },
{ name: 'GitHub Actions', value: 'github-actions', checked: true },
],
},
{
type: 'list',
name: 'framework',
message: 'Frontend framework:',
choices: [
{ name: 'None (Node.js only)', value: 'none' },
{ name: 'React', value: 'react' },
{ name: 'Vue', value: 'vue' },
{ name: 'Next.js', value: 'nextjs' },
],
default: 'none',
},
]);
}
writing() {
const context = {
projectName: this.answers.projectName,
description: this.answers.description,
typescript: this.options.typescript,
docker: this.options.docker,
features: this.answers.features,
framework: this.answers.framework,
hasEslint: this.answers.features.includes('eslint'),
hasPrettier: this.answers.features.includes('prettier'),
hasJest: this.answers.features.includes('jest'),
hasHusky: this.answers.features.includes('husky'),
hasGithubActions: this.answers.features.includes('github-actions'),
};
// Copy package.json
this.fs.copyTpl(
this.templatePath('package.json.ejs'),
this.destinationPath('package.json'),
context
);
// Copy source files
this.fs.copyTpl(
this.templatePath('src/index.ts.ejs'),
this.destinationPath(`src/index.${this.options.typescript ? 'ts' : 'js'}`),
context
);
// Copy config files
if (context.hasEslint) {
this.fs.copyTpl(
this.templatePath('eslint.config.js.ejs'),
this.destinationPath('eslint.config.js'),
context
);
}
if (context.hasPrettier) {
this.fs.copy(
this.templatePath('.prettierrc'),
this.destinationPath('.prettierrc')
);
}
if (this.options.typescript) {
this.fs.copyTpl(
this.templatePath('tsconfig.json.ejs'),
this.destinationPath('tsconfig.json'),
context
);
}
if (this.options.docker) {
this.fs.copyTpl(
this.templatePath('Dockerfile.ejs'),
this.destinationPath('Dockerfile'),
context
);
this.fs.copyTpl(
this.templatePath('docker-compose.yml.ejs'),
this.destinationPath('docker-compose.yml'),
context
);
}
if (context.hasGithubActions) {
this.fs.copyTpl(
this.templatePath('.github/workflows/ci.yml.ejs'),
this.destinationPath('.github/workflows/ci.yml'),
context
);
}
// Copy static files
this.fs.copy(
this.templatePath('.gitignore.template'),
this.destinationPath('.gitignore')
);
this.fs.copyTpl(
this.templatePath('README.md.ejs'),
this.destinationPath('README.md'),
context
);
}
async install() {
const pkgManager = this.answers.packageManager;
this.log(`\nInstalling dependencies with ${chalk.cyan(pkgManager)}...`);
if (pkgManager === 'npm') {
this.spawnCommandSync('npm', ['install']);
} else if (pkgManager === 'yarn') {
this.spawnCommandSync('yarn', ['install']);
} else if (pkgManager === 'pnpm') {
this.spawnCommandSync('pnpm', ['install']);
}
}
end() {
this.log('\n');
this.log(chalk.green('Project created successfully!'));
this.log('\nNext steps:');
this.log(` ${chalk.cyan('cd')} ${this.answers.projectName}`);
if (this.answers.packageManager === 'npm') {
this.log(` ${chalk.cyan('npm run dev')}`);
} else if (this.answers.packageManager === 'yarn') {
this.log(` ${chalk.cyan('yarn dev')}`);
} else {
this.log(` ${chalk.cyan('pnpm dev')}`);
}
this.log('\n');
}
}
Yeoman Templates¶
<%# generators/app/templates/package.json.ejs %>
{
"name": "<%= projectName %>",
"version": "0.1.0",
"description": "<%= description %>",
"main": "dist/index.js",
<% if (typescript) { %>
"types": "dist/index.d.ts",
<% } %>
"scripts": {
<% if (typescript) { %>
"build": "tsc",
"dev": "ts-node src/index.ts",
<% } else { %>
"dev": "node src/index.js",
<% } %>
<% if (hasJest) { %>
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
<% } %>
<% if (hasEslint) { %>
"lint": "eslint .",
"lint:fix": "eslint . --fix",
<% } %>
<% if (hasPrettier) { %>
"format": "prettier --write .",
"format:check": "prettier --check .",
<% } %>
<% if (hasHusky) { %>
"prepare": "husky install",
<% } %>
"clean": "rm -rf dist"
},
"devDependencies": {
<% if (typescript) { %>
"typescript": "^5.0.0",
"ts-node": "^10.9.0",
"@types/node": "^20.0.0",
<% } %>
<% if (hasEslint) { %>
"eslint": "^9.0.0",
<% if (typescript) { %>
"typescript-eslint": "^7.0.0",
<% } %>
<% } %>
<% if (hasPrettier) { %>
"prettier": "^3.0.0",
<% } %>
<% if (hasJest) { %>
"jest": "^29.0.0",
<% if (typescript) { %>
"ts-jest": "^29.0.0",
"@types/jest": "^29.0.0",
<% } %>
<% } %>
<% if (hasHusky) { %>
"husky": "^9.0.0",
"lint-staged": "^15.0.0",
<% } %>
},
<% if (hasHusky) { %>
"lint-staged": {
"*.{js,ts,tsx}": [
<% if (hasEslint) { %>
"eslint --fix",
<% } %>
<% if (hasPrettier) { %>
"prettier --write"
<% } %>
]
},
<% } %>
"engines": {
"node": ">=18.0.0"
}
}
Component Sub-Generator¶
// generators/component/index.js
import Generator from 'yeoman-generator';
import path from 'path';
export default class extends Generator {
constructor(args, opts) {
super(args, opts);
this.argument('name', {
type: String,
required: true,
description: 'Component name',
});
}
async prompting() {
this.answers = await this.prompt([
{
type: 'list',
name: 'type',
message: 'Component type:',
choices: ['functional', 'page', 'layout'],
default: 'functional',
},
{
type: 'confirm',
name: 'hasStyles',
message: 'Include CSS module?',
default: true,
},
{
type: 'confirm',
name: 'hasTests',
message: 'Include tests?',
default: true,
},
]);
}
writing() {
const componentName = this.options.name;
const pascalName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
const kebabName = componentName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
const basePath = this.answers.type === 'page'
? 'src/pages'
: this.answers.type === 'layout'
? 'src/layouts'
: 'src/components';
const context = {
componentName: pascalName,
kebabName,
hasStyles: this.answers.hasStyles,
};
// Component file
this.fs.copyTpl(
this.templatePath('component.tsx.ejs'),
this.destinationPath(path.join(basePath, pascalName, `${pascalName}.tsx`)),
context
);
// Index file
this.fs.copyTpl(
this.templatePath('index.ts.ejs'),
this.destinationPath(path.join(basePath, pascalName, 'index.ts')),
context
);
// Styles
if (this.answers.hasStyles) {
this.fs.copyTpl(
this.templatePath('styles.module.css.ejs'),
this.destinationPath(path.join(basePath, pascalName, `${pascalName}.module.css`)),
context
);
}
// Tests
if (this.answers.hasTests) {
this.fs.copyTpl(
this.templatePath('component.test.tsx.ejs'),
this.destinationPath(path.join(basePath, pascalName, `${pascalName}.test.tsx`)),
context
);
}
}
end() {
this.log(`\nComponent ${this.options.name} created successfully!`);
}
}
Using Yeoman¶
# Install generator globally
npm install -g generator-my-project
# Generate new project
yo my-project
# Generate component
yo my-project:component Button
# Generate with options
yo my-project --typescript --docker
Package.json Scripts¶
Plop Integration¶
{
"scripts": {
"generate": "plop",
"g:component": "plop component",
"g:api": "plop api-endpoint",
"g:migration": "plop migration",
"g:hook": "plop hook"
},
"devDependencies": {
"plop": "^4.0.0"
}
}
Copier Integration¶
{
"scripts": {
"scaffold": "copier copy https://github.com/org/template .",
"update-template": "copier update"
}
}
Best Practices¶
Template Organization¶
templates/
# Plop templates
plop/
generators/
component.js
api-endpoint.js
hook.js
templates/
component/
component.tsx.hbs
index.ts.hbs
styles.module.css.hbs
api/
controller.ts.hbs
service.ts.hbs
# Copier template
copier-python-package/
copier.yml
template/
hooks/
# Yeoman generator
generator-my-project/
generators/
app/
component/
Naming Conventions¶
// Good - Consistent naming helpers
plop.setHelper('pascalCase', (text) => text.replace(/(^\w|-\w)/g, clearAndUpper));
plop.setHelper('camelCase', (text) => text.replace(/-./g, (m) => m[1].toUpperCase()));
plop.setHelper('kebabCase', (text) => text.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase());
plop.setHelper('snakeCase', (text) => text.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase());
Validation¶
// Good - Comprehensive validation
{
type: 'input',
name: 'name',
validate: (value) => {
if (!value) return 'Name is required';
if (value.length < 2) return 'Name must be at least 2 characters';
if (!/^[A-Z][a-zA-Z0-9]*$/.test(value)) {
return 'Name must be PascalCase (e.g., MyComponent)';
}
return true;
},
}
References¶
- Plop Documentation
- Copier Documentation
- Yeoman Documentation
- Handlebars Documentation
- Jinja2 Documentation
Status: Active