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
Install
npx skills add https://github.com/tenequm/skills/tree/main/skills/typescript-dev
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tenequm-skills@llmmart
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
- Let the compiler optimize. Write plain components and computations; reserve
useMemo/useCallbackfor the rare case where you need a value to be a stable effect dependency. - Model state as discriminated unions, not loose booleans (
{ status: "loading" } | { status: "error"; error }) so impossible states are unrepresentable. - Extend native props with
React.ComponentProps<"el">instead of re-declaring HTML attributes by hand. - Use
use()overuseContext()- it works after early returns and inside conditionals. - Semantic color tokens only, and always pair
bg-*with the matchingtext-*-foreground. biome check --writeis your one local command;biome ciin pipelines.- Rolldown is the default bundler in Vite 8 - no opt-in needed; split stable vendor code with Rolldown's
codeSplitting(see vite.md). - Pin exact versions for tooling that rewrites code (
babel-plugin-react-compiler,@biomejs/biome) to avoid surprise diffs between releases. - Keep secrets off the client - only
VITE_-prefixed env vars reach browser code viaimport.meta.env. - Test through
vite.config.ts- Vitest reuses your build config, so tests see the same aliases and transforms;vitest runin CI,jsdomfor 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/
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.
Reviews (0)
No reviews yet.
No comments yet.