Files
claude-skills-reference/engineering-team/senior-frontend/scripts/component_generator.py
Alireza Rezvani 829a197c2b fix(skill): rewrite senior-frontend with React/Next.js content (#63) (#118)
Replace placeholder content with real frontend development guidance:

References:
- react_patterns.md: Compound Components, Render Props, Custom Hooks
- nextjs_optimization_guide.md: Server/Client Components, ISR, caching
- frontend_best_practices.md: Accessibility, testing, TypeScript patterns

Scripts:
- frontend_scaffolder.py: Generate Next.js/React projects with features
- component_generator.py: Generate React components with tests/stories
- bundle_analyzer.py: Analyze package.json for optimization opportunities

SKILL.md:
- Added table of contents
- Numbered workflow steps
- Removed marketing language
- Added trigger phrases in description

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 03:40:48 +01:00

330 lines
8.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""
React Component Generator
Generates React/Next.js component files with TypeScript, Tailwind CSS,
and optional test files following best practices.
Usage:
python component_generator.py Button --dir src/components/ui
python component_generator.py ProductCard --type client --with-test
python component_generator.py UserProfile --type server --with-story
"""
import argparse
import os
import sys
from pathlib import Path
from datetime import datetime
# Component templates
TEMPLATES = {
"client": '''\'use client\';
import {{ useState }} from 'react';
import {{ cn }} from '@/lib/utils';
interface {name}Props {{
className?: string;
children?: React.ReactNode;
}}
export function {name}({{ className, children }}: {name}Props) {{
return (
<div className={{cn('', className)}}>
{{children}}
</div>
);
}}
''',
"server": '''import {{ cn }} from '@/lib/utils';
interface {name}Props {{
className?: string;
children?: React.ReactNode;
}}
export async function {name}({{ className, children }}: {name}Props) {{
return (
<div className={{cn('', className)}}>
{{children}}
</div>
);
}}
''',
"hook": '''import {{ useState, useEffect, useCallback }} from 'react';
interface Use{name}Options {{
// Add options here
}}
interface Use{name}Return {{
// Add return type here
isLoading: boolean;
error: Error | null;
}}
export function use{name}(options: Use{name}Options = {{}}): Use{name}Return {{
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {{
// Effect logic here
}}, []);
return {{
isLoading,
error,
}};
}}
''',
"test": '''import {{ render, screen }} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {{ {name} }} from './{name}';
describe('{name}', () => {{
it('renders correctly', () => {{
render(<{name}>Test content</{name}>);
expect(screen.getByText('Test content')).toBeInTheDocument();
}});
it('applies custom className', () => {{
render(<{name} className="custom-class">Content</{name}>);
expect(screen.getByText('Content').parentElement).toHaveClass('custom-class');
}});
// Add more tests here
}});
''',
"story": '''import type {{ Meta, StoryObj }} from '@storybook/react';
import {{ {name} }} from './{name}';
const meta: Meta<typeof {name}> = {{
title: 'Components/{name}',
component: {name},
tags: ['autodocs'],
argTypes: {{
className: {{
control: 'text',
description: 'Additional CSS classes',
}},
}},
}};
export default meta;
type Story = StoryObj<typeof {name}>;
export const Default: Story = {{
args: {{
children: 'Default content',
}},
}};
export const WithCustomClass: Story = {{
args: {{
className: 'bg-blue-100 p-4',
children: 'Styled content',
}},
}};
''',
"index": '''export {{ {name} }} from './{name}';
export type {{ {name}Props }} from './{name}';
''',
}
def to_pascal_case(name: str) -> str:
"""Convert string to PascalCase."""
# Handle kebab-case and snake_case
words = name.replace('-', '_').split('_')
return ''.join(word.capitalize() for word in words)
def to_kebab_case(name: str) -> str:
"""Convert PascalCase to kebab-case."""
result = []
for i, char in enumerate(name):
if char.isupper() and i > 0:
result.append('-')
result.append(char.lower())
return ''.join(result)
def generate_component(
name: str,
output_dir: Path,
component_type: str = "client",
with_test: bool = False,
with_story: bool = False,
with_index: bool = True,
flat: bool = False,
) -> dict:
"""Generate component files."""
pascal_name = to_pascal_case(name)
kebab_name = to_kebab_case(pascal_name)
# Determine output path
if flat:
component_dir = output_dir
else:
component_dir = output_dir / pascal_name
files_created = []
# Create directory
component_dir.mkdir(parents=True, exist_ok=True)
# Generate main component file
if component_type == "hook":
main_file = component_dir / f"use{pascal_name}.ts"
template = TEMPLATES["hook"]
else:
main_file = component_dir / f"{pascal_name}.tsx"
template = TEMPLATES[component_type]
content = template.format(name=pascal_name)
main_file.write_text(content)
files_created.append(str(main_file))
# Generate test file
if with_test and component_type != "hook":
test_file = component_dir / f"{pascal_name}.test.tsx"
test_content = TEMPLATES["test"].format(name=pascal_name)
test_file.write_text(test_content)
files_created.append(str(test_file))
# Generate story file
if with_story and component_type != "hook":
story_file = component_dir / f"{pascal_name}.stories.tsx"
story_content = TEMPLATES["story"].format(name=pascal_name)
story_file.write_text(story_content)
files_created.append(str(story_file))
# Generate index file
if with_index and not flat:
index_file = component_dir / "index.ts"
index_content = TEMPLATES["index"].format(name=pascal_name)
index_file.write_text(index_content)
files_created.append(str(index_file))
return {
"name": pascal_name,
"type": component_type,
"directory": str(component_dir),
"files": files_created,
}
def print_result(result: dict, verbose: bool = False) -> None:
"""Print generation result."""
print(f"\n{'='*50}")
print(f"Component Generated: {result['name']}")
print(f"{'='*50}")
print(f"Type: {result['type']}")
print(f"Directory: {result['directory']}")
print(f"\nFiles created:")
for file in result['files']:
print(f" - {file}")
print(f"{'='*50}\n")
# Print usage hint
if result['type'] != 'hook':
print("Usage:")
print(f" import {{ {result['name']} }} from '@/components/{result['name']}';")
print(f"\n <{result['name']}>Content</{result['name']}>")
else:
print("Usage:")
print(f" import {{ use{result['name']} }} from '@/hooks/use{result['name']}';")
print(f"\n const {{ isLoading, error }} = use{result['name']}();")
def main():
parser = argparse.ArgumentParser(
description="Generate React/Next.js components with TypeScript and Tailwind CSS"
)
parser.add_argument(
"name",
help="Component name (PascalCase or kebab-case)"
)
parser.add_argument(
"--dir", "-d",
default="src/components",
help="Output directory (default: src/components)"
)
parser.add_argument(
"--type", "-t",
choices=["client", "server", "hook"],
default="client",
help="Component type (default: client)"
)
parser.add_argument(
"--with-test",
action="store_true",
help="Generate test file"
)
parser.add_argument(
"--with-story",
action="store_true",
help="Generate Storybook story file"
)
parser.add_argument(
"--no-index",
action="store_true",
help="Skip generating index.ts file"
)
parser.add_argument(
"--flat",
action="store_true",
help="Create files directly in output dir without subdirectory"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be generated without creating files"
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Enable verbose output"
)
args = parser.parse_args()
output_dir = Path(args.dir)
pascal_name = to_pascal_case(args.name)
if args.dry_run:
print(f"\nDry run - would generate:")
print(f" Component: {pascal_name}")
print(f" Type: {args.type}")
print(f" Directory: {output_dir / pascal_name if not args.flat else output_dir}")
print(f" Test: {'Yes' if args.with_test else 'No'}")
print(f" Story: {'Yes' if args.with_story else 'No'}")
return
try:
result = generate_component(
name=args.name,
output_dir=output_dir,
component_type=args.type,
with_test=args.with_test,
with_story=args.with_story,
with_index=not args.no_index,
flat=args.flat,
)
print_result(result, args.verbose)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()