@3lineas/grooveskills 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -70,6 +70,9 @@ npx @3lineas/grooveskills --skills best-practices hono --agents antigravity clau
70
70
  | :--- | :--- | :--- | :--- |
71
71
  | `best-practices` | **Universal Best Practices** | Multi-language clean code, early returns, no barrel files, strict typing, <200 lines. | **Always suggested by default** (Any language/file) |
72
72
  | `hono` | **Hono Architecture (Valibot)** | Enterprise Hono standard: Valibot validation, Actions layer, RPC bindings, AppError. | Detected `hono`, `@hono/valibot-validator`, etc. |
73
+ | `react-core` | **React Core Fundamentals** | Opinionated React fundamentals: function components, JSX, immutable state, core hooks, lists, and controlled inputs. | Detected `react`, `react-dom` |
74
+ | `react-advanced` | **React 19 Advanced & Compiler Architecture** | React 19 platform features: React Compiler 1.0, concurrent rendering, `use()`, Actions, virtualization, and RSC boundaries. | Detected `react`, `react-dom` |
75
+ | `react-create-page` | **React Page & View Architecture** | Declarative page coordinators, domain hooks, isolated API actions, Valibot + Formisch forms, and atomic co-location. | Detected `react`, `react-dom` |
73
76
 
74
77
  ---
75
78
 
@@ -56,6 +56,27 @@ var SKILL_CATALOG = [
56
56
  description: "Enterprise Hono API standard with Valibot validation, isolated Actions layer, and RPC bindings",
57
57
  dependencies: ["hono", "@hono/valibot-validator", "@hono/standard-validator"],
58
58
  directoryName: "hono"
59
+ },
60
+ {
61
+ id: "react-core",
62
+ name: "React Core Fundamentals",
63
+ description: "Opinionated React fundamentals: function components, JSX, immutable state, core hooks, lists, and controlled inputs",
64
+ dependencies: ["react", "react-dom"],
65
+ directoryName: "react-core"
66
+ },
67
+ {
68
+ id: "react-advanced",
69
+ name: "React 19 Advanced & Compiler Architecture",
70
+ description: "Modern React 19 platform features: React Compiler auto-memoization, concurrent rendering, use(), Actions, virtualization, and RSC boundaries",
71
+ dependencies: ["react", "react-dom"],
72
+ directoryName: "react-advanced"
73
+ },
74
+ {
75
+ id: "react-create-page",
76
+ name: "React Page & View Architecture",
77
+ description: "Clean architecture for React pages: declarative page coordinators, domain hooks, isolated API actions, Valibot + Formisch forms, and atomic co-location",
78
+ dependencies: ["react", "react-dom"],
79
+ directoryName: "react-create-page"
59
80
  }
60
81
  ];
61
82
 
@@ -449,4 +470,4 @@ async function runGrooveSkills(options = {}) {
449
470
  export {
450
471
  runGrooveSkills
451
472
  };
452
- //# sourceMappingURL=chunk-TYA2EELM.js.map
473
+ //# sourceMappingURL=chunk-6FZFZCBX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/domains/detector/detect-agents.ts","../src/domains/detector/detect-dependencies.ts","../src/domains/registry/skill-catalog.ts","../src/domains/detector/project-detector.ts","../src/domains/ui/ui-orchestrator.ts","../src/domains/ui/banners.ts","../src/domains/ui/prompt-skills.ts","../src/domains/ui/prompt-agents.ts","../src/domains/ui/prompt-confirm-overwrite.ts","../src/domains/installer/verify-skill-exists.ts","../src/domains/installer/install-orchestrator.ts","../src/domains/adapters/antigravity.adapter.ts","../src/domains/global-store/global-paths.ts","../src/domains/adapters/link-helper.ts","../src/domains/adapters/claude.adapter.ts","../src/domains/adapters/cursor.adapter.ts","../src/domains/adapters/gemini.adapter.ts","../src/domains/global-store/sync-global-store.ts","../src/domains/installer/resolve-skills-source.ts","../src/main.ts"],"sourcesContent":["import { readdirSync } from \"node:fs\";\nimport type { AgentId } from \"@/types/agent.types\";\n\nexport function detectAgents(targetPath: string): AgentId[] {\n const detected: AgentId[] = [];\n\n try {\n const entries = new Set(readdirSync(targetPath));\n\n if (entries.has(\".agents\")) {\n detected.push(\"antigravity\");\n }\n if (entries.has(\".claude\")) {\n detected.push(\"claude\");\n }\n if (entries.has(\".cursor\")) {\n detected.push(\"cursor\");\n }\n if (entries.has(\".gemini\")) {\n detected.push(\"gemini\");\n }\n } catch {\n // If directory cannot be read, return empty list gracefully\n }\n\n return detected;\n}\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport interface PackageJsonDetection {\n dependencies: string[];\n exists: boolean;\n}\n\nexport function detectDependencies(targetPath: string): PackageJsonDetection {\n const packageJsonPath = join(targetPath, \"package.json\");\n\n if (!existsSync(packageJsonPath)) {\n return { dependencies: [], exists: false };\n }\n\n try {\n const raw = readFileSync(packageJsonPath, \"utf-8\");\n const parsed = JSON.parse(raw) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n\n const direct = Object.keys(parsed.dependencies ?? {});\n const dev = Object.keys(parsed.devDependencies ?? {});\n const all = Array.from(new Set([...direct, ...dev]));\n\n return { dependencies: all, exists: true };\n } catch {\n return { dependencies: [], exists: true };\n }\n}\n","import type { SkillMetadata } from \"@/types/skill.types\";\n\nexport const SKILL_CATALOG: SkillMetadata[] = [\n {\n id: \"best-practices\",\n name: \"Universal Best Practices\",\n description: \"Multi-language clean code standards, early returns, no barrel files, strict typing, <200 lines\",\n dependencies: [],\n directoryName: \"best-practices\",\n },\n {\n id: \"hono\",\n name: \"Hono Architecture (Valibot)\",\n description: \"Enterprise Hono API standard with Valibot validation, isolated Actions layer, and RPC bindings\",\n dependencies: [\"hono\", \"@hono/valibot-validator\", \"@hono/standard-validator\"],\n directoryName: \"hono\",\n },\n {\n id: \"react-core\",\n name: \"React Core Fundamentals\",\n description: \"Opinionated React fundamentals: function components, JSX, immutable state, core hooks, lists, and controlled inputs\",\n dependencies: [\"react\", \"react-dom\"],\n directoryName: \"react-core\",\n },\n {\n id: \"react-advanced\",\n name: \"React 19 Advanced & Compiler Architecture\",\n description: \"Modern React 19 platform features: React Compiler auto-memoization, concurrent rendering, use(), Actions, virtualization, and RSC boundaries\",\n dependencies: [\"react\", \"react-dom\"],\n directoryName: \"react-advanced\",\n },\n {\n id: \"react-create-page\",\n name: \"React Page & View Architecture\",\n description: \"Clean architecture for React pages: declarative page coordinators, domain hooks, isolated API actions, Valibot + Formisch forms, and atomic co-location\",\n dependencies: [\"react\", \"react-dom\"],\n directoryName: \"react-create-page\",\n },\n];\n","import { detectAgents } from \"@/domains/detector/detect-agents\";\nimport { detectDependencies } from \"@/domains/detector/detect-dependencies\";\nimport { SKILL_CATALOG } from \"@/domains/registry/skill-catalog\";\nimport type { ProjectContext } from \"@/types/context.types\";\n\nexport function detectProjectContext(targetPath: string): ProjectContext {\n const detectedAgents = detectAgents(targetPath);\n const { dependencies, exists } = detectDependencies(targetPath);\n\n const suggestedSkillIds: string[] = [\"best-practices\"];\n\n for (const skill of SKILL_CATALOG) {\n if (skill.dependencies.length === 0) {\n continue;\n }\n const hasMatch = skill.dependencies.some((dep) => dependencies.includes(dep));\n if (hasMatch && !suggestedSkillIds.includes(skill.id)) {\n suggestedSkillIds.push(skill.id);\n }\n }\n\n return {\n targetPath,\n detectedDependencies: dependencies,\n suggestedSkillIds,\n detectedAgents,\n packageJsonExists: exists,\n };\n}\n","import * as p from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport { renderBanner } from \"@/domains/ui/banners\";\nimport { promptSkills } from \"@/domains/ui/prompt-skills\";\nimport { promptAgents } from \"@/domains/ui/prompt-agents\";\nimport { promptConfirmOverwrite } from \"@/domains/ui/prompt-confirm-overwrite\";\nimport { verifySkillExists } from \"@/domains/installer/verify-skill-exists\";\nimport { installSkills } from \"@/domains/installer/install-orchestrator\";\nimport { SKILL_CATALOG } from \"@/domains/registry/skill-catalog\";\nimport type { ProjectContext } from \"@/types/context.types\";\nimport type { AgentId } from \"@/types/agent.types\";\n\nexport async function runInteractiveUi(context: ProjectContext): Promise<void> {\n console.log(renderBanner());\n p.intro(pc.magenta(\"Welcome to GrooveSkills! Let's tailor your AI agents with clean architectural standards.\"));\n\n if (context.detectedDependencies.length > 0) {\n p.note(\n `Detected dependencies: ${context.detectedDependencies.map((d) => pc.bold(d)).join(\", \")}\\n` +\n `Active AI folders: ${context.detectedAgents.map((a) => pc.green(a)).join(\", \") || \"None (will be created)\"}`,\n \"Project Environment\"\n );\n }\n\n const selectedSkills = await promptSkills(context.suggestedSkillIds);\n if (p.isCancel(selectedSkills)) {\n p.cancel(\"Installation cancelled. Have a wonderful day!\");\n process.exit(0);\n }\n\n const selectedAgents = await promptAgents(context.detectedAgents);\n if (p.isCancel(selectedAgents)) {\n p.cancel(\"Installation cancelled. Have a wonderful day!\");\n process.exit(0);\n }\n\n let forceOverwrite = true;\n const anyExists = (selectedSkills as string[]).some((skillId) => {\n const meta = SKILL_CATALOG.find((s) => s.id === skillId);\n return meta ? verifySkillExists(context.targetPath, meta.directoryName, selectedAgents as AgentId[]) : false;\n });\n\n if (anyExists) {\n const confirm = await promptConfirmOverwrite();\n if (p.isCancel(confirm) || !confirm) {\n p.cancel(\"Installation stopped to protect existing skills.\");\n process.exit(0);\n }\n forceOverwrite = Boolean(confirm);\n }\n\n const s = p.spinner();\n s.start(\"Installing and adapting skills across your configured AI agents...\");\n\n const result = await installSkills({\n targetPath: context.targetPath,\n selectedSkillIds: selectedSkills as string[],\n selectedAgents: selectedAgents as AgentId[],\n forceOverwrite,\n });\n\n s.stop(pc.green(\"All skills have been successfully installed and configured!\"));\n\n p.note(\n `Configured skills: ${result.installedSkills.map((s) => pc.bold(s)).join(\", \")}\\n` +\n `Target agents: ${result.configuredAgents.map((a) => pc.cyan(a)).join(\", \")}`,\n \"Installation Summary\"\n );\n\n p.outro(pc.green(\"Your AI programming pair is now empowered with your team's architectural habits! 🚀\"));\n}\n","import pc from \"picocolors\";\n\nexport function renderBanner(): string {\n const title = pc.bold(pc.white(\" GROOVE SKILLS \"));\n const badge = pc.bgMagenta(pc.black(title));\n const subtitle = pc.dim(\"Multi-Agent AI Skills & Architectural Standards\");\n\n return `${badge} ${subtitle}\\n`;\n}\n","import * as p from \"@clack/prompts\";\nimport { SKILL_CATALOG } from \"@/domains/registry/skill-catalog\";\n\nexport async function promptSkills(suggestedSkillIds: string[]): Promise<string[] | symbol> {\n const options = SKILL_CATALOG.map((skill) => {\n const isSuggested = suggestedSkillIds.includes(skill.id);\n return {\n value: skill.id,\n label: skill.name,\n hint: isSuggested ? \"Suggested based on your project stack\" : skill.description,\n };\n });\n\n const selected = await p.multiselect({\n message: \"Which skills would you like to install in this project?\",\n options,\n initialValues: suggestedSkillIds,\n required: true,\n });\n\n return selected;\n}\n","import * as p from \"@clack/prompts\";\nimport type { AgentId } from \"@/types/agent.types\";\n\nexport async function promptAgents(detectedAgents: AgentId[]): Promise<AgentId[] | symbol> {\n const options: Array<{ value: AgentId; label: string; hint: string }> = [\n { value: \"antigravity\", label: \"Antigravity\", hint: \".agents/skills (Google Agentic IDE)\" },\n { value: \"claude\", label: \"Claude Code\", hint: \".claude/skills (Anthropic Claude Code)\" },\n { value: \"cursor\", label: \"Cursor\", hint: \".cursor/rules/*.mdc (Cursor IDE Rules)\" },\n { value: \"gemini\", label: \"Gemini Code Assist\", hint: \".gemini/skills (Google Gemini)\" },\n ];\n\n const initialValues: AgentId[] = detectedAgents.length > 0 ? detectedAgents : [\"antigravity\"];\n\n const selected = await p.multiselect({\n message: \"Which AI agents should be configured?\",\n options,\n initialValues,\n required: true,\n });\n\n return selected as AgentId[] | symbol;\n}\n","import * as p from \"@clack/prompts\";\n\nexport async function promptConfirmOverwrite(): Promise<boolean | symbol> {\n return await p.confirm({\n message: \"Some skills already exist in this project. Would you like to overwrite them with the latest version?\",\n initialValue: true,\n });\n}\n","import { lstatSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { AgentId } from \"@/types/agent.types\";\n\nconst AGENT_DESTINATION_MAP: Record<AgentId, (dir: string) => string> = {\n antigravity: (dir) => join(\".agents\", \"skills\", dir),\n claude: (dir) => join(\".claude\", \"skills\", dir),\n cursor: (dir) => join(\".cursor\", \"rules\", `${dir}.mdc`),\n gemini: (dir) => join(\".gemini\", \"skills\", dir),\n};\n\nexport function verifySkillExists(\n targetPath: string,\n skillDirectoryName: string,\n agents: AgentId[]\n): boolean {\n for (const agent of agents) {\n const relativePathResolver = AGENT_DESTINATION_MAP[agent];\n const destination = join(targetPath, relativePathResolver(skillDirectoryName));\n\n try {\n lstatSync(destination);\n return true;\n } catch {\n // Path does not exist\n }\n }\n\n return false;\n}\n","import { join } from \"node:path\";\nimport { AntigravityAdapter } from \"@/domains/adapters/antigravity.adapter\";\nimport { ClaudeAdapter } from \"@/domains/adapters/claude.adapter\";\nimport { CursorAdapter } from \"@/domains/adapters/cursor.adapter\";\nimport { GeminiAdapter } from \"@/domains/adapters/gemini.adapter\";\nimport { syncGlobalStore } from \"@/domains/global-store/sync-global-store\";\nimport { getGlobalSkillsDir } from \"@/domains/global-store/global-paths\";\nimport { SKILL_CATALOG } from \"@/domains/registry/skill-catalog\";\nimport type { AgentAdapter } from \"@/domains/adapters/adapter.interface\";\nimport type { InstallOptions, InstallResult } from \"@/types/installer.types\";\nimport type { AgentId } from \"@/types/agent.types\";\n\nconst ADAPTER_REGISTRY: Record<AgentId, AgentAdapter> = {\n antigravity: new AntigravityAdapter(),\n claude: new ClaudeAdapter(),\n cursor: new CursorAdapter(),\n gemini: new GeminiAdapter(),\n};\n\nexport async function installSkills(options: InstallOptions): Promise<InstallResult> {\n syncGlobalStore();\n\n const globalSkillsDir = getGlobalSkillsDir();\n const createdFiles: string[] = [];\n\n for (const skillId of options.selectedSkillIds) {\n const skill = SKILL_CATALOG.find((item) => item.id === skillId);\n if (!skill) {\n continue;\n }\n\n const skillSourceDir = join(globalSkillsDir, skill.directoryName);\n\n for (const agentId of options.selectedAgents) {\n const adapter = ADAPTER_REGISTRY[agentId];\n if (!adapter) {\n continue;\n }\n\n const files = await adapter.installSkill(options.targetPath, skill, skillSourceDir);\n createdFiles.push(...files);\n }\n }\n\n return {\n installedSkills: options.selectedSkillIds,\n configuredAgents: options.selectedAgents,\n createdFiles,\n };\n}\n","import { mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getGlobalSkillsDir } from \"@/domains/global-store/global-paths\";\nimport { createOrReplaceSymlink } from \"@/domains/adapters/link-helper\";\nimport type { AgentAdapter } from \"@/domains/adapters/adapter.interface\";\nimport type { SkillMetadata } from \"@/types/skill.types\";\n\nexport class AntigravityAdapter implements AgentAdapter {\n readonly id = \"antigravity\" as const;\n readonly name = \"Antigravity\";\n readonly targetRelativeDir = \".agents/skills\";\n\n async installSkill(\n targetPath: string,\n skill: SkillMetadata,\n _skillSourceDir: string\n ): Promise<string[]> {\n const parentDir = join(targetPath, this.targetRelativeDir);\n if (!existsSync(parentDir)) {\n mkdirSync(parentDir, { recursive: true });\n }\n\n const destination = join(parentDir, skill.directoryName);\n const globalSkillDir = join(getGlobalSkillsDir(), skill.directoryName);\n\n createOrReplaceSymlink(globalSkillDir, destination, \"dir\");\n\n return [destination];\n }\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function getGlobalStoreDir(): string {\n return join(homedir(), \".grooveskills\");\n}\n\nexport function getGlobalSkillsDir(): string {\n return join(getGlobalStoreDir(), \"skills\");\n}\n\nexport function getGlobalCursorRulesDir(): string {\n return join(getGlobalStoreDir(), \"cursor-rules\");\n}\n","import { lstatSync, unlinkSync, rmSync, symlinkSync } from \"node:fs\";\n\nfunction removeExistingLinkPath(path: string): void {\n try {\n const lstat = lstatSync(path);\n\n if (lstat.isDirectory() && !lstat.isSymbolicLink()) {\n rmSync(path, { recursive: true, force: true });\n return;\n }\n\n unlinkSync(path);\n } catch {\n // If path does not exist, exit early\n }\n}\n\nexport function createOrReplaceSymlink(\n target: string,\n linkPath: string,\n type: \"dir\" | \"file\"\n): void {\n removeExistingLinkPath(linkPath);\n symlinkSync(target, linkPath, type);\n}\n\n","import { mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getGlobalSkillsDir } from \"@/domains/global-store/global-paths\";\nimport { createOrReplaceSymlink } from \"@/domains/adapters/link-helper\";\nimport type { AgentAdapter } from \"@/domains/adapters/adapter.interface\";\nimport type { SkillMetadata } from \"@/types/skill.types\";\n\nexport class ClaudeAdapter implements AgentAdapter {\n readonly id = \"claude\" as const;\n readonly name = \"Claude Code\";\n readonly targetRelativeDir = \".claude/skills\";\n\n async installSkill(\n targetPath: string,\n skill: SkillMetadata,\n _skillSourceDir: string\n ): Promise<string[]> {\n const parentDir = join(targetPath, this.targetRelativeDir);\n if (!existsSync(parentDir)) {\n mkdirSync(parentDir, { recursive: true });\n }\n\n const destination = join(parentDir, skill.directoryName);\n const globalSkillDir = join(getGlobalSkillsDir(), skill.directoryName);\n\n createOrReplaceSymlink(globalSkillDir, destination, \"dir\");\n\n return [destination];\n }\n}\n","import { mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getGlobalCursorRulesDir } from \"@/domains/global-store/global-paths\";\nimport { createOrReplaceSymlink } from \"@/domains/adapters/link-helper\";\nimport type { AgentAdapter } from \"@/domains/adapters/adapter.interface\";\nimport type { SkillMetadata } from \"@/types/skill.types\";\n\nexport class CursorAdapter implements AgentAdapter {\n readonly id = \"cursor\" as const;\n readonly name = \"Cursor\";\n readonly targetRelativeDir = \".cursor/rules\";\n\n async installSkill(\n targetPath: string,\n skill: SkillMetadata,\n _skillSourceDir: string\n ): Promise<string[]> {\n const rulesDir = join(targetPath, this.targetRelativeDir);\n if (!existsSync(rulesDir)) {\n mkdirSync(rulesDir, { recursive: true });\n }\n\n const filename = `${skill.id}.mdc`;\n const destination = join(rulesDir, filename);\n const globalRuleFile = join(getGlobalCursorRulesDir(), filename);\n\n createOrReplaceSymlink(globalRuleFile, destination, \"file\");\n\n return [destination];\n }\n}\n","import { mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getGlobalSkillsDir } from \"@/domains/global-store/global-paths\";\nimport { createOrReplaceSymlink } from \"@/domains/adapters/link-helper\";\nimport type { AgentAdapter } from \"@/domains/adapters/adapter.interface\";\nimport type { SkillMetadata } from \"@/types/skill.types\";\n\nexport class GeminiAdapter implements AgentAdapter {\n readonly id = \"gemini\" as const;\n readonly name = \"Gemini Code Assist\";\n readonly targetRelativeDir = \".gemini/skills\";\n\n async installSkill(\n targetPath: string,\n skill: SkillMetadata,\n _skillSourceDir: string\n ): Promise<string[]> {\n const parentDir = join(targetPath, this.targetRelativeDir);\n if (!existsSync(parentDir)) {\n mkdirSync(parentDir, { recursive: true });\n }\n\n const destination = join(parentDir, skill.directoryName);\n const globalSkillDir = join(getGlobalSkillsDir(), skill.directoryName);\n\n createOrReplaceSymlink(globalSkillDir, destination, \"dir\");\n\n return [destination];\n }\n}\n","import { cpSync, mkdirSync, existsSync, writeFileSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getGlobalSkillsDir, getGlobalCursorRulesDir } from \"@/domains/global-store/global-paths\";\nimport { resolveSkillsSourceRoot } from \"@/domains/installer/resolve-skills-source\";\nimport { SKILL_CATALOG } from \"@/domains/registry/skill-catalog\";\n\nexport function syncGlobalStore(): void {\n const bundledSkillsDir = resolveSkillsSourceRoot();\n const globalSkillsDir = getGlobalSkillsDir();\n const globalCursorRulesDir = getGlobalCursorRulesDir();\n\n if (!existsSync(globalSkillsDir)) {\n mkdirSync(globalSkillsDir, { recursive: true });\n }\n if (!existsSync(globalCursorRulesDir)) {\n mkdirSync(globalCursorRulesDir, { recursive: true });\n }\n\n cpSync(bundledSkillsDir, globalSkillsDir, { recursive: true });\n\n for (const skill of SKILL_CATALOG) {\n const skillMdPath = join(globalSkillsDir, skill.directoryName, \"SKILL.md\");\n if (!existsSync(skillMdPath)) {\n continue;\n }\n\n const content = readFileSync(skillMdPath, \"utf-8\");\n const mdcHeader = `---\ndescription: ${skill.description}\nglobs: [\"**/*\"]\nalwaysApply: false\n---\n\n`;\n const cleanedBody = content.replace(/^---[\\s\\S]*?---\\s*/, \"\");\n const targetMdc = join(globalCursorRulesDir, `${skill.id}.mdc`);\n writeFileSync(targetMdc, `${mdcHeader}${cleanedBody}`, \"utf-8\");\n }\n}\n","import { fileURLToPath } from \"node:url\";\nimport { dirname, resolve, join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\n\nexport function resolveSkillsSourceRoot(): string {\n const currentFilePath = fileURLToPath(import.meta.url);\n const currentDir = dirname(currentFilePath);\n\n // 1. From dist/ in published npm package or local build: dist/../skills\n const candidateFromDist = resolve(currentDir, \"../skills\");\n if (existsSync(candidateFromDist)) {\n return candidateFromDist;\n }\n\n // 2. From src/domains/installer/ during development: ../../../skills\n const candidateFromSrc = resolve(currentDir, \"../../../skills\");\n if (existsSync(candidateFromSrc)) {\n return candidateFromSrc;\n }\n\n // 3. Search upward for the package root containing a skills directory\n let searchDir = currentDir;\n for (let i = 0; i < 5; i++) {\n const candidate = join(searchDir, \"skills\");\n if (existsSync(candidate)) {\n return candidate;\n }\n const parent = dirname(searchDir);\n if (parent === searchDir) {\n break;\n }\n searchDir = parent;\n }\n\n throw new Error(\n `GrooveSkills internal error: Unable to locate bundled skills directory from ${currentDir}`\n );\n}\n","import { detectProjectContext } from \"@/domains/detector/project-detector\";\nimport { runInteractiveUi } from \"@/domains/ui/ui-orchestrator\";\nimport { installSkills } from \"@/domains/installer/install-orchestrator\";\nimport type { AgentId } from \"@/types/agent.types\";\n\nexport interface CliOptions {\n skills?: string[];\n agents?: AgentId[];\n yes?: boolean;\n}\n\nexport async function runGrooveSkills(options: CliOptions = {}): Promise<void> {\n const targetPath = process.cwd();\n const context = detectProjectContext(targetPath);\n\n if (options.skills && options.skills.length > 0 && options.agents && options.agents.length > 0) {\n await installSkills({\n targetPath,\n selectedSkillIds: options.skills,\n selectedAgents: options.agents,\n forceOverwrite: Boolean(options.yes),\n });\n return;\n }\n\n await runInteractiveUi(context);\n}\n"],"mappings":"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","names":["p","pc","p","p","join","join","existsSync","join","join","lstatSync","join","existsSync","mkdirSync","existsSync","join","join","existsSync","mkdirSync","mkdirSync","existsSync","join","join","existsSync","mkdirSync","mkdirSync","existsSync","join","join","existsSync","mkdirSync","mkdirSync","existsSync","readFileSync","join","join","existsSync","existsSync","mkdirSync","join","readFileSync","join","pc","s","confirm"]}
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runGrooveSkills
4
- } from "./chunk-TYA2EELM.js";
4
+ } from "./chunk-6FZFZCBX.js";
5
5
 
6
6
  // bin/cli.ts
7
7
  import { Command } from "commander";
package/dist/main.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runGrooveSkills
3
- } from "./chunk-TYA2EELM.js";
3
+ } from "./chunk-6FZFZCBX.js";
4
4
  export {
5
5
  runGrooveSkills
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@3lineas/grooveskills",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Multi-agent CLI to install opinionated AI coding skills and architectural standards (Hono, React, Next.js, and more)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,147 @@
1
+ ---
2
+ name: react-advanced
3
+ description: Use when building with React 19 platform features and rendering architecture: React Compiler (auto-memoization), concurrent rendering (useTransition, useDeferredValue, Suspense), the use() hook, Actions (useActionState, useOptimistic, useFormStatus), ref-as-prop, <Context value>, compound components, context splitting, error boundaries, portals, virtualization (@tanstack/react-virtual), code-splitting, and RSC / 'use client' boundaries. TRIGGER when optimizing React performance, adopting React 19 APIs, working with forms/mutations, or architecting complex React component trees. Always adhere to 'best-practices' and 'react-core'.
4
+ ---
5
+
6
+ # React Advanced: React 19 Platform & Rendering Architecture
7
+
8
+ Opinionated reference for **React 19** and **React Compiler 1.0** capabilities, concurrent rendering primitives, async Actions, advanced component composition, performance engineering, and React Server Component (RSC) boundaries.
9
+
10
+ ---
11
+
12
+ ## 1. Golden Rules (Non-negotiable)
13
+
14
+ 1. **React Compiler First — Stop Reflexive Manual Memoization**:
15
+ - Do not wrap every derived value or handler in `useMemo`, `useCallback`, or `React.memo` by reflex in new code.
16
+ - Reserve manual `useMemo`/`useCallback` strictly for **effect dependency stability** (`useEffect` identity contracts) or code skipped by the compiler.
17
+ - Never strip existing manual memoization blindly; respect `preserve-manual-memoization` lint warnings.
18
+ 2. **React 19 Idioms Only (No Legacy Wrappers)**:
19
+ - Receive `ref` as a plain prop on function components (`ComponentProps<"input">`)—never use `forwardRef` in new code.
20
+ - Render `<MyContext value={val}>` directly—never use legacy `<MyContext.Provider>`.
21
+ - Render native `<title>`, `<meta>`, and `<link>` tags directly in components—never add `react-helmet`.
22
+ 3. **Actions for Async Mutations & Forms**:
23
+ - Use `useActionState`, `useOptimistic`, `useFormStatus`, and `<form action={fn}>` for form submissions and async mutations instead of manual `isLoading`/`error` `useState` boilerplate.
24
+ 4. **Concurrent Responsiveness & Post-`await` Transitions**:
25
+ - Use `useTransition` for non-urgent state updates and `useDeferredValue` instead of fixed `setTimeout` debouncing for expensive derived renders.
26
+ - Inside `startTransition(async () => { ... })`, always re-wrap state updates that happen **after an `await`** in a nested `startTransition(() => setState(...))`.
27
+ 5. **Safe `use()` Hook Usage**:
28
+ - `use(promiseOrContext)` may be called conditionally after early returns, but **never inside `try / catch`**. Handle rejected promises with an **Error Boundary**.
29
+ - Never create a new promise inline during client render (`use(fetchData())`); pass a stable promise created on the server, in a parent, or in an event/transition.
30
+ 6. **Context Architecture by Change Frequency**:
31
+ - Native Context has no selector and bypasses `React.memo`. Never combine high-frequency state (e.g. cursor/scroll) and low-frequency state (e.g. theme/auth) in a single Context. Split contexts by update rate and memoize provider values.
32
+ 7. **Virtualize Large Lists & Split by Route**:
33
+ - Virtualize lists exceeding ~100 items using `@tanstack/react-virtual`.
34
+ - Code-split with `React.lazy` + `Suspense` at **route or heavy-feature boundaries**, avoiding micro-chunks (<10KB).
35
+ 8. **Strict RSC & `'use server'` Security Boundary**:
36
+ - Treat `'use client'` as a **module dependency graph** boundary: props crossing into client modules must be serializable.
37
+ - Treat every `'use server'` function as a public HTTP endpoint: validate inputs with **Valibot** (`* as v from "valibot"`) and verify authorization inside the function before executing mutations.
38
+ 9. **GrooveSkills Architecture Compliance**:
39
+ - Enforce `< 200` lines per file, mandatory `@/...` path aliases, zero barrel files (`index.ts`), and early returns across all components and hooks.
40
+
41
+ ---
42
+
43
+ ## 2. Compiler, Concurrency & React 19 Actions
44
+
45
+ ```tsx
46
+ import { startTransition, useActionState, useDeferredValue, useState } from "react";
47
+ import { updateFilterPreferenceAction } from "@/actions/catalog/update-filter-preference.action";
48
+ import type { Product } from "@/types/product.types";
49
+
50
+ export function ProductExplorer({ catalog }: { catalog: Product[] }) {
51
+ const [query, setQuery] = useState<string>("");
52
+ const deferredQuery = useDeferredValue(query, "");
53
+
54
+ const [state, saveAction, isPending] = useActionState(
55
+ async (_prev: { error?: string }, formData: FormData) => {
56
+ const savedQuery = String(formData.get("query") ?? "");
57
+ return await updateFilterPreferenceAction({ query: savedQuery });
58
+ },
59
+ {}
60
+ );
61
+
62
+ function handleAsyncRefresh(fetchNext: () => Promise<string>): void {
63
+ startTransition(async () => {
64
+ const nextQuery = await fetchNext();
65
+ // Always re-wrap state updates after await in startTransition:
66
+ startTransition(() => setQuery(nextQuery));
67
+ });
68
+ }
69
+
70
+ const matches = catalog.filter((item) =>
71
+ item.name.toLowerCase().includes(deferredQuery.toLowerCase())
72
+ );
73
+
74
+ return (
75
+ <section>
76
+ <form action={saveAction}>
77
+ <input name="query" value={query} onChange={(e) => setQuery(e.target.value)} />
78
+ <button type="submit" disabled={isPending}>Save Filter</button>
79
+ {state.error && <p role="alert">{state.error}</p>}
80
+ </form>
81
+ <ul style={{ opacity: query !== deferredQuery ? 0.6 : 1 }}>
82
+ {matches.map((p) => <li key={p.id}>{p.name}</li>)}
83
+ </ul>
84
+ </section>
85
+ );
86
+ }
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 3. RSC & `'use server'` Boundary with Valibot
92
+
93
+ Server Functions (`'use server'`) are public-facing boundaries. Always validate inputs with **Valibot** and authorize the caller before executing business logic.
94
+
95
+ ```tsx
96
+ "use server";
97
+
98
+ import * as v from "valibot";
99
+ import { requireAuthenticatedUser } from "@/actions/auth/require-authenticated-user.action";
100
+
101
+ const commentSchema = v.object({
102
+ articleId: v.pipe(v.string(), v.nonEmpty("Article ID is required.")),
103
+ body: v.pipe(
104
+ v.string(),
105
+ v.trim(),
106
+ v.minLength(3, "Please write at least 3 characters before posting.")
107
+ ),
108
+ });
109
+
110
+ export async function publishCommentServerAction(formData: FormData) {
111
+ const parsed = v.safeParse(commentSchema, {
112
+ articleId: formData.get("articleId"),
113
+ body: formData.get("body"),
114
+ });
115
+
116
+ if (!parsed.success) {
117
+ return { success: false, error: parsed.issues[0].message };
118
+ }
119
+
120
+ const user = await requireAuthenticatedUser();
121
+ return { success: true, userId: user.id, message: "Thank you! Your comment has been published." };
122
+ }
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 4. Anti-Patterns to Avoid
128
+
129
+ | Anti-Pattern | Why It Fails | Fix |
130
+ | :--- | :--- | :--- |
131
+ | Hand-memoizing everything with React Compiler active | Adds visual noise and redundant cache overhead | Write plain code; use `useMemo` only as an effect-dependency escape hatch |
132
+ | Stripping existing `useMemo` blindly | Can break effect dependency stability contracts | Respect `preserve-manual-memoization` lint warnings |
133
+ | Updating state after `await` in `startTransition` without re-wrapping | Post-`await` update escapes the transition; `isPending` ends prematurely | Wrap post-`await` state updates in `startTransition(() => ...)` |
134
+ | Calling `use(promise)` inside `try / catch` or creating promises in client render | Suspense throws internal signals; inline promises recreate every render | Use Error Boundaries for rejections; create promises above the component |
135
+ | Using `forwardRef`, `<Context.Provider>`, or `react-helmet` | Deprecated/superseded in React 19 | Pass `ref` as prop, render `<Context value>`, use native `<title>`/`<meta>` |
136
+ | Unvalidated `'use server'` arguments | `'use server'` endpoints are public and untrusted | Validate with Valibot (`* as v from "valibot"`) and check auth first |
137
+
138
+ ---
139
+
140
+ ## 5. Reference Navigation
141
+
142
+ - **[react-compiler.md](references/react-compiler.md)**: Compiler adoption, ESLint v6+, escape hatches, and detection checklist.
143
+ - **[concurrent-rendering.md](references/concurrent-rendering.md)**: `useTransition`, post-`await` rule, `useDeferredValue`, `Suspense`, and streaming SSR.
144
+ - **[react19-actions-and-apis.md](references/react19-actions-and-apis.md)**: `use()`, `useActionState`, `useOptimistic`, `useFormStatus`, `ref` prop, `<Context value>`, and native metadata.
145
+ - **[component-patterns.md](references/component-patterns.md)**: Compound components, context splitting, `use-context-selector`, error boundaries, and portals.
146
+ - **[performance.md](references/performance.md)**: `@tanstack/react-virtual`, route-first `React.lazy`, Profiler workflow, and anti-pattern table.
147
+ - **[rsc-boundary.md](references/rsc-boundary.md)**: Module-graph `'use client'` rules, serializable props, `'use server'` Valibot security, and React version pinning.
@@ -0,0 +1,117 @@
1
+ # Advanced Component Patterns
2
+
3
+ > Source of truth: [useContext](https://react.dev/reference/react/useContext), [createPortal](https://react.dev/reference/react-dom/createPortal), and [Component (error boundaries)](https://react.dev/reference/react/Component).
4
+
5
+ ## Compound Components
6
+
7
+ Compound components let a parent and its sub-components coordinate through implicit shared state via Context so consumers compose pieces declaratively without prop drilling. In GrooveSkills projects, keep each sub-component under 200 lines or extract sub-components into dedicated files under the same domain folder.
8
+
9
+ ```tsx
10
+ import { createContext, useContext, useMemo, useState, type ReactNode } from "react";
11
+
12
+ interface TabsContextValue {
13
+ activeTab: string;
14
+ setActiveTab: (id: string) => void;
15
+ }
16
+
17
+ const TabsContext = createContext<TabsContextValue | null>(null);
18
+
19
+ export function useTabsContext(): TabsContextValue {
20
+ const ctx = useContext(TabsContext);
21
+ if (!ctx) {
22
+ throw new Error("Tab components must be rendered inside <TabsRoot>.");
23
+ }
24
+ return ctx;
25
+ }
26
+
27
+ export function TabsRoot({
28
+ defaultTab,
29
+ children,
30
+ }: {
31
+ defaultTab: string;
32
+ children: ReactNode;
33
+ }) {
34
+ const [activeTab, setActiveTab] = useState<string>(defaultTab);
35
+ const value = useMemo(() => ({ activeTab, setActiveTab }), [activeTab]);
36
+
37
+ return <TabsContext value={value}>{children}</TabsContext>;
38
+ }
39
+ ```
40
+
41
+ ## Context Optimization
42
+
43
+ Native context has **no selector**. When a provider value changes, **every** consumer of that context re-renders, regardless of which slice each consumer reads:
44
+
45
+ - `React.memo` does **not** block context-driven re-renders.
46
+ - `use(Context)` adds *conditional reading* but **not** *selective subscription*.
47
+ - The React Compiler does **not** solve large-context over-rendering.
48
+
49
+ ### Native fix 1 — Memoize the provider value
50
+
51
+ A provider value object created inline is a new reference every render. Memoize it when passing an object literal:
52
+
53
+ ```tsx
54
+ const authValue = useMemo(() => ({ user, signOut }), [user, signOut]);
55
+ return <AuthContext value={authValue}>{children}</AuthContext>;
56
+ ```
57
+
58
+ ### Native fix 2 — Split contexts by change frequency
59
+
60
+ Separate slow-changing state from fast-changing state so frequent updates never re-render consumers of stable data:
61
+
62
+ ```tsx
63
+ // ❌ BAD: theme (rare) and pointer (frequent) share one context.
64
+ const UIContext = createContext<{ theme: ThemeMode; pointer: Point } | null>(null);
65
+
66
+ // ✅ GOOD: independent contexts; pointer churn never re-renders theme consumers.
67
+ export const ThemeContext = createContext<ThemeMode>("light");
68
+ export const PointerContext = createContext<Point>({ x: 0, y: 0 });
69
+ ```
70
+
71
+ ### Userland fix — `use-context-selector` or External Stores
72
+
73
+ When a single store holds many slices and consumers must subscribe to a single slice, use `use-context-selector` or a dedicated external store (`useSyncExternalStore` / Zustand / Nanostores).
74
+
75
+ ## Custom Hooks vs Render Props
76
+
77
+ - **Custom Hooks (`use-*.ts`)**: Default choice for sharing stateful logic cleanly across components.
78
+ - **Render Props**: Useful for headless UI wrappers that own behavior/DOM measurement and delegate markup rendering to the caller.
79
+
80
+ ## Error Boundaries
81
+
82
+ Error boundaries catch errors thrown during render, lifecycle methods, and constructors of the tree below them. For async and event-handler errors, pair `react-error-boundary` (`useErrorBoundary().showBoundary(error)`) with React 19's root `onCaughtError` / `onUncaughtError` hooks.
83
+
84
+ ```tsx
85
+ import { useErrorBoundary } from "react-error-boundary";
86
+ import { saveRecordAction } from "@/actions/records/save-record.action";
87
+
88
+ export function SaveRecordButton({ payload }: { payload: string }) {
89
+ const { showBoundary } = useErrorBoundary();
90
+
91
+ async function handleSave(): Promise<void> {
92
+ try {
93
+ await saveRecordAction(payload);
94
+ } catch (error) {
95
+ showBoundary(error);
96
+ }
97
+ }
98
+
99
+ return (
100
+ <button type="button" onClick={handleSave}>
101
+ Save Changes
102
+ </button>
103
+ );
104
+ }
105
+ ```
106
+
107
+ ## Portals
108
+
109
+ `createPortal` renders children into a different DOM node (`document.body`) while keeping them in the same place in the **React tree** (preserving Context and React event bubbling). Use portals for modals, dialogs, tooltips, and toasts that must escape parent `overflow: hidden` or `z-index` stacking contexts.
110
+
111
+ ## Anti-Patterns
112
+
113
+ | Anti-pattern | Why it is wrong | Detection |
114
+ |---|---|---|
115
+ | One large context for high-frequency state | Every consumer re-renders on any change; `memo` cannot block it | Profiler shows unrelated consumers re-rendering |
116
+ | Expecting `React.memo` to stop context re-renders | Context bypasses prop comparison | Memoized child still re-renders on context change |
117
+ | Expecting native boundaries to catch async/event errors | Native boundaries catch only render-phase errors | Unhandled promise rejections in click handlers |
@@ -0,0 +1,130 @@
1
+ # Concurrent Rendering
2
+
3
+ > Source of truth: [useTransition](https://react.dev/reference/react/useTransition), [useDeferredValue](https://react.dev/reference/react/useDeferredValue), [Suspense](https://react.dev/reference/react/Suspense), and [renderToPipeableStream](https://react.dev/reference/react-dom/server/renderToPipeableStream).
4
+
5
+ Concurrent rendering lets React prepare a new UI in the background and interrupt it for more urgent work. Urgent updates (typing, clicking) stay responsive while expensive updates (filtering thousands of rows, navigating) render without blocking input.
6
+
7
+ ## `useTransition`
8
+
9
+ `useTransition` marks a state update as a non-urgent **transition**. React renders the transition in the background, keeps the current UI interactive, and exposes `isPending` so the UI can show a subtle pending indicator without a hard fallback.
10
+
11
+ ```tsx
12
+ import { useState, useTransition } from "react";
13
+ import { ExpensiveReports } from "@/components/reports/expensive-reports";
14
+ import { HomeOverview } from "@/components/home/home-overview";
15
+ import { TabBar } from "@/components/navigation/tab-bar";
16
+
17
+ export function TabbedView() {
18
+ const [tab, setTab] = useState<"home" | "reports">("home");
19
+ const [isPending, startTransition] = useTransition();
20
+
21
+ function selectTab(next: "home" | "reports"): void {
22
+ startTransition(() => setTab(next));
23
+ }
24
+
25
+ return (
26
+ <>
27
+ <TabBar active={tab} onSelect={selectTab} busy={isPending} />
28
+ {tab === "reports" ? <ExpensiveReports /> : <HomeOverview />}
29
+ </>
30
+ );
31
+ }
32
+ ```
33
+
34
+ ### React 19 async transitions and the post-`await` gotcha
35
+
36
+ React 19 allows `startTransition` to receive an async function (an "Action"). Only updates issued **synchronously within the transition scope** belong to the transition. Updates after an `await` run in a fresh task and escape it unless re-wrapped.
37
+
38
+ ```tsx
39
+ // ❌ BAD: setResult runs after await, outside the transition; isPending ends early.
40
+ startTransition(async () => {
41
+ const data = await fetchData();
42
+ setResult(data);
43
+ });
44
+
45
+ // ✅ GOOD: re-wrap the post-await update so it stays part of the transition.
46
+ startTransition(async () => {
47
+ const data = await fetchData();
48
+ startTransition(() => setResult(data));
49
+ });
50
+ ```
51
+
52
+ ## `startTransition` vs `useDeferredValue`
53
+
54
+ `startTransition` marks an update non-urgent at the **call site** (event handlers). `useDeferredValue` marks a **value** as allowed to lag when an expensive subtree trails a fast-changing input.
55
+
56
+ Prefer `useDeferredValue` over a fixed `setTimeout` debounce for expensive *derived renders*: it is interruptible and device-adaptive.
57
+
58
+ ```tsx
59
+ import { useDeferredValue, useState } from "react";
60
+
61
+ export interface RowItem {
62
+ id: string;
63
+ label: string;
64
+ }
65
+
66
+ export function FilterList({ rows }: { rows: RowItem[] }) {
67
+ const [text, setText] = useState<string>("");
68
+ const deferredText = useDeferredValue(text, "");
69
+
70
+ const filtered = rows.filter((r) => r.label.includes(deferredText));
71
+ const isStale = text !== deferredText;
72
+
73
+ return (
74
+ <>
75
+ <input value={text} onChange={(e) => setText(e.target.value)} />
76
+ <ul style={{ opacity: isStale ? 0.6 : 1 }}>
77
+ {filtered.map((r) => (
78
+ <li key={r.id}>{r.label}</li>
79
+ ))}
80
+ </ul>
81
+ </>
82
+ );
83
+ }
84
+ ```
85
+
86
+ ## Suspense for Data
87
+
88
+ A component suspends when it reads a not-yet-resolved promise via `use()`. The nearest `<Suspense fallback>` shows until the promise resolves.
89
+
90
+ ```tsx
91
+ import { Suspense, use } from "react";
92
+ import type { User } from "@/types/user.types";
93
+ import { ProfileSkeleton } from "@/components/profile/profile-skeleton";
94
+
95
+ export function ProfileSection({ userPromise }: { userPromise: Promise<User> }) {
96
+ return (
97
+ <Suspense fallback={<ProfileSkeleton />}>
98
+ <ProfileHeader userPromise={userPromise} />
99
+ </Suspense>
100
+ );
101
+ }
102
+
103
+ function ProfileHeader({ userPromise }: { userPromise: Promise<User> }) {
104
+ const user = use(userPromise);
105
+ return <h1>{user.name}</h1>;
106
+ }
107
+ ```
108
+
109
+ ### Resetting a boundary with `key`
110
+
111
+ Changing the `key` on a Suspense boundary (or a component inside it) discards the previous tree and re-suspends when switching the displayed resource:
112
+
113
+ ```tsx
114
+ <Suspense fallback={<ProfileSkeleton />}>
115
+ <ProfileLoader key={userId} userId={userId} />
116
+ </Suspense>
117
+ ```
118
+
119
+ ## Streaming SSR (React Level)
120
+
121
+ For Node servers, `renderToPipeableStream` streams HTML as Suspense boundaries resolve (`renderToReadableStream` for Web Streams / Edge / Cloudflare Workers). Design a meaningful **shell** (header, navigation, layout) that flushes immediately, and let slower data stream behind Suspense.
122
+
123
+ ## Anti-Patterns
124
+
125
+ | Anti-pattern | Why it is wrong | Detection |
126
+ |---|---|---|
127
+ | State update after `await` in an async transition | Escapes the transition; `isPending` ends early | UI flips to non-pending mid-operation |
128
+ | Fixed debounce/throttle for expensive derived renders | `useDeferredValue` is interruptible and adaptive | Janky typing on slow devices; hard-coded timers |
129
+ | One root spinner instead of a streamed shell | Blocks first paint on the slowest data | Long blank time-to-first-byte |
130
+ | Suspense fallback that mismatches final layout | Causes layout shift on reveal | Visible jump when content arrives |
@@ -0,0 +1,99 @@
1
+ # Performance Architecture
2
+
3
+ > Source of truth: [lazy](https://react.dev/reference/react/lazy) and [Suspense](https://react.dev/reference/react/Suspense).
4
+
5
+ With the React Compiler handling fine-grained memoization, performance engineering focuses on three architectural levers: rendering fewer DOM nodes (**virtualization**), shipping less JavaScript up front (**code-splitting**), and measuring before optimizing (**profiling**).
6
+
7
+ ## Virtualization
8
+
9
+ A list of thousands of rows mounts thousands of DOM nodes, degrading initial render and scroll responsiveness. Virtualization renders only the rows inside (and near) the viewport.
10
+
11
+ ### Library Comparison
12
+
13
+ | Library | Character | Use when |
14
+ |---|---|---|
15
+ | `@tanstack/react-virtual` | Modern, headless, strongest TypeScript support; full markup control | Default choice for custom layouts and strict TypeScript projects |
16
+ | `react-virtuoso` | Batteries-included (auto-sizing, grouping, sticky headers, chat feeds) | Rich variable-height list features out of the box |
17
+ | `react-window` | Legacy/minimal fixed-size lists | Maintaining existing codebases |
18
+
19
+ ```tsx
20
+ import { useRef } from "react";
21
+ import { useVirtualizer } from "@tanstack/react-virtual";
22
+ import type { LogEntry } from "@/types/log.types";
23
+ import { LogRowView } from "@/components/logs/log-row-view";
24
+
25
+ export function VirtualLogList({ rows }: { rows: LogEntry[] }) {
26
+ const parentRef = useRef<HTMLDivElement>(null);
27
+ const virtualizer = useVirtualizer({
28
+ count: rows.length,
29
+ getScrollElement: () => parentRef.current,
30
+ estimateSize: () => 40,
31
+ overscan: 8,
32
+ });
33
+
34
+ return (
35
+ <div ref={parentRef} style={{ height: 600, overflow: "auto" }}>
36
+ <div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
37
+ {virtualizer.getVirtualItems().map((virtualRow) => (
38
+ <div
39
+ key={virtualRow.key}
40
+ style={{
41
+ position: "absolute",
42
+ top: 0,
43
+ transform: `translateY(${virtualRow.start}px)`,
44
+ width: "100%",
45
+ }}
46
+ >
47
+ <LogRowView row={rows[virtualRow.index]} />
48
+ </div>
49
+ ))}
50
+ </div>
51
+ </div>
52
+ );
53
+ }
54
+ ```
55
+
56
+ **Keep row renderers light:** Once virtualized, per-item render cost becomes the bottleneck. Avoid heavy computation or deep subtrees inside `LogRowView`.
57
+
58
+ ## Code-Splitting
59
+
60
+ `React.lazy` + `Suspense` defers loading a component's bundle until it is rendered.
61
+
62
+ ```tsx
63
+ import { lazy, Suspense } from "react";
64
+ import { RouteSkeleton } from "@/components/layout/route-skeleton";
65
+
66
+ const AnalyticsDashboard = lazy(() => import("@/components/analytics/analytics-dashboard"));
67
+
68
+ export function AnalyticsRoute() {
69
+ return (
70
+ <Suspense fallback={<RouteSkeleton />}>
71
+ <AnalyticsDashboard />
72
+ </Suspense>
73
+ );
74
+ }
75
+ ```
76
+
77
+ ### Strategy
78
+
79
+ - **Split by route or heavy feature first** (charts, rich-text editors, admin modals).
80
+ - **Do not over-split** tiny (<10KB) components, which creates network request waterfalls.
81
+ - **Match fallback dimensions** to prevent Cumulative Layout Shift (CLS).
82
+
83
+ ## Profiling Workflow
84
+
85
+ 1. **Profile against a production/profiling build** (development builds add instrumentation overhead).
86
+ 2. **Record the interaction** in React DevTools Profiler and inspect the flamegraph and "Why did this render?".
87
+ 3. **Verify `"Memo ✨"` badges** to confirm the React Compiler optimized hot-path components.
88
+
89
+ ## Anti-Pattern → Detection → Fix Catalog
90
+
91
+ | Anti-pattern | Why it is wrong | How to detect | Fix |
92
+ |---|---|---|---|
93
+ | Hand-memoizing everything | Redundant with React Compiler | Compiler on, code cluttered with `useMemo` | Remove; keep only effect-stability escape hatches |
94
+ | Removing manual memo blindly | Breaks effect dependency identity contracts | `preserve-manual-memoization` lint warnings | Restore load-bearing memo |
95
+ | Large high-frequency context | Re-renders all consumers on every tick | Profiler highlights unrelated consumers | Split contexts by change frequency |
96
+ | `use()` inside `try / catch` or inline client `fetch()` | Swallows Suspense signals or loops infinitely | Infinite network requests or uncaught suspense | Use Error Boundary; hoist promise creation |
97
+ | State update after `await` in `startTransition` | Escapes transition; `isPending` ends early | Pending spinner disappears before UI updates | Re-wrap post-`await` setter in `startTransition` |
98
+ | Manual debounce for derived renders | Fixed delay on fast/slow devices alike | Input lag on low-end devices | Use `useDeferredValue` |
99
+ | Unvirtualized lists >100 rows | Mounts thousands of DOM nodes | Slow mount and scroll jank in Profiler | Virtualize with `@tanstack/react-virtual` |