Claude Skill

typescript-dev

Builds full-stack TypeScript apps with Vite 8, React 19, Tailwind CSS v4, shadcn/ui, Biome, Vitest, and Hono. Covers the frontend (Vite/Rolldown build and dev server, type-safe React 19, strict TypeScript 6.0, Tailwind/shadcn styling, Biome lint/format, Vitest) and the Hono 4 bac

LLM Mart · 0 points · 18 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download tenequm-skills-skills_typescript-dev-1ff2284.zip · 49 KB
Part of tenequm/skills — 25 skills

Install

skills CLI npx skills add https://github.com/tenequm/skills/tree/main/skills/typescript-dev
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tenequm-skills@llmmart
Git git clone https://github.com/tenequm/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tenequm/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

TypeScript Frontend Development

One coherent stack for building type-safe TypeScript apps: Vite 8 (build + dev server, Rolldown-powered), React 19.2 with the React Compiler, TypeScript 6.0 (strict), Tailwind CSS v4.3 + shadcn/ui for styling, Biome 2.4 for linting and formatting, Vitest 4 for testing, and Hono 4 for the backend/edge API. The pieces are designed to fit together - this skill covers how they wire up and the sharp edges that span more than one of them. Hono's RPC client (hc) shares server types directly with the React frontend, so the front and back end stay type-safe end to end without codegen.

The body below is the cross-cutting layer: the rules that bite when these tools meet, plus one working end-to-end setup. Each tool also has a deep-dive reference - read the one you need:

  • references/vite.md - Vite 8 config, dev server, proxy, HMR, Rolldown, code splitting, build optimization, deployment.
  • references/react.md - React 19 patterns: Actions, use(), Activity, useEffectEvent, document metadata, and the React Compiler.
  • references/typescript.md - Strict TypeScript 6.0 config and patterns: tsconfig defaults, generics, utility types, import defer, tsgo.
  • references/tailwind.md - Tailwind CSS v4 CSS-first config, OKLCH theming, dark mode, v4.3 utilities.
  • references/shadcn.md - shadcn/ui CLI, component authoring with CVA + data-slot, registries, Radix vs Base UI.
  • references/biome.md - Biome config, biome check, domains, type-aware linting, GritQL, ESLint/Prettier migration.
  • references/vitest.md - Vitest config, Testing Library, jsdom/happy-dom, coverage, browser mode, projects.
  • references/hono.md - Hono 4 web framework: routing, context, middleware, validation (Zod), end-to-end type-safe RPC, OpenAPI, helpers, and multi-runtime deployment (Workers/Node/Bun/Deno).

Version targets

Tool Version Note
Vite 8.1.2 Rolldown is the single default bundler
@vitejs/plugin-react 6.0.3 v6 removed the inline babel option
React / react-dom 19.2.7 React Compiler is stable (1.0)
babel-plugin-react-compiler 1.0.0 pin with --save-exact
TypeScript 6.0.3 last JS-based TS; TS 7.0 (tsgo) now RC
Tailwind CSS 4.3.2 CSS-first config, no JS config file
shadcn/ui CLI 4.12.0 create is an alias of init
Biome 2.5.2 single binary for lint + format + imports
Vitest 4.1.9 Vite-native test runner; reuses vite.config
Hono 4.12.27 Web Standards backend/edge framework; no v5

Cross-cutting critical rules

These are the rules that fail in confusing ways precisely because they sit at the seam between two tools. The single-tool details live in the references.

Vite plugin order: framework plugins first, react() last

When a framework plugin (TanStack Router/Start, etc.) generates routes or transforms code, it must run before @vitejs/plugin-react so React's Fast Refresh transform sees the final output. Wrong order causes route-generation failures and broken HMR.

plugins: [
  tanstackStart(),   // or tanstackRouter() for SPA - framework first
  tailwindcss(),
  react(),           // React plugin last among framework plugins
]

React Compiler replaces manual memoization - and changes how you wire Vite

React Compiler 1.0 auto-memoizes components, computations, and callbacks at build time. Write plain components; do not reach for useMemo/useCallback/memo. The catch lives at the Vite seam: @vitejs/plugin-react v6 removed the inline babel option, so the old react({ babel: { plugins: [...] } }) wiring no longer works. The compiler now runs through a separate Babel plugin:

import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import babel from '@rolldown/plugin-babel'

plugins: [react(), babel({ presets: [reactCompilerPreset()] })]

Install: pnpm add -D @rolldown/plugin-babel @babel/core babel-plugin-react-compiler @types/babel__core.

This also ripples into Biome: useExhaustiveDependencies can't tell the compiler is handling deps for you, so most compiler users turn it off (see biome.md).

Tailwind v4 is CSS-first - there is no tailwind.config.js

Tailwind v4 configures everything in CSS via @theme, @utility, @plugin, @source. Never create or look for tailwind.config.js/.ts. The Vite integration is the @tailwindcss/vite plugin (no PostCSS config either). If you find a tailwind.config.js in a v4 project, it is leftover - delete it and migrate the values into CSS. Full details in tailwind.md.

Style with semantic tokens, never raw palette or dynamic class names

<div className="bg-primary text-primary-foreground">   // respects theme + dark mode
<div className="bg-blue-500 text-white">               // breaks theming - avoid

And never assemble class names from fragments (bg-${color}-500) - Tailwind's scanner only sees complete literal strings, so dynamic names silently produce no CSS. Use a lookup map of full class strings.

TypeScript 6.0 changed the defaults - lean on them, don't fight them

TS 6.0 bakes in much of what used to be manual: strict and noUncheckedSideEffectImports are now on by default, so drop them from a fresh tsconfig. But two new defaults will break builds if you ignore them: types now defaults to [] (add "types": ["node"] if you use Node globals) and module/target shifted (module defaults to esnext, not nodenext). baseUrl is deprecated - use prefixed paths instead. See typescript.md for the full 6.0 tsconfig and migration notes.

One Biome command, and files.includes is the only include key

Run biome check (or biome ci) - it formats, lints, and organizes imports in a single pass; never split into separate lint+format calls. And in Biome 2.x the only file-selection key is files.includes (with the s); files.ignore/files.include/files.exclude do not exist and throw Found an unknown key. Exclude with negation: "includes": ["**", "!**/routeTree.gen.ts"]. More in biome.md.

Hono RPC ties the backend's types to the React frontend - keep them in sync

When the API is Hono, the React app talks to it through the hc<AppType>() client, which imports the server's exported typeof app directly. That shared type is the seam: it only works if both sides run the same Hono version and both tsconfig.json set "strict": true (a mismatch throws "Type instantiation is excessively deep"). Two more rules that bite at this seam: handlers must specify status codes (c.json(data, 200)) for the client to infer responses, and routes the client calls must not use c.notFound(). As the route count grows, compile the client type once (hcWithType) so the IDE stays fast. Full details in hono.md.

End-to-end setup

A minimal but complete React + TypeScript + Tailwind + Biome project. Swap the framework plugin for your router/SSR choice (see vite.md for TanStack and Cloudflare variants).

vite.config.ts

import { defineConfig } from 'vite'
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import babel from '@rolldown/plugin-babel'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    tailwindcss(),
    react(),
    babel({ presets: [reactCompilerPreset()] }),
  ],
  resolve: {
    alias: { '@': new URL('./src', import.meta.url).pathname },
  },
})

import.meta.url is the ESM-correct way to resolve paths - there is no __dirname in an ESM config, and Vite configs are ESM-only.

tsconfig.json (TypeScript 6.0)

{
  "compilerOptions": {
    // strict + noUncheckedSideEffectImports are ON by default in 6.0 - omitted on purpose
    "target": "es2023",
    "module": "preserve",
    "moduleResolution": "bundler",
    "moduleDetection": "force",
    "jsx": "react-jsx",
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "erasableSyntaxOnly": true,
    "skipLibCheck": true,
    "types": [],
    "paths": { "@/*": ["./src/*"] }
  }
}

module: preserve + moduleResolution: bundler is the right pairing for a Vite-bundled app; use nodenext instead only for Node-executed code. types: [] keeps ambient @types/* from leaking in globally - add ["node"] (or others) explicitly when needed.

biome.json

{
  "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
  "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
  "files": { "includes": ["**", "!**/components/ui", "!**/routeTree.gen.ts"] },
  "formatter": { "enabled": true, "indentStyle": "space", "lineWidth": 100 },
  "linter": {
    "enabled": true,
    "rules": { "preset": "recommended" },
    "domains": { "react": "recommended" }
  },
  "javascript": { "formatter": { "quoteStyle": "double" } },
  "assist": { "enabled": true, "actions": { "source": { "organizeImports": "on" } } }
}

src/styles.css

@import "tailwindcss";

:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --radius: 0.5rem;
}
.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
}

The @import "tailwindcss"; line is load-bearing: the @tailwindcss/vite plugin alone produces no styles without it - a missing import is the classic "Tailwind renders nothing" footgun. Use @theme inline (not plain @theme) for tokens that reference CSS variables, so they track dark-mode changes.

A component, the way the whole stack wants it

Plain function, ref as a regular prop (no forwardRef), native element props via React.ComponentProps, variants via CVA, data-slot for styling hooks, and no manual memoization - the compiler handles it.

import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        outline: "border border-input bg-background hover:bg-accent",
        ghost: "hover:bg-accent hover:text-accent-foreground",
      },
      size: { default: "h-9 px-4 py-2", sm: "h-8 px-3", lg: "h-10 px-8" },
    },
    defaultVariants: { variant: "default", size: "default" },
  }
)

function Button({
  className,
  variant,
  size,
  ref,
  ...props
}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>) {
  return (
    <button
      ref={ref}
      data-slot="button"
      className={cn(buttonVariants({ variant, size }), className)}
      {...props}
    />
  )
}

Note the cn() order: defaults first, consumer className last, so tailwind-merge's last-wins resolution lets callers override.

Best practices

  1. Let the compiler optimize. Write plain components and computations; reserve useMemo/useCallback for the rare case where you need a value to be a stable effect dependency.
  2. Model state as discriminated unions, not loose booleans ({ status: "loading" } | { status: "error"; error }) so impossible states are unrepresentable.
  3. Extend native props with React.ComponentProps<"el"> instead of re-declaring HTML attributes by hand.
  4. Use use() over useContext() - it works after early returns and inside conditionals.
  5. Semantic color tokens only, and always pair bg-* with the matching text-*-foreground.
  6. biome check --write is your one local command; biome ci in pipelines.
  7. Rolldown is the default bundler in Vite 8 - no opt-in needed; split stable vendor code with Rolldown's codeSplitting (see vite.md).
  8. Pin exact versions for tooling that rewrites code (babel-plugin-react-compiler, @biomejs/biome) to avoid surprise diffs between releases.
  9. Keep secrets off the client - only VITE_-prefixed env vars reach browser code via import.meta.env.
  10. Test through vite.config.ts - Vitest reuses your build config, so tests see the same aliases and transforms; vitest run in CI, jsdom for component tests.

Resources

Files (skills)
  • references
    • biome.md 9.5 KB
      # Biome
      
      Fast, unified linting, formatting, and import organization for JS/TS/JSX/CSS/GraphQL in a single binary. Biome **2.5** (latest `2.5.2`) does type-aware linting without the TypeScript compiler, GritQL plugins for custom rules, and domain-based rule grouping. Zero config by default, ~97% Prettier compatibility.
      
      ## Critical rules
      
      ### `files.includes` is the only file key
      
      Biome 2.x supports only `files.includes` (with the `s`). There is **no** `files.ignore`, `files.include`, or `files.exclude` - any of them throws `Found an unknown key`. The valid `files` keys are `includes`, `maxSize`, `ignoreUnknown`. Exclude with negation patterns:
      
      ```json
      { "files": { "includes": ["**", "!**/routeTree.gen.ts", "!**/generated/**"] } }
      ```
      
      For paths the scanner must skip entirely (even for assists), use the `!!` force-ignore prefix - it replaces the deprecated `experimentalScannerIgnores`:
      
      ```json
      { "files": { "includes": ["**", "!!**/legacy-vendor/**"] } }
      ```
      
      ### One command: `biome check`
      
      `biome check` runs formatter + linter + import organizer in one pass. Never split into separate `biome lint` and `biome format` in CI - use `biome check` (or `biome ci` for CI mode).
      
      ```bash
      biome check --write .            # apply safe fixes
      biome check --write --unsafe .   # include unsafe fixes (review the diff)
      ```
      
      Removing unused imports/variables is classified **unsafe** (an external caller might reference the symbol), so plain `--write` reports but doesn't delete them - use `--write --unsafe` or remove by hand. Prefer `--write` over the `--fix` alias for consistency.
      
      ### Pin versions, migrate after upgrades
      
      ```bash
      pnpm add --save-dev --save-exact @biomejs/biome@latest
      pnpm biome migrate --write
      ```
      
      The `$schema` is version-pinned; after bumping the binary, the CLI errors with `The configuration schema version does not match the CLI version` until you run `biome migrate --write`. Do it as part of the upgrade.
      
      ### `biome.json` at the project root
      
      One config at the root; monorepo packages use `"extends": "//"` to inherit. Never reference it with a relative path like `"../../biome.json"`.
      
      ## Quick start
      
      ```bash
      pnpm add --save-dev --save-exact @biomejs/biome
      pnpm biome init
      ```
      
      ### Recommended config (React/TypeScript)
      
      ```json
      {
        "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
        "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
        "files": { "includes": ["**", "!**/components/ui", "!**/routeTree.gen.ts"] },
        "formatter": { "enabled": true, "indentStyle": "space", "lineWidth": 100 },
        "linter": {
          "enabled": true,
          "rules": { "preset": "recommended" },
          "domains": { "react": "recommended" }
        },
        "javascript": { "formatter": { "quoteStyle": "double" } },
        "assist": { "enabled": true, "actions": { "source": { "organizeImports": "on" } } }
      }
      ```
      
      Biome 2.5 **deprecated `linter.rules.recommended`** in favor of `linter.rules.preset` (`"recommended"` or `"all"`); the boolean still works but emits a warning - run `biome migrate --write` to convert. The `domains` values (`"recommended"`/`"all"`/`"none"`) are unaffected.
      
      ### IDE setup
      
      VS Code - install `biomejs.biome`:
      
      ```json
      {
        "editor.defaultFormatter": "biomejs.biome",
        "editor.formatOnSave": true,
        "editor.codeActionsOnSave": {
          "source.fixAll.biome": "explicit",
          "source.organizeImports.biome": "explicit"
        }
      }
      ```
      
      Zed uses the Biome extension natively. Note the spelling: Zed's inline-config key is `inline_config` (snake_case); the VS Code extension uses `inlineConfig` (camelCase).
      
      ### CI
      
      ```bash
      pnpm biome ci .                                          # no writes, non-zero exit on errors
      pnpm biome ci --reporter=github .                        # GitHub Actions annotations
      pnpm biome ci --reporter=concise .                       # short one-line-per-diagnostic output
      ```
      
      `--reporter=concise` (Biome 2.5) prints one `file:line:col: rule: message` line per diagnostic - the recommended reporter when an AI coding agent reads the output, since it saves tokens versus the default rich format.
      
      ## Configuration details
      
      ### Import organizer
      
      The organizer (a Biome Assist action, not a lint rule) merges duplicates, sorts by distance, and supports custom grouping:
      
      ```json
      {
        "assist": { "actions": { "source": { "organizeImports": {
          "level": "on",
          "options": { "groups": [
            { "source": "builtin" }, { "source": "external" },
            { "source": "internal", "match": "@company/*" }, { "source": "relative" }
          ] }
        } } } }
      }
      ```
      
      ### Per-subsystem includes and overrides
      
      Each subsystem (`linter`, `formatter`, `assist`) has its own `includes`, applied after `files.includes` (can only narrow). `overrides` apply different settings to file patterns - the field is `includes` (with `s`):
      
      ```json
      {
        "overrides": [
          { "includes": ["**/components/ui/**"], "linter": { "rules": { "style": { "useComponentExportOnlyModules": "off" } } } },
          { "includes": ["**/*.test.ts"], "linter": { "rules": { "suspicious": { "noConsole": "off" } } } }
        ]
      }
      ```
      
      ### Monorepo
      
      Root holds shared config; packages inherit with `"extends": "//"` and add their own `linter.rules` as needed.
      
      ## Domains
      
      Domains group lint rules by technology - enable what your stack uses. Levels: `"recommended"` (stable only), `"all"` (includes nursery), `"none"`.
      
      ```json
      { "linter": { "domains": { "react": "recommended", "test": "recommended", "types": "all" } } }
      ```
      
      Common domains: `react` (auto-detected at `react >= 16`), `next`, `solid`, `vue`, `test` (jest/vitest/mocha/ava), `playwright`, `drizzle`, `project` (cross-file: `noImportCycles`, `noUnresolvedImports`), `types` (type inference: `noFloatingPromises`, `noMisusedPromises`). The `project` and `types` domains trigger a file scan with small overhead.
      
      ## Type-aware linting
      
      Biome has its own Rust type-inference engine - no `typescript` dependency needed. Enable the `types` domain.
      
      | Rule | Catches |
      |------|---------|
      | `noFloatingPromises` | unhandled promises (missing await/return/void) |
      | `noMisusedPromises` | promises in conditionals or array callbacks |
      | `useAwaitThenable` | awaiting non-thenables |
      | `noUnnecessaryConditions` | always-true/false conditions |
      
      ```ts
      async function loadData() { fetch("/api") }        // ERROR: floating promise
      async function loadData() { void fetch("/api") }   // OK: explicit fire-and-forget
      ```
      
      ### React Compiler interaction
      
      If you use the React Compiler, `useExhaustiveDependencies` can't tell the compiler is handling memoization, so most compiler users turn it off:
      
      ```json
      { "linter": { "rules": { "correctness": { "useExhaustiveDependencies": "off" } } } }
      ```
      
      ### Tailwind v4 CSS
      
      To lint CSS that uses Tailwind at-rules, enable `css.parser.tailwindDirectives` so Biome parses `@theme`, `@utility`, and `@apply` instead of erroring on them.
      
      ## GritQL custom rules
      
      Declarative pattern-matching for project-specific rules. Register `.grit` files as plugins:
      
      ```json
      { "plugins": ["./lint-rules/no-object-assign.grit"] }
      ```
      
      ```grit
      `$fn($args)` where {
        $fn <: `Object.assign`,
        register_diagnostic(span = $fn, message = "Prefer object spread over Object.assign()")
      }
      ```
      
      Target languages: JavaScript (default), CSS, and JSON.
      
      ## Suppression
      
      ```ts
      // biome-ignore lint/suspicious/noConsole: needed for debugging
      console.log("debug")
      // biome-ignore-all lint/suspicious/noConsole: logger module      (file-level)
      // biome-ignore-start lint/style/useConst: legacy ... biome-ignore-end  (range)
      ```
      
      Biome requires explanation text after the colon.
      
      ## Migration from ESLint/Prettier
      
      ```bash
      pnpm biome migrate eslint --write     # legacy + flat configs, plugin mapping, .eslintignore
      pnpm biome migrate prettier --write   # maps tabWidth/useTabs/singleQuote/trailingComma
      ```
      
      After removing ESLint (which respected `.gitignore`), enable VCS integration so Biome ignores the same files:
      
      ```json
      { "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true } }
      ```
      
      ## CLI reference
      
      ```bash
      biome check --write .            # primary command
      biome check --changed .          # only VCS-changed files
      biome check --staged .           # only staged (good for pre-commit)
      biome lint --only=types .        # run just type-aware rules
      biome lint --enforce-assist .    # fail CI when assist actions remain unapplied
      biome check --watch .            # re-run on file changes (2.5; read-only, no --write/--fix)
      biome explain noFloatingPromises # explain a rule
      biome migrate --write            # after a version bump
      biome upgrade                    # 2.5: self-upgrade standalone (Homebrew/binary) installs
      ```
      
      Biome 2.5 also adds `formatter.delimiterSpacing` (pads `[ 1, 2 ]` / `{ a }` / `( x )` - an accessibility aid for dyslexia; behavior varies per language) and can now format/lint `.svg` files.
      
      ## Gotchas
      
      1. **Only `files.includes` exists** - not `ignore`/`include`/`exclude`.
      2. **`organizeImports` is under `assist.actions.source`**, not a top-level key.
      3. **`overrides` disabling linter+formatter still run assists** - the import organizer will still rewrite a "skipped" file, silently dirtying your tree. Use `files.includes` negation to fully exclude.
      4. **`package.json` reformatting loop:** Biome's default JSON formatter uses tabs; pnpm writes 2-space `package.json`, so each install/check fight. Either exclude it (`"!**/package.json"`) or override JSON to spaces:
      
      ```json
      { "overrides": [{ "includes": ["**/package.json"], "json": { "formatter": { "indentStyle": "space", "indentWidth": 2 } } }] }
      ```
      
      ## Resources
      
      - Docs: https://biomejs.dev/ - Config reference: https://biomejs.dev/reference/configuration/
      - Domains: https://biomejs.dev/linter/domains/ - Migrate: https://biomejs.dev/guides/migrate-eslint-prettier/
      
    • hono.md 27.3 KB
      # Hono
      
      Hono (Japanese for "flame") is a small, ultrafast web framework built entirely on Web
      Standards (`Request`/`Response`/`fetch`). One codebase runs on Cloudflare Workers, Deno,
      Bun, Node.js, Vercel, Netlify, AWS Lambda, Lambda@Edge, and Fastly Compute. Zero
      dependencies; the `hono/tiny` preset is under 14kB. It is the backend/edge counterpart to
      this stack's React frontend - its RPC client (`hc`) shares server types directly with
      React, giving end-to-end type safety without code generation.
      
      Version target: **hono@4.12.27** (Hono 4 is current; there is no v5). Node.js >= 18.14.1.
      Adapters/middleware are versioned independently (`@hono/node-server@2`, `@hono/zod-validator`,
      `@hono/zod-openapi@1`).
      
      > **Keep Hono patched - the 4.12.x line has shipped frequent security fixes.** 4.12.27 alone
      > fixes two SSR issues: `hono/jsx` stored context process-wide instead of per request, so a value
      > read after an `await` in an async component could leak across concurrent requests
      > (GHSA-hvrm-45r6-mjfj), and `hono/css` `cx()` marked its output as pre-escaped, allowing XSS via
      > untrusted class names (GHSA-w62v-xxxg-mg59). Pin to the latest patch, not an older 4.12.x.
      
      > **For anything this file does not cover, fetch Hono's own LLM-optimized docs** - they are
      > the fastest authoritative source and are kept in sync with releases:
      > - Full docs (one file, ~360KB): https://hono.dev/llms-full.txt
      > - Core-only (smaller): https://hono.dev/llms-small.txt
      > - Index of all doc pages: https://hono.dev/llms.txt
      >
      > This reference is the curated 80% you need most often; the `llms-*.txt` files are the
      > exhaustive long tail (every middleware option, every runtime's getting-started, edge cases).
      
      ```sh
      npm create hono@latest my-app          # scaffold (prompts for a template)
      npm create hono@latest my-app -- --template cloudflare-workers --pm pnpm --install
      npm i hono                              # add to an existing project
      ```
      
      ## Mental model
      
      - A handler returns a `Response` (or `c.text()`/`c.json()`/etc., which build one). Exactly
        one handler runs per request.
      - Middleware is `async (c, next) => { ... await next() ... }`. Code before `next()` runs on
        the way in; code after runs on the way out (onion model). Return a `Response` from
        middleware to short-circuit. Returning nothing (after `await next()`) continues the chain.
      - Execution order = registration order. Register middleware (`app.use`) and fallbacks
        (`app.get('*', ...)`) relative to routes accordingly.
      - Hono catches throws from handlers/middleware and routes them to `app.onError` (or a 500),
        so `next()` never throws - no try/catch needed around it.
      
      ```ts
      import { Hono } from 'hono'
      
      const app = new Hono()
      app.get('/', (c) => c.text('Hono!'))
      
      export default app // entry point for Cloudflare Workers, Bun, Deno
      ```
      
      ## Routing
      
      ```ts
      app.get('/', (c) => c.text('GET /'))
      app.post('/', (c) => c.text('POST /'))
      app.put('/', (c) => c.text('PUT /'))
      app.delete('/', (c) => c.text('DELETE /'))
      app.all('/hello', (c) => c.text('Any method'))          // any HTTP method
      app.on('PURGE', '/cache', (c) => c.text('PURGE'))       // custom method
      app.on(['PUT', 'DELETE'], '/post', (c) => c.text('..')) // multiple methods
      app.on('GET', ['/a', '/b'], (c) => c.text('..'))        // multiple paths
      
      app.get('/wild/*/card', (c) => c.text('wildcard'))      // wildcard
      app.get('/user/:name', (c) => c.text(c.req.param('name')))           // param
      app.get('/api/animal/:type?', (c) => c.text('..'))                   // optional param
      app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => c.text('..'))   // regexp param
      app.get('/posts/:filename{.+\\.png}', (c) => c.text('..'))           // slashes via regexp
      
      // Chained routes on one path
      app
        .get('/endpoint', (c) => c.text('GET'))
        .post((c) => c.text('POST'))
        .delete((c) => c.text('DELETE'))
      ```
      
      **Priority is registration order**, and the first matching handler wins and stops dispatch.
      Put middleware and specific routes *above* wildcard fallbacks:
      
      ```ts
      app.get('/book/a', (c) => c.text('a'))        // GET /book/a -> 'a'
      app.get('/book/:slug', (c) => c.text('common')) // GET /book/b -> 'common'
      
      app.use(logger())                              // middleware first
      app.get('/foo', (c) => c.text('foo'))
      app.get('*', (c) => c.text('fallback'))        // fallback last
      ```
      
      **HEAD is automatic.** Hono converts HEAD to GET and strips the body before route matching,
      so `app.head(...)` / `app.on('HEAD', ...)` handlers are never called. Add HEAD-specific
      headers in middleware checking `c.req.method === 'HEAD'`.
      
      ### Grouping and sub-apps
      
      `app.route(path, subApp)` mounts a sub-`Hono`. This is how you split a large app into files
      *without* losing type inference (see RPC). `basePath()` prefixes all routes on an instance.
      
      ```ts
      // books.ts
      const books = new Hono()
      books.get('/', (c) => c.text('List'))     // GET /books
      books.get('/:id', (c) => c.text('One'))   // GET /books/:id
      export default books
      
      // index.ts
      const app = new Hono()
      app.route('/books', books)
      const api = new Hono().basePath('/api')   // all routes under /api
      ```
      
      Watch grouping order: `app.route('/two', two)` snapshots `two`'s routes *at call time*, so
      register child routes onto `two` before mounting `two` onto `app`, or you get 404s.
      
      ## Context (`c`)
      
      The `Context` is created per request and lives until the response is returned.
      
      **Responders** (each returns a `Response`):
      
      ```ts
      c.text('Hello', 201, { 'X-Msg': 'hi' })   // text/plain
      c.json({ ok: true }, 200)                  // application/json
      c.html('<h1>Hi</h1>')                      // text/html
      c.body('raw', 201, { 'Content-Type': 'text/plain' })
      c.redirect('/', 301)                       // default 302
      c.notFound()                               // customizable via app.notFound()
      c.status(201)                              // set status without returning yet
      c.header('X-Message', 'hi')                // set a response header
      c.res                                      // the in-progress Response (read/mutate in mw)
      ```
      
      **Per-request state** - `c.set` / `c.get` / `c.var`. Type it via the `Variables` generic so
      handlers see the right types:
      
      ```ts
      type Variables = { user: { id: string } }
      const app = new Hono<{ Variables: Variables }>()
      
      app.use(async (c, next) => {
        c.set('user', { id: '123' })
        await next()
      })
      app.get('/', (c) => c.json(c.get('user')))   // or c.var.user
      ```
      
      State lives only for the current request; it is never shared across requests.
      
      **Bindings / env** - on Cloudflare Workers, KV/D1/R2/secrets are `c.env.*`. Type them with
      the `Bindings` generic:
      
      ```ts
      type Bindings = { MY_KV: KVNamespace; TOKEN: string }
      const app = new Hono<{ Bindings: Bindings }>()
      
      app.get('/', async (c) => {
        const v = await c.env.MY_KV.get('key')
        c.executionCtx.waitUntil(c.env.MY_KV.put('k', 'v')) // background work
        return c.text(v ?? '')
      })
      ```
      
      `c.render()` / `c.setRenderer()` set a layout in middleware then render content per route.
      `c.error` holds a thrown error inside post-`next()` middleware.
      
      ## HonoRequest (`c.req`)
      
      ```ts
      c.req.param('id')          // single path param (literal-typed from the route)
      c.req.param()              // all path params
      c.req.query('q')           // single query value
      c.req.query()              // all query values
      c.req.queries('tags')      // repeated query -> string[]
      c.req.header('User-Agent') // single header (pass the exact name)
      c.req.header()             // all headers, keys LOWERCASED
      
      await c.req.json()         // parse application/json body
      await c.req.text()         // text/plain body
      await c.req.parseBody()    // multipart/form-data or x-www-form-urlencoded
      await c.req.formData()     // FormData
      await c.req.arrayBuffer()  // ArrayBuffer
      await c.req.blob()         // Blob
      
      c.req.valid('json')        // validated data (see Validation)
      c.req.path                 // pathname
      c.req.url                  // full URL string
      c.req.method               // 'GET'
      c.req.raw                  // the underlying Web `Request` (e.g. c.req.raw.cf on Workers)
      await cloneRawRequest(c.req) // clone even after body was consumed by a validator
      ```
      
      `parseBody()` notes: `body['foo[]']` is always `(string | File)[]`; `{ all: true }` collects
      repeated same-name fields into arrays; `{ dot: true }` expands `obj.key` keys into nested
      objects.
      
      ## Middleware
      
      ```ts
      app.use(logger())              // all methods, all routes
      app.use('/posts/*', cors())    // scoped by path
      app.post('/posts/*', basicAuth({ username, password })) // method + path
      
      // Inline custom middleware
      app.use('/message/*', async (c, next) => {
        await next()
        c.header('x-message', 'after handler')
      })
      ```
      
      For reusable, type-safe middleware use `createMiddleware` from `hono/factory` - it preserves
      `Context`/`next` types and lets you declare the `Variables` it sets:
      
      ```ts
      import { createMiddleware } from 'hono/factory'
      
      const auth = createMiddleware<{ Variables: { user: { id: string } } }>(
        async (c, next) => {
          c.set('user', { id: '123' })
          await next()
        }
      )
      ```
      
      **Type inference accumulates across chained `.use()`.** Each `.use()` returns a new instance
      with merged `Variables`, so later handlers see every preceding middleware's variables without
      declaring a combined `Env` upfront:
      
      ```ts
      const app = new Hono()
        .use(authMiddleware)   // sets `user`
        .use(dbMiddleware)     // sets `db`
        .get('/', (c) => c.json({ user: c.var.user, hasDb: !!c.var.db }))
      ```
      
      To configure middleware from `c.env` (Workers can't read env at module scope), wrap it:
      
      ```ts
      app.use('*', async (c, next) => cors({ origin: c.env.CORS_ORIGIN })(c, next))
      ```
      
      `ContextVariableMap` module augmentation adds variable types *globally* - convenient for
      app-wide middleware, but it makes `c.get(...)` look typed even in handlers where the
      middleware never ran, hiding `undefined` bugs. Prefer the `Variables` generic or chained
      `.use()` typing.
      
      ### Built-in middleware (import from `hono/<name>`)
      
      Auth & security: `basic-auth`, `bearer-auth`, `jwt`, `jwk`, `cors`, `csrf`, `secure-headers`,
      `ip-restriction`. Body/response: `body-limit`, `compress`, `etag`, `cache`, `pretty-json`,
      `trailing-slash`. Observability: `logger`, `timing`, `request-id`. Control flow: `combine`,
      `method-override`, `context-storage`, `timeout`, `language`. Plus the `powered-by` and
      `jsx-renderer` middleware.
      
      ```ts
      import { cors } from 'hono/cors'
      app.use('/api/*', cors({
        origin: ['https://example.com'],           // string | string[] | (origin, c) => string
        allowMethods: ['GET', 'POST', 'OPTIONS'],
        allowHeaders: ['X-Custom-Header'],
        exposeHeaders: ['Content-Length'],
        credentials: true,
        maxAge: 600,
      }))
      // `origin`/`allowMethods` accept callbacks for per-origin logic. CORS must run before routes.
      
      import { jwt } from 'hono/jwt'
      import type { JwtVariables } from 'hono/jwt'
      const app = new Hono<{ Variables: JwtVariables }>()
      app.use('/auth/*', jwt({ secret: 'very-secret', alg: 'HS256', issuer: 'me' }))
      app.get('/auth/page', (c) => c.json(c.get('jwtPayload')))
      // Reads `Authorization: Bearer <token>` by default; set `cookie` or `headerName` to change.
      // `alg`: HS256/384/512, RS*, PS*, ES*, EdDSA. For c.env secrets, wrap like cors above.
      
      import { secureHeaders } from 'hono/secure-headers'
      import { csrf } from 'hono/csrf'
      import { logger } from 'hono/logger'
      app.use(secureHeaders(), csrf({ origin: 'https://example.com' }), logger())
      
      import { basicAuth } from 'hono/basic-auth'
      import { bearerAuth } from 'hono/bearer-auth'
      app.use('/admin/*', basicAuth({ username: 'hono', password: 'secret' }))
      app.use('/api/*', bearerAuth({ token: 'a-static-token' }))  // or { verifyToken: async (t, c) => boolean }
      ```
      
      ## Validation
      
      Hono ships a thin `validator`; pair it with a schema library for real validation. The
      validated value is read with `c.req.valid(target)`. Targets: `json`, `form`, `query`,
      `header`, `param`, `cookie`.
      
      ```ts
      import { validator } from 'hono/validator'
      app.post('/posts', validator('form', (value, c) => {
        if (typeof value.body !== 'string') return c.text('Invalid!', 400)
        return { body: value.body }            // return = the validated value
      }), (c) => c.json({ body: c.req.valid('form').body }))
      ```
      
      Prefer the **Zod validator middleware** (Zod 4 supported):
      
      ```ts
      import { z } from 'zod'
      import { zValidator } from '@hono/zod-validator'
      
      const app = new Hono().post(
        '/posts',
        zValidator('form', z.object({ title: z.string(), body: z.string() })),
        (c) => {
          const { title, body } = c.req.valid('form') // fully typed
          return c.json({ ok: true }, 201)
        }
      )
      ```
      
      Or `@hono/standard-validator`'s `sValidator` for any [Standard Schema](https://standardschema.dev)
      library (Zod, Valibot, ArkType) with one adapter. Run multiple validators to check different
      parts: `validator('param', ...)`, `validator('query', ...)`, `validator('json', ...)`.
      
      Gotchas: validating `json`/`form` requires the matching `Content-Type` on the request or the
      body parses to `{}` (set it in tests too). For `header`, use **lowercase** keys
      (`value['idempotency-key']`).
      
      ## RPC - end-to-end type safety
      
      The flagship feature: export the server app's type, and the `hc` client infers every input
      and output - no codegen, no schema duplication. This is the natural way to connect a Hono
      backend to the React frontend in this stack.
      
      ```ts
      // server.ts
      import { Hono } from 'hono'
      import { z } from 'zod'
      import { zValidator } from '@hono/zod-validator'
      
      const route = new Hono()
        .post('/posts',
          zValidator('form', z.object({ title: z.string(), body: z.string() })),
          (c) => c.json({ ok: true, message: 'Created!' }, 201)
        )
        .get('/posts/:id',
          zValidator('query', z.object({ page: z.coerce.number().optional() })),
          (c) => c.json({ title: 'Night', body: 'sleep' }, 200)
        )
      
      export type AppType = typeof route   // share the type with the client
      export default route
      ```
      
      ```ts
      // client.ts (runs in the React app)
      import { hc } from 'hono/client'
      import type { AppType } from './server'
      
      const client = hc<AppType>('http://localhost:8787/')
      
      const res = await client.posts.$post({ form: { title: 'Hi', body: '...' } })
      if (res.ok) console.log((await res.json()).message)  // typed
      
      // Path params via [':id']; params/query MUST be strings even if validated to numbers
      const res2 = await client.posts[':id'].$get({ param: { id: '123' }, query: { page: '1' } })
      ```
      
      Status codes flow through types: `c.json(data, 404)` makes `res.status === 404` narrow the
      JSON type. Helpers: `InferRequestType<typeof client.x.$post>`, `InferResponseType<...>`,
      `client.x.$url()` (needs absolute base URL), `client.x.$path()`, `parseResponse(...)` (parses
      by Content-Type and throws on non-ok). Pass `{ init: { credentials: 'include' } }` or
      `{ headers: { Authorization: '...' } }` to `hc` for cookies/auth.
      
      **Do not** use `c.notFound()` on routes the client calls - its result can't be inferred. Use
      `c.json({ error: '...' }, 404)` instead. Global `onError` responses aren't auto-inferred;
      merge them with `ApplyGlobalResponse<typeof app, { 500: { json: { error: string } } }>`.
      
      Larger apps: chain `.route()` and export the chained result's type:
      
      ```ts
      const routes = app.route('/authors', authors).route('/books', books)
      export type AppType = typeof routes
      ```
      
      **Two RPC requirements that bite:**
      1. `"strict": true` in `tsconfig.json` on *both* client and server (a monorepo split needs
         matching Hono versions, or you get "Type instantiation is excessively deep").
      2. Type instantiation is heavy - many routes slow the IDE. The recommended fix is to compile
         the client type once so `tsserver` doesn't recompute it:
      
      ```ts
      import { hc } from 'hono/client'
      import { app } from './app'
      export type Client = ReturnType<typeof hc<typeof app>>
      export const hcWithType = (...args: Parameters<typeof hc>): Client =>
        hc<typeof app>(...args)   // use hcWithType instead of hc
      ```
      
      ## OpenAPI
      
      `@hono/zod-openapi` extends Hono so the same Zod schema validates requests *and* generates an
      OpenAPI 3 document. Serve interactive docs with Swagger UI (`@hono/swagger-ui`) or Scalar.
      
      ```ts
      import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
      
      const UserSchema = z.object({
        id: z.string().openapi({ example: '123' }),
        name: z.string().openapi({ example: 'John' }),
      }).openapi('User')   // registers as #/components/schemas/User
      
      const route = createRoute({
        method: 'get',
        path: '/users/{id}',
        request: { params: z.object({ id: z.string().min(3) }) },
        responses: {
          200: { content: { 'application/json': { schema: UserSchema } }, description: 'A user' },
        },
      })
      
      const app = new OpenAPIHono()
      app.openapi(route, (c) => {
        const { id } = c.req.valid('param')
        return c.json({ id, name: 'Ultra-man' }, 200)  // specify the status code, even 200
      })
      app.doc('/doc', { openapi: '3.0.0', info: { version: '1.0.0', title: 'My API' } })
      ```
      
      `OpenAPIHono` is a drop-in `Hono` (supports `.route()`, RPC `typeof`, etc.). Same
      Content-Type rule as validation: a JSON body needs `Content-Type: application/json` or
      `c.req.valid('json')` is `{}`.
      
      ## Error handling
      
      ```ts
      import { HTTPException } from 'hono/http-exception'
      
      throw new HTTPException(401, { message: 'Unauthorized' })          // text response
      throw new HTTPException(401, { res: customResponse })              // full Response control
      throw new HTTPException(401, { message, cause })                   // attach arbitrary cause
      
      app.onError((err, c) => {
        if (err instanceof HTTPException) return err.getResponse()
        console.error(err)
        return c.text('Internal Server Error', 500)
      })
      app.notFound((c) => c.text('Custom 404', 404))
      ```
      
      `notFound`/`onError` fire only on the top-level app; route-level `onError` takes priority over
      a parent's. `HTTPException.getResponse()` is not `Context`-aware - reapply context headers if
      needed.
      
      ## Helpers (import from `hono/<name>`)
      
      ```ts
      // hono/cookie
      import { getCookie, setCookie, deleteCookie, getSignedCookie, setSignedCookie } from 'hono/cookie'
      setCookie(c, 'name', 'value', { httpOnly: true, secure: true, sameSite: 'Lax', maxAge: 3600 })
      const v = getCookie(c, 'name')
      await setSignedCookie(c, 'name', 'value', secret)   // signed cookies are async (WebCrypto)
      
      // hono/streaming - streaming & Server-Sent Events
      import { stream, streamText, streamSSE } from 'hono/streaming'
      app.get('/sse', (c) => streamSSE(c, async (stream) => {
        while (!stream.aborted) {
          await stream.writeSSE({ data: new Date().toISOString(), event: 'tick', id: String(id++) })
          await stream.sleep(1000)
        }
      }))
      // Note: errors thrown inside the stream callback do NOT trigger app.onError (response started).
      
      // hono/jwt - mint/verify tokens yourself (the jwt() middleware only verifies incoming ones)
      import { sign, verify, decode } from 'hono/jwt'
      const token = await sign({ sub: 'user123', exp: Math.floor(Date.now() / 1000) + 300 }, secret)
      const payload = await verify(token, secret, 'HS256')  // throws JwtTokenExpired/-Invalid/... on failure
      const { header, payload: p } = decode(token)          // inspect WITHOUT verifying (debug only)
      // verify() auto-checks exp/nbf/iat/iss when those claims are present. alg default HS256;
      // supports HS*/RS*/PS*/ES*/EdDSA. Catch the typed errors (JwtTokenExpired, etc.) to branch.
      
      // hono/context-storage - reach the Context from outside a handler (DB layer, logger, util)
      import { contextStorage, getContext } from 'hono/context-storage'
      app.use(contextStorage())                       // requires AsyncLocalStorage support
      const currentUser = () => getContext<Env>().var.user  // works anywhere downstream
      // Cloudflare Workers: needs the `nodejs_compat` (or `nodejs_als`) compatibility flag.
      // `tryGetContext()` returns undefined instead of throwing when no context is active.
      ```
      
      Other helpers: `hono/factory` (`createFactory`, `createMiddleware`, `createHandlers`,
      `createApp`), `hono/jwt` (sign/verify/decode utilities), `hono/adapter` (`env(c)` for
      runtime-agnostic env access), `hono/html`, `hono/css`, `hono/ssg`, `hono/proxy`,
      `hono/conninfo`, `hono/accepts`, `hono/route`, `hono/testing` (`testClient`).
      
      The Factory helper keeps `Env` types DRY and enables RoR-style "controllers" without losing
      inference (the one sanctioned way - see Best practices):
      
      ```ts
      import { createFactory } from 'hono/factory'
      
      type Env = { Bindings: { MY_DB: D1Database }; Variables: { db: DrizzleD1Database } }
      const factory = createFactory<Env>({
        initApp: (app) => app.use(async (c, next) => { c.set('db', drizzle(c.env.MY_DB)); await next() }),
      })
      const app = factory.createApp()          // Env applied once
      const handlers = factory.createHandlers(logger(), (c) => c.json(c.var.db.select()))
      app.get('/posts', ...handlers)
      ```
      
      ## JSX (server-side rendering)
      
      `hono/jsx` renders HTML on the server (and works on the client). Configure
      `tsconfig.json`: `"jsx": "react-jsx"`, `"jsxImportSource": "hono/jsx"`, and use a `.tsx`
      file. Components are plain functions typed with `FC`. This is separate from the React
      frontend - use it for server-rendered HTML responses, not as a React replacement.
      
      ```tsx
      import type { FC } from 'hono/jsx'
      const Layout: FC = (props) => <html><body>{props.children}</body></html>
      app.get('/', (c) => c.html(<Layout><h1>Hello</h1></Layout>))
      ```
      
      ## Runtimes & deployment
      
      Same app, different entry point. Pick the matching `create-hono` template.
      
      **Cloudflare Workers** (`export default app`) - develop/deploy with Wrangler (`npm run dev`
      serves on :8787, `npm run deploy`). Bindings (KV/D1/R2/secrets) come in via `c.env`; type
      them with the `Bindings` generic and generate types with `wrangler types`.
      
      **Node.js** - needs the adapter `@hono/node-server`:
      
      ```ts
      import { serve } from '@hono/node-server'
      import { serveStatic } from '@hono/node-server/serve-static'
      const app = new Hono()
      app.use('/static/*', serveStatic({ root: './' }))
      serve({ fetch: app.fetch, port: 3000 }, (info) => console.log(info.port))
      // graceful shutdown:
      const server = serve(app)
      process.on('SIGINT', () => { server.close(); process.exit(0) })
      ```
      
      **Bun** - `export default { port: 3000, fetch: app.fetch }`. **Deno** -
      `Deno.serve(app.fetch)`; import from `jsr:@hono/hono` and keep all hono imports on one
      version. **Vercel / Netlify / AWS Lambda / Lambda@Edge / Fastly / Supabase Edge Functions /
      Next.js** each have a template and a thin adapter; the handler logic is identical.
      
      **Static files** - `serveStatic` is runtime-specific: `hono/cloudflare-workers`,
      `@hono/node-server/serve-static`, `hono/bun`, or `hono/deno`. All take `{ root, path,
      rewriteRequestPath, onFound }`; mount it on a wildcard route (`app.use('/static/*', serveStatic({ root: './' }))`).
      
      ## Realtime (WebSocket)
      
      `upgradeWebSocket()` adds server-side WebSockets, imported from the runtime adapter
      (`hono/cloudflare-workers`, `hono/deno`, `hono/bun`, or `@hono/node-server`). It returns a
      handler that supplies `onOpen`/`onMessage`/`onClose`/`onError` callbacks. WS routes also work
      with RPC: the client gets a typed `client.ws.$ws()`.
      
      ```ts
      // Cloudflare Workers / Deno
      import { upgradeWebSocket } from 'hono/cloudflare-workers'
      const wsApp = app.get('/ws', upgradeWebSocket((c) => ({
        onMessage(event, ws) { ws.send(`echo: ${event.data}`) },
        onClose() { console.log('closed') },
      })))
      export type WsApp = typeof wsApp   // hc<WsApp>(...).ws.$ws() on the client
      
      // Bun: export `{ fetch: app.fetch, websocket }` (import websocket from 'hono/bun')
      // Node: install `ws`; pass a WebSocketServer to serve({ fetch, websocket: { server: wss } })
      ```
      
      Gotcha: `onOpen` is not supported on Cloudflare Workers, and header-modifying middleware
      (e.g. CORS) on a WS route throws "immutable headers" because `upgradeWebSocket` sets headers
      internally - keep such middleware off WS routes.
      
      ## Testing
      
      `app.request()` runs the app in-process against a Web `Request` - no server needed, works on
      every runtime. Pass mock bindings as the 3rd arg.
      
      ```ts
      import { describe, it, expect } from 'vitest'
      
      describe('api', () => {
        it('GET /posts', async () => {
          const res = await app.request('/posts')
          expect(res.status).toBe(200)
        })
      
        it('POST /posts (json)', async () => {
          const res = await app.request('/posts', {
            method: 'POST',
            body: JSON.stringify({ message: 'hi' }),
            headers: { 'Content-Type': 'application/json' }, // required for json validators
          })
          expect(res.status).toBe(201)
        })
      
        it('uses mock env', async () => {
          const res = await app.request('/posts', {}, { DB: mockD1, API_HOST: 'example.com' })
        })
      })
      ```
      
      For a typed test client mirroring the RPC client, use `testClient(app)` from `hono/testing`.
      On Cloudflare Workers, Cloudflare recommends `@cloudflare/vitest-pool-workers`. (This skill's
      [vitest.md](vitest.md) covers the runner itself.)
      
      ## Best practices
      
      1. **Write handlers inline after the path** - `app.get('/books/:id', (c) => ...)`. A separate
         `const handler = (c: Context) => ...` loses path-param inference. If you must extract,
         use `factory.createHandlers()`.
      2. **Chain routes** (`.get(...).post(...)`) and export `typeof app` - that's what makes RPC
         types work. Split large apps with `.route()`, chaining the mounts.
      3. **Let the compiler accumulate types** via chained `.use()` instead of hand-writing a
         combined `Env`. Reserve `ContextVariableMap` for truly app-wide middleware.
      4. **Always specify the status code** in `c.json(data, status)` on routes the client or
         OpenAPI consumes - the status is part of the inferred/documented type.
      5. **Avoid `c.notFound()`** on RPC routes; return `c.json({ error }, 404)`.
      6. **Order matters**: middleware and specific routes before wildcards; CORS before routes.
      7. **Set `Content-Type`** on `json`/`form` requests (including tests) or the body is `{}`.
      8. **Keep Hono one version** across client/server, and compile the RPC client type
         (`hcWithType`) once the route count grows, to keep the IDE fast.
      9. **Pick the right preset/router**: `hono` (default, `SmartRouter` = fast + full features),
         `hono/quick` (fast registration, good for per-request init like some edges), `hono/tiny`
         (smallest). Override with `new Hono({ router: new RegExpRouter() })` only if needed.
      
      ## Resources
      
      **LLM-optimized docs (fetch these first when this file falls short):**
      - Full docs, one file: https://hono.dev/llms-full.txt
      - Core-only / smaller: https://hono.dev/llms-small.txt
      - Doc-page index: https://hono.dev/llms.txt
      
      **Official docs:**
      - Home / getting started: https://hono.dev/docs/
      - API reference - App: https://hono.dev/docs/api/hono - Context:
        https://hono.dev/docs/api/context - Request: https://hono.dev/docs/api/request - Routing:
        https://hono.dev/docs/api/routing
      - Guides - RPC: https://hono.dev/docs/guides/rpc - Validation:
        https://hono.dev/docs/guides/validation - Middleware:
        https://hono.dev/docs/guides/middleware - Testing: https://hono.dev/docs/guides/testing -
        Best practices: https://hono.dev/docs/guides/best-practices - JSX:
        https://hono.dev/docs/guides/jsx
      - Built-in middleware index: https://hono.dev/docs/middleware/builtin/basic-auth
      - Helpers (cookie, jwt, streaming, factory, websocket, ...): https://hono.dev/docs/helpers/cookie
      - Third-party middleware catalog: https://hono.dev/docs/middleware/third-party
      
      **Repos & packages:**
      - Core: https://github.com/honojs/hono
      - Official middleware monorepo (47 packages incl. zod-validator, zod-openapi, swagger-ui,
        clerk-auth, oauth-providers, otel, mcp, trpc-server): https://github.com/honojs/middleware
      - Examples (basic, blog, durable-objects, nextjs-stack, pages-stack, jsx-ssr):
        https://github.com/honojs/examples
      - Node.js adapter: https://github.com/honojs/node-server - Scaffolder:
        https://github.com/honojs/create-hono
      
      **Key ecosystem packages:** `@hono/zod-validator`, `@hono/standard-validator`,
      `@hono/zod-openapi` + `@hono/swagger-ui` (OpenAPI), `@hono/node-server` (Node), Zod
      (https://zod.dev), Valibot (https://valibot.dev), ArkType (https://arktype.io), Standard
      Schema (https://standardschema.dev).
      </content>
      </invoke>
      
    • react.md 8.1 KB
      # React 19
      
      Patterns for type-safe React 19.2 components. The headline shift from older React: **the React Compiler handles memoization**, `ref` is a normal prop, and `use()` reads context and promises without the old hook-placement rules. Write plain components and let the tooling optimize. For TypeScript specifics (props typing, generics, tsconfig) see [typescript.md](typescript.md).
      
      ## Critical rules
      
      ### `ref` is a prop - no `forwardRef`
      
      ```tsx
      // React 19: ref is a regular prop
      function Input({ ref, ...props }: React.ComponentProps<"input"> & { ref?: React.Ref<HTMLInputElement> }) {
        return <input ref={ref} {...props} />
      }
      ```
      
      `React.ComponentProps<"input">` already includes `ref` in React 19's types, so for plain DOM-wrapping components you usually just destructure `ref` from props without declaring it.
      
      ### No manual memoization
      
      The compiler auto-memoizes return values, expensive computations, and callbacks. Drop `memo`, `useMemo`, `useCallback` from the common path:
      
      ```tsx
      // Plain code - compiler memoizes sorting, the callback, and the JSX
      function List({ items, onSelect }: { items: Item[]; onSelect: (id: string) => void }) {
        const sorted = items.toSorted(compare)
        return sorted.map((item) => <Row key={item.id} onClick={() => onSelect(item.id)} />)
      }
      ```
      
      ### Extend native element props
      
      ```tsx
      type ButtonProps = React.ComponentProps<"button"> & { variant?: "primary" | "ghost" }
      ```
      
      ### `use()` over `useContext()`
      
      `use()` can read context after early returns and inside conditionals - `useContext` cannot. Pair it with a factory hook that throws on a missing provider so consumers never null-check:
      
      ```tsx
      const AuthContext = createContext<AuthState | null>(null)
      
      function useAuth(): AuthState {
        const ctx = use(AuthContext)
        if (ctx === null) throw new Error("useAuth must be used within AuthProvider")
        return ctx
      }
      ```
      
      ## React 19 patterns
      
      ### Component authoring
      
      Plain functions with `data-slot` for styling hooks (the shadcn convention). No `forwardRef`, no `FC`:
      
      ```tsx
      function Card({ className, ...props }: React.ComponentProps<"div">) {
        return <div data-slot="card" className={cn("rounded-xl border bg-card", className)} {...props} />
      }
      ```
      
      ### Actions
      
      Async transitions handle pending state, errors, and form resets. `useActionState` for forms:
      
      ```tsx
      function UpdateProfile({ userId }: { userId: string }) {
        const [error, submitAction, isPending] = useActionState(
          async (_prev: string | null, formData: FormData) => {
            const result = await updateProfile(userId, formData)
            return result.error ?? null
          },
          null
        )
        return (
          <form action={submitAction}>
            <input name="displayName" required />
            <button type="submit" disabled={isPending}>{isPending ? "Saving..." : "Save"}</button>
            {error && <p className="text-destructive">{error}</p>}
          </form>
        )
      }
      ```
      
      `useTransition` for non-form Actions; `useOptimistic` for instant feedback:
      
      ```tsx
      const [isPending, startTransition] = useTransition()
      // onClick={() => startTransition(async () => { await onDelete() })}
      
      const [optimisticLikes, addOptimisticLike] = useOptimistic(likes, (prev) => prev + 1)
      ```
      
      ### `use()` hook
      
      Reads promises (suspends until resolved) and context, conditionally. The promise must come from a loader/cache, **not** be created during render:
      
      ```tsx
      function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
        const comments = use(commentsPromise) // parent wraps this in <Suspense>
        return <ul>{comments.map((c) => <li key={c.id}>{c.text}</li>)}</ul>
      }
      ```
      
      ### Activity (19.2, stable)
      
      Preserve the state of hidden UI. Hidden children keep state and DOM but unmount effects. The API is `mode="visible" | "hidden"`:
      
      ```tsx
      {tabs.map((tab) => (
        <Activity key={tab.id} mode={activeTab === tab.id ? "visible" : "hidden"}>
          <tab.component />
        </Activity>
      ))}
      ```
      
      `hidden` hides via `display: none`, cleans up effects, preserves state, and pre-renders children at low priority for faster reveals. DOM side effects (video/audio) persist when hidden - add `useLayoutEffect` cleanup if needed.
      
      ### useEffectEvent (19.2, stable)
      
      Extract non-reactive logic from effects. The event function always sees the latest props/state without being an effect dependency:
      
      ```tsx
      function ChatRoom({ roomId, theme }: { roomId: string; theme: string }) {
        const onConnected = useEffectEvent(() => showNotification("Connected!", theme))
        useEffect(() => {
          const conn = createConnection(roomId)
          conn.on("connected", () => onConnected())
          conn.connect()
          return () => conn.disconnect()
        }, [roomId]) // theme is NOT a dep - it's read via the effect event
      }
      ```
      
      Rules: only call from inside effects/other effect events, never pass to children or list in dependency arrays, never call during render. (`useExhaustiveDependencies` in Biome and the react-hooks ESLint rule both understand it - upgrade to the latest plugin version.)
      
      ### Document metadata
      
      Render `<title>`, `<meta>`, `<link>` directly in components - React hoists them to `<head>`:
      
      ```tsx
      <title>{post.title}</title>
      <meta name="description" content={post.excerpt} />
      <link rel="canonical" href={`https://example.com/posts/${post.slug}`} />
      ```
      
      ### Context as provider, ref cleanup
      
      ```tsx
      <ThemeContext value="dark">{children}</ThemeContext>   // no .Provider
      
      <div ref={(node) => {
        const observer = new ResizeObserver(handleResize)
        if (node) observer.observe(node)
        return () => observer.disconnect()                    // cleanup return
      }} />
      ```
      
      ## React Compiler
      
      `babel-plugin-react-compiler` (stable **1.0**) analyzes code at build time and inserts memoization, replacing manual `useMemo`/`useCallback`/`memo` in most cases.
      
      ### Setup with Vite
      
      `@vitejs/plugin-react` v6 **removed** the inline `babel` option, so the compiler runs through `@rolldown/plugin-babel`:
      
      ```ts
      import react, { reactCompilerPreset } from '@vitejs/plugin-react'
      import babel from '@rolldown/plugin-babel'
      
      plugins: [react(), babel({ presets: [reactCompilerPreset()] })]
      ```
      
      ```bash
      pnpm add -D --save-exact babel-plugin-react-compiler
      pnpm add -D @rolldown/plugin-babel @babel/core @types/babel__core
      ```
      
      `reactCompilerPreset({ compilationMode: 'annotation' })` compiles only components marked `"use memo"`; `target: '17' | '18'` supports older React (needs `react-compiler-runtime`).
      
      ### ESLint integration
      
      The compiler's lint rules now ship **inside `eslint-plugin-react-hooks`** (current major v7, flat config by default, rules in the `recommended` preset). The standalone `eslint-plugin-react-compiler` is merged in - remove it if present. New compiler-powered rules catch things like `setState` in render (`set-state-in-render`).
      
      ### What not to do
      
      ```tsx
      // Don't - the compiler handles all of this
      const Memo = memo(MyComponent)
      const value = useMemo(() => expensive(data), [data])
      const cb = useCallback(() => handler(id), [id])
      ```
      
      Manual memoization still applies when you need a **stable value as an effect dependency**, or a value shared across many components (the compiler memoizes per-component). Opt a component out with the `"use no memo"` directive. Optimized components show a "Memo ✨" badge in React DevTools.
      
      ## Worth knowing (newer surface)
      
      - **Partial Pre-rendering (19.2, stable)**: pre-render the static shell of a page, then finish it at request time. New react-dom APIs `prerender` (produce a prelude + a resumable state) and `resume`/`resumeToPipeableStream`/`resumeAndPrerender` continue rendering where the prerender left off. This is a framework/SSR-layer feature - reach for it through your framework, not hand-wired in an SPA.
      - **`<ViewTransition>`** is **Canary/Experimental only** in 19.2 - do not ship it as a stable API.
      - **`cacheSignal`** (RSC) tells you when a `cache()` lifetime is over.
      - **`captureOwnerStack()`** (dev-only) returns the component owner stack for better debugging.
      - **Performance Tracks**: React 19.2 adds Scheduler/Components tracks to Chrome DevTools profiles.
      - **`useDeferredValue`** gained an `initialValue` option.
      
      ## Resources
      
      - React 19.2 blog: https://react.dev/blog/2025/10/01/react-19-2
      - React Compiler: https://react.dev/learn/react-compiler
      - Compiler 1.0: https://react.dev/blog/2025/10/07/react-compiler-1
      - API reference: https://react.dev/reference/react
      
    • shadcn.md 6.9 KB
      # shadcn/ui
      
      Copy-in component patterns built on Tailwind v4 and a primitive library (Radix UI or Base UI). shadcn/ui is not a dependency you import - the CLI (latest `4.12.0`) writes component source into your project, which you then own and edit. For the Tailwind layer (theming, tokens) see [tailwind.md](tailwind.md).
      
      ## Component authoring pattern
      
      The canonical shadcn component is a **plain function** with `data-slot` for styling hooks, native props via `React.ComponentProps`, and variants via CVA. No `forwardRef` (React 19 - `ref` is a prop). Recent components also expose `data-variant`/`data-size` and a wider size scale.
      
      ```tsx
      import { cva, type VariantProps } from "class-variance-authority"
      import { cn } from "@/lib/utils"
      
      const buttonVariants = cva(
        "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50",
        {
          variants: {
            variant: {
              default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
              destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
              outline: "border border-input bg-background hover:bg-accent",
              ghost: "hover:bg-accent hover:text-accent-foreground",
              link: "text-primary underline-offset-4 hover:underline",
            },
            size: { default: "h-9 px-4 py-2", sm: "h-8 px-3", lg: "h-10 px-8", icon: "size-9" },
          },
          defaultVariants: { variant: "default", size: "default" },
        }
      )
      
      function Button({
        className, variant, size, ...props
      }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>) {
        return (
          <button
            data-slot="button"
            data-variant={variant}
            className={cn(buttonVariants({ variant, size }), className)}
            {...props}
          />
        )
      }
      ```
      
      The `cn` helper merges class names with conflict resolution:
      
      ```ts
      // src/lib/utils.ts
      import { clsx, type ClassValue } from "clsx"
      import { twMerge } from "tailwind-merge"
      export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }
      ```
      
      ## CLI
      
      ```bash
      pnpm dlx shadcn@latest init        # scaffold config + tokens + lib/utils
      pnpm dlx shadcn@latest add button card form   # add components
      pnpm dlx shadcn@latest add button --overwrite # update an existing component
      pnpm dlx shadcn@latest add button --dry-run   # preview what add would write, no changes
      pnpm dlx shadcn@latest add button --diff      # show a diff against your current files
      pnpm dlx shadcn@latest add button --view      # print the item's source without writing
      ```
      
      `--dry-run`, `--diff`, and `--view` (CLI 4.12) let you inspect an item before it touches your tree - useful before overwriting a component you've edited.
      
      `init` adds `@import "shadcn/tailwind.css"` to your global CSS (custom variants like `data-open:` / `data-closed:` and utilities like `no-scrollbar`). `shadcn eject` inlines that file if you want full control.
      
      - **`create` is an alias of `init`** (not a separate command). `--defaults` resolves to `--template=next --preset=nova`.
      - **`--base radix | base`** chooses the primitive library at init; docs are split per base.
      - **`--pointer`** opts into Tailwind v4's `cursor: default` button behavior (v4 switched buttons away from `cursor: pointer`).
      - **`--rtl`** / `migrate rtl` set up right-to-left support (rewrites `ml-4` -> `ms-4`, `text-left` -> `text-start`); `components.json` carries `rtl: true`.
      - **`shadcn docs`** fetches component documentation/API for an agent to read; `--base` selects radix or base.
      
      ### Registries
      
      Add from community/private registries, including any public GitHub repo as a **source registry** (no build step):
      
      ```bash
      pnpm dlx shadcn@latest add @acme/button          # named registry
      pnpm dlx shadcn@latest add username/repo/item     # GitHub source registry
      pnpm dlx shadcn@latest registry validate          # validate before publishing
      ```
      
      Discover items with `search` (aliased as `list`). The registries arg is optional - omit it to
      search every registry in `components.json`:
      
      ```bash
      pnpm dlx shadcn@latest search @acme -q button -t ui   # filter by type: ui, block, hook (CSV)
      pnpm dlx shadcn@latest search --json                  # machine-readable output
      ```
      
      `--type`/`-t` and `--json` are new in CLI 4.11; before 4.11 `search` always printed JSON, so if
      you script it, pass `--json` explicitly now that the default is human-readable.
      
      Presets are encoded codes that rewrite component code (not just colors): `shadcn preset decode <code>`, `shadcn apply <code> --only theme`.
      
      ### Visual styles
      
      `init`/`create` offers built-in visual styles that rewrite component code (not only CSS variables): **Vega** (classic), **Nova** (compact), **Maia** (soft/rounded), **Lyra** (boxy/sharp, mono fonts), **Mira** (dense), plus newer **Luma** and **Rhea**. Available for both Radix and Base UI.
      
      ### MCP server
      
      ```bash
      pnpm dlx shadcn@latest mcp init    # exposes registry/search/add to an AI client
      ```
      
      ### Recent additions (4.12)
      
      - **Chat-interface component family** - `MessageScroller`, `Message`, `Bubble`, `Attachment`, and `Marker` for building chat UIs.
      - **`@shadcn/react`** - a new package of unstyled, headless primitives (first one shipped: `@shadcn/react/message-scroller`) for when you want behavior without the styled shadcn layer.
      - **`scroll-fade` and `shimmer`** CSS utilities added to the shadcn utility set.
      
      ## Primitives: Radix vs Base UI
      
      Both are fully supported and selectable at init. Radix now uses the **unified `radix-ui` package** (not per-component `@radix-ui/react-*`):
      
      ```tsx
      import { Slot } from "radix-ui"
      ```
      
      `migrate radix` rewrites old `@radix-ui/react-*` imports to the unified package. Base UI is a co-equal rebuild of every component with the same abstraction.
      
      ## Common patterns
      
      ### Card
      
      ```tsx
      <div className="rounded-xl border bg-card text-card-foreground shadow">
        <div className="flex flex-col space-y-1.5 p-6">
          <h3 className="font-semibold leading-none tracking-tight">Title</h3>
          <p className="text-sm text-muted-foreground">Description</p>
        </div>
        <div className="p-6 pt-0">Content</div>
      </div>
      ```
      
      ### Form field (with Zod)
      
      Pair shadcn form components with React Hook Form + Zod, or React 19 Actions + `useActionState` for server-driven validation (see [react.md](react.md) and [typescript.md](typescript.md) for the Zod v4 pattern).
      
      ### Accessibility
      
      ```tsx
      <button className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
      <span className="sr-only">Close dialog</span>
      <button className="disabled:cursor-not-allowed disabled:opacity-50" disabled>
      ```
      
      ## Troubleshooting
      
      - **Dark mode flash:** add `suppressHydrationWarning` to `<html>`; ensure the theme provider uses `attribute="class"`.
      - **Component overwritten on `add`:** you own the file - re-running `add` without `--overwrite` skips existing files.
      
      ## Resources
      
      - Docs: https://ui.shadcn.com/docs - CLI: https://ui.shadcn.com/docs/cli
      - Changelog: https://ui.shadcn.com/docs/changelog - Registries: https://ui.shadcn.com/docs/registry
      
    • tailwind.md 6.3 KB
      # Tailwind CSS v4
      
      Utility-first styling, CSS-first configuration. Tailwind **v4.3** (latest `4.3.2`) configures everything in CSS - there is no `tailwind.config.js`. In a Vite project the integration is the `@tailwindcss/vite` plugin (no PostCSS config). For shadcn/ui component authoring see [shadcn.md](shadcn.md).
      
      ## CSS-first configuration
      
      Everything lives in your CSS entry. There is no JS/TS config file - migrate any old config into CSS:
      
      - `theme.extend.colors` -> `@theme { --color-*: ... }`
      - `plugins` -> `@plugin "..."` or `@utility`
      - `content` -> `@source "..."`
      - `tailwindcss-animate` -> `@import "tw-animate-css"`
      - `@layer utilities` -> `@utility name { ... }`
      
      ```css
      @import "tailwindcss";
      
      @utility tab-highlight-none { -webkit-tap-highlight-color: transparent; }
      @custom-variant pointer-fine (@media (pointer: fine));
      @source not "./legacy";
      ```
      
      The `@import "tailwindcss";` line is mandatory - the Vite plugin alone emits nothing without it. A missing import is the classic "Tailwind produces no styles" bug.
      
      ## Theming with CSS variables
      
      shadcn/ui maps semantic CSS variables to Tailwind utilities. Define variables under `:root` / `.dark`, then bridge them with `@theme inline`:
      
      ```css
      :root {
        --background: oklch(1 0 0);
        --foreground: oklch(0.145 0 0);
        --primary: oklch(0.205 0 0);
        --primary-foreground: oklch(0.985 0 0);
        --muted: oklch(0.97 0 0);
        --border: oklch(0.922 0 0);
        --radius: 0.5rem;
      }
      .dark {
        --background: oklch(0.145 0 0);
        --foreground: oklch(0.985 0 0);
        --primary: oklch(0.922 0 0);
        --primary-foreground: oklch(0.205 0 0);
      }
      
      @theme inline {
        --color-background: var(--background);
        --color-primary: var(--primary);
        --color-primary-foreground: var(--primary-foreground);
      }
      ```
      
      **`@theme` vs `@theme inline`:** plain `@theme` defines static tokens (overridable by plugins); `@theme inline` references CSS variables so the utility *follows* dark-mode changes. Use `inline` whenever a token points at a `var(--...)` that flips between `:root` and `.dark`.
      
      ## Critical rules
      
      ### Semantic tokens, paired foreground
      
      ```tsx
      <div className="bg-primary text-primary-foreground">    // respects theme + dark mode
      <div className="bg-blue-500 text-white">                // breaks theming - avoid
      ```
      
      Always pair `bg-*` with the matching `text-*-foreground`. Background utilities omit the `-background` suffix (`bg-muted text-muted-foreground`).
      
      ### Never build class names dynamically
      
      ```tsx
      <div className={`bg-${color}-500`}>                      // scanner can't see it - no CSS emitted
      const map = { red: "bg-red-500", blue: "bg-blue-500" } as const
      <div className={map[color]}>                             // complete literal strings
      ```
      
      ### `cn()` merge order
      
      Defaults first, consumer `className` last, so tailwind-merge's last-wins lets callers override:
      
      ```tsx
      className={cn(buttonVariants({ variant, size }), className)}   // correct
      ```
      
      ### Transition only what changes
      
      `transition-all` thrashes layout. Name the properties, and respect reduced motion:
      
      ```tsx
      <div className="transition-colors duration-200">
      <div className="motion-safe:animate-fade-in">
      ```
      
      ## Layout and responsiveness
      
      Mobile-first breakpoints; container queries are first-class in v4 (no plugin):
      
      ```tsx
      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
      <div className="@container">
        <div className="grid gap-4 @sm:grid-cols-2 @lg:grid-cols-3">  // responds to container, not viewport
      </div>
      ```
      
      Dark mode: prefer semantic colors (auto-flip) over manual `dark:` overrides. Use `next-themes` for the toggle (`attribute="class"`), and add `suppressHydrationWarning` to `<html>` to avoid a flash.
      
      ## Version-specific features
      
      ### v4.1 / v4.2
      
      ```tsx
      <h1 className="text-shadow-sm">                          // text shadows (4.1)
      <div className="mask-linear-to-b">                       // gradient masks (4.1)
      <input className="user-valid:border-success user-invalid:border-destructive" /> // (4.1)
      <div className="pbs-4 pbe-8 mbs-2 border-bs-2">          // logical block props (4.2)
      <div className="bg-mauve-100 text-olive-900">            // new palettes: mauve/olive/mist/taupe (4.2)
      ```
      
      The positioning utilities `start-*`/`end-*` are **deprecated** in favor of `inset-s-*`/`inset-e-*` (don't confuse with logical padding `ps-*`/`pe-*`, which are fine).
      
      ### v4.3 (current)
      
      ```tsx
      <div className="scrollbar-thin scrollbar-thumb-muted scrollbar-gutter-stable"> // scrollbar utils
      <div className="@container-size">                        // size container for cqb/cqh units
      <img className="zoom-110">                               // CSS zoom utilities
      <pre className="tab-4">                                  // tab-size
      ```
      
      In CSS you can now stack and group `@variant`: `@variant hover:focus { ... }` and `@variant hover, focus { ... }`, and pass `--default(...)` to `--value(...)`/`--modifier(...)` in custom functional utilities.
      
      ### Animations
      
      ```css
      @import "tw-animate-css";
      ```
      
      ```tsx
      <div className="animate-fade-in">
      ```
      
      ## OKLCH colors
      
      shadcn/ui colors use `oklch(lightness chroma hue)`: lightness 0-1, chroma 0-0.4 (0 = gray), hue 0-360. OKLCH gives perceptually uniform lightness, so dark-mode variants are easy to derive by adjusting L. Base neutral palettes: Neutral, Zinc, Slate, Stone, Gray.
      
      ## Notes
      
      - A first-class `@tailwindcss/webpack` loader exists (added v4.2) for Next.js/webpack/Turbopack projects - relevant if you're not on Vite.
      - If you lint CSS with Biome, enable `css.parser.tailwindDirectives` so it understands `@theme`/`@utility`/`@apply` (see [biome.md](biome.md)).
      
      ## Troubleshooting
      
      - **Colors not updating:** confirm the variable is in your CSS, `@theme inline` includes the mapping, then clear the build cache.
      - **`tailwind.config.js` present:** delete it; run `npx @tailwindcss/upgrade` to migrate to CSS-first.
      - **Classes not detected:** check `@source` covers your component paths and that no class name is constructed dynamically.
      - **A custom-token utility renders nothing:** a class like `bg-brand` whose token is not mapped under `@theme`/`@theme inline` emits no CSS and no error - tsc, Biome, and the Vite build all stay green. Cross-check the utility against your mapped tokens; a typo'd or unmapped token fails silently.
      
      ## Resources
      
      - Docs: https://tailwindcss.com/docs - v4.3 blog: https://tailwindcss.com/blog/tailwindcss-v4-3
      - Vite plugin: https://tailwindcss.com/docs/installation/using-vite
      
    • typescript.md 7.6 KB
      # TypeScript 6.0
      
      Strict TypeScript for React. **TS 6.0** (latest `6.0.3`) is a deliberate bridge release: "the last release based on the current JavaScript codebase," pre-staging the breaking changes that the Go-native rewrite (tsgo / TS 7.0) will enforce. Target 6.0 today - it is stable and shippable, and most of its new defaults bake in settings you used to set by hand.
      
      ## What changed in 6.0 (and why your tsconfig shrinks)
      
      Several flags the old hand-tuned React tsconfig set manually are now **defaults**, so you delete them:
      
      - `strict` is **on by default**.
      - `noUncheckedSideEffectImports` is **on by default**.
      
      Two new defaults will **break builds** if ignored:
      
      - `types` now defaults to `[]`. Ambient `@types/*` no longer leak in globally - add what you need explicitly (`"types": ["node"]`).
      - `module` defaults to `esnext` and `target` to a floating current-year ES version (currently `es2025`); they no longer default to `nodenext`. Pick deliberately per project type (below).
      - `rootDir` now defaults to the tsconfig directory rather than being inferred from inputs - set it explicitly for non-trivial layouts.
      
      Deprecations and removals to migrate off:
      
      - `baseUrl` is **deprecated** - use prefixed `paths` (`"@/*": ["./src/*"]`) only.
      - `moduleResolution: classic` is **removed**; `node`/`node10` is deprecated. Use `bundler` or `nodenext`.
      - `esModuleInterop` and `allowSyntheticDefaultImports` can no longer be set to `false` (safe interop is always on).
      - `target: es5` is deprecated (lowest target is now ES2015); `downlevelIteration` errors.
      - `--module amd|umd|systemjs|none` and `--outFile` are removed.
      - Import-assertion `assert {}` syntax errors - use import-attributes `with {}`.
      - Legacy `module Foo {}` namespace syntax is a hard error - use `namespace`.
      
      You can silence 6.0 deprecation errors temporarily with `"ignoreDeprecations": "6.0"`, but TS 7.0 removes the flags outright - treat it as a migration window, not a fix.
      
      ## Strict tsconfig for a Vite React app
      
      ```jsonc
      {
        "compilerOptions": {
          // strict, noUncheckedSideEffectImports: ON by default in 6.0
          "target": "es2023",
          "module": "preserve",
          "moduleResolution": "bundler",
          "moduleDetection": "force",
          "jsx": "react-jsx",
          "verbatimModuleSyntax": true,
          "isolatedModules": true,
          "erasableSyntaxOnly": true,
          "noUncheckedIndexedAccess": true,
          "exactOptionalPropertyTypes": true,
          "skipLibCheck": true,
          "types": [],
          "paths": { "@/*": ["./src/*"] }
        }
      }
      ```
      
      **`bundler` vs `nodenext`.** For code a bundler consumes (a Vite app), `module: preserve` + `moduleResolution: bundler` is correct - it lets you write extensionless imports and leaves module syntax for Vite/Rolldown. For code Node runs directly (scripts, a server entry), use `module: nodenext` (which sets resolution to match) and write real `.js` extensions on relative imports.
      
      **`erasableSyntaxOnly`** (since 5.8) forbids TS constructs that emit runtime code (enums, parameter properties, namespaces with values), so your `.ts` files are pure type-erasable. This is what makes **Node's native type stripping** - now stable (Node 24.12 / 25.2) - work: Node can run `.ts` directly when paired with `erasableSyntaxOnly` + `verbatimModuleSyntax`. Keep it on for portability.
      
      ## Patterns
      
      ### Component props
      
      ```tsx
      type ButtonProps = React.ComponentProps<"button"> & { variant?: "primary" | "secondary"; isLoading?: boolean }
      
      // Polymorphic "as" prop
      type PolymorphicProps<E extends React.ElementType> = { as?: E } & Omit<React.ComponentProps<E>, "as">
      function Text<E extends React.ElementType = "span">({ as, ...props }: PolymorphicProps<E>) {
        const Component = as || "span"
        return <Component {...props} />
      }
      ```
      
      ### Discriminated unions over booleans
      
      Make impossible states unrepresentable:
      
      ```tsx
      type AsyncState<T> =
        | { status: "idle" }
        | { status: "loading" }
        | { status: "error"; error: Error }
        | { status: "success"; data: T }
      ```
      
      A `switch` over `status` with a `never` default gives exhaustiveness checking.
      
      ### `satisfies` for config literals
      
      Preserves literal types while validating shape (unlike a `Record<string, T>` annotation, which widens):
      
      ```tsx
      const routes = {
        home: { path: "/" },
        about: { path: "/about" },
      } satisfies Record<string, { path: string }>
      routes.home // autocompletes
      ```
      
      ### Hook and event types
      
      ```tsx
      const [user, setUser] = useState<User | null>(null)          // explicit for null init
      const inputRef = useRef<HTMLInputElement>(null)               // React 19: RefObject<T | null>
      const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => e.preventDefault()
      ```
      
      Reducers use a discriminated-union action type; `useReducer(reducer, initial)` infers the rest.
      
      ### Generic components
      
      ```tsx
      function Select<T>({ items, value, onChange, getKey, getLabel }: {
        items: T[]; value: T; onChange: (item: T) => void; getKey: (item: T) => string; getLabel: (item: T) => string
      }) {
        return (
          <select value={getKey(value)} onChange={(e) => {
            const item = items.find((i) => getKey(i) === e.target.value)
            if (item) onChange(item)
          }}>
            {items.map((item) => <option key={getKey(item)} value={getKey(item)}>{getLabel(item)}</option>)}
          </select>
        )
      }
      ```
      
      ### `import defer` (TS 5.9+)
      
      Defers module evaluation until first property access - useful for heavy, conditionally-used modules. Namespace imports only, and it is not downleveled, so it requires `module: preserve | esnext` and a runtime/bundler that supports it:
      
      ```tsx
      import defer * as heavy from "./heavy-feature.js"
      // heavy.* not evaluated until first access
      ```
      
      ### Zod v4 validation
      
      ```tsx
      import { z } from "zod"
      const UserSchema = z.object({ name: z.string().min(1), email: z.email() })
      type User = z.infer<typeof UserSchema>
      
      const result = UserSchema.safeParse(Object.fromEntries(formData))
      if (!result.success) {
        const flat = z.flattenError(result.error) // Zod v4 field-level errors
        return flat.fieldErrors
      }
      ```
      
      ## tsgo / TypeScript 7
      
      The native (Go) compiler - "about 10 times faster than TypeScript 6.0" - is now at **Release Candidate** (`npm i -D typescript@rc`, `tsc` drop-in), with the team planning to "release TypeScript 7.0 within the next month." The type-checking logic is a methodical port of 6.0 and is "structurally identical," so results match; the remaining gap is a stable programmatic API (deferred to 7.1). Try it on real CI/editor workflows today.
      
      - **Side-by-side with 6.0:** 7.0 ships its own `tsc`; the compat package `@typescript/typescript6` provides a `tsc6` binary and re-exports the 6.0 API. Because tools like typescript-eslint import `typescript` directly, coexist via npm aliases: `"typescript": "npm:@typescript/typescript6@^6.0.0"` plus `"typescript-7": "npm:typescript@rc"`. Nightlies still publish as `@typescript/native-preview` (binary `tsgo`).
      - **Parallelism controls:** `--checkers` (default 4 type-check workers), `--builders` (parallel project-reference builds), and `--singleThreaded` (for debugging or resource-limited CI). Watch mode was rebuilt on a Go port of Parcel's file-watcher.
      - **7.0 hardens 6.0's deprecations into errors:** `target: es5`, `downlevelIteration`, `moduleResolution: node/node10/classic`, `module: amd/umd/systemjs/none`, and `baseUrl` are no longer supported; `esModuleInterop`/`allowSyntheticDefaultImports`/`alwaysStrict` cannot be `false`. Adopting 6.0's defaults now makes the 7.0 jump a no-op.
      
      ## Resources
      
      - TS 6.0 announcement: https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/
      - TS 7.0 RC: https://devblogs.microsoft.com/typescript/announcing-typescript-7-0-rc/ - tsgo / TS 7: https://github.com/microsoft/typescript-go
      - Release notes: https://www.typescriptlang.org/docs/handbook/release-notes/
      
    • vite.md 12.6 KB
      # Vite 8
      
      Build tooling and dev server. Vite 8 (stable, latest `8.1.2`) ships **Rolldown** - a Rust-based bundler from the Vite team - as its single default bundler, replacing both esbuild and Rollup. It is ESM-only and requires Node.js 20.19+ / 22.12+.
      
      ## Vite 8 essentials
      
      - **Rolldown is the default**, no opt-in. "Vite 8 ships with Rolldown as its single, unified, Rust-based bundler." Build times drop dramatically vs the old esbuild+Rollup split.
      - **ESM-only config.** `vite.config.ts` must use `import`/`export`; `require()` is not supported in config files.
      - **Default browser target** is `'baseline-widely-available'`, which in Vite 8 resolves to `['chrome111', 'edge111', 'firefox114', 'safari16.4']` (bumped from Vite 7's 107/107/104/16). Override with `build.target: 'es2022'` or an explicit list.
      - **Default minifiers changed:** JavaScript is minified by **Oxc** (`build.minify` default `'oxc'`), CSS by **Lightning CSS** (`build.cssMinify` default `'lightningcss'`). `build.minify: 'esbuild'` still works but is deprecated and requires installing `esbuild` yourself.
      - **Install grew ~15 MB** vs Vite 7 (Lightning CSS + the Rolldown binary are now regular dependencies).
      
      ## New in Vite 8.1
      
      - **Wasm ESM integration (stable)** - import a `.wasm` file and call its exports directly: `import { add } from './add.wasm'`. No plugin needed.
      - **Experimental Bundled Dev Mode** (`experimental.bundledDev: true` or `--experimental-bundle`) - serves bundled files in dev instead of the classic unbundled server. Aimed at huge apps that suffer from module count (~15x faster startup in a 10k-component test); may not work with all third-party plugins yet.
      - **Experimental Chunk Import Map** (`build.chunkImportMap`) - uses an import map so a changed chunk's hash doesn't cascade new hashes to every importer, improving long-term cache hit rates. Does not compose with `experimental.renderBuiltUrl`.
      - **Lightning CSS as the future default** - Vite is working toward making Lightning CSS the default CSS transformer in the next major. Opt in early with `css: { transformer: 'lightningcss' }`.
      - `import.meta.glob` gained a `caseSensitive` option; `html.additionalAssetSources` lets asset discovery see custom HTML elements/attributes.
      
      ## Configuration
      
      ### SPA with TanStack Router
      
      ```ts
      import { defineConfig } from 'vite'
      import { tanstackRouter } from '@tanstack/router-plugin/vite'
      import react, { reactCompilerPreset } from '@vitejs/plugin-react'
      import babel from '@rolldown/plugin-babel'
      import tailwindcss from '@tailwindcss/vite'
      
      export default defineConfig({
        plugins: [
          tanstackRouter({ autoCodeSplitting: true }), // framework plugin first
          tailwindcss(),
          react(),
          babel({ presets: [reactCompilerPreset()] }),
        ],
        resolve: { alias: { '@': new URL('./src', import.meta.url).pathname } },
      })
      ```
      
      ### Full-stack with TanStack Start + Cloudflare
      
      ```ts
      import { defineConfig } from 'vite'
      import { tanstackStart } from '@tanstack/react-start/plugin/vite'
      import { cloudflare } from '@cloudflare/vite-plugin'
      import react, { reactCompilerPreset } from '@vitejs/plugin-react'
      import babel from '@rolldown/plugin-babel'
      import tailwindcss from '@tailwindcss/vite'
      
      export default defineConfig({
        plugins: [
          cloudflare(),
          tanstackStart(),     // includes the router plugin internally - do NOT add both
          tailwindcss(),
          react(),
          babel({ presets: [reactCompilerPreset()] }),
        ],
      })
      ```
      
      `tanstackStart({ spa: { enabled: true } })` runs SPA mode; `tanstackStart({ prerender: { enabled: true, crawlLinks: true } })` enables SSG.
      
      ### Path aliases
      
      Two options. Manual alias (works everywhere):
      
      ```ts
      resolve: { alias: { '@': new URL('./src', import.meta.url).pathname } }
      ```
      
      Or **new in Vite 8**, let Vite read tsconfig `paths` directly so you don't mirror them:
      
      ```ts
      resolve: { tsconfigPaths: true }
      ```
      
      Caveat: the native resolver does **not** follow tsconfig project references. In a solution-style setup (`tsconfig.json` -> `tsconfig.app.json` via `references`) where the `paths` live in the referenced file, `tsconfigPaths: true` silently fails to resolve `@/*` - fall back to an explicit `resolve.alias` there.
      
      ### Environment variables
      
      Files: `.env`, `.env.local`, `.env.[mode]`, `.env.[mode].local`. Only `VITE_`-prefixed vars are exposed to client code via `import.meta.env`; everything else stays server-side. Built-in constants: `import.meta.env.MODE`, `.DEV`, `.PROD`, `.SSR`, `.BASE_URL`. Type them in `src/vite-env.d.ts`:
      
      ```ts
      interface ImportMetaEnv { readonly VITE_API_URL: string }
      interface ImportMeta { readonly env: ImportMetaEnv }
      ```
      
      On Cloudflare, keep two channels straight: `VITE_`-prefixed `.env` values are statically injected into the **client** bundle at build time, while `.dev.vars` / Worker bindings are **runtime** server env passed to the handler - they are not available to client code, and vice versa.
      
      ## Plugin ecosystem
      
      - **`@vitejs/plugin-react`** (v6) - Fast Refresh + JSX transform. v6 dropped Babel as a dependency (React Refresh runs through Oxc) and **removed the inline `babel` option**; run Babel-based transforms like the React Compiler through `@rolldown/plugin-babel` instead. Place last among framework plugins. v6 requires Vite 8 (use v5 if you must stay on Vite 7).
      - **`@tailwindcss/vite`** - native Tailwind v4 integration, no PostCSS config. API unchanged across v4.x.
      - **`@tanstack/router-plugin/vite`** - file-based routes; `tanstackRouter({ autoCodeSplitting: true })`. Must precede `react()`.
      - **`@tanstack/react-start/plugin/vite`** - full-stack TanStack Start; bundles the router plugin (don't add both).
      - **`@cloudflare/vite-plugin`** - runs Worker code in `workerd` during dev via the Environment API, matching production.
      
      ## Dev server
      
      ### Proxy
      
      ```ts
      server: {
        proxy: {
          '/api': { target: 'http://localhost:8787', changeOrigin: true, rewrite: (p) => p.replace(/^\/api/, '') },
          '/ws': { target: 'ws://localhost:8787', ws: true },
        },
      }
      ```
      
      Proxy and most `server.*` changes are **not** hot-reloaded - restart the dev server after editing them.
      
      ### allowedHosts (tunnels and custom domains)
      
      By default Vite rejects requests whose Host header it doesn't recognize, which surfaces as `Blocked request. This host (...) is not allowed.` when you hit the dev server through ngrok, a custom domain, or a fallback port. Add the host:
      
      ```ts
      server: { allowedHosts: ['.ngrok-free.app', 'dev.example.com'] }
      ```
      
      Setting it to `true` disables the check entirely and is a DNS-rebinding risk - scope it to known hosts.
      
      ### forwardConsole (new in Vite 8)
      
      `server.forwardConsole` forwards browser runtime console output to the Vite server terminal. It defaults to auto - **on when an AI coding agent is detected**, off otherwise - which is handy when an agent is driving the build and can't see the browser console.
      
      ### HMR troubleshooting
      
      | Symptom | Fix |
      |---------|-----|
      | Full reload instead of HMR | Ensure `@vitejs/plugin-react` is loaded and a file exports a single component |
      | HMR not connecting behind a proxy | Set `server.ws.clientPort` (e.g. `443`) |
      | CSS not updating | Confirm `@tailwindcss/vite` is in plugins and `@import "tailwindcss";` is in your CSS entry |
      | Stale chunk after a build | Hard-refresh (`Cmd/Ctrl+Shift+R`) to bust the cached bundle |
      
      The WebSocket knobs (`protocol`/`host`/`port`/`path`/`clientPort`/`timeout`/`server`) moved from `server.hmr.*` to `server.ws.*`. The old `server.hmr.*` keys are deprecated but auto-synced, so existing configs keep working; write new ones under `server.ws`.
      
      ### File warmup
      
      ```ts
      server: { warmup: { clientFiles: ['./src/routes/__root.tsx', './src/components/*.tsx'] } }
      ```
      
      ### `cloudflare:workers` import errors
      
      `Failed to resolve import "cloudflare:workers"` (or `node:*` / `buffer` "externalized for browser compatibility") means Worker-only code is reaching the client graph - common with `createServerFn` + `import { env } from "cloudflare:workers"`, or web3/Solana SDKs that pull Node built-ins. Keep server-only imports in server modules; if a dependency forces it, externalize via `build.rolldownOptions.external`. Note `vite build` validates **all** emitted chunks (even behind dynamic import), so lazy-loading a heavy server chunk alone won't exclude it from the Worker build.
      
      ## Build optimization
      
      ### Code splitting (Rolldown)
      
      The object form of `output.manualChunks` is **removed** in Vite 8 and the function form is deprecated - both will break or warn. Use Rolldown's `codeSplitting` via `build.rolldownOptions` (note: `build.rollupOptions` is now a deprecated alias of `build.rolldownOptions`):
      
      ```ts
      build: {
        rolldownOptions: {
          output: {
            // Rolldown's advanced chunking; see https://rolldown.rs/in-depth/manual-code-splitting
            advancedChunks: {
              groups: [
                { name: 'react-vendor', test: /node_modules\/(react|react-dom)\// },
                { name: 'tanstack', test: /node_modules\/@tanstack\// },
              ],
            },
          },
        },
      }
      ```
      
      Route-based splitting still comes for free with `tanstackRouter({ autoCodeSplitting: true })` - each route becomes its own chunk and shared code is extracted automatically.
      
      ### Build defaults
      
      | Option | Default | Note |
      |--------|---------|------|
      | `build.target` | `baseline-widely-available` | chrome111/edge111/firefox114/safari16.4 |
      | `build.minify` | `'oxc'` (client), `false` (SSR) | Oxc minifier, 30-90x faster than terser |
      | `build.cssMinify` | `'lightningcss'` | set `'esbuild'` to revert (must install esbuild) |
      | `build.sourcemap` | `false` | use `'hidden'` for error tracking without exposing source |
      | `build.assetsInlineLimit` | `4096` | bytes below which assets inline as base64 |
      | `build.cssCodeSplit` | `true` | CSS stays with its async chunk |
      | `build.chunkSizeWarningLimit` | `500` | kB; large web3 deps routinely trip this (informational) |
      
      ### Bundle analysis
      
      ```ts
      import { visualizer } from 'rollup-plugin-visualizer'
      // in plugins, gated to a mode:
      mode === 'analyze' && visualizer({ filename: 'stats.html', open: true, gzipSize: true })
      ```
      
      Vite 8 also ships **Vite DevTools for Rolldown** (`@vitejs/devtools-rolldown`) for analyzing production builds. Run analysis with `pnpm vite build --mode analyze`.
      
      ### Tree shaking
      
      Rolldown tree-shakes unused exports. Help it: use named ESM imports (`import { Button }`, not `import * as UI`), mark side-effect-free packages with `"sideEffects": false`, and avoid barrel files that re-export everything.
      
      ### Chunk load errors after deploy
      
      ```ts
      window.addEventListener('vite:preloadError', (e) => { e.preventDefault(); window.location.reload() })
      ```
      
      Serve `index.html` with `Cache-Control: no-cache` so clients don't hold stale asset references.
      
      ## Migrating Vite 7 to Vite 8
      
      Rolldown is built in, so remove any `rolldown-vite` aliasing. If you're coming straight from stock Vite 7, the team recommends an intermediate hop to isolate Rolldown-specific issues: first alias `vite` to `rolldown-vite` on Vite 7, fix any fallout, then upgrade to Vite 8 and undo the alias.
      
      ```jsonc
      // Vite 7 intermediate step, then drop this for "vite": "^8.0.0"
      { "devDependencies": { "vite": "npm:rolldown-vite@7.2.2" } }
      ```
      
      `rolldown-vite` lives at `github.com/vitejs/rolldown-vite` (now **archived** - it was a technical preview, not an unrelated `nicepkg` repo). Other Vite 8 migration notes: `optimizeDeps.esbuildOptions` -> `optimizeDeps.rolldownOptions`; the `esbuild` config option -> `oxc`; `worker.rollupOptions` -> `worker.rolldownOptions`; consistent CJS interop and dropped format-sniffing resolution may surface edge cases (see https://vite.dev/guide/migration).
      
      ## Environment API
      
      The Environment API (formalized in Vite 6, now in **Release Candidate**) gives each target - browser, Node, edge - its own module graph, plugin pipeline, and build config. Most apps never touch it directly: `@cloudflare/vite-plugin` and `@tanstack/react-start` configure environments for you. Frameworks coordinate multi-environment builds through the `buildApp` builder hook. Direct use is for framework/runtime authors.
      
      ## Deployment
      
      ### Cloudflare Workers (via TanStack Start)
      
      ```jsonc
      // wrangler.jsonc
      {
        "name": "my-app",
        "compatibility_date": "2025-01-01",
        "compatibility_flags": ["nodejs_compat"],
        "main": "./dist/server/index.js",
        "assets": { "directory": "./dist/client" }
      }
      ```
      
      ```bash
      pnpm vite build && pnpm wrangler deploy
      ```
      
      ### Static SPA / SSG
      
      `vite build` produces `dist/` for any static host. For prerendering, enable `tanstackStart({ prerender: { enabled: true, crawlLinks: true } })`.
      
      ## Resources
      
      - Guide: https://vite.dev/guide/ - Vite 8 blog: https://vite.dev/blog/announcing-vite8
      - Migration: https://vite.dev/guide/migration - Build options: https://vite.dev/config/build-options
      - Rolldown: https://rolldown.rs - Cloudflare plugin: https://developers.cloudflare.com/workers/vite-plugin/
      
    • vitest.md 5.4 KB
      # Vitest
      
      The Vite-native test runner. Vitest **4** (latest `4.1.9`) reuses your `vite.config.ts` - same plugins, resolve aliases, and transforms - so tests see the app exactly as the bundler builds it. It requires **Vite >= 6.0.0 and Node >= 20.0.0**, and works with the Vite 8 / Rolldown stack (the old "Vitest 3.2+ for Vite 7" floor is superseded; use Vitest 4.x today).
      
      ## Configuration
      
      Vitest reads `vite.config.ts` by default - put the `test` block there and import `defineConfig` from `vitest/config` (not `vite`) to get typed test options. A separate `vitest.config.ts` is only needed when test settings must diverge from the build config.
      
      ```ts
      // vite.config.ts
      /// <reference types="vitest/config" />
      import { defineConfig } from 'vitest/config'
      import react from '@vitejs/plugin-react'
      
      export default defineConfig({
        plugins: [react()],
        test: {
          globals: true,                    // optional: skip importing test/expect
          environment: 'jsdom',             // 'jsdom' | 'happy-dom' | 'node' | 'edge-runtime'
          setupFiles: ['./src/test/setup.ts'],
          include: ['src/**/*.{test,spec}.{ts,tsx}'],
          css: true,                        // process CSS imports per Vite rules
          coverage: {
            provider: 'v8',                 // default; or 'istanbul'
            include: ['src/**/*.{ts,tsx}'], // v4: required to report uncovered files
            reporter: ['text', 'html', 'lcov'],
          },
        },
      })
      ```
      
      ```ts
      // src/test/setup.ts
      import '@testing-library/jest-dom/vitest'   // registers DOM matchers with Vitest's expect
      ```
      
      If `globals: true`, add `"types": ["vitest/globals"]` to tsconfig `compilerOptions`. **jsdom vs happy-dom:** jsdom is the safer default for React component tests; happy-dom is faster but covers a smaller API surface.
      
      Install set:
      
      ```bash
      pnpm add -D vitest @vitejs/plugin-react jsdom \
        @testing-library/react @testing-library/dom @testing-library/jest-dom \
        @testing-library/user-event @vitest/coverage-v8
      ```
      
      `@testing-library/react` (16.x) added React 19 support in 16.1 and requires the separate `@testing-library/dom` peer.
      
      ## Component test
      
      ```tsx
      // src/components/Counter.test.tsx
      import { render, screen } from '@testing-library/react'
      import userEvent from '@testing-library/user-event'
      import { expect, test } from 'vitest'   // omit if globals: true
      import { Counter } from './Counter'
      
      test('increments on click', async () => {
        const user = userEvent.setup()
        render(<Counter />)
        expect(screen.getByText('Count: 0')).toBeInTheDocument()
        await user.click(screen.getByRole('button', { name: /increment/i }))
        expect(screen.getByText('Count: 1')).toBeInTheDocument()
      })
      ```
      
      ## CLI
      
      ```bash
      vitest                  # watch mode (default)
      vitest run              # single run - use in CI
      vitest --ui             # @vitest/ui dashboard
      vitest run --coverage   # enable coverage
      vitest --typecheck      # type-level test mode
      vitest --project unit   # filter to a project (repeatable)
      ```
      
      ## Coverage
      
      The default provider is **v8** (`@vitest/coverage-v8`); `istanbul` is the alternative. Since Vitest 3.2 the v8 provider uses AST-based remapping, so accuracy matches istanbul. **v4 breaking change:** `coverage.all` and `coverage.extensions` were removed - the default now reports only covered files, so set `coverage.include` to surface uncovered ones. When Vitest detects an AI coding agent, the `text` reporter auto-trims output (`skipFull: true` + a summary) to save tokens.
      
      ## Browser Mode
      
      Runs tests in a real browser. In Vitest 4 the provider is an **imported factory object**, not a string, and the old `@vitest/browser` package is gone:
      
      ```ts
      import { playwright } from '@vitest/browser-playwright'
      
      test: {
        browser: {
          enabled: true,
          provider: playwright(),
          instances: [{ browser: 'chromium' }],   // at least one required
          headless: true,
        },
      }
      ```
      
      Set it up with `npx vitest init browser`. Providers: `@vitest/browser-playwright` (recommended, supports parallelism), `@vitest/browser-webdriverio`, and `@vitest/browser-preview` (local only - **not** for CI, it simulates events rather than driving Chrome DevTools Protocol). Render with `vitest-browser-react`; import `page`/`userEvent` from `vitest/browser`. Browser tests are not an "environment" - mix them with Node tests via `test.projects` (below).
      
      ## Projects (not workspace)
      
      `vitest.workspace.ts` + `defineWorkspace` is **deprecated since 3.2**. Use the `test.projects` field in the root config:
      
      ```ts
      export default defineConfig({
        test: {
          projects: [
            'packages/*',
            { extends: true, test: { name: 'unit', environment: 'jsdom', include: ['**/*.unit.test.ts'] } },
            { test: { name: 'node', environment: 'node', include: ['**/*.node.test.ts'] } },
          ],
        },
      })
      ```
      
      `extends: true` inherits root plugins/options (default is no inheritance). Use `defineProject` for standalone project files - root-only keys (`coverage`, `reporters`) error inside a project. Root-level `coverage`/`reporters` stay global.
      
      ## Notes
      
      - **Vitest 5** is in beta: benchmarking API rewrite, `*.sequential` removed (use `concurrent: false`), stricter browser locators, no parent-dir config lookup. Stay on 4.x for production.
      - Migrating from v3: `vite-node` was replaced by Vite's Module Runner; tinypool was removed (`maxThreads`/`maxForks` -> `maxWorkers`); the `workspace` -> `projects` rename is finalized.
      
      ## Resources
      
      - Guide: https://vitest.dev/guide/ - Config: https://vitest.dev/config/
      - Browser Mode: https://vitest.dev/guide/browser/ - Migration: https://vitest.dev/guide/migration
      
  • CHANGELOG.md 7.1 KB
    # Changelog
    
    All notable changes to this skill will be documented in this file.
    
    The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
    and this skill adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
    
    ## [Unreleased]
    
    ## [0.3.5] - 2026-09-09
    
    ### Changed
    - Description condensed to fit the repo's 250-character limit.
    
    ## [0.3.4] - 2026-08-21
    
    ### Changed
    
    - Declared ClawHub browse categories (`development`) and topics in `metadata`, so the release pipeline publishes them instead of leaving the skill in the `other` category.
    
    ### Removed
    
    - `skill-card.md`. The ClawHub CLI strips a root `skill-card.md` from every publish and the registry generates its own card, so the authored file never reached ClawHub.
    
    ## [0.3.3] - 2026-08-07
    
    ### Changed
    
    - Trimmed the frontmatter description to what-plus-when; dropped the trailing 20-term trigger-keyword sentence.
    
    ## [0.3.2] - 2026-07-22
    
    ### Added
    
    - skill-card.md release record following NVIDIA's skill-card format
    - metadata.openclaw block (emoji, homepage) for ClawHub display
    
    ## [0.3.1] - 2026-07-10
    
    ### Changed
    - CHANGELOG preamble pinned to Keep a Changelog 2.0.0 (format unchanged; KaC 2.0.0 keeps existing changelogs valid).
    
    ## [0.3.0] - 2026-07-01
    
    ### Changed
    - Refreshed version pins: vite 8.0.16->8.1.2, @vitejs/plugin-react 6.0.2->6.0.3,
      tailwindcss 4.3.0->4.3.2, @biomejs/biome 2.4.16->2.5.2, vitest 4.1.8->4.1.9,
      hono 4.12.25->4.12.27, shadcn CLI table 4.11.0->4.12.0 (metadata.upstream + Version targets + reference intros).
    - Biome: `linter.rules.recommended` is deprecated in favor of `linter.rules.preset` (Biome 2.5);
      updated both canonical config examples (SKILL.md + biome.md) to `"preset": "recommended"` and noted `biome migrate`.
    - Vite: HMR WebSocket options moved from `server.hmr.*` to `server.ws.*`; corrected the HMR
      troubleshooting entry to `server.ws.clientPort` and noted the deprecation/auto-sync.
    - TypeScript: reframed the tsgo/TS 7 section - TS 7.0 is now RC (~10x faster, targeted for stable
      "within the next month"), with `@typescript/typescript6`/`tsc6` side-by-side install and
      `--checkers`/`--builders`/`--singleThreaded` parallelism flags.
    
    ### Added
    - Vite 8.1: WASM ESM integration (stable direct `.wasm` imports), experimental Bundled Dev Mode
      (`experimental.bundledDev`) and Chunk Import Map (`build.chunkImportMap`), Lightning CSS being
      evaluated as the next-major default CSS transformer.
    - React: Partial Pre-rendering (`prerender`/`resume` APIs, 19.2) added to the newer-surface list.
    - Biome: `--reporter=concise` (token-saving agent reporter), read-only `--watch` mode,
      `formatter.delimiterSpacing`, and `biome upgrade`.
    - shadcn 4.12: chat-interface components + `@shadcn/react` headless package, `scroll-fade`/`shimmer`
      utilities, and `add` inspection flags (`--dry-run`/`--diff`/`--view`).
    - Vite caveat: `resolve.tsconfigPaths: true` does not follow tsconfig project references (solution-style
      configs) - use an explicit `resolve.alias` there.
    - Tailwind footgun: a utility referencing a token not mapped in `@theme`/`@theme inline` emits no CSS and no error.
    
    ### Security
    - Bumped Hono pin to 4.12.27, covering two SSR advisories: `hono/jsx` cross-request context
      disclosure (GHSA-hvrm-45r6-mjfj) and `hono/css` `cx()` XSS escaping bypass (GHSA-w62v-xxxg-mg59).
    
    Verified against: vite@8.1.2, @vitejs/plugin-react@6.0.3, tailwindcss@4.3.2, @biomejs/biome@2.5.2, vitest@4.1.9, hono@4.12.27
    
    ## [0.2.0] - 2026-06-09
    
    ### Added
    - references/hono.md - comprehensive Hono 4.12 reference: mental model, routing, Context,
      HonoRequest, middleware (built-in + `createMiddleware` + chained type inference), validation
      (`hono/validator`, `@hono/zod-validator`, Standard Schema), end-to-end type-safe RPC (`hc`,
      status-code inference, larger-app chaining, `hcWithType` IDE-perf fix), OpenAPI
      (`@hono/zod-openapi`), error handling (`HTTPException`/`onError`), helpers (cookie, streaming/SSE,
      JWT sign/verify/decode, context-storage `getContext`, factory), realtime WebSocket
      (`upgradeWebSocket` + RPC `$ws`), auth middleware (basic/bearer/jwt), server-side JSX,
      static files across runtimes, multi-runtime deployment (Workers/Node/Bun/Deno), and testing.
      Includes a prominent pointer to Hono's `llms-full.txt`/`llms.txt` as the authoritative
      long-tail source, plus a categorized resource/link section.
    - SKILL.md: Hono added to the stack overview, references list, and version-targets table; new
      cross-cutting rule on the Hono RPC seam (version match, `strict: true`, status codes, no
      `c.notFound()` on RPC routes).
    - references/shadcn.md: documented the `search`/`list` command - new `-t, --type` and `--json`
      flags, optional `[registries]` arg (searches all registries in `components.json` when omitted),
      and the 4.11 switch of default output from JSON to human-readable.
    
    ### Changed
    - Repositioned the skill from "frontend" to full-stack TypeScript: description and intro now
      cover the Hono backend/edge layer and its RPC integration with the React frontend.
    - Bumped documented shadcn CLI version 4.10.0 -> 4.11.0 (SKILL.md version table + shadcn.md).
    
    Verified against: hono@4.12.25, @hono/node-server@2.0.4, @hono/zod-validator@0.8.0, @hono/zod-openapi@1.4.0
    
    ## [0.1.0] - 2026-06-05
    
    ### Added
    - Initial release. Merges the former `vite`, `react-typescript`, `shadcn-tailwind`, and `biome`
      skills into one cohesive TypeScript frontend skill, plus net-new Vitest coverage.
    - SKILL.md cross-cutting layer: stack overview, version targets, the rules that bite at the
      seams between tools, and one end-to-end working setup (vite.config.ts, tsconfig.json,
      biome.json, styles.css, a canonical component).
    - references/vite.md - Vite 8 (Rolldown default), dev server, code splitting, build, deployment.
    - references/react.md - React 19.2 patterns and the React Compiler 1.0.
    - references/typescript.md - TypeScript 6.0 config and patterns.
    - references/tailwind.md - Tailwind CSS v4.3 CSS-first config and theming.
    - references/shadcn.md - shadcn/ui CLI 4.10 and component authoring.
    - references/biome.md - Biome 2.4 lint/format/imports.
    - references/vitest.md - Vitest 4 testing (net-new; not present in any source skill).
    
    ### Notes
    - Retargets the former Vite content from Vite 7 to Vite 8: Rolldown is the single default
      bundler, object-form `manualChunks` removed in favor of Rolldown `codeSplitting`,
      `build.rollupOptions` -> `build.rolldownOptions`, default minifiers Oxc (JS) and
      Lightning CSS, browser target chrome111/edge111/firefox114/safari16.4, React Compiler wired
      via `reactCompilerPreset` + `@rolldown/plugin-babel`.
    - TypeScript content advanced from 5.9 to 6.0 (strict default-on, `types: []`, module/target
      default shifts, `baseUrl` deprecated, `erasableSyntaxOnly`, tsgo note).
    - shadcn CLI corrected from 3.0 to 4.10 (`create` is an alias of `init`, unified `radix-ui`
      import, GitHub source registries, new styles). Tailwind advanced 4.2 -> 4.3. Biome 2.4.13 -> 2.4.16.
    
    Verified against: vite@8.0.16, @vitejs/plugin-react@6.0.2, react@19.2.7, typescript@6.0.3, tailwindcss@4.3.0, @biomejs/biome@2.4.16, vitest@4.1.8, babel-plugin-react-compiler@1.0.0, class-variance-authority@0.7.1
    
  • LICENSE.txt 8.9 KB
    Apache License
    Version 2.0, January 2004
    https://www.apache.org/licenses/
    
    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    
    1. Definitions.
    
    "License" shall mean the terms and conditions for use, reproduction, and
    distribution as defined by Sections 1 through 9 of this document.
    
    "Licensor" shall mean the copyright owner or entity authorized by the
    copyright owner that is granting the License.
    
    "Legal Entity" shall mean the union of the acting entity and all other
    entities that control, are controlled by, or are under common control with
    that entity. For the purposes of this definition, "control" means (i) the
    power, direct or indirect, to cause the direction or management of such
    entity, whether by contract or otherwise, or (ii) ownership of fifty percent
    (50%) or more of the outstanding shares, or (iii) beneficial ownership of
    such entity.
    
    "You" (or "Your") shall mean an individual or Legal Entity exercising
    permissions granted by this License.
    
    "Source" form shall mean the preferred form for making modifications,
    including but not limited to software source code, documentation source, and
    configuration files.
    
    "Object" form shall mean any form resulting from mechanical transformation or
    translation of a Source form, including but not limited to compiled object
    code, generated documentation, and conversions to other media types.
    
    "Work" shall mean the work of authorship, whether in Source or Object form,
    made available under the License, as indicated by a copyright notice that is
    included in or attached to the work (an example is provided in the Appendix
    below).
    
    "Derivative Works" shall mean any work, whether in Source or Object form,
    that is based on (or derived from) the Work and for which the editorial
    revisions, annotations, elaborations, or other modifications represent, as a
    whole, an original work of authorship. For the purposes of this License,
    Derivative Works shall not include works that remain separable from, or
    merely link (or bind by name) to the interfaces of, the Work and Derivative
    Works thereof.
    
    "Contribution" shall mean any work of authorship, including the original
    version of the Work and any modifications or additions to that Work or
    Derivative Works thereof, that is intentionally submitted to Licensor for
    inclusion in the Work by the copyright owner or by an individual or Legal
    Entity authorized to submit on behalf of the copyright owner. For the
    purposes of this definition, "submitted" means any form of electronic, verbal,
    or written communication sent to the Licensor or its representatives,
    including but not limited to communication on electronic mailing lists, source
    code control systems, and issue tracking systems that are managed by, or on
    behalf of, the Licensor for the purpose of discussing and improving the Work,
    but excluding communication that is conspicuously marked or otherwise
    designated in writing by the copyright owner as "Not a Contribution."
    
    "Contributor" shall mean Licensor and any individual or Legal Entity on
    behalf of whom a Contribution has been received by Licensor and subsequently
    incorporated within the Work.
    
    2. Grant of Copyright License. Subject to the terms and conditions of this
    License, each Contributor hereby grants to You a perpetual, worldwide,
    non-exclusive, no-charge, royalty-free, irrevocable copyright license to
    reproduce, prepare Derivative Works of, publicly display, publicly perform,
    sublicense, and distribute the Work and such Derivative Works in Source or
    Object form.
    
    3. Grant of Patent License. Subject to the terms and conditions of this
    License, each Contributor hereby grants to You a perpetual, worldwide,
    non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this
    section) patent license to make, have made, use, offer to sell, sell, import,
    and otherwise transfer the Work, where such license applies only to those
    patent claims licensable by such Contributor that are necessarily infringed by
    their Contribution(s) alone or by combination of their Contribution(s) with
    the Work to which such Contribution(s) was submitted. If You institute patent
    litigation against any entity (including a cross-claim or counterclaim in a
    lawsuit) alleging that the Work or a Contribution incorporated within the Work
    constitutes direct or contributory patent infringement, then any patent
    licenses granted to You under this License for that Work shall terminate as of
    the date such litigation is filed.
    
    4. Redistribution. You may reproduce and distribute copies of the Work or
    Derivative Works thereof in any medium, with or without modifications, and in
    Source or Object form, provided that You meet the following conditions:
    
    (a) You must give any other recipients of the Work or Derivative Works a copy
    of this License; and
    
    (b) You must cause any modified files to carry prominent notices stating that
    You changed the files; and
    
    (c) You must retain, in the Source form of any Derivative Works that You
    distribute, all copyright, patent, trademark, and attribution notices from
    the Source form of the Work, excluding those notices that do not pertain to
    any part of the Derivative Works; and
    
    (d) If the Work includes a "NOTICE" text file as part of its distribution,
    then any Derivative Works that You distribute must include a readable copy of
    the attribution notices contained within such NOTICE file, excluding those
    notices that do not pertain to any part of the Derivative Works, in at least
    one of the following places: within a NOTICE text file distributed as part of
    the Derivative Works; within the Source form or documentation, if provided
    along with the Derivative Works; or, within a display generated by the
    Derivative Works, if and wherever such third-party notices normally appear.
    The contents of the NOTICE file are for informational purposes only and do not
    modify the License. You may add Your own attribution notices within Derivative
    Works that You distribute, alongside or as an addendum to the NOTICE text from
    the Work, provided that such additional attribution notices cannot be
    construed as modifying the License.
    
    You may add Your own copyright statement to Your modifications and may provide
    additional or different license terms and conditions for use, reproduction, or
    distribution of Your modifications, or for any such Derivative Works as a
    whole, provided Your use, reproduction, and distribution of the Work otherwise
    complies with the conditions stated in this License.
    
    5. Submission of Contributions. Unless You explicitly state otherwise, any
    Contribution intentionally submitted for inclusion in the Work by You to the
    Licensor shall be under the terms and conditions of this License, without any
    additional terms or conditions. Notwithstanding the above, nothing herein
    shall supersede or modify the terms of any separate license agreement you may
    have executed with Licensor regarding such Contributions.
    
    6. Trademarks. This License does not grant permission to use the trade names,
    trademarks, service marks, or product names of the Licensor, except as
    required for reasonable and customary use in describing the origin of the Work
    and reproducing the content of the NOTICE file.
    
    7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
    writing, Licensor provides the Work (and each Contributor provides its
    Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied, including, without limitation, any warranties
    or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    PARTICULAR PURPOSE. You are solely responsible for determining the
    appropriateness of using or redistributing the Work and assume any risks
    associated with Your exercise of permissions under this License.
    
    8. Limitation of Liability. In no event and under no legal theory, whether in
    tort (including negligence), contract, or otherwise, unless required by
    applicable law (such as deliberate and grossly negligent acts) or agreed to in
    writing, shall any Contributor be liable to You for damages, including any
    direct, indirect, special, incidental, or consequential damages of any
    character arising as a result of this License or out of the use or inability to
    use the Work (including but not limited to damages for loss of goodwill, work
    stoppage, computer failure or malfunction, or any and all other commercial
    damages or losses), even if such Contributor has been advised of the
    possibility of such damages.
    
    9. Accepting Warranty or Additional Liability. While redistributing the Work
    or Derivative Works thereof, You may choose to offer, and charge a fee for,
    acceptance of support, warranty, indemnity, or other liability obligations
    and/or rights consistent with this License. However, in accepting such
    obligations, You may act only on Your own behalf and on Your sole
    responsibility, not on behalf of any other Contributor, and only if You agree
    to indemnify, defend, and hold each Contributor harmless for any liability
    incurred by, or claims asserted against, such Contributor by reason of your
    accepting any such warranty or additional liability.
    
    END OF TERMS AND CONDITIONS
    
  • SKILL.md 13.8 KB
    ---
    name: typescript-dev
    description: Full-stack TypeScript with Vite 8, React 19, Tailwind v4, shadcn/ui, Biome, Vitest, and Hono 4. Use when setting up or working in a TypeScript project - components, styling, build and HMR, tests, lint/CI, or a Hono API with type-safe RPC.
    metadata:
      version: "0.3.5"
      categories: "development"
      topics: "typescript, vite, react, tailwind, hono"
      openclaw:
        homepage: https://github.com/tenequm/skills/tree/main/skills/typescript-dev
        emoji: "🟦"
      upstream: "vite@8.1.2, @vitejs/plugin-react@6.0.3, react@19.2.7, typescript@6.0.3, tailwindcss@4.3.2, @biomejs/biome@2.5.2, vitest@4.1.9, babel-plugin-react-compiler@1.0.0, class-variance-authority@0.7.1, hono@4.12.27"
    ---
    
    # TypeScript Frontend Development
    
    One coherent stack for building type-safe TypeScript apps: **Vite 8** (build + dev server, Rolldown-powered), **React 19.2** with the React Compiler, **TypeScript 6.0** (strict), **Tailwind CSS v4.3 + shadcn/ui** for styling, **Biome 2.4** for linting and formatting, **Vitest 4** for testing, and **Hono 4** for the backend/edge API. The pieces are designed to fit together - this skill covers how they wire up and the sharp edges that span more than one of them. Hono's RPC client (`hc`) shares server types directly with the React frontend, so the front and back end stay type-safe end to end without codegen.
    
    The body below is the cross-cutting layer: the rules that bite when these tools meet, plus one working end-to-end setup. Each tool also has a deep-dive reference - read the one you need:
    
    - **[references/vite.md](references/vite.md)** - Vite 8 config, dev server, proxy, HMR, Rolldown, code splitting, build optimization, deployment.
    - **[references/react.md](references/react.md)** - React 19 patterns: Actions, `use()`, Activity, `useEffectEvent`, document metadata, and the React Compiler.
    - **[references/typescript.md](references/typescript.md)** - Strict TypeScript 6.0 config and patterns: tsconfig defaults, generics, utility types, `import defer`, tsgo.
    - **[references/tailwind.md](references/tailwind.md)** - Tailwind CSS v4 CSS-first config, OKLCH theming, dark mode, v4.3 utilities.
    - **[references/shadcn.md](references/shadcn.md)** - shadcn/ui CLI, component authoring with CVA + `data-slot`, registries, Radix vs Base UI.
    - **[references/biome.md](references/biome.md)** - Biome config, `biome check`, domains, type-aware linting, GritQL, ESLint/Prettier migration.
    - **[references/vitest.md](references/vitest.md)** - Vitest config, Testing Library, jsdom/happy-dom, coverage, browser mode, projects.
    - **[references/hono.md](references/hono.md)** - Hono 4 web framework: routing, context, middleware, validation (Zod), end-to-end type-safe RPC, OpenAPI, helpers, and multi-runtime deployment (Workers/Node/Bun/Deno).
    
    ## Version targets
    
    | Tool | Version | Note |
    |------|---------|------|
    | Vite | 8.1.2 | Rolldown is the single default bundler |
    | @vitejs/plugin-react | 6.0.3 | v6 removed the inline `babel` option |
    | React / react-dom | 19.2.7 | React Compiler is stable (1.0) |
    | babel-plugin-react-compiler | 1.0.0 | pin with `--save-exact` |
    | TypeScript | 6.0.3 | last JS-based TS; TS 7.0 (tsgo) now RC |
    | Tailwind CSS | 4.3.2 | CSS-first config, no JS config file |
    | shadcn/ui CLI | 4.12.0 | `create` is an alias of `init` |
    | Biome | 2.5.2 | single binary for lint + format + imports |
    | Vitest | 4.1.9 | Vite-native test runner; reuses vite.config |
    | Hono | 4.12.27 | Web Standards backend/edge framework; no v5 |
    
    ## Cross-cutting critical rules
    
    These are the rules that fail in confusing ways precisely because they sit at the seam between two tools. The single-tool details live in the references.
    
    ### Vite plugin order: framework plugins first, `react()` last
    
    When a framework plugin (TanStack Router/Start, etc.) generates routes or transforms code, it must run before `@vitejs/plugin-react` so React's Fast Refresh transform sees the final output. Wrong order causes route-generation failures and broken HMR.
    
    ```ts
    plugins: [
      tanstackStart(),   // or tanstackRouter() for SPA - framework first
      tailwindcss(),
      react(),           // React plugin last among framework plugins
    ]
    ```
    
    ### React Compiler replaces manual memoization - and changes how you wire Vite
    
    React Compiler 1.0 auto-memoizes components, computations, and callbacks at build time. Write plain components; do not reach for `useMemo`/`useCallback`/`memo`. The catch lives at the Vite seam: **`@vitejs/plugin-react` v6 removed the inline `babel` option**, so the old `react({ babel: { plugins: [...] } })` wiring no longer works. The compiler now runs through a separate Babel plugin:
    
    ```ts
    import react, { reactCompilerPreset } from '@vitejs/plugin-react'
    import babel from '@rolldown/plugin-babel'
    
    plugins: [react(), babel({ presets: [reactCompilerPreset()] })]
    ```
    
    Install: `pnpm add -D @rolldown/plugin-babel @babel/core babel-plugin-react-compiler @types/babel__core`.
    
    This also ripples into Biome: `useExhaustiveDependencies` can't tell the compiler is handling deps for you, so most compiler users turn it off (see [biome.md](references/biome.md)).
    
    ### Tailwind v4 is CSS-first - there is no `tailwind.config.js`
    
    Tailwind v4 configures everything in CSS via `@theme`, `@utility`, `@plugin`, `@source`. Never create or look for `tailwind.config.js`/`.ts`. The Vite integration is the `@tailwindcss/vite` plugin (no PostCSS config either). If you find a `tailwind.config.js` in a v4 project, it is leftover - delete it and migrate the values into CSS. Full details in [tailwind.md](references/tailwind.md).
    
    ### Style with semantic tokens, never raw palette or dynamic class names
    
    ```tsx
    <div className="bg-primary text-primary-foreground">   // respects theme + dark mode
    <div className="bg-blue-500 text-white">               // breaks theming - avoid
    ```
    
    And never assemble class names from fragments (`bg-${color}-500`) - Tailwind's scanner only sees complete literal strings, so dynamic names silently produce no CSS. Use a lookup map of full class strings.
    
    ### TypeScript 6.0 changed the defaults - lean on them, don't fight them
    
    TS 6.0 bakes in much of what used to be manual: `strict` and `noUncheckedSideEffectImports` are now **on by default**, so drop them from a fresh tsconfig. But two new defaults will break builds if you ignore them: `types` now defaults to `[]` (add `"types": ["node"]` if you use Node globals) and `module`/`target` shifted (`module` defaults to `esnext`, not `nodenext`). `baseUrl` is deprecated - use prefixed `paths` instead. See [typescript.md](references/typescript.md) for the full 6.0 tsconfig and migration notes.
    
    ### One Biome command, and `files.includes` is the only include key
    
    Run `biome check` (or `biome ci`) - it formats, lints, and organizes imports in a single pass; never split into separate `lint`+`format` calls. And in Biome 2.x the only file-selection key is `files.includes` (with the `s`); `files.ignore`/`files.include`/`files.exclude` do not exist and throw `Found an unknown key`. Exclude with negation: `"includes": ["**", "!**/routeTree.gen.ts"]`. More in [biome.md](references/biome.md).
    
    ### Hono RPC ties the backend's types to the React frontend - keep them in sync
    
    When the API is Hono, the React app talks to it through the `hc<AppType>()` client, which
    imports the server's exported `typeof app` directly. That shared type is the seam: it only
    works if **both sides run the same Hono version** and both `tsconfig.json` set `"strict": true`
    (a mismatch throws "Type instantiation is excessively deep"). Two more rules that bite at this
    seam: handlers must specify status codes (`c.json(data, 200)`) for the client to infer
    responses, and routes the client calls must not use `c.notFound()`. As the route count grows,
    compile the client type once (`hcWithType`) so the IDE stays fast. Full details in
    [hono.md](references/hono.md).
    
    ## End-to-end setup
    
    A minimal but complete React + TypeScript + Tailwind + Biome project. Swap the framework plugin for your router/SSR choice (see [vite.md](references/vite.md) for TanStack and Cloudflare variants).
    
    ### vite.config.ts
    
    ```ts
    import { defineConfig } from 'vite'
    import react, { reactCompilerPreset } from '@vitejs/plugin-react'
    import babel from '@rolldown/plugin-babel'
    import tailwindcss from '@tailwindcss/vite'
    
    export default defineConfig({
      plugins: [
        tailwindcss(),
        react(),
        babel({ presets: [reactCompilerPreset()] }),
      ],
      resolve: {
        alias: { '@': new URL('./src', import.meta.url).pathname },
      },
    })
    ```
    
    `import.meta.url` is the ESM-correct way to resolve paths - there is no `__dirname` in an ESM config, and Vite configs are ESM-only.
    
    ### tsconfig.json (TypeScript 6.0)
    
    ```jsonc
    {
      "compilerOptions": {
        // strict + noUncheckedSideEffectImports are ON by default in 6.0 - omitted on purpose
        "target": "es2023",
        "module": "preserve",
        "moduleResolution": "bundler",
        "moduleDetection": "force",
        "jsx": "react-jsx",
        "verbatimModuleSyntax": true,
        "isolatedModules": true,
        "noUncheckedIndexedAccess": true,
        "exactOptionalPropertyTypes": true,
        "erasableSyntaxOnly": true,
        "skipLibCheck": true,
        "types": [],
        "paths": { "@/*": ["./src/*"] }
      }
    }
    ```
    
    `module: preserve` + `moduleResolution: bundler` is the right pairing for a Vite-bundled app; use `nodenext` instead only for Node-executed code. `types: []` keeps ambient `@types/*` from leaking in globally - add `["node"]` (or others) explicitly when needed.
    
    ### biome.json
    
    ```json
    {
      "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
      "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
      "files": { "includes": ["**", "!**/components/ui", "!**/routeTree.gen.ts"] },
      "formatter": { "enabled": true, "indentStyle": "space", "lineWidth": 100 },
      "linter": {
        "enabled": true,
        "rules": { "preset": "recommended" },
        "domains": { "react": "recommended" }
      },
      "javascript": { "formatter": { "quoteStyle": "double" } },
      "assist": { "enabled": true, "actions": { "source": { "organizeImports": "on" } } }
    }
    ```
    
    ### src/styles.css
    
    ```css
    @import "tailwindcss";
    
    :root {
      --background: oklch(1 0 0);
      --foreground: oklch(0.145 0 0);
      --primary: oklch(0.205 0 0);
      --primary-foreground: oklch(0.985 0 0);
      --radius: 0.5rem;
    }
    .dark {
      --background: oklch(0.145 0 0);
      --foreground: oklch(0.985 0 0);
      --primary: oklch(0.922 0 0);
      --primary-foreground: oklch(0.205 0 0);
    }
    
    @theme inline {
      --color-background: var(--background);
      --color-foreground: var(--foreground);
      --color-primary: var(--primary);
      --color-primary-foreground: var(--primary-foreground);
    }
    ```
    
    The `@import "tailwindcss";` line is load-bearing: the `@tailwindcss/vite` plugin alone produces no styles without it - a missing import is the classic "Tailwind renders nothing" footgun. Use `@theme inline` (not plain `@theme`) for tokens that reference CSS variables, so they track dark-mode changes.
    
    ### A component, the way the whole stack wants it
    
    Plain function, `ref` as a regular prop (no `forwardRef`), native element props via `React.ComponentProps`, variants via CVA, `data-slot` for styling hooks, and no manual memoization - the compiler handles it.
    
    ```tsx
    import { cva, type VariantProps } from "class-variance-authority"
    import { cn } from "@/lib/utils"
    
    const buttonVariants = cva(
      "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors disabled:opacity-50",
      {
        variants: {
          variant: {
            default: "bg-primary text-primary-foreground hover:bg-primary/90",
            outline: "border border-input bg-background hover:bg-accent",
            ghost: "hover:bg-accent hover:text-accent-foreground",
          },
          size: { default: "h-9 px-4 py-2", sm: "h-8 px-3", lg: "h-10 px-8" },
        },
        defaultVariants: { variant: "default", size: "default" },
      }
    )
    
    function Button({
      className,
      variant,
      size,
      ref,
      ...props
    }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>) {
      return (
        <button
          ref={ref}
          data-slot="button"
          className={cn(buttonVariants({ variant, size }), className)}
          {...props}
        />
      )
    }
    ```
    
    Note the `cn()` order: defaults first, consumer `className` last, so tailwind-merge's last-wins resolution lets callers override.
    
    ## Best practices
    
    1. **Let the compiler optimize.** Write plain components and computations; reserve `useMemo`/`useCallback` for the rare case where you need a value to be a stable effect dependency.
    2. **Model state as discriminated unions, not loose booleans** (`{ status: "loading" } | { status: "error"; error }`) so impossible states are unrepresentable.
    3. **Extend native props with `React.ComponentProps<"el">`** instead of re-declaring HTML attributes by hand.
    4. **Use `use()` over `useContext()`** - it works after early returns and inside conditionals.
    5. **Semantic color tokens only**, and always pair `bg-*` with the matching `text-*-foreground`.
    6. **`biome check --write`** is your one local command; `biome ci` in pipelines.
    7. **Rolldown is the default bundler in Vite 8** - no opt-in needed; split stable vendor code with Rolldown's `codeSplitting` (see [vite.md](references/vite.md)).
    8. **Pin exact versions for tooling that rewrites code** (`babel-plugin-react-compiler`, `@biomejs/biome`) to avoid surprise diffs between releases.
    9. **Keep secrets off the client** - only `VITE_`-prefixed env vars reach browser code via `import.meta.env`.
    10. **Test through `vite.config.ts`** - Vitest reuses your build config, so tests see the same aliases and transforms; `vitest run` in CI, `jsdom` for component tests.
    
    ## Resources
    
    - Vite: https://vite.dev/guide/ - Vite 8 blog: https://vite.dev/blog/announcing-vite8
    - React 19.2: https://react.dev/blog/2025/10/01/react-19-2 - Compiler: https://react.dev/learn/react-compiler
    - TypeScript 6.0: https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/
    - Tailwind CSS: https://tailwindcss.com/docs - shadcn/ui: https://ui.shadcn.com/docs
    - Biome: https://biomejs.dev/
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related