Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:51:59 +08:00
commit 38e80921c8
89 changed files with 20480 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""
Generate React component file from template with substitutions.
Replaces placeholders in template with component-specific values.
"""
import sys
import argparse
import os
def read_template(template_path: str) -> str:
"""Read template file content."""
try:
with open(template_path, 'r') as f:
return f.read()
except FileNotFoundError:
raise FileNotFoundError(f"Template file not found: {template_path}")
def generate_component(name: str, props_interface: str, template_content: str, description: str = None) -> str:
"""
Generate component code by substituting placeholders in template.
Args:
name: Component name (PascalCase)
props_interface: Props interface name
template_content: Template file content
description: Brief component description
Returns:
str: Generated component code
"""
# Convert PascalCase to kebab-case for file names
kebab_name = ''.join(['-' + c.lower() if c.isupper() else c for c in name]).lstrip('-')
# Perform substitutions
substitutions = {
'${COMPONENT_NAME}': name,
'${PROPS_INTERFACE}': props_interface,
'${STYLE_IMPORT}': f"import styles from './{name}.module.css';",
'${DESCRIPTION}': description or f"{name} component",
'${COMPONENT_NAME_KEBAB}': kebab_name,
}
result = template_content
for placeholder, value in substitutions.items():
result = result.replace(placeholder, value)
return result
def main():
parser = argparse.ArgumentParser(description='Generate React component from template')
parser.add_argument('--name', required=True, help='Component name (PascalCase)')
parser.add_argument('--type', default='simple', choices=['simple', 'with-hooks', 'container'], help='Component type')
parser.add_argument('--props-interface', required=True, help='Props interface name')
parser.add_argument('--template', required=True, help='Template file path')
parser.add_argument('--output', help='Output file path (optional, prints to stdout if not provided)')
parser.add_argument('--description', help='Component description')
args = parser.parse_args()
try:
# Read template
template_content = read_template(args.template)
# Generate component
component_code = generate_component(
args.name,
args.props_interface,
template_content,
args.description
)
# Output
if args.output:
os.makedirs(os.path.dirname(args.output), exist_ok=True)
with open(args.output, 'w') as f:
f.write(component_code)
print(f"✅ Component generated: {args.output}")
else:
print(component_code)
sys.exit(0)
except Exception as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""
Validate component naming conventions.
Ensures component names follow PascalCase, are descriptive, and avoid reserved words.
"""
import sys
import re
import argparse
# Reserved component names that should be avoided
RESERVED_WORDS = {
'Component', 'Element', 'Node', 'React', 'ReactNode', 'Fragment',
'Props', 'State', 'Context', 'Provider', 'Consumer', 'Children',
'Ref', 'Key', 'Type', 'Class', 'Function', 'Object', 'Array',
'String', 'Number', 'Boolean', 'Symbol', 'Null', 'Undefined'
}
def is_pascal_case(name):
"""
Check if name is in PascalCase format.
Args:
name: String to validate
Returns:
bool: True if PascalCase, False otherwise
"""
# PascalCase: starts with uppercase, contains only alphanumeric
pattern = r'^[A-Z][a-zA-Z0-9]*$'
return bool(re.match(pattern, name))
def validate_component_name(name):
"""
Validate component name against conventions.
Args:
name: Component name to validate
Returns:
tuple: (is_valid: bool, error_message: str or None)
"""
# Check length
if len(name) < 2:
return False, "Component name must be at least 2 characters long"
# Check for special characters
if not name.replace('_', '').isalnum():
return False, "Component name should only contain alphanumeric characters"
# Check PascalCase
if not is_pascal_case(name):
return False, f"Component name '{name}' must be in PascalCase (e.g., UserProfile, TodoList)"
# Check reserved words
if name in RESERVED_WORDS:
return False, f"'{name}' is a reserved word. Choose a more descriptive name."
# Check descriptiveness (not too generic)
if len(name) < 4:
return False, f"Component name '{name}' is too short. Use a more descriptive name (e.g., UserCard, not UC)"
# Check doesn't start with common anti-patterns
anti_patterns = ['My', 'The', 'New', 'Test']
if any(name.startswith(pattern) for pattern in anti_patterns):
return False, f"Avoid starting component names with '{name[:3]}...'. Be more specific about what it does."
return True, None
def suggest_valid_name(name):
"""
Suggest a valid component name if the provided one is invalid.
Args:
name: Invalid component name
Returns:
str: Suggested valid name
"""
# Convert to PascalCase
suggested = ''.join(word.capitalize() for word in re.split(r'[-_\s]+', name))
# Remove special characters
suggested = re.sub(r'[^a-zA-Z0-9]', '', suggested)
# Ensure starts with uppercase
if suggested and not suggested[0].isupper():
suggested = suggested.capitalize()
return suggested if suggested else "MyComponent"
def main():
parser = argparse.ArgumentParser(description='Validate React component naming conventions')
parser.add_argument('--name', required=True, help='Component name to validate')
parser.add_argument('--suggest', action='store_true', help='Suggest a valid name if invalid')
args = parser.parse_args()
is_valid, error = validate_component_name(args.name)
if is_valid:
print(f"'{args.name}' is a valid component name")
sys.exit(0)
else:
print(f"❌ Invalid component name: {error}", file=sys.stderr)
if args.suggest:
suggested = suggest_valid_name(args.name)
print(f"💡 Suggested name: {suggested}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""
Generate TypeScript props interface from user input.
Converts simple prop specifications into proper TypeScript interface definitions.
"""
import sys
import argparse
from typing import List, Tuple
# Type mapping from simple names to TypeScript types
TYPE_MAPPING = {
'string': 'string',
'str': 'string',
'number': 'number',
'num': 'number',
'int': 'number',
'boolean': 'boolean',
'bool': 'boolean',
'function': '() => void',
'func': '() => void',
'callback': '() => void',
'array': 'any[]',
'arr': 'any[]',
'object': 'Record<string, any>',
'obj': 'Record<string, any>',
'react-node': 'React.ReactNode',
'node': 'React.ReactNode',
'children': 'React.ReactNode',
'element': 'React.ReactElement',
'style': 'React.CSSProperties',
'class': 'string',
'classname': 'string',
}
def parse_prop_spec(prop_spec: str) -> Tuple[str, str, bool]:
"""
Parse a single prop specification.
Format: "propName:type" or "propName:type:optional"
Args:
prop_spec: Prop specification string
Returns:
tuple: (prop_name, ts_type, is_optional)
"""
parts = prop_spec.strip().split(':')
if len(parts) < 2:
raise ValueError(f"Invalid prop specification: '{prop_spec}'. Expected format: 'propName:type' or 'propName:type:optional'")
prop_name = parts[0].strip()
type_name = parts[1].strip().lower()
is_optional = len(parts) > 2 and parts[2].strip().lower() in ('optional', 'opt', '?', 'true')
# Map to TypeScript type
ts_type = TYPE_MAPPING.get(type_name, type_name)
return prop_name, ts_type, is_optional
def generate_props_interface(name: str, props: List[str], include_common: bool = True) -> str:
"""
Generate TypeScript props interface.
Args:
name: Component name (will become {name}Props)
props: List of prop specifications
include_common: Whether to include common props (children, className, etc.)
Returns:
str: TypeScript interface definition
"""
interface_name = f"{name}Props"
lines = [f"interface {interface_name} {{"]
# Add custom props
for prop_spec in props:
if not prop_spec.strip():
continue
prop_name, ts_type, is_optional = parse_prop_spec(prop_spec)
optional_marker = '?' if is_optional else ''
lines.append(f" {prop_name}{optional_marker}: {ts_type};")
# Add common props if requested
if include_common:
# Only add children if not already specified
if not any('children' in prop for prop in props):
lines.append(" children?: React.ReactNode;")
# Only add className if not already specified
if not any('className' in prop or 'class' in prop.lower() for prop in props):
lines.append(" className?: string;")
lines.append("}")
return '\n'.join(lines)
def main():
parser = argparse.ArgumentParser(description='Generate TypeScript props interface')
parser.add_argument('--name', required=True, help='Component name')
parser.add_argument('--props', required=True, help='Comma-separated prop specifications (e.g., "userId:string,onUpdate:function,isActive:boolean:optional")')
parser.add_argument('--no-common', action='store_true', help='Do not include common props (children, className)')
args = parser.parse_args()
# Parse prop specifications
prop_specs = [p.strip() for p in args.props.split(',') if p.strip()]
try:
interface = generate_props_interface(
args.name,
prop_specs,
include_common=not args.no_common
)
print(interface)
sys.exit(0)
except ValueError as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
Generate style file (CSS Modules or Styled Components).
Creates scoped styles for React components.
"""
import sys
import argparse
import os
def read_template(template_path: str) -> str:
"""Read template file content."""
try:
with open(template_path, 'r') as f:
return f.read()
except FileNotFoundError:
raise FileNotFoundError(f"Template file not found: {template_path}")
def generate_style(name: str, approach: str, template_content: str) -> str:
"""
Generate style code by substituting placeholders in template.
Args:
name: Component name (PascalCase)
approach: Styling approach (css-modules, styled-components, tailwind)
template_content: Template file content
Returns:
str: Generated style code
"""
# Convert PascalCase to kebab-case
kebab_name = ''.join(['-' + c.lower() if c.isupper() else c for c in name]).lstrip('-')
# Perform substitutions
substitutions = {
'${COMPONENT_NAME}': name,
'${COMPONENT_NAME_KEBAB}': kebab_name,
'${BASE_STYLES}': """ display: flex;
flex-direction: column;
gap: 1rem;""",
}
result = template_content
for placeholder, value in substitutions.items():
result = result.replace(placeholder, value)
return result
def main():
parser = argparse.ArgumentParser(description='Generate React component style file')
parser.add_argument('--name', required=True, help='Component name (PascalCase)')
parser.add_argument('--approach', default='css-modules', choices=['css-modules', 'styled-components', 'tailwind'], help='Styling approach')
parser.add_argument('--template', required=True, help='Style template file path')
parser.add_argument('--output', help='Output file path (optional, prints to stdout if not provided)')
args = parser.parse_args()
try:
# Read template
template_content = read_template(args.template)
# Generate style
style_code = generate_style(
args.name,
args.approach,
template_content
)
# Output
if args.output:
os.makedirs(os.path.dirname(args.output), exist_ok=True)
with open(args.output, 'w') as f:
f.write(style_code)
print(f"✅ Style file generated: {args.output}")
else:
print(style_code)
sys.exit(0)
except Exception as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""
Generate test file with React Testing Library.
Creates comprehensive test suite for React components.
"""
import sys
import argparse
import os
def read_template(template_path: str) -> str:
"""Read template file content."""
try:
with open(template_path, 'r') as f:
return f.read()
except FileNotFoundError:
raise FileNotFoundError(f"Template file not found: {template_path}")
def generate_test(component_name: str, component_path: str, template_content: str) -> str:
"""
Generate test code by substituting placeholders in template.
Args:
component_name: Component name (PascalCase)
component_path: Path to component file
template_content: Template file content
Returns:
str: Generated test code
"""
# Calculate relative import path
import_path = f'./{component_name}'
# Basic test cases (can be expanded based on props analysis)
test_cases = f"""
it('renders without crashing', () => {{
render(<{component_name} />);
}});
it('renders children correctly', () => {{
render(<{component_name}>Test Content</{component_name}>);
expect(screen.getByText('Test Content')).toBeInTheDocument();
}});
it('applies custom className', () => {{
const {{ container }} = render(<{component_name} className="custom-class" />);
expect(container.firstChild).toHaveClass('custom-class');
}});
""".strip()
# Perform substitutions
substitutions = {
'${COMPONENT_NAME}': component_name,
'${IMPORT_PATH}': import_path,
'${TEST_CASES}': test_cases,
}
result = template_content
for placeholder, value in substitutions.items():
result = result.replace(placeholder, value)
return result
def main():
parser = argparse.ArgumentParser(description='Generate React component test file')
parser.add_argument('--component-name', required=True, help='Component name (PascalCase)')
parser.add_argument('--component-path', required=True, help='Path to component file')
parser.add_argument('--template', required=True, help='Test template file path')
parser.add_argument('--output', help='Output file path (optional, prints to stdout if not provided)')
args = parser.parse_args()
try:
# Read template
template_content = read_template(args.template)
# Generate test
test_code = generate_test(
args.component_name,
args.component_path,
template_content
)
# Output
if args.output:
os.makedirs(os.path.dirname(args.output), exist_ok=True)
with open(args.output, 'w') as f:
f.write(test_code)
print(f"✅ Test file generated: {args.output}")
else:
print(test_code)
sys.exit(0)
except Exception as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()