Initial commit
This commit is contained in:
89
skills/frontend-component/functions/component_generator.py
Executable file
89
skills/frontend-component/functions/component_generator.py
Executable 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()
|
||||
115
skills/frontend-component/functions/name_validator.py
Executable file
115
skills/frontend-component/functions/name_validator.py
Executable 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()
|
||||
125
skills/frontend-component/functions/props_interface_generator.py
Executable file
125
skills/frontend-component/functions/props_interface_generator.py
Executable 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()
|
||||
85
skills/frontend-component/functions/style_generator.py
Executable file
85
skills/frontend-component/functions/style_generator.py
Executable 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()
|
||||
100
skills/frontend-component/functions/test_generator.py
Executable file
100
skills/frontend-component/functions/test_generator.py
Executable 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()
|
||||
Reference in New Issue
Block a user