diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..d7e8f35 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(cmd /c \"dir \"\"d:\\\\Usuario\\\\Desktop\\\\trdtyyyy\\\\Portfolio\\\\app\\\\projects\"\" /s\")", + "Bash(cmd /c \"del /f \"\"d:\\\\Usuario\\\\Desktop\\\\trdtyyyy\\\\Portfolio\\\\components\\\\project-detail-modal.tsx\"\"\")", + "Bash(powershell -Command:*)", + "WebFetch(domain:www.repetto-a.com)", + "WebFetch(domain:localhost)" + ] + } +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..76440ac --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,34 @@ +{ + "permissions": { + "allow": [ + "Bash(dir:*)", + "Bash(npx next build:*)", + "Bash(npx tsc:*)", + "Bash(npm ls:*)", + "Bash(npm install:*)", + "Bash(pnpm add:*)", + "Bash(npx playwright install:*)", + "Bash(curl:*)", + "Bash(pnpm dev)", + "Bash(timeout 20 bash -c 'while ! curl -s -o /dev/null -w \"\"%{http_code}\"\" http://localhost:3000 2>/dev/null | grep -q \"\"200\"\"; do sleep 2; echo \"\"Waiting...\"\"; done; echo \"\"Server ready!\"\"')", + "Bash(npx playwright test:*)", + "Bash(timeout 25 bash:*)", + "Bash(netstat:*)", + "Bash(findstr:*)", + "Bash(taskkill:*)", + "Bash(timeout 30 bash:*)", + "Bash(node -e:*)", + "Bash(node:*)", + "Bash(npx:*)", + "Bash(npm run build:*)", + "Bash(grep:*)", + "WebFetch(domain:nude-project.com)", + "Bash(git commit:*)", + "Bash(echo:*)", + "Bash(git add:*)", + "Bash(GIT_TRACE=1 git commit:*)", + "Bash(cmd /c \"del /f \"\"d:\\\\Usuario\\\\Desktop\\\\trdtyyyy\\\\Portfolio\\\\app\\\\projects\\\\[id]\\\\page.tsx\"\" && del /f \"\"d:\\\\Usuario\\\\Desktop\\\\trdtyyyy\\\\Portfolio\\\\app\\\\projects\\\\[id]\\\\not-found.tsx\"\"\")", + "Bash(cmd /c \"del /f \"\"d:\\\\Usuario\\\\Desktop\\\\trdtyyyy\\\\Portfolio\\\\app\\\\projects\\\\[id]\\\\page.tsx\"\"\")" + ] + } +} diff --git a/.claude/skills/agent-skills-main/agent-skills-main/.github/workflows/react-best-practices-ci.yml b/.claude/skills/agent-skills-main/agent-skills-main/.github/workflows/react-best-practices-ci.yml new file mode 100644 index 0000000..814ccfe --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/.github/workflows/react-best-practices-ci.yml @@ -0,0 +1,33 @@ +name: React Best Practices CI + +on: + push: + branches: [main] + paths: + - 'skills/react-best-practices/**' + - 'packages/react-best-practices-build/**' + pull_request: + branches: [main] + paths: + - 'skills/react-best-practices/**' + - 'packages/react-best-practices-build/**' + +jobs: + validate: + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/react-best-practices-build + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v2 + with: + version: 10.24.0 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + cache-dependency-path: packages/react-best-practices-build/pnpm-lock.yaml + - run: pnpm install + - run: pnpm validate + - run: pnpm build diff --git a/.claude/skills/agent-skills-main/agent-skills-main/.gitignore b/.claude/skills/agent-skills-main/agent-skills-main/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/.claude/skills/agent-skills-main/agent-skills-main/AGENTS.md b/.claude/skills/agent-skills-main/agent-skills-main/AGENTS.md new file mode 100644 index 0000000..faeb88e --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/AGENTS.md @@ -0,0 +1,110 @@ +# AGENTS.md + +This file provides guidance to AI coding agents (Claude Code, Cursor, Copilot, etc.) when working with code in this repository. + +## Repository Overview + +A collection of skills for Claude.ai and Claude Code for working with Vercel deployments. Skills are packaged instructions and scripts that extend Claude's capabilities. + +## Creating a New Skill + +### Directory Structure + +``` +skills/ + {skill-name}/ # kebab-case directory name + SKILL.md # Required: skill definition + scripts/ # Required: executable scripts + {script-name}.sh # Bash scripts (preferred) + {skill-name}.zip # Required: packaged for distribution +``` + +### Naming Conventions + +- **Skill directory**: `kebab-case` (e.g., `vercel-deploy`, `log-monitor`) +- **SKILL.md**: Always uppercase, always this exact filename +- **Scripts**: `kebab-case.sh` (e.g., `deploy.sh`, `fetch-logs.sh`) +- **Zip file**: Must match directory name exactly: `{skill-name}.zip` + +### SKILL.md Format + +```markdown +--- +name: {skill-name} +description: {One sentence describing when to use this skill. Include trigger phrases like "Deploy my app", "Check logs", etc.} +--- + +# {Skill Title} + +{Brief description of what the skill does.} + +## How It Works + +{Numbered list explaining the skill's workflow} + +## Usage + +```bash +bash /mnt/skills/user/{skill-name}/scripts/{script}.sh [args] +``` + +**Arguments:** +- `arg1` - Description (defaults to X) + +**Examples:** +{Show 2-3 common usage patterns} + +## Output + +{Show example output users will see} + +## Present Results to User + +{Template for how Claude should format results when presenting to users} + +## Troubleshooting + +{Common issues and solutions, especially network/permissions errors} +``` + +### Best Practices for Context Efficiency + +Skills are loaded on-demand — only the skill name and description are loaded at startup. The full `SKILL.md` loads into context only when the agent decides the skill is relevant. To minimize context usage: + +- **Keep SKILL.md under 500 lines** — put detailed reference material in separate files +- **Write specific descriptions** — helps the agent know exactly when to activate the skill +- **Use progressive disclosure** — reference supporting files that get read only when needed +- **Prefer scripts over inline code** — script execution doesn't consume context (only output does) +- **File references work one level deep** — link directly from SKILL.md to supporting files + +### Script Requirements + +- Use `#!/bin/bash` shebang +- Use `set -e` for fail-fast behavior +- Write status messages to stderr: `echo "Message" >&2` +- Write machine-readable output (JSON) to stdout +- Include a cleanup trap for temp files +- Reference the script path as `/mnt/skills/user/{skill-name}/scripts/{script}.sh` + +### Creating the Zip Package + +After creating or updating a skill: + +```bash +cd skills +zip -r {skill-name}.zip {skill-name}/ +``` + +### End-User Installation + +Document these two installation methods for users: + +**Claude Code:** +```bash +cp -r skills/{skill-name} ~/.claude/skills/ +``` + +**claude.ai:** +Add the skill to project knowledge or paste SKILL.md contents into the conversation. + +If the skill requires network access, instruct users to add required domains at `claude.ai/settings/capabilities`. diff --git a/.claude/skills/agent-skills-main/agent-skills-main/CLAUDE.md b/.claude/skills/agent-skills-main/agent-skills-main/CLAUDE.md new file mode 100644 index 0000000..47dc3e3 --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/.claude/skills/agent-skills-main/agent-skills-main/README.md b/.claude/skills/agent-skills-main/agent-skills-main/README.md new file mode 100644 index 0000000..2dc45bd --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/README.md @@ -0,0 +1,147 @@ +# Agent Skills + +A collection of skills for AI coding agents. Skills are packaged instructions and scripts that extend agent capabilities. + +Skills follow the [Agent Skills](https://agentskills.io/) format. + +## Available Skills + +### react-best-practices + +React and Next.js performance optimization guidelines from Vercel Engineering. Contains 40+ rules across 8 categories, prioritized by impact. + +**Use when:** +- Writing new React components or Next.js pages +- Implementing data fetching (client or server-side) +- Reviewing code for performance issues +- Optimizing bundle size or load times + +**Categories covered:** +- Eliminating waterfalls (Critical) +- Bundle size optimization (Critical) +- Server-side performance (High) +- Client-side data fetching (Medium-High) +- Re-render optimization (Medium) +- Rendering performance (Medium) +- JavaScript micro-optimizations (Low-Medium) + +### web-design-guidelines + +Review UI code for compliance with web interface best practices. Audits your code for 100+ rules covering accessibility, performance, and UX. + +**Use when:** +- "Review my UI" +- "Check accessibility" +- "Audit design" +- "Review UX" +- "Check my site against best practices" + +**Categories covered:** +- Accessibility (aria-labels, semantic HTML, keyboard handlers) +- Focus States (visible focus, focus-visible patterns) +- Forms (autocomplete, validation, error handling) +- Animation (prefers-reduced-motion, compositor-friendly transforms) +- Typography (curly quotes, ellipsis, tabular-nums) +- Images (dimensions, lazy loading, alt text) +- Performance (virtualization, layout thrashing, preconnect) +- Navigation & State (URL reflects state, deep-linking) +- Dark Mode & Theming (color-scheme, theme-color meta) +- Touch & Interaction (touch-action, tap-highlight) +- Locale & i18n (Intl.DateTimeFormat, Intl.NumberFormat) + +### react-native-guidelines + +React Native best practices optimized for AI agents. Contains 16 rules across 7 sections covering performance, architecture, and platform-specific patterns. + +**Use when:** +- Building React Native or Expo apps +- Optimizing mobile performance +- Implementing animations or gestures +- Working with native modules or platform APIs + +**Categories covered:** +- Performance (Critical) - FlashList, memoization, heavy computation +- Layout (High) - flex patterns, safe areas, keyboard handling +- Animation (High) - Reanimated, gesture handling +- Images (Medium) - expo-image, caching, lazy loading +- State Management (Medium) - Zustand patterns, React Compiler +- Architecture (Medium) - monorepo structure, imports +- Platform (Medium) - iOS/Android specific patterns + +### composition-patterns + +React composition patterns that scale. Helps avoid boolean prop proliferation through compound components, state lifting, and internal composition. + +**Use when:** +- Refactoring components with many boolean props +- Building reusable component libraries +- Designing flexible APIs +- Reviewing component architecture + +**Patterns covered:** +- Extracting compound components +- Lifting state to reduce props +- Composing internals for flexibility +- Avoiding prop drilling + +### vercel-deploy-claimable + +Deploy applications and websites to Vercel instantly. Designed for use with claude.ai and Claude Desktop to enable deployments directly from conversations. Deployments are "claimable" - users can transfer ownership to their own Vercel account. + +**Use when:** +- "Deploy my app" +- "Deploy this to production" +- "Push this live" +- "Deploy and give me the link" + +**Features:** +- Auto-detects 40+ frameworks from `package.json` +- Returns preview URL (live site) and claim URL (transfer ownership) +- Handles static HTML projects automatically +- Excludes `node_modules` and `.git` from uploads + +**How it works:** +1. Packages your project into a tarball +2. Detects framework (Next.js, Vite, Astro, etc.) +3. Uploads to deployment service +4. Returns preview URL and claim URL + +**Output:** +``` +Deployment successful! + +Preview URL: https://skill-deploy-abc123.vercel.app +Claim URL: https://vercel.com/claim-deployment?code=... +``` + +## Installation + +```bash +npx skills add vercel-labs/agent-skills +``` + +## Usage + +Skills are automatically available once installed. The agent will use them when relevant tasks are detected. + +**Examples:** +``` +Deploy my app +``` +``` +Review this React component for performance issues +``` +``` +Help me optimize this Next.js page +``` + +## Skill Structure + +Each skill contains: +- `SKILL.md` - Instructions for the agent +- `scripts/` - Helper scripts for automation (optional) +- `references/` - Supporting documentation (optional) + +## License + +MIT diff --git a/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/.gitignore b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/package.json b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/package.json new file mode 100644 index 0000000..1afbe9c --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/package.json @@ -0,0 +1,31 @@ +{ + "name": "react-best-practices-build", + "version": "1.0.0", + "description": "Build tooling for React Best Practices and React Native Guidelines skills", + "type": "module", + "scripts": { + "build": "pnpm build-agents && pnpm extract-tests", + "build-agents": "tsx src/build.ts", + "build-all": "tsx src/build.ts --all", + "build-react": "tsx src/build.ts --skill=react-best-practices", + "build-rn": "tsx src/build.ts --skill=react-native-skills", + "build-composition": "tsx src/build.ts --skill=composition-patterns", + "validate": "tsx src/validate.ts", + "extract-tests": "tsx src/extract-tests.ts", + "migrate": "tsx src/migrate.ts", + "dev": "pnpm build && pnpm validate" + }, + "keywords": [ + "react", + "performance", + "guidelines", + "llm", + "agents" + ], + "license": "MIT", + "devDependencies": { + "@types/node": "^20.0.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + } +} diff --git a/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/pnpm-lock.yaml b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/pnpm-lock.yaml new file mode 100644 index 0000000..c042951 --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/pnpm-lock.yaml @@ -0,0 +1,342 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.19.29 + tsx: + specifier: ^4.7.0 + version: 4.21.0 + typescript: + specifier: ^5.3.0 + version: 5.9.3 + +packages: + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@types/node@20.19.29': + resolution: {integrity: sha512-YrT9ArrGaHForBaCNwFjoqJWmn8G1Pr7+BH/vwyLHciA9qT/wSiuOhxGCT50JA5xLvFBd6PIiGkE3afxcPE1nw==} + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + +snapshots: + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@types/node@20.19.29': + dependencies: + undici-types: 6.21.0 + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + resolve-pkg-maps@1.0.0: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.2 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} diff --git a/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/build.ts b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/build.ts new file mode 100644 index 0000000..155ff0a --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/build.ts @@ -0,0 +1,320 @@ +#!/usr/bin/env node +/** + * Build script to compile individual rule files into AGENTS.md + */ + +import { readdir, readFile, writeFile } from 'fs/promises' +import { join } from 'path' +import { Rule, Section, GuidelinesDocument, ImpactLevel } from './types.js' +import { parseRuleFile, RuleFile } from './parser.js' +import { SKILLS, SkillConfig, DEFAULT_SKILL } from './config.js' + +// Parse command line arguments +const args = process.argv.slice(2) +const upgradeVersion = args.includes('--upgrade-version') +const skillArg = args.find((arg) => arg.startsWith('--skill=')) +const skillName = skillArg ? skillArg.split('=')[1] : null +const buildAll = args.includes('--all') + +/** + * Increment a semver-style version string (e.g., "0.1.0" -> "0.1.1", "1.0" -> "1.1") + */ +function incrementVersion(version: string): string { + const parts = version.split('.').map(Number) + // Increment the last part + parts[parts.length - 1]++ + return parts.join('.') +} + +/** + * Generate markdown from rules + */ +function generateMarkdown( + sections: Section[], + metadata: { + version: string + organization: string + date: string + abstract: string + references?: string[] + }, + skillConfig: SkillConfig +): string { + let md = `# ${skillConfig.title}\n\n` + md += `**Version ${metadata.version}** \n` + md += `${metadata.organization} \n` + md += `${metadata.date}\n\n` + md += `> **Note:** \n` + md += `> This document is mainly for agents and LLMs to follow when maintaining, \n` + md += `> generating, or refactoring ${skillConfig.description}. Humans \n` + md += `> may also find it useful, but guidance here is optimized for automation \n` + md += `> and consistency by AI-assisted workflows.\n\n` + md += `---\n\n` + md += `## Abstract\n\n` + md += `${metadata.abstract}\n\n` + md += `---\n\n` + md += `## Table of Contents\n\n` + + // Generate TOC + sections.forEach((section) => { + md += `${section.number}. [${section.title}](#${ + section.number + }-${section.title.toLowerCase().replace(/\s+/g, '-')}) — **${ + section.impact + }**\n` + section.rules.forEach((rule) => { + // GitHub generates anchors from the full heading text: "1.1 Title" -> "#11-title" + const anchor = `${rule.id} ${rule.title}` + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^\w-]/g, '') // Remove special characters except hyphens + md += ` - ${rule.id} [${rule.title}](#${anchor})\n` + }) + }) + + md += `\n---\n\n` + + // Generate sections + sections.forEach((section) => { + md += `## ${section.number}. ${section.title}\n\n` + md += `**Impact: ${section.impact}${ + section.impactDescription ? ` (${section.impactDescription})` : '' + }**\n\n` + if (section.introduction) { + md += `${section.introduction}\n\n` + } + + section.rules.forEach((rule) => { + md += `### ${rule.id} ${rule.title}\n\n` + md += `**Impact: ${rule.impact}${ + rule.impactDescription ? ` (${rule.impactDescription})` : '' + }**\n\n` + md += `${rule.explanation}\n\n` + + rule.examples.forEach((example) => { + if (example.description) { + md += `**${example.label}: ${example.description}**\n\n` + } else { + md += `**${example.label}:**\n\n` + } + // Only generate code block if there's actual code + if (example.code && example.code.trim()) { + md += `\`\`\`${example.language || 'typescript'}\n` + md += `${example.code}\n` + md += `\`\`\`\n\n` + } + if (example.additionalText) { + md += `${example.additionalText}\n\n` + } + }) + + if (rule.references && rule.references.length > 0) { + md += `Reference: ${rule.references + .map((ref) => `[${ref}](${ref})`) + .join(', ')}\n\n` + } + }) + + md += `---\n\n` + }) + + // Add references section + if (metadata.references && metadata.references.length > 0) { + md += `## References\n\n` + metadata.references.forEach((ref, i) => { + md += `${i + 1}. [${ref}](${ref})\n` + }) + } + + return md +} + +/** + * Build a single skill + */ +async function buildSkill(skillConfig: SkillConfig) { + console.log(`\nBuilding ${skillConfig.name}...`) + console.log(` Rules directory: ${skillConfig.rulesDir}`) + console.log(` Output file: ${skillConfig.outputFile}`) + + // Read all rule files (exclude files starting with _ and README.md) + const files = await readdir(skillConfig.rulesDir) + const ruleFiles = files + .filter((f) => f.endsWith('.md') && !f.startsWith('_') && f !== 'README.md') + .sort() // Sort filenames for consistent ordering across systems + + const ruleData: RuleFile[] = [] + for (const file of ruleFiles) { + const filePath = join(skillConfig.rulesDir, file) + try { + const parsed = await parseRuleFile(filePath, skillConfig.sectionMap) + ruleData.push(parsed) + } catch (error) { + console.error(` Error parsing ${file}:`, error) + } + } + + // Group rules by section + const sectionsMap = new Map() + + ruleData.forEach(({ section, rule }) => { + if (!sectionsMap.has(section)) { + sectionsMap.set(section, { + number: section, + title: `Section ${section}`, // Will be overridden by section metadata + impact: rule.impact, + rules: [], + }) + } + sectionsMap.get(section)!.rules.push(rule) + }) + + // Sort rules within each section by title (using en-US locale for consistency across environments) + sectionsMap.forEach((section) => { + section.rules.sort((a, b) => + a.title.localeCompare(b.title, 'en-US', { sensitivity: 'base' }) + ) + + // Assign IDs based on sorted order + section.rules.forEach((rule, index) => { + rule.id = `${section.number}.${index + 1}` + rule.subsection = index + 1 + }) + }) + + // Convert to array and sort + const sections = Array.from(sectionsMap.values()).sort( + (a, b) => a.number - b.number + ) + + // Read section metadata from consolidated _sections.md file + const sectionsFile = join(skillConfig.rulesDir, '_sections.md') + try { + const sectionsContent = await readFile(sectionsFile, 'utf-8') + + // Parse sections using regex to match each section block + const sectionBlocks = sectionsContent + .split(/(?=^## \d+\. )/m) + .filter(Boolean) + + for (const block of sectionBlocks) { + // Extract section number and title, removing section ID in parentheses + const headerMatch = block.match(/^## (\d+)\.\s+(.+?)(?:\s+\([^)]+\))?$/m) + if (!headerMatch) continue + + const sectionNumber = parseInt(headerMatch[1]) + const sectionTitle = headerMatch[2].trim() // Strip (id) for output + + // Extract impact (format: **Impact:** CRITICAL) + const impactMatch = block.match(/\*\*Impact:\*\*\s+(\w+(?:-\w+)?)/i) + const impactLevel = impactMatch + ? (impactMatch[1].toUpperCase().replace(/-/g, '-') as ImpactLevel) + : 'MEDIUM' + + // Extract description (format: **Description:** text) + const descMatch = block.match(/\*\*Description:\*\*\s+(.+?)(?=\n\n##|$)/s) + const description = descMatch ? descMatch[1].trim() : '' + + // Update section if it exists + const section = sections.find((s) => s.number === sectionNumber) + if (section) { + section.title = sectionTitle + section.impact = impactLevel + section.introduction = description + } + } + } catch (error) { + console.warn(' Warning: Could not read _sections.md, using defaults') + } + + // Read metadata + let metadata + try { + const metadataContent = await readFile(skillConfig.metadataFile, 'utf-8') + metadata = JSON.parse(metadataContent) + } catch { + metadata = { + version: '1.0.0', + organization: 'Engineering', + date: new Date().toLocaleDateString('en-US', { + month: 'long', + year: 'numeric', + }), + abstract: `Performance optimization guide for ${skillConfig.description}, ordered by impact.`, + } + } + + // Upgrade version if flag is passed + if (upgradeVersion) { + const oldVersion = metadata.version + metadata.version = incrementVersion(oldVersion) + console.log(` Upgrading version: ${oldVersion} -> ${metadata.version}`) + + // Write updated metadata.json + await writeFile( + skillConfig.metadataFile, + JSON.stringify(metadata, null, 2) + '\n', + 'utf-8' + ) + console.log(` ✓ Updated metadata.json`) + + // Update SKILL.md frontmatter if it exists + const skillFile = join(skillConfig.skillDir, 'SKILL.md') + try { + const skillContent = await readFile(skillFile, 'utf-8') + const updatedSkillContent = skillContent.replace( + /^(---[\s\S]*?version:\s*)"[^"]*"([\s\S]*?---)$/m, + `$1"${metadata.version}"$2` + ) + await writeFile(skillFile, updatedSkillContent, 'utf-8') + console.log(` ✓ Updated SKILL.md`) + } catch { + // SKILL.md doesn't exist, skip + } + } + + // Generate markdown + const markdown = generateMarkdown(sections, metadata, skillConfig) + + // Write output + await writeFile(skillConfig.outputFile, markdown, 'utf-8') + + console.log( + ` ✓ Built AGENTS.md with ${sections.length} sections and ${ruleData.length} rules` + ) +} + +/** + * Main build function + */ +async function build() { + try { + console.log('Building AGENTS.md from rules...') + + if (buildAll) { + // Build all skills + for (const skill of Object.values(SKILLS)) { + await buildSkill(skill) + } + } else if (skillName) { + // Build specific skill + const skill = SKILLS[skillName] + if (!skill) { + console.error(`Unknown skill: ${skillName}`) + console.error(`Available skills: ${Object.keys(SKILLS).join(', ')}`) + process.exit(1) + } + await buildSkill(skill) + } else { + // Build default skill (backwards compatibility) + await buildSkill(SKILLS[DEFAULT_SKILL]) + } + + console.log('\n✓ Build complete') + } catch (error) { + console.error('Build failed:', error) + process.exit(1) + } +} + +build() diff --git a/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/config.ts b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/config.ts new file mode 100644 index 0000000..25dc075 --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/config.ts @@ -0,0 +1,98 @@ +/** + * Configuration for the build tooling + */ + +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +// Base paths +export const SKILLS_DIR = join(__dirname, '../../..', 'skills') +export const BUILD_DIR = join(__dirname, '..') + +// Skill configurations +export interface SkillConfig { + name: string + title: string + description: string + skillDir: string + rulesDir: string + metadataFile: string + outputFile: string + sectionMap: Record +} + +export const SKILLS: Record = { + 'react-best-practices': { + name: 'react-best-practices', + title: 'React Best Practices', + description: 'React and Next.js codebases', + skillDir: join(SKILLS_DIR, 'react-best-practices'), + rulesDir: join(SKILLS_DIR, 'react-best-practices/rules'), + metadataFile: join(SKILLS_DIR, 'react-best-practices/metadata.json'), + outputFile: join(SKILLS_DIR, 'react-best-practices/AGENTS.md'), + sectionMap: { + async: 1, + bundle: 2, + server: 3, + client: 4, + rerender: 5, + rendering: 6, + js: 7, + advanced: 8, + }, + }, + 'react-native-skills': { + name: 'react-native-skills', + title: 'React Native Skills', + description: 'React Native codebases', + skillDir: join(SKILLS_DIR, 'react-native-skills'), + rulesDir: join(SKILLS_DIR, 'react-native-skills/rules'), + metadataFile: join(SKILLS_DIR, 'react-native-skills/metadata.json'), + outputFile: join(SKILLS_DIR, 'react-native-skills/AGENTS.md'), + sectionMap: { + rendering: 1, + 'list-performance': 2, + animation: 3, + scroll: 4, + navigation: 5, + 'react-state': 6, + state: 7, + 'react-compiler': 8, + ui: 9, + 'design-system': 10, + monorepo: 11, + imports: 12, + js: 13, + fonts: 14, + }, + }, + 'composition-patterns': { + name: 'composition-patterns', + title: 'React Composition Patterns', + description: 'React codebases using composition', + skillDir: join(SKILLS_DIR, 'composition-patterns'), + rulesDir: join(SKILLS_DIR, 'composition-patterns/rules'), + metadataFile: join(SKILLS_DIR, 'composition-patterns/metadata.json'), + outputFile: join(SKILLS_DIR, 'composition-patterns/AGENTS.md'), + sectionMap: { + architecture: 1, + state: 2, + patterns: 3, + react19: 4, + }, + }, +} + +// Default skill (for backwards compatibility) +export const DEFAULT_SKILL = 'react-best-practices' + +// Legacy exports for backwards compatibility +export const SKILL_DIR = SKILLS[DEFAULT_SKILL].skillDir +export const RULES_DIR = SKILLS[DEFAULT_SKILL].rulesDir +export const METADATA_FILE = SKILLS[DEFAULT_SKILL].metadataFile +export const OUTPUT_FILE = SKILLS[DEFAULT_SKILL].outputFile + +// Test cases are build artifacts, not part of the skill +export const TEST_CASES_FILE = join(BUILD_DIR, 'test-cases.json') diff --git a/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/extract-tests.ts b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/extract-tests.ts new file mode 100644 index 0000000..d1d4bd2 --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/extract-tests.ts @@ -0,0 +1,77 @@ +#!/usr/bin/env node +/** + * Extract test cases from rules for LLM evaluation + */ + +import { readdir, writeFile } from 'fs/promises' +import { join } from 'path' +import { Rule, TestCase } from './types.js' +import { parseRuleFile } from './parser.js' +import { RULES_DIR, TEST_CASES_FILE } from './config.js' + +/** + * Extract test cases from a rule + */ +function extractTestCases(rule: Rule): TestCase[] { + const testCases: TestCase[] = [] + + rule.examples.forEach((example, index) => { + const isBad = example.label.toLowerCase().includes('incorrect') || + example.label.toLowerCase().includes('wrong') || + example.label.toLowerCase().includes('bad') + const isGood = example.label.toLowerCase().includes('correct') || + example.label.toLowerCase().includes('good') + + if (isBad || isGood) { + testCases.push({ + ruleId: rule.id, + ruleTitle: rule.title, + type: isBad ? 'bad' : 'good', + code: example.code, + language: example.language || 'typescript', + description: example.description || `${example.label} example for ${rule.title}` + }) + } + }) + + return testCases +} + +/** + * Main extraction function + */ +async function extractTests() { + try { + console.log('Extracting test cases from rules...') + console.log(`Rules directory: ${RULES_DIR}`) + console.log(`Output file: ${TEST_CASES_FILE}`) + + const files = await readdir(RULES_DIR) + const ruleFiles = files.filter(f => f.endsWith('.md') && !f.startsWith('_') && f !== 'README.md') + + const allTestCases: TestCase[] = [] + + for (const file of ruleFiles) { + const filePath = join(RULES_DIR, file) + try { + const { rule } = await parseRuleFile(filePath) + const testCases = extractTestCases(rule) + allTestCases.push(...testCases) + } catch (error) { + console.error(`Error processing ${file}:`, error) + } + } + + // Write test cases as JSON + await writeFile(TEST_CASES_FILE, JSON.stringify(allTestCases, null, 2), 'utf-8') + + console.log(`✓ Extracted ${allTestCases.length} test cases to ${TEST_CASES_FILE}`) + console.log(` - Bad examples: ${allTestCases.filter(tc => tc.type === 'bad').length}`) + console.log(` - Good examples: ${allTestCases.filter(tc => tc.type === 'good').length}`) + } catch (error) { + console.error('Extraction failed:', error) + process.exit(1) + } +} + +extractTests() diff --git a/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/migrate.ts b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/migrate.ts new file mode 100644 index 0000000..f71a755 --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/migrate.ts @@ -0,0 +1,177 @@ +#!/usr/bin/env node +/** + * Migration script to split RPG.md into individual rule files + * This is a one-time script to help migrate existing content + */ + +import { readFile, writeFile, mkdir } from 'fs/promises' +import { join } from 'path' +import { existsSync } from 'fs' +import { SKILL_DIR, RULES_DIR } from './config.js' + +const RPG_FILE = join(SKILL_DIR, 'RPG.md') + +/** + * Extract section number and title from heading + */ +function parseSectionHeading(line: string): { number: number; title: string } | null { + const match = line.match(/^##\s+(\d+)\.\s+(.+)$/) + if (match) { + return { + number: parseInt(match[1]), + title: match[2].trim() + } + } + return null +} + +/** + * Extract rule number and title from heading + */ +function parseRuleHeading(line: string): { section: number; subsection: number; title: string } | null { + const match = line.match(/^###\s+(\d+)\.(\d+)\s+(.+)$/) + if (match) { + return { + section: parseInt(match[1]), + subsection: parseInt(match[2]), + title: match[3].trim() + } + } + return null +} + +/** + * Extract impact from line + */ +function extractImpact(line: string): { impact: string; description?: string } | null { + const match = line.match(/\*\*Impact:\s*(\w+(?:-\w+)?)\s*(?:\(([^)]+)\))?/i) + if (match) { + return { + impact: match[1].toUpperCase().replace(/-/g, '-'), + description: match[2] + } + } + return null +} + +async function migrate() { + try { + console.log('Migrating RPG.md to individual rule files...') + + if (!existsSync(RPG_FILE)) { + console.error(`RPG.md not found at ${RPG_FILE}`) + process.exit(1) + } + + // Ensure rules directory exists + if (!existsSync(RULES_DIR)) { + await mkdir(RULES_DIR, { recursive: true }) + } + + const content = await readFile(RPG_FILE, 'utf-8') + const lines = content.split('\n') + + let currentSection: { number: number; title: string; impact?: string; introduction?: string } | null = null + let currentRule: { section: number; subsection: number; title: string; content: string[] } | null = null + let inCodeBlock = false + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + + // Check for section heading + const sectionInfo = parseSectionHeading(line) + if (sectionInfo) { + // Save previous section if exists + if (currentSection) { + const sectionFile = join(RULES_DIR, `section-${currentSection.number}.md`) + let sectionContent = `# ${currentSection.number}. ${currentSection.title}\n\n` + if (currentSection.impact) { + sectionContent += `**Impact: ${currentSection.impact}**\n\n` + } + if (currentSection.introduction) { + sectionContent += `## Introduction\n\n${currentSection.introduction}\n` + } + await writeFile(sectionFile, sectionContent, 'utf-8') + } + + currentSection = sectionInfo + currentRule = null + + // Look for impact on next few lines + for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) { + const impactInfo = extractImpact(lines[j]) + if (impactInfo) { + currentSection.impact = impactInfo.impact + break + } + } + + // Collect introduction text until first rule + let introduction: string[] = [] + for (let j = i + 1; j < lines.length; j++) { + if (parseRuleHeading(lines[j])) { + break + } + if (!lines[j].match(/^###/)) { + introduction.push(lines[j]) + } + } + currentSection.introduction = introduction.join('\n').trim() + continue + } + + // Check for rule heading + const ruleInfo = parseRuleHeading(line) + if (ruleInfo) { + // Save previous rule if exists + if (currentRule && currentSection) { + const ruleFile = join(RULES_DIR, `section-${currentRule.section}-rule-${currentRule.subsection}.md`) + const ruleContent = currentRule.content.join('\n') + await writeFile(ruleFile, ruleContent, 'utf-8') + console.log(`Created ${ruleFile}`) + } + + currentRule = { + ...ruleInfo, + content: [line] + } + continue + } + + // Accumulate content for current rule + if (currentRule) { + currentRule.content.push(line) + } + } + + // Save last rule + if (currentRule && currentSection) { + const ruleFile = join(RULES_DIR, `section-${currentRule.section}-rule-${currentRule.subsection}.md`) + const ruleContent = currentRule.content.join('\n') + await writeFile(ruleFile, ruleContent, 'utf-8') + console.log(`Created ${ruleFile}`) + } + + // Save last section + if (currentSection) { + const sectionFile = join(RULES_DIR, `section-${currentSection.number}.md`) + let sectionContent = `# ${currentSection.number}. ${currentSection.title}\n\n` + if (currentSection.impact) { + sectionContent += `**Impact: ${currentSection.impact}**\n\n` + } + if (currentSection.introduction) { + sectionContent += `## Introduction\n\n${currentSection.introduction}\n` + } + await writeFile(sectionFile, sectionContent, 'utf-8') + console.log(`Created ${sectionFile}`) + } + + console.log('\n✓ Migration complete!') + console.log('Note: You may need to manually add frontmatter to rule files.') + } catch (error) { + console.error('Migration failed:', error) + process.exit(1) + } +} + +migrate() diff --git a/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/parser.ts b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/parser.ts new file mode 100644 index 0000000..f9e32c1 --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/parser.ts @@ -0,0 +1,261 @@ +/** + * Parser for rule markdown files + */ + +import { readFile } from 'fs/promises' +import { basename } from 'path' +import { Rule, ImpactLevel } from './types.js' + +export interface RuleFile { + section: number + subsection?: number + rule: Rule +} + +/** + * Parse a rule markdown file into a Rule object + */ +export async function parseRuleFile( + filePath: string, + sectionMap?: Record +): Promise { + const rawContent = await readFile(filePath, 'utf-8') + // Normalize Windows CRLF line endings to LF for consistent parsing + const content = rawContent.replace(/\r\n/g, '\n') + const lines = content.split('\n') + + // Extract frontmatter if present + let frontmatter: Record = {} + let contentStart = 0 + + if (content.startsWith('---')) { + const frontmatterEnd = content.indexOf('---', 3) + if (frontmatterEnd !== -1) { + const frontmatterText = content.slice(3, frontmatterEnd).trim() + frontmatterText.split('\n').forEach((line) => { + const [key, ...valueParts] = line.split(':') + if (key && valueParts.length) { + const value = valueParts.join(':').trim() + frontmatter[key.trim()] = value.replace(/^["']|["']$/g, '') + } + }) + contentStart = frontmatterEnd + 3 + } + } + + // Parse the rule content + const ruleContent = content.slice(contentStart).trim() + const ruleLines = ruleContent.split('\n') + + // Extract title (first # or ## heading) + let title = '' + let titleLine = 0 + for (let i = 0; i < ruleLines.length; i++) { + if (ruleLines[i].startsWith('##')) { + title = ruleLines[i].replace(/^##+\s*/, '').trim() + titleLine = i + break + } + } + + // Extract impact + let impact: Rule['impact'] = 'MEDIUM' + let impactDescription = '' + let explanation = '' + let examples: Rule['examples'] = [] + let references: string[] = [] + + // Parse content after title + let currentExample: { + label: string + description?: string + code: string + language?: string + additionalText?: string + } | null = null + let inCodeBlock = false + let codeBlockLanguage = 'typescript' + let codeBlockContent: string[] = [] + let afterCodeBlock = false + let additionalText: string[] = [] + let hasCodeBlockForCurrentExample = false + + for (let i = titleLine + 1; i < ruleLines.length; i++) { + const line = ruleLines[i] + + // Impact line + if (line.includes('**Impact:')) { + const match = line.match( + /\*\*Impact:\s*(\w+(?:-\w+)?)\s*(?:\(([^)]+)\))?/i + ) + if (match) { + impact = match[1].toUpperCase().replace(/-/g, '-') as ImpactLevel + impactDescription = match[2] || '' + } + continue + } + + // Code block start + if (line.startsWith('```')) { + if (inCodeBlock) { + // End of code block + if (currentExample) { + currentExample.code = codeBlockContent.join('\n') + currentExample.language = codeBlockLanguage + } + codeBlockContent = [] + inCodeBlock = false + afterCodeBlock = true + } else { + // Start of code block + inCodeBlock = true + hasCodeBlockForCurrentExample = true + codeBlockLanguage = line.slice(3).trim() || 'typescript' + codeBlockContent = [] + afterCodeBlock = false + } + continue + } + + if (inCodeBlock) { + codeBlockContent.push(line) + continue + } + + // Example label (Incorrect, Correct, Example, Usage, Implementation, etc.) + // Match pattern: **Label:** or **Label (description):** at end of line + // This distinguishes example labels from inline bold text like "**Trade-off:** some text" + const labelMatch = line.match(/^\*\*([^:]+?):\*?\*?$/) + if (labelMatch) { + // Save previous example if it exists + if (currentExample) { + if (additionalText.length > 0) { + currentExample.additionalText = additionalText.join('\n\n') + additionalText = [] + } + examples.push(currentExample) + } + afterCodeBlock = false + hasCodeBlockForCurrentExample = false + + const fullLabel = labelMatch[1].trim() + // Try to extract description from parentheses if present (handles simple cases) + // For nested parentheses like "Incorrect (O(n) per lookup)", we keep the full label + const descMatch = fullLabel.match( + /^([A-Za-z]+(?:\s+[A-Za-z]+)*)\s*\(([^()]+)\)$/ + ) + currentExample = { + label: descMatch ? descMatch[1].trim() : fullLabel, + description: descMatch ? descMatch[2].trim() : undefined, + code: '', + language: codeBlockLanguage, + } + continue + } + + // Reference links + if (line.startsWith('Reference:') || line.startsWith('References:')) { + // Save current example before processing references + if (currentExample) { + if (additionalText.length > 0) { + currentExample.additionalText = additionalText.join('\n\n') + additionalText = [] + } + examples.push(currentExample) + currentExample = null + } + + const refMatch = line.match(/\[([^\]]+)\]\(([^)]+)\)/g) + if (refMatch) { + references.push( + ...refMatch.map((ref) => { + const m = ref.match(/\[([^\]]+)\]\(([^)]+)\)/) + return m ? m[2] : ref + }) + ) + } + continue + } + + // Regular text (explanation or additional context after examples) + if (line.trim() && !line.startsWith('#')) { + if (!currentExample && !inCodeBlock) { + // Main explanation before any examples + explanation += (explanation ? '\n\n' : '') + line + } else if ( + currentExample && + (afterCodeBlock || !hasCodeBlockForCurrentExample) + ) { + // Text after a code block, or text in a section without a code block + // (e.g., "When NOT to use this pattern:" with bullet points instead of code) + additionalText.push(line) + } + } + } + + // Handle last example if still open + if (currentExample) { + if (additionalText.length > 0) { + currentExample.additionalText = additionalText.join('\n\n') + } + examples.push(currentExample) + } + + // Infer section from filename patterns + // Pattern: area-description.md where area determines section + const filename = basename(filePath) + + // Default section map (for backwards compatibility) + const defaultSectionMap: Record = { + async: 1, + bundle: 2, + server: 3, + client: 4, + rerender: 5, + rendering: 6, + js: 7, + advanced: 8, + } + + const effectiveSectionMap = sectionMap || defaultSectionMap + + // Extract area from filename - try longest prefix match first + // This handles prefixes like "list-performance" vs "list" + const filenameParts = filename.replace('.md', '').split('-') + let section = 0 + + // Try progressively shorter prefixes to find the best match + for (let len = filenameParts.length; len > 0; len--) { + const prefix = filenameParts.slice(0, len).join('-') + if (effectiveSectionMap[prefix] !== undefined) { + section = effectiveSectionMap[prefix] + break + } + } + + // Fall back to frontmatter section if specified + section = frontmatter.section || section || 0 + + const rule: Rule = { + id: '', // Will be assigned by build script based on sorted order + title: frontmatter.title || title, + section: section, + subsection: undefined, + impact: frontmatter.impact || impact, + impactDescription: frontmatter.impactDescription || impactDescription, + explanation: frontmatter.explanation || explanation.trim(), + examples, + references: frontmatter.references + ? frontmatter.references.split(',').map((r: string) => r.trim()) + : references, + tags: frontmatter.tags + ? frontmatter.tags.split(',').map((t: string) => t.trim()) + : undefined, + } + + return { + section, + subsection: 0, + rule, + } +} diff --git a/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/types.ts b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/types.ts new file mode 100644 index 0000000..4f8ad3b --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/types.ts @@ -0,0 +1,53 @@ +/** + * Type definitions for React Performance Guidelines rules + */ + +export type ImpactLevel = 'CRITICAL' | 'HIGH' | 'MEDIUM-HIGH' | 'MEDIUM' | 'LOW-MEDIUM' | 'LOW' + +export interface CodeExample { + label: string // e.g., "Incorrect", "Correct", "Example" + description?: string // Optional description before code + code: string + language?: string // Default: 'typescript' or 'tsx' + additionalText?: string // Optional text after code block (explanations, reasons) +} + +export interface Rule { + id: string // e.g., "1.1", "2.3" + title: string + section: number // Main section number (1-8) + subsection?: number // Subsection number within section + impact: ImpactLevel + impactDescription?: string // e.g., "2-10× improvement" + explanation: string + examples: CodeExample[] + references?: string[] // URLs or citations + tags?: string[] // For categorization/search +} + +export interface Section { + number: number + title: string + impact: ImpactLevel + impactDescription?: string + introduction?: string + rules: Rule[] +} + +export interface GuidelinesDocument { + version: string + organization: string + date: string + abstract: string + sections: Section[] + references?: string[] +} + +export interface TestCase { + ruleId: string + ruleTitle: string + type: 'bad' | 'good' + code: string + language: string + description?: string +} diff --git a/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/validate.ts b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/validate.ts new file mode 100644 index 0000000..8e4b270 --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/src/validate.ts @@ -0,0 +1,110 @@ +#!/usr/bin/env node +/** + * Validate rule files follow the correct structure + */ + +import { readdir } from 'fs/promises' +import { join } from 'path' +import { Rule } from './types.js' +import { parseRuleFile } from './parser.js' +import { RULES_DIR } from './config.js' + +interface ValidationError { + file: string + ruleId?: string + message: string +} + +/** + * Validate a rule + */ +function validateRule(rule: Rule, file: string): ValidationError[] { + const errors: ValidationError[] = [] + + // Note: rule.id is auto-generated during build, not required in source files + + if (!rule.title || rule.title.trim().length === 0) { + errors.push({ file, ruleId: rule.id, message: 'Missing or empty title' }) + } + + if (!rule.explanation || rule.explanation.trim().length === 0) { + errors.push({ file, ruleId: rule.id, message: 'Missing or empty explanation' }) + } + + if (!rule.examples || rule.examples.length === 0) { + errors.push({ file, ruleId: rule.id, message: 'Missing examples (need at least one bad and one good example)' }) + } else { + // Filter out informational examples (notes, trade-offs, etc.) that don't have code + const codeExamples = rule.examples.filter(e => e.code && e.code.trim().length > 0) + + const hasBad = codeExamples.some(e => + e.label.toLowerCase().includes('incorrect') || + e.label.toLowerCase().includes('wrong') || + e.label.toLowerCase().includes('bad') + ) + const hasGood = codeExamples.some(e => + e.label.toLowerCase().includes('correct') || + e.label.toLowerCase().includes('good') || + e.label.toLowerCase().includes('usage') || + e.label.toLowerCase().includes('implementation') || + e.label.toLowerCase().includes('example') + ) + + if (codeExamples.length === 0) { + errors.push({ file, ruleId: rule.id, message: 'Missing code examples' }) + } else if (!hasBad && !hasGood) { + errors.push({ file, ruleId: rule.id, message: 'Missing bad/incorrect or good/correct examples' }) + } + } + + const validImpacts: Rule['impact'][] = ['CRITICAL', 'HIGH', 'MEDIUM-HIGH', 'MEDIUM', 'LOW-MEDIUM', 'LOW'] + if (!validImpacts.includes(rule.impact)) { + errors.push({ file, ruleId: rule.id, message: `Invalid impact level: ${rule.impact}. Must be one of: ${validImpacts.join(', ')}` }) + } + + return errors +} + +/** + * Main validation function + */ +async function validate() { + try { + console.log('Validating rule files...') + console.log(`Rules directory: ${RULES_DIR}`) + + const files = await readdir(RULES_DIR) + const ruleFiles = files.filter(f => f.endsWith('.md') && !f.startsWith('_')) + + const allErrors: ValidationError[] = [] + + for (const file of ruleFiles) { + const filePath = join(RULES_DIR, file) + try { + const { rule } = await parseRuleFile(filePath) + const errors = validateRule(rule, file) + allErrors.push(...errors) + } catch (error) { + allErrors.push({ + file, + message: `Failed to parse: ${error instanceof Error ? error.message : String(error)}` + }) + } + } + + if (allErrors.length > 0) { + console.error('\n✗ Validation failed:\n') + allErrors.forEach(error => { + console.error(` ${error.file}${error.ruleId ? ` (${error.ruleId})` : ''}: ${error.message}`) + }) + process.exit(1) + } else { + console.log(`✓ All ${ruleFiles.length} rule files are valid`) + } + } catch (error) { + console.error('Validation failed:', error) + process.exit(1) + } +} + +validate() diff --git a/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/test-cases.json b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/test-cases.json new file mode 100644 index 0000000..b7778bb --- /dev/null +++ b/.claude/skills/agent-skills-main/agent-skills-main/packages/react-best-practices-build/test-cases.json @@ -0,0 +1,858 @@ +[ + { + "ruleId": "", + "ruleTitle": "Store Event Handlers in Refs", + "type": "bad", + "code": "function useWindowEvent(event: string, handler: (e) => void) {\n useEffect(() => {\n window.addEventListener(event, handler)\n return () => window.removeEventListener(event, handler)\n }, [event, handler])\n}", + "language": "tsx", + "description": "re-subscribes on every render" + }, + { + "ruleId": "", + "ruleTitle": "Store Event Handlers in Refs", + "type": "good", + "code": "import { useEffectEvent } from 'react'\n\nfunction useWindowEvent(event: string, handler: (e) => void) {\n const onEvent = useEffectEvent(handler)\n\n useEffect(() => {\n window.addEventListener(event, onEvent)\n return () => window.removeEventListener(event, onEvent)\n }, [event])\n}", + "language": "tsx", + "description": "stable subscription" + }, + { + "ruleId": "", + "ruleTitle": "Initialize App Once, Not Per Mount", + "type": "bad", + "code": "function Comp() {\n useEffect(() => {\n loadFromStorage()\n checkAuthToken()\n }, [])\n\n // ...\n}", + "language": "tsx", + "description": "runs twice in dev, re-runs on remount" + }, + { + "ruleId": "", + "ruleTitle": "Initialize App Once, Not Per Mount", + "type": "good", + "code": "let didInit = false\n\nfunction Comp() {\n useEffect(() => {\n if (didInit) return\n didInit = true\n loadFromStorage()\n checkAuthToken()\n }, [])\n\n // ...\n}", + "language": "tsx", + "description": "once per app load" + }, + { + "ruleId": "", + "ruleTitle": "useEffectEvent for Stable Callback Refs", + "type": "bad", + "code": "function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {\n const [query, setQuery] = useState('')\n\n useEffect(() => {\n const timeout = setTimeout(() => onSearch(query), 300)\n return () => clearTimeout(timeout)\n }, [query, onSearch])\n}", + "language": "tsx", + "description": "effect re-runs on every callback change" + }, + { + "ruleId": "", + "ruleTitle": "useEffectEvent for Stable Callback Refs", + "type": "good", + "code": "import { useEffectEvent } from 'react';\n\nfunction SearchInput({ onSearch }: { onSearch: (q: string) => void }) {\n const [query, setQuery] = useState('')\n const onSearchEvent = useEffectEvent(onSearch)\n\n useEffect(() => {\n const timeout = setTimeout(() => onSearchEvent(query), 300)\n return () => clearTimeout(timeout)\n }, [query])\n}", + "language": "tsx", + "description": "using React's useEffectEvent" + }, + { + "ruleId": "", + "ruleTitle": "Prevent Waterfall Chains in API Routes", + "type": "bad", + "code": "export async function GET(request: Request) {\n const session = await auth()\n const config = await fetchConfig()\n const data = await fetchData(session.user.id)\n return Response.json({ data, config })\n}", + "language": "typescript", + "description": "config waits for auth, data waits for both" + }, + { + "ruleId": "", + "ruleTitle": "Prevent Waterfall Chains in API Routes", + "type": "good", + "code": "export async function GET(request: Request) {\n const sessionPromise = auth()\n const configPromise = fetchConfig()\n const session = await sessionPromise\n const [config, data] = await Promise.all([\n configPromise,\n fetchData(session.user.id)\n ])\n return Response.json({ data, config })\n}", + "language": "typescript", + "description": "auth and config start immediately" + }, + { + "ruleId": "", + "ruleTitle": "Defer Await Until Needed", + "type": "bad", + "code": "async function handleRequest(userId: string, skipProcessing: boolean) {\n const userData = await fetchUserData(userId)\n \n if (skipProcessing) {\n // Returns immediately but still waited for userData\n return { skipped: true }\n }\n \n // Only this branch uses userData\n return processUserData(userData)\n}", + "language": "typescript", + "description": "blocks both branches" + }, + { + "ruleId": "", + "ruleTitle": "Defer Await Until Needed", + "type": "good", + "code": "async function handleRequest(userId: string, skipProcessing: boolean) {\n if (skipProcessing) {\n // Returns immediately without waiting\n return { skipped: true }\n }\n \n // Fetch only when needed\n const userData = await fetchUserData(userId)\n return processUserData(userData)\n}", + "language": "typescript", + "description": "only blocks when needed" + }, + { + "ruleId": "", + "ruleTitle": "Dependency-Based Parallelization", + "type": "bad", + "code": "const [user, config] = await Promise.all([\n fetchUser(),\n fetchConfig()\n])\nconst profile = await fetchProfile(user.id)", + "language": "typescript", + "description": "profile waits for config unnecessarily" + }, + { + "ruleId": "", + "ruleTitle": "Dependency-Based Parallelization", + "type": "good", + "code": "import { all } from 'better-all'\n\nconst { user, config, profile } = await all({\n async user() { return fetchUser() },\n async config() { return fetchConfig() },\n async profile() {\n return fetchProfile((await this.$.user).id)\n }\n})", + "language": "typescript", + "description": "config and profile run in parallel" + }, + { + "ruleId": "", + "ruleTitle": "Promise.all() for Independent Operations", + "type": "bad", + "code": "const user = await fetchUser()\nconst posts = await fetchPosts()\nconst comments = await fetchComments()", + "language": "typescript", + "description": "sequential execution, 3 round trips" + }, + { + "ruleId": "", + "ruleTitle": "Promise.all() for Independent Operations", + "type": "good", + "code": "const [user, posts, comments] = await Promise.all([\n fetchUser(),\n fetchPosts(),\n fetchComments()\n])", + "language": "typescript", + "description": "parallel execution, 1 round trip" + }, + { + "ruleId": "", + "ruleTitle": "Strategic Suspense Boundaries", + "type": "bad", + "code": "async function Page() {\n const data = await fetchData() // Blocks entire page\n \n return (\n
\n
Sidebar
\n
Header
\n
\n \n
\n
Footer
\n
\n )\n}", + "language": "tsx", + "description": "wrapper blocked by data fetching" + }, + { + "ruleId": "", + "ruleTitle": "Strategic Suspense Boundaries", + "type": "good", + "code": "function Page() {\n return (\n
\n
Sidebar
\n
Header
\n
\n }>\n \n \n
\n
Footer
\n
\n )\n}\n\nasync function DataDisplay() {\n const data = await fetchData() // Only blocks this component\n return
{data.content}
\n}", + "language": "tsx", + "description": "wrapper shows immediately, data streams in" + }, + { + "ruleId": "", + "ruleTitle": "Avoid Barrel File Imports", + "type": "bad", + "code": "import { Check, X, Menu } from 'lucide-react'\n// Loads 1,583 modules, takes ~2.8s extra in dev\n// Runtime cost: 200-800ms on every cold start\n\nimport { Button, TextField } from '@mui/material'\n// Loads 2,225 modules, takes ~4.2s extra in dev", + "language": "tsx", + "description": "imports entire library" + }, + { + "ruleId": "", + "ruleTitle": "Avoid Barrel File Imports", + "type": "good", + "code": "import Check from 'lucide-react/dist/esm/icons/check'\nimport X from 'lucide-react/dist/esm/icons/x'\nimport Menu from 'lucide-react/dist/esm/icons/menu'\n// Loads only 3 modules (~2KB vs ~1MB)\n\nimport Button from '@mui/material/Button'\nimport TextField from '@mui/material/TextField'\n// Loads only what you use", + "language": "tsx", + "description": "imports only what you need" + }, + { + "ruleId": "", + "ruleTitle": "Defer Non-Critical Third-Party Libraries", + "type": "bad", + "code": "import { Analytics } from '@vercel/analytics/react'\n\nexport default function RootLayout({ children }) {\n return (\n \n \n {children}\n \n \n \n )\n}", + "language": "tsx", + "description": "blocks initial bundle" + }, + { + "ruleId": "", + "ruleTitle": "Defer Non-Critical Third-Party Libraries", + "type": "good", + "code": "import dynamic from 'next/dynamic'\n\nconst Analytics = dynamic(\n () => import('@vercel/analytics/react').then(m => m.Analytics),\n { ssr: false }\n)\n\nexport default function RootLayout({ children }) {\n return (\n \n \n {children}\n \n \n \n )\n}", + "language": "tsx", + "description": "loads after hydration" + }, + { + "ruleId": "", + "ruleTitle": "Dynamic Imports for Heavy Components", + "type": "bad", + "code": "import { MonacoEditor } from './monaco-editor'\n\nfunction CodePanel({ code }: { code: string }) {\n return \n}", + "language": "tsx", + "description": "Monaco bundles with main chunk ~300KB" + }, + { + "ruleId": "", + "ruleTitle": "Dynamic Imports for Heavy Components", + "type": "good", + "code": "import dynamic from 'next/dynamic'\n\nconst MonacoEditor = dynamic(\n () => import('./monaco-editor').then(m => m.MonacoEditor),\n { ssr: false }\n)\n\nfunction CodePanel({ code }: { code: string }) {\n return \n}", + "language": "tsx", + "description": "Monaco loads on demand" + }, + { + "ruleId": "", + "ruleTitle": "Deduplicate Global Event Listeners", + "type": "bad", + "code": "function useKeyboardShortcut(key: string, callback: () => void) {\n useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if (e.metaKey && e.key === key) {\n callback()\n }\n }\n window.addEventListener('keydown', handler)\n return () => window.removeEventListener('keydown', handler)\n }, [key, callback])\n}", + "language": "tsx", + "description": "N instances = N listeners" + }, + { + "ruleId": "", + "ruleTitle": "Deduplicate Global Event Listeners", + "type": "good", + "code": "import useSWRSubscription from 'swr/subscription'\n\n// Module-level Map to track callbacks per key\nconst keyCallbacks = new Map void>>()\n\nfunction useKeyboardShortcut(key: string, callback: () => void) {\n // Register this callback in the Map\n useEffect(() => {\n if (!keyCallbacks.has(key)) {\n keyCallbacks.set(key, new Set())\n }\n keyCallbacks.get(key)!.add(callback)\n\n return () => {\n const set = keyCallbacks.get(key)\n if (set) {\n set.delete(callback)\n if (set.size === 0) {\n keyCallbacks.delete(key)\n }\n }\n }\n }, [key, callback])\n\n useSWRSubscription('global-keydown', () => {\n const handler = (e: KeyboardEvent) => {\n if (e.metaKey && keyCallbacks.has(e.key)) {\n keyCallbacks.get(e.key)!.forEach(cb => cb())\n }\n }\n window.addEventListener('keydown', handler)\n return () => window.removeEventListener('keydown', handler)\n })\n}\n\nfunction Profile() {\n // Multiple shortcuts will share the same listener\n useKeyboardShortcut('p', () => { /* ... */ }) \n useKeyboardShortcut('k', () => { /* ... */ })\n // ...\n}", + "language": "tsx", + "description": "N instances = 1 listener" + }, + { + "ruleId": "", + "ruleTitle": "Version and Minimize localStorage Data", + "type": "bad", + "code": "// No version, stores everything, no error handling\nlocalStorage.setItem('userConfig', JSON.stringify(fullUserObject))\nconst data = localStorage.getItem('userConfig')", + "language": "typescript", + "description": "Incorrect example for Version and Minimize localStorage Data" + }, + { + "ruleId": "", + "ruleTitle": "Version and Minimize localStorage Data", + "type": "good", + "code": "const VERSION = 'v2'\n\nfunction saveConfig(config: { theme: string; language: string }) {\n try {\n localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))\n } catch {\n // Throws in incognito/private browsing, quota exceeded, or disabled\n }\n}\n\nfunction loadConfig() {\n try {\n const data = localStorage.getItem(`userConfig:${VERSION}`)\n return data ? JSON.parse(data) : null\n } catch {\n return null\n }\n}\n\n// Migration from v1 to v2\nfunction migrate() {\n try {\n const v1 = localStorage.getItem('userConfig:v1')\n if (v1) {\n const old = JSON.parse(v1)\n saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })\n localStorage.removeItem('userConfig:v1')\n }\n } catch {}\n}", + "language": "typescript", + "description": "Correct example for Version and Minimize localStorage Data" + }, + { + "ruleId": "", + "ruleTitle": "Use Passive Event Listeners for Scrolling Performance", + "type": "bad", + "code": "useEffect(() => {\n const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)\n const handleWheel = (e: WheelEvent) => console.log(e.deltaY)\n \n document.addEventListener('touchstart', handleTouch)\n document.addEventListener('wheel', handleWheel)\n \n return () => {\n document.removeEventListener('touchstart', handleTouch)\n document.removeEventListener('wheel', handleWheel)\n }\n}, [])", + "language": "typescript", + "description": "Incorrect example for Use Passive Event Listeners for Scrolling Performance" + }, + { + "ruleId": "", + "ruleTitle": "Use Passive Event Listeners for Scrolling Performance", + "type": "good", + "code": "useEffect(() => {\n const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)\n const handleWheel = (e: WheelEvent) => console.log(e.deltaY)\n \n document.addEventListener('touchstart', handleTouch, { passive: true })\n document.addEventListener('wheel', handleWheel, { passive: true })\n \n return () => {\n document.removeEventListener('touchstart', handleTouch)\n document.removeEventListener('wheel', handleWheel)\n }\n}, [])", + "language": "typescript", + "description": "Correct example for Use Passive Event Listeners for Scrolling Performance" + }, + { + "ruleId": "", + "ruleTitle": "Use SWR for Automatic Deduplication", + "type": "bad", + "code": "function UserList() {\n const [users, setUsers] = useState([])\n useEffect(() => {\n fetch('/api/users')\n .then(r => r.json())\n .then(setUsers)\n }, [])\n}", + "language": "tsx", + "description": "no deduplication, each instance fetches" + }, + { + "ruleId": "", + "ruleTitle": "Use SWR for Automatic Deduplication", + "type": "good", + "code": "import useSWR from 'swr'\n\nfunction UserList() {\n const { data: users } = useSWR('/api/users', fetcher)\n}", + "language": "tsx", + "description": "multiple instances share one request" + }, + { + "ruleId": "", + "ruleTitle": "Avoid Layout Thrashing", + "type": "bad", + "code": "function layoutThrashing(element: HTMLElement) {\n element.style.width = '100px'\n const width = element.offsetWidth // Forces reflow\n element.style.height = '200px'\n const height = element.offsetHeight // Forces another reflow\n}", + "language": "typescript", + "description": "interleaved reads and writes force reflows" + }, + { + "ruleId": "", + "ruleTitle": "Avoid Layout Thrashing", + "type": "good", + "code": "function updateElementStyles(element: HTMLElement) {\n // Batch all writes together\n element.style.width = '100px'\n element.style.height = '200px'\n element.style.backgroundColor = 'blue'\n element.style.border = '1px solid black'\n \n // Read after all writes are done (single reflow)\n const { width, height } = element.getBoundingClientRect()\n}", + "language": "typescript", + "description": "batch writes, then read once" + }, + { + "ruleId": "", + "ruleTitle": "Avoid Layout Thrashing", + "type": "good", + "code": "function updateElementStyles(element: HTMLElement) {\n element.classList.add('highlighted-box')\n \n const { width, height } = element.getBoundingClientRect()\n}", + "language": "typescript", + "description": "batch reads, then writes" + }, + { + "ruleId": "", + "ruleTitle": "Cache Repeated Function Calls", + "type": "bad", + "code": "function ProjectList({ projects }: { projects: Project[] }) {\n return (\n
\n {projects.map(project => {\n // slugify() called 100+ times for same project names\n const slug = slugify(project.name)\n \n return \n })}\n
\n )\n}", + "language": "typescript", + "description": "redundant computation" + }, + { + "ruleId": "", + "ruleTitle": "Cache Repeated Function Calls", + "type": "good", + "code": "// Module-level cache\nconst slugifyCache = new Map()\n\nfunction cachedSlugify(text: string): string {\n if (slugifyCache.has(text)) {\n return slugifyCache.get(text)!\n }\n const result = slugify(text)\n slugifyCache.set(text, result)\n return result\n}\n\nfunction ProjectList({ projects }: { projects: Project[] }) {\n return (\n
\n {projects.map(project => {\n // Computed only once per unique project name\n const slug = cachedSlugify(project.name)\n \n return \n })}\n
\n )\n}", + "language": "typescript", + "description": "cached results" + }, + { + "ruleId": "", + "ruleTitle": "Cache Property Access in Loops", + "type": "bad", + "code": "for (let i = 0; i < arr.length; i++) {\n process(obj.config.settings.value)\n}", + "language": "typescript", + "description": "3 lookups × N iterations" + }, + { + "ruleId": "", + "ruleTitle": "Cache Property Access in Loops", + "type": "good", + "code": "const value = obj.config.settings.value\nconst len = arr.length\nfor (let i = 0; i < len; i++) {\n process(value)\n}", + "language": "typescript", + "description": "1 lookup total" + }, + { + "ruleId": "", + "ruleTitle": "Cache Storage API Calls", + "type": "bad", + "code": "function getTheme() {\n return localStorage.getItem('theme') ?? 'light'\n}\n// Called 10 times = 10 storage reads", + "language": "typescript", + "description": "reads storage on every call" + }, + { + "ruleId": "", + "ruleTitle": "Cache Storage API Calls", + "type": "good", + "code": "const storageCache = new Map()\n\nfunction getLocalStorage(key: string) {\n if (!storageCache.has(key)) {\n storageCache.set(key, localStorage.getItem(key))\n }\n return storageCache.get(key)\n}\n\nfunction setLocalStorage(key: string, value: string) {\n localStorage.setItem(key, value)\n storageCache.set(key, value) // keep cache in sync\n}", + "language": "typescript", + "description": "Map cache" + }, + { + "ruleId": "", + "ruleTitle": "Combine Multiple Array Iterations", + "type": "bad", + "code": "const admins = users.filter(u => u.isAdmin)\nconst testers = users.filter(u => u.isTester)\nconst inactive = users.filter(u => !u.isActive)", + "language": "typescript", + "description": "3 iterations" + }, + { + "ruleId": "", + "ruleTitle": "Combine Multiple Array Iterations", + "type": "good", + "code": "const admins: User[] = []\nconst testers: User[] = []\nconst inactive: User[] = []\n\nfor (const user of users) {\n if (user.isAdmin) admins.push(user)\n if (user.isTester) testers.push(user)\n if (!user.isActive) inactive.push(user)\n}", + "language": "typescript", + "description": "1 iteration" + }, + { + "ruleId": "", + "ruleTitle": "Early Return from Functions", + "type": "bad", + "code": "function validateUsers(users: User[]) {\n let hasError = false\n let errorMessage = ''\n \n for (const user of users) {\n if (!user.email) {\n hasError = true\n errorMessage = 'Email required'\n }\n if (!user.name) {\n hasError = true\n errorMessage = 'Name required'\n }\n // Continues checking all users even after error found\n }\n \n return hasError ? { valid: false, error: errorMessage } : { valid: true }\n}", + "language": "typescript", + "description": "processes all items even after finding answer" + }, + { + "ruleId": "", + "ruleTitle": "Early Return from Functions", + "type": "good", + "code": "function validateUsers(users: User[]) {\n for (const user of users) {\n if (!user.email) {\n return { valid: false, error: 'Email required' }\n }\n if (!user.name) {\n return { valid: false, error: 'Name required' }\n }\n }\n\n return { valid: true }\n}", + "language": "typescript", + "description": "returns immediately on first error" + }, + { + "ruleId": "", + "ruleTitle": "Hoist RegExp Creation", + "type": "bad", + "code": "function Highlighter({ text, query }: Props) {\n const regex = new RegExp(`(${query})`, 'gi')\n const parts = text.split(regex)\n return <>{parts.map((part, i) => ...)}\n}", + "language": "tsx", + "description": "new RegExp every render" + }, + { + "ruleId": "", + "ruleTitle": "Hoist RegExp Creation", + "type": "good", + "code": "const EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nfunction Highlighter({ text, query }: Props) {\n const regex = useMemo(\n () => new RegExp(`(${escapeRegex(query)})`, 'gi'),\n [query]\n )\n const parts = text.split(regex)\n return <>{parts.map((part, i) => ...)}\n}", + "language": "tsx", + "description": "memoize or hoist" + }, + { + "ruleId": "", + "ruleTitle": "Build Index Maps for Repeated Lookups", + "type": "bad", + "code": "function processOrders(orders: Order[], users: User[]) {\n return orders.map(order => ({\n ...order,\n user: users.find(u => u.id === order.userId)\n }))\n}", + "language": "typescript", + "description": "Incorrect (O(n) per lookup) example for Build Index Maps for Repeated Lookups" + }, + { + "ruleId": "", + "ruleTitle": "Build Index Maps for Repeated Lookups", + "type": "good", + "code": "function processOrders(orders: Order[], users: User[]) {\n const userById = new Map(users.map(u => [u.id, u]))\n\n return orders.map(order => ({\n ...order,\n user: userById.get(order.userId)\n }))\n}", + "language": "typescript", + "description": "Correct (O(1) per lookup) example for Build Index Maps for Repeated Lookups" + }, + { + "ruleId": "", + "ruleTitle": "Early Length Check for Array Comparisons", + "type": "bad", + "code": "function hasChanges(current: string[], original: string[]) {\n // Always sorts and joins, even when lengths differ\n return current.sort().join() !== original.sort().join()\n}", + "language": "typescript", + "description": "always runs expensive comparison" + }, + { + "ruleId": "", + "ruleTitle": "Early Length Check for Array Comparisons", + "type": "good", + "code": "function hasChanges(current: string[], original: string[]) {\n // Early return if lengths differ\n if (current.length !== original.length) {\n return true\n }\n // Only sort when lengths match\n const currentSorted = current.toSorted()\n const originalSorted = original.toSorted()\n for (let i = 0; i < currentSorted.length; i++) {\n if (currentSorted[i] !== originalSorted[i]) {\n return true\n }\n }\n return false\n}", + "language": "typescript", + "description": "Correct (O(1) length check first) example for Early Length Check for Array Comparisons" + }, + { + "ruleId": "", + "ruleTitle": "Use Loop for Min/Max Instead of Sort", + "type": "bad", + "code": "interface Project {\n id: string\n name: string\n updatedAt: number\n}\n\nfunction getLatestProject(projects: Project[]) {\n const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)\n return sorted[0]\n}", + "language": "typescript", + "description": "Incorrect (O(n log n) - sort to find latest) example for Use Loop for Min/Max Instead of Sort" + }, + { + "ruleId": "", + "ruleTitle": "Use Loop for Min/Max Instead of Sort", + "type": "bad", + "code": "function getOldestAndNewest(projects: Project[]) {\n const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)\n return { oldest: sorted[0], newest: sorted[sorted.length - 1] }\n}", + "language": "typescript", + "description": "Incorrect (O(n log n) - sort for oldest and newest) example for Use Loop for Min/Max Instead of Sort" + }, + { + "ruleId": "", + "ruleTitle": "Use Loop for Min/Max Instead of Sort", + "type": "good", + "code": "function getLatestProject(projects: Project[]) {\n if (projects.length === 0) return null\n \n let latest = projects[0]\n \n for (let i = 1; i < projects.length; i++) {\n if (projects[i].updatedAt > latest.updatedAt) {\n latest = projects[i]\n }\n }\n \n return latest\n}\n\nfunction getOldestAndNewest(projects: Project[]) {\n if (projects.length === 0) return { oldest: null, newest: null }\n \n let oldest = projects[0]\n let newest = projects[0]\n \n for (let i = 1; i < projects.length; i++) {\n if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]\n if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]\n }\n \n return { oldest, newest }\n}", + "language": "typescript", + "description": "Correct (O(n) - single loop) example for Use Loop for Min/Max Instead of Sort" + }, + { + "ruleId": "", + "ruleTitle": "Use Set/Map for O(1) Lookups", + "type": "bad", + "code": "const allowedIds = ['a', 'b', 'c', ...]\nitems.filter(item => allowedIds.includes(item.id))", + "language": "typescript", + "description": "Incorrect (O(n) per check) example for Use Set/Map for O(1) Lookups" + }, + { + "ruleId": "", + "ruleTitle": "Use Set/Map for O(1) Lookups", + "type": "good", + "code": "const allowedIds = new Set(['a', 'b', 'c', ...])\nitems.filter(item => allowedIds.has(item.id))", + "language": "typescript", + "description": "Correct (O(1) per check) example for Use Set/Map for O(1) Lookups" + }, + { + "ruleId": "", + "ruleTitle": "Use toSorted() Instead of sort() for Immutability", + "type": "bad", + "code": "function UserList({ users }: { users: User[] }) {\n // Mutates the users prop array!\n const sorted = useMemo(\n () => users.sort((a, b) => a.name.localeCompare(b.name)),\n [users]\n )\n return
{sorted.map(renderUser)}
\n}", + "language": "typescript", + "description": "mutates original array" + }, + { + "ruleId": "", + "ruleTitle": "Use toSorted() Instead of sort() for Immutability", + "type": "good", + "code": "function UserList({ users }: { users: User[] }) {\n // Creates new sorted array, original unchanged\n const sorted = useMemo(\n () => users.toSorted((a, b) => a.name.localeCompare(b.name)),\n [users]\n )\n return
{sorted.map(renderUser)}
\n}", + "language": "typescript", + "description": "creates new array" + }, + { + "ruleId": "", + "ruleTitle": "Animate SVG Wrapper Instead of SVG Element", + "type": "bad", + "code": "function LoadingSpinner() {\n return (\n \n \n \n )\n}", + "language": "tsx", + "description": "animating SVG directly - no hardware acceleration" + }, + { + "ruleId": "", + "ruleTitle": "Animate SVG Wrapper Instead of SVG Element", + "type": "good", + "code": "function LoadingSpinner() {\n return (\n
\n \n \n \n
\n )\n}", + "language": "tsx", + "description": "animating wrapper div - hardware accelerated" + }, + { + "ruleId": "", + "ruleTitle": "Use Explicit Conditional Rendering", + "type": "bad", + "code": "function Badge({ count }: { count: number }) {\n return (\n
\n {count && {count}}\n
\n )\n}\n\n// When count = 0, renders:
0
\n// When count = 5, renders:
5
", + "language": "tsx", + "description": "renders \"0\" when count is 0" + }, + { + "ruleId": "", + "ruleTitle": "Use Explicit Conditional Rendering", + "type": "good", + "code": "function Badge({ count }: { count: number }) {\n return (\n
\n {count > 0 ? {count} : null}\n
\n )\n}\n\n// When count = 0, renders:
\n// When count = 5, renders:
5
", + "language": "tsx", + "description": "renders nothing when count is 0" + }, + { + "ruleId": "", + "ruleTitle": "Hoist Static JSX Elements", + "type": "bad", + "code": "function LoadingSkeleton() {\n return
\n}\n\nfunction Container() {\n return (\n
\n {loading && }\n
\n )\n}", + "language": "tsx", + "description": "recreates element every render" + }, + { + "ruleId": "", + "ruleTitle": "Hoist Static JSX Elements", + "type": "good", + "code": "const loadingSkeleton = (\n
\n)\n\nfunction Container() {\n return (\n
\n {loading && loadingSkeleton}\n
\n )\n}", + "language": "tsx", + "description": "reuses same element" + }, + { + "ruleId": "", + "ruleTitle": "Prevent Hydration Mismatch Without Flickering", + "type": "bad", + "code": "function ThemeWrapper({ children }: { children: ReactNode }) {\n // localStorage is not available on server - throws error\n const theme = localStorage.getItem('theme') || 'light'\n \n return (\n
\n {children}\n
\n )\n}", + "language": "tsx", + "description": "breaks SSR" + }, + { + "ruleId": "", + "ruleTitle": "Prevent Hydration Mismatch Without Flickering", + "type": "bad", + "code": "function ThemeWrapper({ children }: { children: ReactNode }) {\n const [theme, setTheme] = useState('light')\n \n useEffect(() => {\n // Runs after hydration - causes visible flash\n const stored = localStorage.getItem('theme')\n if (stored) {\n setTheme(stored)\n }\n }, [])\n \n return (\n
\n {children}\n
\n )\n}", + "language": "tsx", + "description": "visual flickering" + }, + { + "ruleId": "", + "ruleTitle": "Prevent Hydration Mismatch Without Flickering", + "type": "good", + "code": "function ThemeWrapper({ children }: { children: ReactNode }) {\n return (\n <>\n
\n {children}\n
\n \n \n )\n}", + "language": "tsx", + "description": "no flicker, no hydration mismatch" + }, + { + "ruleId": "", + "ruleTitle": "Suppress Expected Hydration Mismatches", + "type": "bad", + "code": "function Timestamp() {\n return {new Date().toLocaleString()}\n}", + "language": "tsx", + "description": "known mismatch warnings" + }, + { + "ruleId": "", + "ruleTitle": "Suppress Expected Hydration Mismatches", + "type": "good", + "code": "function Timestamp() {\n return (\n \n {new Date().toLocaleString()}\n \n )\n}", + "language": "tsx", + "description": "suppress expected mismatch only" + }, + { + "ruleId": "", + "ruleTitle": "Optimize SVG Precision", + "type": "bad", + "code": "", + "language": "svg", + "description": "excessive precision" + }, + { + "ruleId": "", + "ruleTitle": "Optimize SVG Precision", + "type": "good", + "code": "", + "language": "svg", + "description": "1 decimal place" + }, + { + "ruleId": "", + "ruleTitle": "Use useTransition Over Manual Loading States", + "type": "bad", + "code": "function SearchResults() {\n const [query, setQuery] = useState('')\n const [results, setResults] = useState([])\n const [isLoading, setIsLoading] = useState(false)\n\n const handleSearch = async (value: string) => {\n setIsLoading(true)\n setQuery(value)\n const data = await fetchResults(value)\n setResults(data)\n setIsLoading(false)\n }\n\n return (\n <>\n handleSearch(e.target.value)} />\n {isLoading && }\n \n \n )\n}", + "language": "tsx", + "description": "manual loading state" + }, + { + "ruleId": "", + "ruleTitle": "Use useTransition Over Manual Loading States", + "type": "good", + "code": "import { useTransition, useState } from 'react'\n\nfunction SearchResults() {\n const [query, setQuery] = useState('')\n const [results, setResults] = useState([])\n const [isPending, startTransition] = useTransition()\n\n const handleSearch = (value: string) => {\n setQuery(value) // Update input immediately\n \n startTransition(async () => {\n // Fetch and update results\n const data = await fetchResults(value)\n setResults(data)\n })\n }\n\n return (\n <>\n handleSearch(e.target.value)} />\n {isPending && }\n \n \n )\n}", + "language": "tsx", + "description": "useTransition with built-in pending state" + }, + { + "ruleId": "", + "ruleTitle": "Defer State Reads to Usage Point", + "type": "bad", + "code": "function ShareButton({ chatId }: { chatId: string }) {\n const searchParams = useSearchParams()\n\n const handleShare = () => {\n const ref = searchParams.get('ref')\n shareChat(chatId, { ref })\n }\n\n return \n}", + "language": "tsx", + "description": "subscribes to all searchParams changes" + }, + { + "ruleId": "", + "ruleTitle": "Defer State Reads to Usage Point", + "type": "good", + "code": "function ShareButton({ chatId }: { chatId: string }) {\n const handleShare = () => {\n const params = new URLSearchParams(window.location.search)\n const ref = params.get('ref')\n shareChat(chatId, { ref })\n }\n\n return \n}", + "language": "tsx", + "description": "reads on demand, no subscription" + }, + { + "ruleId": "", + "ruleTitle": "Narrow Effect Dependencies", + "type": "bad", + "code": "useEffect(() => {\n console.log(user.id)\n}, [user])", + "language": "tsx", + "description": "re-runs on any user field change" + }, + { + "ruleId": "", + "ruleTitle": "Narrow Effect Dependencies", + "type": "good", + "code": "useEffect(() => {\n console.log(user.id)\n}, [user.id])", + "language": "tsx", + "description": "re-runs only when id changes" + }, + { + "ruleId": "", + "ruleTitle": "Calculate Derived State During Rendering", + "type": "bad", + "code": "function Form() {\n const [firstName, setFirstName] = useState('First')\n const [lastName, setLastName] = useState('Last')\n const [fullName, setFullName] = useState('')\n\n useEffect(() => {\n setFullName(firstName + ' ' + lastName)\n }, [firstName, lastName])\n\n return

{fullName}

\n}", + "language": "tsx", + "description": "redundant state and effect" + }, + { + "ruleId": "", + "ruleTitle": "Calculate Derived State During Rendering", + "type": "good", + "code": "function Form() {\n const [firstName, setFirstName] = useState('First')\n const [lastName, setLastName] = useState('Last')\n const fullName = firstName + ' ' + lastName\n\n return

{fullName}

\n}", + "language": "tsx", + "description": "derive during render" + }, + { + "ruleId": "", + "ruleTitle": "Subscribe to Derived State", + "type": "bad", + "code": "function Sidebar() {\n const width = useWindowWidth() // updates continuously\n const isMobile = width < 768\n return