Move all skill folders to skills/ directory
- Moved 3 new skill folders to skills/: python-patterns, react-best-practices, release-skills - Removed 48 duplicate skill folders from root (already existed in skills/) - Repository root now only contains: agents/, skills/, and config files Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
441
skills/python-patterns/skill.md
Normal file
441
skills/python-patterns/skill.md
Normal file
@@ -0,0 +1,441 @@
|
||||
---
|
||||
name: python-patterns
|
||||
description: Python development principles and decision-making. Framework selection, async patterns, type hints, project structure. Teaches thinking, not copying.
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep
|
||||
---
|
||||
|
||||
# Python Patterns
|
||||
|
||||
> Python development principles and decision-making for 2025.
|
||||
> **Learn to THINK, not memorize patterns.**
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ How to Use This Skill
|
||||
|
||||
This skill teaches **decision-making principles**, not fixed code to copy.
|
||||
|
||||
- ASK user for framework preference when unclear
|
||||
- Choose async vs sync based on CONTEXT
|
||||
- Don't default to same framework every time
|
||||
|
||||
---
|
||||
|
||||
## 1. Framework Selection (2025)
|
||||
|
||||
### Decision Tree
|
||||
|
||||
```
|
||||
What are you building?
|
||||
│
|
||||
├── API-first / Microservices
|
||||
│ └── FastAPI (async, modern, fast)
|
||||
│
|
||||
├── Full-stack web / CMS / Admin
|
||||
│ └── Django (batteries-included)
|
||||
│
|
||||
├── Simple / Script / Learning
|
||||
│ └── Flask (minimal, flexible)
|
||||
│
|
||||
├── AI/ML API serving
|
||||
│ └── FastAPI (Pydantic, async, uvicorn)
|
||||
│
|
||||
└── Background workers
|
||||
└── Celery + any framework
|
||||
```
|
||||
|
||||
### Comparison Principles
|
||||
|
||||
| Factor | FastAPI | Django | Flask |
|
||||
|--------|---------|--------|-------|
|
||||
| **Best for** | APIs, microservices | Full-stack, CMS | Simple, learning |
|
||||
| **Async** | Native | Django 5.0+ | Via extensions |
|
||||
| **Admin** | Manual | Built-in | Via extensions |
|
||||
| **ORM** | Choose your own | Django ORM | Choose your own |
|
||||
| **Learning curve** | Low | Medium | Low |
|
||||
|
||||
### Selection Questions to Ask:
|
||||
1. Is this API-only or full-stack?
|
||||
2. Need admin interface?
|
||||
3. Team familiar with async?
|
||||
4. Existing infrastructure?
|
||||
|
||||
---
|
||||
|
||||
## 2. Async vs Sync Decision
|
||||
|
||||
### When to Use Async
|
||||
|
||||
```
|
||||
async def is better when:
|
||||
├── I/O-bound operations (database, HTTP, file)
|
||||
├── Many concurrent connections
|
||||
├── Real-time features
|
||||
├── Microservices communication
|
||||
└── FastAPI/Starlette/Django ASGI
|
||||
|
||||
def (sync) is better when:
|
||||
├── CPU-bound operations
|
||||
├── Simple scripts
|
||||
├── Legacy codebase
|
||||
├── Team unfamiliar with async
|
||||
└── Blocking libraries (no async version)
|
||||
```
|
||||
|
||||
### The Golden Rule
|
||||
|
||||
```
|
||||
I/O-bound → async (waiting for external)
|
||||
CPU-bound → sync + multiprocessing (computing)
|
||||
|
||||
Don't:
|
||||
├── Mix sync and async carelessly
|
||||
├── Use sync libraries in async code
|
||||
└── Force async for CPU work
|
||||
```
|
||||
|
||||
### Async Library Selection
|
||||
|
||||
| Need | Async Library |
|
||||
|------|---------------|
|
||||
| HTTP client | httpx |
|
||||
| PostgreSQL | asyncpg |
|
||||
| Redis | aioredis / redis-py async |
|
||||
| File I/O | aiofiles |
|
||||
| Database ORM | SQLAlchemy 2.0 async, Tortoise |
|
||||
|
||||
---
|
||||
|
||||
## 3. Type Hints Strategy
|
||||
|
||||
### When to Type
|
||||
|
||||
```
|
||||
Always type:
|
||||
├── Function parameters
|
||||
├── Return types
|
||||
├── Class attributes
|
||||
├── Public APIs
|
||||
|
||||
Can skip:
|
||||
├── Local variables (let inference work)
|
||||
├── One-off scripts
|
||||
├── Tests (usually)
|
||||
```
|
||||
|
||||
### Common Type Patterns
|
||||
|
||||
```python
|
||||
# These are patterns, understand them:
|
||||
|
||||
# Optional → might be None
|
||||
from typing import Optional
|
||||
def find_user(id: int) -> Optional[User]: ...
|
||||
|
||||
# Union → one of multiple types
|
||||
def process(data: str | dict) -> None: ...
|
||||
|
||||
# Generic collections
|
||||
def get_items() -> list[Item]: ...
|
||||
def get_mapping() -> dict[str, int]: ...
|
||||
|
||||
# Callable
|
||||
from typing import Callable
|
||||
def apply(fn: Callable[[int], str]) -> str: ...
|
||||
```
|
||||
|
||||
### Pydantic for Validation
|
||||
|
||||
```
|
||||
When to use Pydantic:
|
||||
├── API request/response models
|
||||
├── Configuration/settings
|
||||
├── Data validation
|
||||
├── Serialization
|
||||
|
||||
Benefits:
|
||||
├── Runtime validation
|
||||
├── Auto-generated JSON schema
|
||||
├── Works with FastAPI natively
|
||||
└── Clear error messages
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Project Structure Principles
|
||||
|
||||
### Structure Selection
|
||||
|
||||
```
|
||||
Small project / Script:
|
||||
├── main.py
|
||||
├── utils.py
|
||||
└── requirements.txt
|
||||
|
||||
Medium API:
|
||||
├── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
│ ├── models/
|
||||
│ ├── routes/
|
||||
│ ├── services/
|
||||
│ └── schemas/
|
||||
├── tests/
|
||||
└── pyproject.toml
|
||||
|
||||
Large application:
|
||||
├── src/
|
||||
│ └── myapp/
|
||||
│ ├── core/
|
||||
│ ├── api/
|
||||
│ ├── services/
|
||||
│ ├── models/
|
||||
│ └── ...
|
||||
├── tests/
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
### FastAPI Structure Principles
|
||||
|
||||
```
|
||||
Organize by feature or layer:
|
||||
|
||||
By layer:
|
||||
├── routes/ (API endpoints)
|
||||
├── services/ (business logic)
|
||||
├── models/ (database models)
|
||||
├── schemas/ (Pydantic models)
|
||||
└── dependencies/ (shared deps)
|
||||
|
||||
By feature:
|
||||
├── users/
|
||||
│ ├── routes.py
|
||||
│ ├── service.py
|
||||
│ └── schemas.py
|
||||
└── products/
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Django Principles (2025)
|
||||
|
||||
### Django Async (Django 5.0+)
|
||||
|
||||
```
|
||||
Django supports async:
|
||||
├── Async views
|
||||
├── Async middleware
|
||||
├── Async ORM (limited)
|
||||
└── ASGI deployment
|
||||
|
||||
When to use async in Django:
|
||||
├── External API calls
|
||||
├── WebSocket (Channels)
|
||||
├── High-concurrency views
|
||||
└── Background task triggering
|
||||
```
|
||||
|
||||
### Django Best Practices
|
||||
|
||||
```
|
||||
Model design:
|
||||
├── Fat models, thin views
|
||||
├── Use managers for common queries
|
||||
├── Abstract base classes for shared fields
|
||||
|
||||
Views:
|
||||
├── Class-based for complex CRUD
|
||||
├── Function-based for simple endpoints
|
||||
├── Use viewsets with DRF
|
||||
|
||||
Queries:
|
||||
├── select_related() for FKs
|
||||
├── prefetch_related() for M2M
|
||||
├── Avoid N+1 queries
|
||||
└── Use .only() for specific fields
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. FastAPI Principles
|
||||
|
||||
### async def vs def in FastAPI
|
||||
|
||||
```
|
||||
Use async def when:
|
||||
├── Using async database drivers
|
||||
├── Making async HTTP calls
|
||||
├── I/O-bound operations
|
||||
└── Want to handle concurrency
|
||||
|
||||
Use def when:
|
||||
├── Blocking operations
|
||||
├── Sync database drivers
|
||||
├── CPU-bound work
|
||||
└── FastAPI runs in threadpool automatically
|
||||
```
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
```
|
||||
Use dependencies for:
|
||||
├── Database sessions
|
||||
├── Current user / Auth
|
||||
├── Configuration
|
||||
├── Shared resources
|
||||
|
||||
Benefits:
|
||||
├── Testability (mock dependencies)
|
||||
├── Clean separation
|
||||
├── Automatic cleanup (yield)
|
||||
```
|
||||
|
||||
### Pydantic v2 Integration
|
||||
|
||||
```python
|
||||
# FastAPI + Pydantic are tightly integrated:
|
||||
|
||||
# Request validation
|
||||
@app.post("/users")
|
||||
async def create(user: UserCreate) -> UserResponse:
|
||||
# user is already validated
|
||||
...
|
||||
|
||||
# Response serialization
|
||||
# Return type becomes response schema
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Background Tasks
|
||||
|
||||
### Selection Guide
|
||||
|
||||
| Solution | Best For |
|
||||
|----------|----------|
|
||||
| **BackgroundTasks** | Simple, in-process tasks |
|
||||
| **Celery** | Distributed, complex workflows |
|
||||
| **ARQ** | Async, Redis-based |
|
||||
| **RQ** | Simple Redis queue |
|
||||
| **Dramatiq** | Actor-based, simpler than Celery |
|
||||
|
||||
### When to Use Each
|
||||
|
||||
```
|
||||
FastAPI BackgroundTasks:
|
||||
├── Quick operations
|
||||
├── No persistence needed
|
||||
├── Fire-and-forget
|
||||
└── Same process
|
||||
|
||||
Celery/ARQ:
|
||||
├── Long-running tasks
|
||||
├── Need retry logic
|
||||
├── Distributed workers
|
||||
├── Persistent queue
|
||||
└── Complex workflows
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Error Handling Principles
|
||||
|
||||
### Exception Strategy
|
||||
|
||||
```
|
||||
In FastAPI:
|
||||
├── Create custom exception classes
|
||||
├── Register exception handlers
|
||||
├── Return consistent error format
|
||||
└── Log without exposing internals
|
||||
|
||||
Pattern:
|
||||
├── Raise domain exceptions in services
|
||||
├── Catch and transform in handlers
|
||||
└── Client gets clean error response
|
||||
```
|
||||
|
||||
### Error Response Philosophy
|
||||
|
||||
```
|
||||
Include:
|
||||
├── Error code (programmatic)
|
||||
├── Message (human readable)
|
||||
├── Details (field-level when applicable)
|
||||
└── NOT stack traces (security)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Testing Principles
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
| Type | Purpose | Tools |
|
||||
|------|---------|-------|
|
||||
| **Unit** | Business logic | pytest |
|
||||
| **Integration** | API endpoints | pytest + httpx/TestClient |
|
||||
| **E2E** | Full workflows | pytest + DB |
|
||||
|
||||
### Async Testing
|
||||
|
||||
```python
|
||||
# Use pytest-asyncio for async tests
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_endpoint():
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
response = await client.get("/users")
|
||||
assert response.status_code == 200
|
||||
```
|
||||
|
||||
### Fixtures Strategy
|
||||
|
||||
```
|
||||
Common fixtures:
|
||||
├── db_session → Database connection
|
||||
├── client → Test client
|
||||
├── authenticated_user → User with token
|
||||
└── sample_data → Test data setup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Decision Checklist
|
||||
|
||||
Before implementing:
|
||||
|
||||
- [ ] **Asked user about framework preference?**
|
||||
- [ ] **Chosen framework for THIS context?** (not just default)
|
||||
- [ ] **Decided async vs sync?**
|
||||
- [ ] **Planned type hint strategy?**
|
||||
- [ ] **Defined project structure?**
|
||||
- [ ] **Planned error handling?**
|
||||
- [ ] **Considered background tasks?**
|
||||
|
||||
---
|
||||
|
||||
## 11. Anti-Patterns to Avoid
|
||||
|
||||
### ❌ DON'T:
|
||||
- Default to Django for simple APIs (FastAPI may be better)
|
||||
- Use sync libraries in async code
|
||||
- Skip type hints for public APIs
|
||||
- Put business logic in routes/views
|
||||
- Ignore N+1 queries
|
||||
- Mix async and sync carelessly
|
||||
|
||||
### ✅ DO:
|
||||
- Choose framework based on context
|
||||
- Ask about async requirements
|
||||
- Use Pydantic for validation
|
||||
- Separate concerns (routes → services → repos)
|
||||
- Test critical paths
|
||||
|
||||
---
|
||||
|
||||
> **Remember**: Python patterns are about decision-making for YOUR specific context. Don't copy code—think about what serves your application best.
|
||||
570
skills/react-best-practices/skill.md
Normal file
570
skills/react-best-practices/skill.md
Normal file
@@ -0,0 +1,570 @@
|
||||
---
|
||||
name: react-best-practices
|
||||
description: Provides React patterns for hooks, effects, refs, and component design. Covers escape hatches, anti-patterns, and correct effect usage. Must use when reading or writing React components (.tsx, .jsx files with React imports).
|
||||
---
|
||||
|
||||
# React Best Practices
|
||||
|
||||
## Pair with TypeScript
|
||||
|
||||
When working with React, always load both this skill and `typescript-best-practices` together. TypeScript patterns (type-first development, discriminated unions, Zod validation) apply to React code.
|
||||
|
||||
## Core Principle: Effects Are Escape Hatches
|
||||
|
||||
Effects let you "step outside" React to synchronize with external systems. **Most component logic should NOT use Effects.** Before writing an Effect, ask: "Is there a way to do this without an Effect?"
|
||||
|
||||
## When to Use Effects
|
||||
|
||||
Effects are for synchronizing with **external systems**:
|
||||
- Subscribing to browser APIs (WebSocket, IntersectionObserver, resize)
|
||||
- Connecting to third-party libraries not written in React
|
||||
- Setting up/cleaning up event listeners on window/document
|
||||
- Fetching data on mount (though prefer React Query or framework data fetching)
|
||||
- Controlling non-React DOM elements (video players, maps, modals)
|
||||
|
||||
## When NOT to Use Effects
|
||||
|
||||
### Derived State (Calculate During Render)
|
||||
|
||||
```tsx
|
||||
// BAD: Effect for derived state
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
// GOOD: Calculate during render
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
const fullName = firstName + ' ' + lastName;
|
||||
```
|
||||
|
||||
### Expensive Calculations (Use useMemo)
|
||||
|
||||
```tsx
|
||||
// BAD: Effect for caching
|
||||
const [visibleTodos, setVisibleTodos] = useState([]);
|
||||
useEffect(() => {
|
||||
setVisibleTodos(getFilteredTodos(todos, filter));
|
||||
}, [todos, filter]);
|
||||
|
||||
// GOOD: useMemo for expensive calculations
|
||||
const visibleTodos = useMemo(
|
||||
() => getFilteredTodos(todos, filter),
|
||||
[todos, filter]
|
||||
);
|
||||
```
|
||||
|
||||
### Resetting State on Prop Change (Use key)
|
||||
|
||||
```tsx
|
||||
// BAD: Effect to reset state
|
||||
function ProfilePage({ userId }) {
|
||||
const [comment, setComment] = useState('');
|
||||
useEffect(() => {
|
||||
setComment('');
|
||||
}, [userId]);
|
||||
// ...
|
||||
}
|
||||
|
||||
// GOOD: Use key to reset component state
|
||||
function ProfilePage({ userId }) {
|
||||
return <Profile userId={userId} key={userId} />;
|
||||
}
|
||||
|
||||
function Profile({ userId }) {
|
||||
const [comment, setComment] = useState(''); // Resets automatically
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### User Event Handling (Use Event Handlers)
|
||||
|
||||
```tsx
|
||||
// BAD: Event-specific logic in Effect
|
||||
function ProductPage({ product, addToCart }) {
|
||||
useEffect(() => {
|
||||
if (product.isInCart) {
|
||||
showNotification(`Added ${product.name} to cart`);
|
||||
}
|
||||
}, [product]);
|
||||
// ...
|
||||
}
|
||||
|
||||
// GOOD: Logic in event handler
|
||||
function ProductPage({ product, addToCart }) {
|
||||
function buyProduct() {
|
||||
addToCart(product);
|
||||
showNotification(`Added ${product.name} to cart`);
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Notifying Parent of State Changes
|
||||
|
||||
```tsx
|
||||
// BAD: Effect to notify parent
|
||||
function Toggle({ onChange }) {
|
||||
const [isOn, setIsOn] = useState(false);
|
||||
useEffect(() => {
|
||||
onChange(isOn);
|
||||
}, [isOn, onChange]);
|
||||
// ...
|
||||
}
|
||||
|
||||
// GOOD: Update both in event handler
|
||||
function Toggle({ onChange }) {
|
||||
const [isOn, setIsOn] = useState(false);
|
||||
function updateToggle(nextIsOn) {
|
||||
setIsOn(nextIsOn);
|
||||
onChange(nextIsOn);
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
// BEST: Fully controlled component
|
||||
function Toggle({ isOn, onChange }) {
|
||||
function handleClick() {
|
||||
onChange(!isOn);
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Chains of Effects
|
||||
|
||||
```tsx
|
||||
// BAD: Effect chain
|
||||
useEffect(() => {
|
||||
if (card !== null && card.gold) {
|
||||
setGoldCardCount(c => c + 1);
|
||||
}
|
||||
}, [card]);
|
||||
|
||||
useEffect(() => {
|
||||
if (goldCardCount > 3) {
|
||||
setRound(r => r + 1);
|
||||
setGoldCardCount(0);
|
||||
}
|
||||
}, [goldCardCount]);
|
||||
|
||||
// GOOD: Calculate derived state, update in event handler
|
||||
const isGameOver = round > 5;
|
||||
|
||||
function handlePlaceCard(nextCard) {
|
||||
setCard(nextCard);
|
||||
if (nextCard.gold) {
|
||||
if (goldCardCount < 3) {
|
||||
setGoldCardCount(goldCardCount + 1);
|
||||
} else {
|
||||
setGoldCardCount(0);
|
||||
setRound(round + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Effect Dependencies
|
||||
|
||||
### Never Suppress the Linter
|
||||
|
||||
```tsx
|
||||
// BAD: Suppressing linter hides bugs
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
setCount(count + increment);
|
||||
}, 1000);
|
||||
return () => clearInterval(id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// GOOD: Fix the code, not the linter
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
setCount(c => c + increment);
|
||||
}, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [increment]);
|
||||
```
|
||||
|
||||
### Use Updater Functions to Remove State Dependencies
|
||||
|
||||
```tsx
|
||||
// BAD: messages in dependencies causes reconnection on every message
|
||||
useEffect(() => {
|
||||
connection.on('message', (msg) => {
|
||||
setMessages([...messages, msg]);
|
||||
});
|
||||
// ...
|
||||
}, [messages]); // Reconnects on every message!
|
||||
|
||||
// GOOD: Updater function removes dependency
|
||||
useEffect(() => {
|
||||
connection.on('message', (msg) => {
|
||||
setMessages(msgs => [...msgs, msg]);
|
||||
});
|
||||
// ...
|
||||
}, []); // No messages dependency needed
|
||||
```
|
||||
|
||||
### Move Objects/Functions Inside Effects
|
||||
|
||||
```tsx
|
||||
// BAD: Object created each render triggers Effect
|
||||
function ChatRoom({ roomId }) {
|
||||
const options = { serverUrl, roomId }; // New object each render
|
||||
useEffect(() => {
|
||||
const connection = createConnection(options);
|
||||
connection.connect();
|
||||
return () => connection.disconnect();
|
||||
}, [options]); // Reconnects every render!
|
||||
}
|
||||
|
||||
// GOOD: Create object inside Effect
|
||||
function ChatRoom({ roomId }) {
|
||||
useEffect(() => {
|
||||
const options = { serverUrl, roomId };
|
||||
const connection = createConnection(options);
|
||||
connection.connect();
|
||||
return () => connection.disconnect();
|
||||
}, [roomId, serverUrl]); // Only reconnects when values change
|
||||
}
|
||||
```
|
||||
|
||||
### useEffectEvent for Non-Reactive Logic
|
||||
|
||||
```tsx
|
||||
// BAD: theme change reconnects chat
|
||||
function ChatRoom({ roomId, theme }) {
|
||||
useEffect(() => {
|
||||
const connection = createConnection(serverUrl, roomId);
|
||||
connection.on('connected', () => {
|
||||
showNotification('Connected!', theme);
|
||||
});
|
||||
connection.connect();
|
||||
return () => connection.disconnect();
|
||||
}, [roomId, theme]); // Reconnects on theme change!
|
||||
}
|
||||
|
||||
// GOOD: useEffectEvent for non-reactive logic
|
||||
function ChatRoom({ roomId, theme }) {
|
||||
const onConnected = useEffectEvent(() => {
|
||||
showNotification('Connected!', theme);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const connection = createConnection(serverUrl, roomId);
|
||||
connection.on('connected', () => {
|
||||
onConnected();
|
||||
});
|
||||
connection.connect();
|
||||
return () => connection.disconnect();
|
||||
}, [roomId]); // theme no longer causes reconnection
|
||||
}
|
||||
```
|
||||
|
||||
### Wrap Callback Props with useEffectEvent
|
||||
|
||||
```tsx
|
||||
// BAD: Callback prop in dependencies
|
||||
function ChatRoom({ roomId, onReceiveMessage }) {
|
||||
useEffect(() => {
|
||||
connection.on('message', onReceiveMessage);
|
||||
// ...
|
||||
}, [roomId, onReceiveMessage]); // Reconnects if parent re-renders
|
||||
}
|
||||
|
||||
// GOOD: Wrap callback in useEffectEvent
|
||||
function ChatRoom({ roomId, onReceiveMessage }) {
|
||||
const onMessage = useEffectEvent(onReceiveMessage);
|
||||
|
||||
useEffect(() => {
|
||||
connection.on('message', onMessage);
|
||||
// ...
|
||||
}, [roomId]); // Stable dependency list
|
||||
}
|
||||
```
|
||||
|
||||
## Effect Cleanup
|
||||
|
||||
### Always Clean Up Subscriptions
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
const connection = createConnection(serverUrl, roomId);
|
||||
connection.connect();
|
||||
return () => connection.disconnect(); // REQUIRED
|
||||
}, [roomId]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleScroll(e) {
|
||||
console.log(window.scrollY);
|
||||
}
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll); // REQUIRED
|
||||
}, []);
|
||||
```
|
||||
|
||||
### Data Fetching with Ignore Flag
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
async function fetchData() {
|
||||
const result = await fetchTodos(userId);
|
||||
if (!ignore) {
|
||||
setTodos(result);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
|
||||
return () => {
|
||||
ignore = true; // Prevents stale data from old requests
|
||||
};
|
||||
}, [userId]);
|
||||
```
|
||||
|
||||
### Development Double-Fire Is Intentional
|
||||
|
||||
React remounts components in development to verify cleanup works. If you see effects firing twice, don't try to prevent it with refs:
|
||||
|
||||
```tsx
|
||||
// BAD: Hiding the symptom
|
||||
const didInit = useRef(false);
|
||||
useEffect(() => {
|
||||
if (didInit.current) return;
|
||||
didInit.current = true;
|
||||
// ...
|
||||
}, []);
|
||||
|
||||
// GOOD: Fix the cleanup
|
||||
useEffect(() => {
|
||||
const connection = createConnection();
|
||||
connection.connect();
|
||||
return () => connection.disconnect(); // Proper cleanup
|
||||
}, []);
|
||||
```
|
||||
|
||||
## Refs
|
||||
|
||||
### Use Refs for Values That Don't Affect Rendering
|
||||
|
||||
```tsx
|
||||
// GOOD: Ref for timeout ID (doesn't affect UI)
|
||||
const timeoutRef = useRef(null);
|
||||
|
||||
function handleClick() {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
// ...
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// BAD: Using ref for displayed value
|
||||
const countRef = useRef(0);
|
||||
countRef.current++; // UI won't update!
|
||||
```
|
||||
|
||||
### Never Read/Write ref.current During Render
|
||||
|
||||
```tsx
|
||||
// BAD: Reading ref during render
|
||||
function MyComponent() {
|
||||
const ref = useRef(0);
|
||||
ref.current++; // Mutating during render!
|
||||
return <div>{ref.current}</div>; // Reading during render!
|
||||
}
|
||||
|
||||
// GOOD: Read/write refs in event handlers and effects
|
||||
function MyComponent() {
|
||||
const ref = useRef(0);
|
||||
|
||||
function handleClick() {
|
||||
ref.current++; // OK in event handler
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = someValue; // OK in effect
|
||||
}, [someValue]);
|
||||
}
|
||||
```
|
||||
|
||||
### Ref Callbacks for Dynamic Lists
|
||||
|
||||
```tsx
|
||||
// BAD: Can't call useRef in a loop
|
||||
{items.map((item) => {
|
||||
const ref = useRef(null); // Rule violation!
|
||||
return <li ref={ref} />;
|
||||
})}
|
||||
|
||||
// GOOD: Ref callback with Map
|
||||
const itemsRef = useRef(new Map());
|
||||
|
||||
{items.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
ref={(node) => {
|
||||
if (node) {
|
||||
itemsRef.current.set(item.id, node);
|
||||
} else {
|
||||
itemsRef.current.delete(item.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
```
|
||||
|
||||
### useImperativeHandle for Controlled Exposure
|
||||
|
||||
```tsx
|
||||
// Limit what parent can access
|
||||
function MyInput({ ref }) {
|
||||
const realInputRef = useRef(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus() {
|
||||
realInputRef.current.focus();
|
||||
},
|
||||
// Parent can ONLY call focus(), not access full DOM node
|
||||
}));
|
||||
|
||||
return <input ref={realInputRef} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Hooks
|
||||
|
||||
### Hooks Share Logic, Not State
|
||||
|
||||
```tsx
|
||||
// Each call gets independent state
|
||||
function StatusBar() {
|
||||
const isOnline = useOnlineStatus(); // Own state
|
||||
}
|
||||
|
||||
function SaveButton() {
|
||||
const isOnline = useOnlineStatus(); // Separate state instance
|
||||
}
|
||||
```
|
||||
|
||||
### Name Hooks useXxx Only If They Use Hooks
|
||||
|
||||
```tsx
|
||||
// BAD: useXxx but doesn't use hooks
|
||||
function useSorted(items) {
|
||||
return items.slice().sort();
|
||||
}
|
||||
|
||||
// GOOD: Regular function
|
||||
function getSorted(items) {
|
||||
return items.slice().sort();
|
||||
}
|
||||
|
||||
// GOOD: Uses hooks, so prefix with use
|
||||
function useAuth() {
|
||||
return useContext(AuthContext);
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid "Lifecycle" Hooks
|
||||
|
||||
```tsx
|
||||
// BAD: Custom lifecycle hooks
|
||||
function useMount(fn) {
|
||||
useEffect(() => {
|
||||
fn();
|
||||
}, []); // Missing dependency, linter can't catch it
|
||||
}
|
||||
|
||||
// GOOD: Use useEffect directly
|
||||
useEffect(() => {
|
||||
doSomething();
|
||||
}, [doSomething]);
|
||||
```
|
||||
|
||||
### Keep Custom Hooks Focused
|
||||
|
||||
```tsx
|
||||
// GOOD: Focused, concrete use cases
|
||||
useChatRoom({ serverUrl, roomId });
|
||||
useOnlineStatus();
|
||||
useFormInput(initialValue);
|
||||
|
||||
// BAD: Generic, abstract hooks
|
||||
useMount(fn);
|
||||
useEffectOnce(fn);
|
||||
useUpdateEffect(fn);
|
||||
```
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Controlled vs Uncontrolled
|
||||
|
||||
```tsx
|
||||
// Uncontrolled: component owns state
|
||||
function SearchInput() {
|
||||
const [query, setQuery] = useState('');
|
||||
return <input value={query} onChange={e => setQuery(e.target.value)} />;
|
||||
}
|
||||
|
||||
// Controlled: parent owns state
|
||||
function SearchInput({ query, onQueryChange }) {
|
||||
return <input value={query} onChange={e => onQueryChange(e.target.value)} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Prefer Composition Over Prop Drilling
|
||||
|
||||
```tsx
|
||||
// BAD: Prop drilling
|
||||
<App user={user}>
|
||||
<Layout user={user}>
|
||||
<Header user={user}>
|
||||
<Avatar user={user} />
|
||||
</Header>
|
||||
</Layout>
|
||||
</App>
|
||||
|
||||
// GOOD: Composition with children
|
||||
<App>
|
||||
<Layout>
|
||||
<Header avatar={<Avatar user={user} />} />
|
||||
</Layout>
|
||||
</App>
|
||||
|
||||
// GOOD: Context for truly global state
|
||||
<UserContext.Provider value={user}>
|
||||
<App />
|
||||
</UserContext.Provider>
|
||||
```
|
||||
|
||||
### flushSync for Synchronous DOM Updates
|
||||
|
||||
```tsx
|
||||
// When you need to read DOM immediately after state update
|
||||
import { flushSync } from 'react-dom';
|
||||
|
||||
function handleAdd() {
|
||||
flushSync(() => {
|
||||
setTodos([...todos, newTodo]);
|
||||
});
|
||||
// DOM is now updated, safe to read
|
||||
listRef.current.lastChild.scrollIntoView();
|
||||
}
|
||||
```
|
||||
|
||||
## Summary: Decision Tree
|
||||
|
||||
1. **Need to respond to user interaction?** Use event handler
|
||||
2. **Need computed value from props/state?** Calculate during render
|
||||
3. **Need cached expensive calculation?** Use useMemo
|
||||
4. **Need to reset state on prop change?** Use key prop
|
||||
5. **Need to synchronize with external system?** Use Effect with cleanup
|
||||
6. **Need non-reactive code in Effect?** Use useEffectEvent
|
||||
7. **Need mutable value that doesn't trigger render?** Use ref
|
||||
335
skills/release-skills/skill.md
Normal file
335
skills/release-skills/skill.md
Normal file
@@ -0,0 +1,335 @@
|
||||
---
|
||||
name: release-skills
|
||||
description: Universal release workflow. Auto-detects version files and changelogs. Supports Node.js, Python, Rust, Claude Plugin, and generic projects. Use when user says "release", "发布", "new version", "bump version", "push", "推送".
|
||||
---
|
||||
|
||||
# Release Skills
|
||||
|
||||
Universal release workflow supporting any project type with multi-language changelog.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Just run `/release-skills` - auto-detects your project configuration.
|
||||
|
||||
## Supported Projects
|
||||
|
||||
| Project Type | Version File | Auto-Detected |
|
||||
|--------------|--------------|---------------|
|
||||
| Node.js | package.json | ✓ |
|
||||
| Python | pyproject.toml | ✓ |
|
||||
| Rust | Cargo.toml | ✓ |
|
||||
| Claude Plugin | marketplace.json | ✓ |
|
||||
| Generic | VERSION / version.txt | ✓ |
|
||||
|
||||
## Options
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--dry-run` | Preview changes without executing |
|
||||
| `--major` | Force major version bump |
|
||||
| `--minor` | Force minor version bump |
|
||||
| `--patch` | Force patch version bump |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Detect Project Configuration
|
||||
|
||||
1. Check for `.releaserc.yml` (optional config override)
|
||||
2. Auto-detect version file by scanning (priority order):
|
||||
- `package.json` (Node.js)
|
||||
- `pyproject.toml` (Python)
|
||||
- `Cargo.toml` (Rust)
|
||||
- `marketplace.json` or `.claude-plugin/marketplace.json` (Claude Plugin)
|
||||
- `VERSION` or `version.txt` (Generic)
|
||||
3. Scan for changelog files using glob patterns:
|
||||
- `CHANGELOG*.md`
|
||||
- `HISTORY*.md`
|
||||
- `CHANGES*.md`
|
||||
4. Identify language of each changelog by filename suffix
|
||||
5. Display detected configuration
|
||||
|
||||
**Language Detection Rules**:
|
||||
|
||||
| Filename Pattern | Language |
|
||||
|------------------|----------|
|
||||
| `CHANGELOG.md` (no suffix) | en (default) |
|
||||
| `CHANGELOG.zh.md` / `CHANGELOG_CN.md` / `CHANGELOG.zh-CN.md` | zh |
|
||||
| `CHANGELOG.ja.md` / `CHANGELOG_JP.md` | ja |
|
||||
| `CHANGELOG.ko.md` / `CHANGELOG_KR.md` | ko |
|
||||
| `CHANGELOG.de.md` / `CHANGELOG_DE.md` | de |
|
||||
| `CHANGELOG.fr.md` / `CHANGELOG_FR.md` | fr |
|
||||
| `CHANGELOG.es.md` / `CHANGELOG_ES.md` | es |
|
||||
| `CHANGELOG.{lang}.md` | Corresponding language code |
|
||||
|
||||
**Output Example**:
|
||||
```
|
||||
Project detected:
|
||||
Version file: package.json (1.2.3)
|
||||
Changelogs:
|
||||
- CHANGELOG.md (en)
|
||||
- CHANGELOG.zh.md (zh)
|
||||
- CHANGELOG.ja.md (ja)
|
||||
```
|
||||
|
||||
### Step 2: Analyze Changes Since Last Tag
|
||||
|
||||
```bash
|
||||
LAST_TAG=$(git tag --sort=-v:refname | head -1)
|
||||
git log ${LAST_TAG}..HEAD --oneline
|
||||
git diff ${LAST_TAG}..HEAD --stat
|
||||
```
|
||||
|
||||
Categorize by conventional commit types:
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| feat | New features |
|
||||
| fix | Bug fixes |
|
||||
| docs | Documentation |
|
||||
| refactor | Code refactoring |
|
||||
| perf | Performance improvements |
|
||||
| test | Test changes |
|
||||
| style | Formatting, styling |
|
||||
| chore | Maintenance (skip in changelog) |
|
||||
|
||||
**Breaking Change Detection**:
|
||||
- Commit message starts with `BREAKING CHANGE`
|
||||
- Commit body/footer contains `BREAKING CHANGE:`
|
||||
- Removed public APIs, renamed exports, changed interfaces
|
||||
|
||||
If breaking changes detected, warn user: "Breaking changes detected. Consider major version bump (--major flag)."
|
||||
|
||||
### Step 3: Determine Version Bump
|
||||
|
||||
Rules (in priority order):
|
||||
1. User flag `--major/--minor/--patch` → Use specified
|
||||
2. BREAKING CHANGE detected → Major bump (1.x.x → 2.0.0)
|
||||
3. `feat:` commits present → Minor bump (1.2.x → 1.3.0)
|
||||
4. Otherwise → Patch bump (1.2.3 → 1.2.4)
|
||||
|
||||
Display version change: `1.2.3 → 1.3.0`
|
||||
|
||||
### Step 4: Generate Multi-language Changelogs
|
||||
|
||||
For each detected changelog file:
|
||||
|
||||
1. **Identify language** from filename suffix
|
||||
2. **Generate content in that language**:
|
||||
- Section titles in target language
|
||||
- Change descriptions written naturally in target language (not translated)
|
||||
- Date format: YYYY-MM-DD (universal)
|
||||
3. **Insert at file head** (preserve existing content)
|
||||
|
||||
**Section Title Translations** (built-in):
|
||||
|
||||
| Type | en | zh | ja | ko | de | fr | es |
|
||||
|------|----|----|----|----|----|----|-----|
|
||||
| feat | Features | 新功能 | 新機能 | 새로운 기능 | Funktionen | Fonctionnalités | Características |
|
||||
| fix | Fixes | 修复 | 修正 | 수정 | Fehlerbehebungen | Corrections | Correcciones |
|
||||
| docs | Documentation | 文档 | ドキュメント | 문서 | Dokumentation | Documentation | Documentación |
|
||||
| refactor | Refactor | 重构 | リファクタリング | 리팩토링 | Refactoring | Refactorisation | Refactorización |
|
||||
| perf | Performance | 性能优化 | パフォーマンス | 성능 | Leistung | Performance | Rendimiento |
|
||||
| breaking | Breaking Changes | 破坏性变更 | 破壊的変更 | 주요 변경사항 | Breaking Changes | Changements majeurs | Cambios importantes |
|
||||
|
||||
**Changelog Format**:
|
||||
|
||||
```markdown
|
||||
## {VERSION} - {YYYY-MM-DD}
|
||||
|
||||
### Features
|
||||
- Description of new feature
|
||||
|
||||
### Fixes
|
||||
- Description of fix
|
||||
|
||||
### Documentation
|
||||
- Description of docs changes
|
||||
```
|
||||
|
||||
Only include sections that have changes. Omit empty sections.
|
||||
|
||||
**Multi-language Example**:
|
||||
|
||||
English (CHANGELOG.md):
|
||||
```markdown
|
||||
## 1.3.0 - 2026-01-22
|
||||
|
||||
### Features
|
||||
- Add user authentication module
|
||||
- Support OAuth2 login
|
||||
|
||||
### Fixes
|
||||
- Fix memory leak in connection pool
|
||||
```
|
||||
|
||||
Chinese (CHANGELOG.zh.md):
|
||||
```markdown
|
||||
## 1.3.0 - 2026-01-22
|
||||
|
||||
### 新功能
|
||||
- 新增用户认证模块
|
||||
- 支持 OAuth2 登录
|
||||
|
||||
### 修复
|
||||
- 修复连接池内存泄漏问题
|
||||
```
|
||||
|
||||
Japanese (CHANGELOG.ja.md):
|
||||
```markdown
|
||||
## 1.3.0 - 2026-01-22
|
||||
|
||||
### 新機能
|
||||
- ユーザー認証モジュールを追加
|
||||
- OAuth2 ログインをサポート
|
||||
|
||||
### 修正
|
||||
- コネクションプールのメモリリークを修正
|
||||
```
|
||||
|
||||
### Step 5: Update Version File
|
||||
|
||||
1. Read version file (JSON/TOML/text)
|
||||
2. Update version number
|
||||
3. Write back (preserve formatting)
|
||||
|
||||
**Version Paths by File Type**:
|
||||
|
||||
| File | Path |
|
||||
|------|------|
|
||||
| package.json | `$.version` |
|
||||
| pyproject.toml | `project.version` |
|
||||
| Cargo.toml | `package.version` |
|
||||
| marketplace.json | `$.metadata.version` |
|
||||
| VERSION / version.txt | Direct content |
|
||||
|
||||
### Step 6: Commit and Tag
|
||||
|
||||
```bash
|
||||
git add <all modified files>
|
||||
git commit -m "chore: release v{VERSION}"
|
||||
git tag v{VERSION}
|
||||
```
|
||||
|
||||
**Note**: Do NOT add Co-Authored-By line. This is a release commit, not a code contribution.
|
||||
|
||||
**Important**: Do NOT push to remote. User will push manually when ready.
|
||||
|
||||
**Post-Release Output**:
|
||||
```
|
||||
Release v1.3.0 created locally.
|
||||
|
||||
To publish:
|
||||
git push origin main
|
||||
git push origin v1.3.0
|
||||
```
|
||||
|
||||
## Configuration (.releaserc.yml)
|
||||
|
||||
Optional config file in project root to override defaults:
|
||||
|
||||
```yaml
|
||||
# .releaserc.yml - Optional configuration
|
||||
|
||||
# Version file (auto-detected if not specified)
|
||||
version:
|
||||
file: package.json
|
||||
path: $.version # JSONPath for JSON, dotted path for TOML
|
||||
|
||||
# Changelog files (auto-detected if not specified)
|
||||
changelog:
|
||||
files:
|
||||
- path: CHANGELOG.md
|
||||
lang: en
|
||||
- path: CHANGELOG.zh.md
|
||||
lang: zh
|
||||
- path: CHANGELOG.ja.md
|
||||
lang: ja
|
||||
|
||||
# Section mapping (conventional commit type → changelog section)
|
||||
# Use null to skip a type in changelog
|
||||
sections:
|
||||
feat: Features
|
||||
fix: Fixes
|
||||
docs: Documentation
|
||||
refactor: Refactor
|
||||
perf: Performance
|
||||
test: Tests
|
||||
chore: null
|
||||
|
||||
# Commit message format
|
||||
commit:
|
||||
message: "chore: release v{version}"
|
||||
|
||||
# Tag format
|
||||
tag:
|
||||
prefix: v # Results in v1.0.0
|
||||
sign: false
|
||||
|
||||
# Additional files to include in release commit
|
||||
include:
|
||||
- README.md
|
||||
- package.json
|
||||
```
|
||||
|
||||
## Dry-Run Mode
|
||||
|
||||
When `--dry-run` is specified:
|
||||
|
||||
```
|
||||
=== DRY RUN MODE ===
|
||||
|
||||
Project detected:
|
||||
Version file: package.json (1.2.3)
|
||||
Changelogs: CHANGELOG.md (en), CHANGELOG.zh.md (zh)
|
||||
|
||||
Last tag: v1.2.3
|
||||
Proposed version: v1.3.0
|
||||
|
||||
Changes detected:
|
||||
feat: add user authentication
|
||||
feat: support oauth2 login
|
||||
fix: memory leak in pool
|
||||
|
||||
Changelog preview (en):
|
||||
## 1.3.0 - 2026-01-22
|
||||
### Features
|
||||
- Add user authentication module
|
||||
- Support OAuth2 login
|
||||
### Fixes
|
||||
- Fix memory leak in connection pool
|
||||
|
||||
Changelog preview (zh):
|
||||
## 1.3.0 - 2026-01-22
|
||||
### 新功能
|
||||
- 新增用户认证模块
|
||||
- 支持 OAuth2 登录
|
||||
### 修复
|
||||
- 修复连接池内存泄漏问题
|
||||
|
||||
Files to modify:
|
||||
- package.json
|
||||
- CHANGELOG.md
|
||||
- CHANGELOG.zh.md
|
||||
|
||||
No changes made. Run without --dry-run to execute.
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
/release-skills # Auto-detect version bump
|
||||
/release-skills --dry-run # Preview only
|
||||
/release-skills --minor # Force minor bump
|
||||
/release-skills --patch # Force patch bump
|
||||
/release-skills --major # Force major bump (with confirmation)
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
||||
Trigger this skill when user requests:
|
||||
- "release", "发布", "create release", "new version", "新版本"
|
||||
- "bump version", "update version", "更新版本"
|
||||
- "prepare release"
|
||||
- "push to remote" (with uncommitted changes)
|
||||
|
||||
**Important**: If user says "just push" or "直接 push" with uncommitted changes, STILL follow all steps above first.
|
||||
Reference in New Issue
Block a user