From 213d0de58825a53fa88da9ca4c4ee240ed78814c Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Sun, 8 Mar 2026 15:39:22 -0600 Subject: [PATCH 1/3] feat: add @tanstack/intent AI agent skills for Router and Start Add SKILL.md files across 11 packages to help AI coding agents generate correct TanStack Router and Start code. Skills cover common patterns, API usage, and failure modes that agents frequently get wrong. Packages with skills: - router-core (10 skills): core concepts, search/path params, navigation, data loading, auth/guards, code splitting, not-found/errors, type safety, SSR - react-router (3): React bindings, Router+Query composition, migration from React Router - start-client-core (6): Start setup, server functions, middleware, execution model, server routes, deployment - react-start (2): React Start bindings, migration from Next.js - virtual-file-routes (1): programmatic route tree building - router-plugin (1): bundler plugin configuration - start-server-core (1): server-side runtime - solid-router (1): Solid bindings - vue-router (1): Vue bindings - solid-start (1): Solid Start bindings - vue-start (1): Vue Start bindings Each package includes bin/intent.js shim, _artifacts/ metadata, and @tanstack/intent devDependency. --- .github/workflows/check-skills.yml | 172 +++ .github/workflows/notify-playbooks.yml | 54 + .github/workflows/validate-skills.yml | 52 + _artifacts/domain_map.yaml | 1091 +++++++++++++++++ _artifacts/skill_spec.md | 193 +++ _artifacts/skill_tree.yaml | 279 +++++ _artifacts/start_domain_map.yaml | 482 ++++++++ _artifacts/start_skill_tree.yaml | 158 +++ packages/react-router/bin/intent.js | 20 + packages/react-router/package.json | 9 +- .../skills/compositions/router-query/SKILL.md | 408 ++++++ .../migrate-from-react-router/SKILL.md | 486 ++++++++ .../react-router/skills/react-router/SKILL.md | 493 ++++++++ packages/react-start/bin/intent.js | 20 + packages/react-start/package.json | 11 +- .../skills/_artifacts/domain_map.yaml | 482 ++++++++ .../skills/_artifacts/skill_spec.md | 94 ++ .../skills/_artifacts/skill_tree.yaml | 158 +++ .../lifecycle/migrate-from-nextjs/SKILL.md | 434 +++++++ .../react-start/skills/react-start/SKILL.md | 285 +++++ packages/router-core/bin/intent.js | 20 + packages/router-core/package.json | 9 +- .../router-core/skills/router-core/SKILL.md | 139 +++ .../router-core/auth-and-guards/SKILL.md | 454 +++++++ .../router-core/code-splitting/SKILL.md | 322 +++++ .../skills/router-core/data-loading/SKILL.md | 484 ++++++++ .../skills/router-core/navigation/SKILL.md | 448 +++++++ .../router-core/not-found-and-errors/SKILL.md | 439 +++++++ .../skills/router-core/path-params/SKILL.md | 382 ++++++ .../skills/router-core/search-params/SKILL.md | 355 ++++++ .../references/validation-patterns.md | 379 ++++++ .../skills/router-core/ssr/SKILL.md | 435 +++++++ .../skills/router-core/type-safety/SKILL.md | 500 ++++++++ packages/router-plugin/bin/intent.js | 20 + packages/router-plugin/package.json | 9 +- .../skills/_artifacts/domain_map.yaml | 62 + .../skills/_artifacts/skill_spec.md | 24 + .../skills/_artifacts/skill_tree.yaml | 22 + .../skills/router-plugin/SKILL.md | 236 ++++ packages/solid-router/bin/intent.js | 20 + packages/solid-router/package.json | 9 +- .../skills/_artifacts/domain_map.yaml | 61 + .../skills/_artifacts/skill_spec.md | 24 + .../skills/_artifacts/skill_tree.yaml | 20 + .../solid-router/skills/solid-router/SKILL.md | 471 +++++++ packages/solid-start/bin/intent.js | 20 + packages/solid-start/package.json | 9 +- .../skills/_artifacts/domain_map.yaml | 60 + .../skills/_artifacts/skill_spec.md | 24 + .../skills/_artifacts/skill_tree.yaml | 21 + .../solid-start/skills/solid-start/SKILL.md | 276 +++++ packages/start-client-core/bin/intent.js | 20 + packages/start-client-core/package.json | 9 +- .../skills/_artifacts/domain_map.yaml | 482 ++++++++ .../skills/_artifacts/skill_spec.md | 94 ++ .../skills/_artifacts/skill_tree.yaml | 158 +++ .../skills/start-core/SKILL.md | 212 ++++ .../skills/start-core/deployment/SKILL.md | 306 +++++ .../start-core/execution-model/SKILL.md | 297 +++++ .../skills/start-core/middleware/SKILL.md | 361 ++++++ .../start-core/server-functions/SKILL.md | 338 +++++ .../skills/start-core/server-routes/SKILL.md | 278 +++++ packages/start-server-core/bin/intent.js | 20 + packages/start-server-core/package.json | 9 +- .../skills/_artifacts/domain_map.yaml | 65 + .../skills/_artifacts/skill_spec.md | 24 + .../skills/_artifacts/skill_tree.yaml | 23 + .../skills/start-server-core/SKILL.md | 244 ++++ packages/virtual-file-routes/bin/intent.js | 20 + packages/virtual-file-routes/package.json | 9 +- .../skills/_artifacts/domain_map.yaml | 64 + .../skills/_artifacts/skill_spec.md | 24 + .../skills/_artifacts/skill_tree.yaml | 23 + .../skills/virtual-file-routes/SKILL.md | 219 ++++ packages/vue-router/bin/intent.js | 20 + packages/vue-router/package.json | 9 +- .../skills/_artifacts/domain_map.yaml | 64 + .../skills/_artifacts/skill_spec.md | 24 + .../skills/_artifacts/skill_tree.yaml | 22 + .../vue-router/skills/vue-router/SKILL.md | 387 ++++++ packages/vue-start/bin/intent.js | 20 + packages/vue-start/package.json | 9 +- .../skills/_artifacts/domain_map.yaml | 63 + .../vue-start/skills/_artifacts/skill_spec.md | 24 + .../skills/_artifacts/skill_tree.yaml | 22 + packages/vue-start/skills/vue-start/SKILL.md | 302 +++++ pnpm-lock.yaml | 42 + 87 files changed, 15431 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/check-skills.yml create mode 100644 .github/workflows/notify-playbooks.yml create mode 100644 .github/workflows/validate-skills.yml create mode 100644 _artifacts/domain_map.yaml create mode 100644 _artifacts/skill_spec.md create mode 100644 _artifacts/skill_tree.yaml create mode 100644 _artifacts/start_domain_map.yaml create mode 100644 _artifacts/start_skill_tree.yaml create mode 100755 packages/react-router/bin/intent.js create mode 100644 packages/react-router/skills/compositions/router-query/SKILL.md create mode 100644 packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md create mode 100644 packages/react-router/skills/react-router/SKILL.md create mode 100755 packages/react-start/bin/intent.js create mode 100644 packages/react-start/skills/_artifacts/domain_map.yaml create mode 100644 packages/react-start/skills/_artifacts/skill_spec.md create mode 100644 packages/react-start/skills/_artifacts/skill_tree.yaml create mode 100644 packages/react-start/skills/lifecycle/migrate-from-nextjs/SKILL.md create mode 100644 packages/react-start/skills/react-start/SKILL.md create mode 100755 packages/router-core/bin/intent.js create mode 100644 packages/router-core/skills/router-core/SKILL.md create mode 100644 packages/router-core/skills/router-core/auth-and-guards/SKILL.md create mode 100644 packages/router-core/skills/router-core/code-splitting/SKILL.md create mode 100644 packages/router-core/skills/router-core/data-loading/SKILL.md create mode 100644 packages/router-core/skills/router-core/navigation/SKILL.md create mode 100644 packages/router-core/skills/router-core/not-found-and-errors/SKILL.md create mode 100644 packages/router-core/skills/router-core/path-params/SKILL.md create mode 100644 packages/router-core/skills/router-core/search-params/SKILL.md create mode 100644 packages/router-core/skills/router-core/search-params/references/validation-patterns.md create mode 100644 packages/router-core/skills/router-core/ssr/SKILL.md create mode 100644 packages/router-core/skills/router-core/type-safety/SKILL.md create mode 100755 packages/router-plugin/bin/intent.js create mode 100644 packages/router-plugin/skills/_artifacts/domain_map.yaml create mode 100644 packages/router-plugin/skills/_artifacts/skill_spec.md create mode 100644 packages/router-plugin/skills/_artifacts/skill_tree.yaml create mode 100644 packages/router-plugin/skills/router-plugin/SKILL.md create mode 100755 packages/solid-router/bin/intent.js create mode 100644 packages/solid-router/skills/_artifacts/domain_map.yaml create mode 100644 packages/solid-router/skills/_artifacts/skill_spec.md create mode 100644 packages/solid-router/skills/_artifacts/skill_tree.yaml create mode 100644 packages/solid-router/skills/solid-router/SKILL.md create mode 100755 packages/solid-start/bin/intent.js create mode 100644 packages/solid-start/skills/_artifacts/domain_map.yaml create mode 100644 packages/solid-start/skills/_artifacts/skill_spec.md create mode 100644 packages/solid-start/skills/_artifacts/skill_tree.yaml create mode 100644 packages/solid-start/skills/solid-start/SKILL.md create mode 100755 packages/start-client-core/bin/intent.js create mode 100644 packages/start-client-core/skills/_artifacts/domain_map.yaml create mode 100644 packages/start-client-core/skills/_artifacts/skill_spec.md create mode 100644 packages/start-client-core/skills/_artifacts/skill_tree.yaml create mode 100644 packages/start-client-core/skills/start-core/SKILL.md create mode 100644 packages/start-client-core/skills/start-core/deployment/SKILL.md create mode 100644 packages/start-client-core/skills/start-core/execution-model/SKILL.md create mode 100644 packages/start-client-core/skills/start-core/middleware/SKILL.md create mode 100644 packages/start-client-core/skills/start-core/server-functions/SKILL.md create mode 100644 packages/start-client-core/skills/start-core/server-routes/SKILL.md create mode 100755 packages/start-server-core/bin/intent.js create mode 100644 packages/start-server-core/skills/_artifacts/domain_map.yaml create mode 100644 packages/start-server-core/skills/_artifacts/skill_spec.md create mode 100644 packages/start-server-core/skills/_artifacts/skill_tree.yaml create mode 100644 packages/start-server-core/skills/start-server-core/SKILL.md create mode 100755 packages/virtual-file-routes/bin/intent.js create mode 100644 packages/virtual-file-routes/skills/_artifacts/domain_map.yaml create mode 100644 packages/virtual-file-routes/skills/_artifacts/skill_spec.md create mode 100644 packages/virtual-file-routes/skills/_artifacts/skill_tree.yaml create mode 100644 packages/virtual-file-routes/skills/virtual-file-routes/SKILL.md create mode 100755 packages/vue-router/bin/intent.js create mode 100644 packages/vue-router/skills/_artifacts/domain_map.yaml create mode 100644 packages/vue-router/skills/_artifacts/skill_spec.md create mode 100644 packages/vue-router/skills/_artifacts/skill_tree.yaml create mode 100644 packages/vue-router/skills/vue-router/SKILL.md create mode 100755 packages/vue-start/bin/intent.js create mode 100644 packages/vue-start/skills/_artifacts/domain_map.yaml create mode 100644 packages/vue-start/skills/_artifacts/skill_spec.md create mode 100644 packages/vue-start/skills/_artifacts/skill_tree.yaml create mode 100644 packages/vue-start/skills/vue-start/SKILL.md diff --git a/.github/workflows/check-skills.yml b/.github/workflows/check-skills.yml new file mode 100644 index 00000000000..9888015bb04 --- /dev/null +++ b/.github/workflows/check-skills.yml @@ -0,0 +1,172 @@ +# check-skills.yml — Drop this into your library repo's .github/workflows/ +# +# Checks for stale intent skills after a release and opens a review PR +# if any skills need attention. The PR body includes a prompt you can +# paste into Claude Code, Cursor, or any coding agent to update them. +# +# Triggers: new release published, or manual workflow_dispatch. +# +# Template variables (replaced by `intent setup`): +# @tanstack/react-router — e.g. @tanstack/query +# +# Adapted for TanStack Router monorepo: loops over all packages with skills. + +name: Check Skills + +on: + release: + types: [published] + workflow_dispatch: {} + +permissions: + contents: write + pull-requests: write + +jobs: + check: + name: Check for stale skills + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install intent CLI + run: npm install -g @tanstack/intent + + - name: Install dependencies + run: npm install --ignore-scripts + env: + npm_config_legacy_peer_deps: 'true' + + - name: Check staleness + id: stale + run: | + # Monorepo: collect stale reports from all packages with skills + ALL_OUTPUT="[" + FIRST=true + for dir in packages/*/; do + if [ -d "$dir/skills" ]; then + PKG_OUTPUT=$(cd "$dir" && npx @tanstack/intent stale --json 2>/dev/null) || true + if [ -n "$PKG_OUTPUT" ] && [ "$PKG_OUTPUT" != "[]" ] && [ "$PKG_OUTPUT" != "No intent-enabled packages found." ]; then + if [ "$FIRST" = true ]; then + FIRST=false + else + ALL_OUTPUT="${ALL_OUTPUT}," + fi + # Strip outer brackets and append entries + ENTRIES=$(echo "$PKG_OUTPUT" | node -e " + const input = require('fs').readFileSync('/dev/stdin','utf8').trim(); + try { const arr = JSON.parse(input); process.stdout.write(JSON.stringify(arr).slice(1, -1)); } catch {} + ") + ALL_OUTPUT="${ALL_OUTPUT}${ENTRIES}" + fi + fi + done + ALL_OUTPUT="${ALL_OUTPUT}]" + + echo "$ALL_OUTPUT" + + # Check if any skills need review + NEEDS_REVIEW=$(echo "$ALL_OUTPUT" | node -e " + const input = require('fs').readFileSync('/dev/stdin','utf8'); + try { + const reports = JSON.parse(input); + const stale = reports.flatMap(r => + r.skills.filter(s => s.needsReview).map(s => ({ library: r.library, skill: s.name, reasons: s.reasons })) + ); + if (stale.length > 0) { + console.log(JSON.stringify(stale)); + } + } catch {} + ") + + if [ -z "$NEEDS_REVIEW" ]; then + echo "has_stale=false" >> "$GITHUB_OUTPUT" + else + echo "has_stale=true" >> "$GITHUB_OUTPUT" + # Escape for multiline GH output + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + echo "stale_json<<$EOF" >> "$GITHUB_OUTPUT" + echo "$NEEDS_REVIEW" >> "$GITHUB_OUTPUT" + echo "$EOF" >> "$GITHUB_OUTPUT" + fi + + - name: Build summary + if: steps.stale.outputs.has_stale == 'true' + id: summary + run: | + node -e " + const stale = JSON.parse(process.env.STALE_JSON); + const lines = stale.map(s => + '- **' + s.skill + '** (' + s.library + '): ' + s.reasons.join(', ') + ); + const summary = lines.join('\n'); + + const prompt = [ + 'Review and update the following stale intent skills for TanStack Router:', + '', + ...stale.map(s => '- ' + s.skill + ': ' + s.reasons.join(', ')), + '', + 'For each stale skill:', + '1. Read the current SKILL.md file', + '2. Check what changed in the library since the skill was last updated', + '3. Update the skill content to reflect current APIs and behavior', + '4. Run \`npx @tanstack/intent validate\` to verify the updated skill', + ].join('\n'); + + // Write outputs + const fs = require('fs'); + const env = fs.readFileSync(process.env.GITHUB_OUTPUT, 'utf8'); + const eof = require('crypto').randomBytes(15).toString('base64'); + fs.appendFileSync(process.env.GITHUB_OUTPUT, + 'summary<<' + eof + '\n' + summary + '\n' + eof + '\n' + + 'prompt<<' + eof + '\n' + prompt + '\n' + eof + '\n' + ); + " + env: + STALE_JSON: ${{ steps.stale.outputs.stale_json }} + + - name: Open review PR + if: steps.stale.outputs.has_stale == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ github.event.release.tag_name || 'manual' }}" + BRANCH="skills/review-${VERSION}" + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git commit --allow-empty -m "chore: review stale skills for ${VERSION}" + git push origin "$BRANCH" + + gh pr create \ + --title "Review stale skills (${VERSION})" \ + --body "$(cat <<'PREOF' + ## Stale Skills Detected + + The following skills may need updates after the latest release: + + ${{ steps.summary.outputs.summary }} + + --- + + ### Update Prompt + + Paste this into your coding agent (Claude Code, Cursor, etc.): + + ~~~ + ${{ steps.summary.outputs.prompt }} + ~~~ + + PREOF + )" \ + --head "$BRANCH" \ + --base main diff --git a/.github/workflows/notify-playbooks.yml b/.github/workflows/notify-playbooks.yml new file mode 100644 index 00000000000..5e583930e72 --- /dev/null +++ b/.github/workflows/notify-playbooks.yml @@ -0,0 +1,54 @@ +# notify-intent.yml — Drop this into your library repo's .github/workflows/ +# +# Fires a repository_dispatch event to TanStack/intent whenever docs or +# source files change on merge to main. This triggers the skill staleness +# check workflow in the intent repo. +# +# Requirements: +# - A fine-grained PAT with contents:write on TanStack/intent stored +# as the INTENT_NOTIFY_TOKEN repository secret. +# +# Template variables (replaced by `intent setup`): +# @tanstack/react-router — e.g. @tanstack/query +# docs/** — e.g. docs/** +# packages/*/src/** — e.g. packages/query-core/src/** +# +# Adapted for TanStack Router monorepo: watches all packages. + +name: Notify Intent + +on: + push: + branches: [main] + paths: + - 'docs/**' + - 'packages/*/src/**' + +jobs: + notify: + name: Notify TanStack Intent + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Collect changed files + id: changes + run: | + FILES=$(git diff --name-only HEAD~1 HEAD | jq -R -s -c 'split("\n") | map(select(length > 0))') + echo "files=$FILES" >> "$GITHUB_OUTPUT" + + - name: Dispatch to intent repo + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.INTENT_NOTIFY_TOKEN }} + repository: TanStack/intent + event-type: skill-check + client-payload: | + { + "package": "@tanstack/router", + "sha": "${{ github.sha }}", + "changed_files": ${{ steps.changes.outputs.files }} + } diff --git a/.github/workflows/validate-skills.yml b/.github/workflows/validate-skills.yml new file mode 100644 index 00000000000..8f39716aa98 --- /dev/null +++ b/.github/workflows/validate-skills.yml @@ -0,0 +1,52 @@ +# validate-skills.yml — Drop this into your library repo's .github/workflows/ +# +# Validates skill files on PRs that touch the skills/ directory. +# Ensures frontmatter is correct, names match paths, and files stay under +# the 500-line limit. + +name: Validate Skills + +on: + pull_request: + paths: + - 'skills/**' + - '**/skills/**' + +jobs: + validate: + name: Validate skill files + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install intent CLI + run: npm install -g @tanstack/intent + + - name: Find and validate skills + run: | + # Find all directories containing SKILL.md files + SKILLS_DIR="" + if [ -d "skills" ]; then + SKILLS_DIR="skills" + elif [ -d "packages" ]; then + # Monorepo — find skills/ under packages + for dir in packages/*/skills; do + if [ -d "$dir" ]; then + echo "Validating $dir..." + intent validate "$dir" + fi + done + exit 0 + fi + + if [ -n "$SKILLS_DIR" ]; then + intent validate "$SKILLS_DIR" + else + echo "No skills/ directory found — skipping validation." + fi diff --git a/_artifacts/domain_map.yaml b/_artifacts/domain_map.yaml new file mode 100644 index 00000000000..4b5d6f8386b --- /dev/null +++ b/_artifacts/domain_map.yaml @@ -0,0 +1,1091 @@ +# domain_map.yaml +# Generated by skill-domain-discovery +# Library: TanStack Router +# Version: 1.166.2 +# Date: 2026-03-07 +# Status: reviewed + +library: + name: '@tanstack/react-router' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Type-safe router for React and Solid with built-in SWR caching, + JSON-first search params, file-based route generation, and + end-to-end type inference. + primary_framework: 'React' + +domains: + - name: 'Defining Routes' + slug: 'defining-routes' + description: >- + Setting up route trees via file-based, code-based, or virtual + file routes, configuring the router instance, and registering + types for end-to-end inference. + + - name: 'Navigating' + slug: 'navigating' + description: >- + Moving between routes using Links, imperative navigation, + redirects, preloading, active states, and navigation blocking. + + - name: 'Managing URL State' + slug: 'managing-url-state' + description: >- + Reading and writing search params and path params with + validation, serialization, middlewares, and type-safe adapters. + + - name: 'Loading Data' + slug: 'loading-data' + description: >- + Fetching data via route loaders, coordinating external caches, + managing SWR caching, deferred loading, and error/pending states. + + - name: 'Protecting Routes' + slug: 'protecting-routes' + description: >- + Authentication guards, authorization checks, RBAC patterns, + and redirect-based access control via beforeLoad. + + - name: 'Rendering and Layout' + slug: 'rendering-and-layout' + description: >- + Outlets, nested layouts, pathless layouts, code splitting, + error boundaries, not-found handling, route masking, scroll + restoration, and document head management. + + - name: 'Type Safety' + slug: 'type-safety' + description: >- + Router type registration, inference patterns, narrowing with + from, TypeScript performance, and avoiding unnecessary type + annotations. + + - name: 'Server-Side Rendering' + slug: 'server-side-rendering' + description: >- + Non-streaming and streaming SSR setup, hydration, data + serialization, and isomorphic router creation. + +skills: + # ── Defining Routes ────────────────────────────────────────────── + - name: 'Route Setup' + slug: 'route-setup' + domain: 'defining-routes' + description: >- + Set up a TanStack Router project with file-based, code-based, + or virtual file routes, configure the router instance, and + register types. + type: core + packages: + - '@tanstack/react-router' + - '@tanstack/router-core' + - '@tanstack/router-plugin' + - '@tanstack/router-generator' + - '@tanstack/router-cli' + - '@tanstack/virtual-file-routes' + - '@tanstack/eslint-plugin-router' + covers: + - createRouter + - createRootRoute + - createRootRouteWithContext + - createRoute + - createFileRoute + - createLazyFileRoute + - addChildren + - routeTree.gen.ts + - declare module Register + - TanStackRouter bundler plugin + - tsr.config.json + - virtual file routes API + - file naming conventions + - route matching and sorting + tasks: + - 'Scaffold a new TanStack Router project' + - 'Configure file-based routing with Vite plugin' + - 'Set up code-based route tree manually' + - 'Configure virtual file routes for custom conventions' + - 'Register router types via module declaration' + - 'Set up ESLint plugin for route property ordering' + reference_candidates: + - topic: 'file naming conventions' + reason: '>10 distinct conventions (dot separator, $ token, _ prefix/suffix, () groups, [] escaping, index, route.tsx, __root)' + failure_modes: + - mistake: 'Missing router type registration' + mechanism: >- + Without declare module Register, top-level exports like + Link, useNavigate, useParams have no type safety. Code + compiles but all route paths are untyped strings. + wrong_pattern: | + const router = createRouter({ routeTree }) + // no declare module — Link to="" has no autocomplete + correct_pattern: | + const router = createRouter({ routeTree }) + declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } + } + source: 'docs/router/guide/creating-a-router.md' + priority: CRITICAL + status: active + skills: ['route-setup', 'type-safety'] + + - mistake: 'Not committing routeTree.gen.ts' + mechanism: >- + routeTree.gen.ts is runtime code, not a build artifact. + Without it in version control, other developers cannot + build or run the project. + source: 'docs/router/faq.md' + priority: HIGH + status: active + + - mistake: 'Wrong route property order breaks type inference' + mechanism: >- + Properties like beforeLoad must come before loader in + createRoute/createFileRoute calls. Incorrect order causes + context types to not flow into loader. + wrong_pattern: | + createFileRoute('/posts')({ + loader: ({ context }) => context.queryClient.ensureQueryData(postsQuery), + beforeLoad: ({ context }) => ({ queryClient: context.queryClient }), + }) + correct_pattern: | + createFileRoute('/posts')({ + beforeLoad: ({ context }) => ({ queryClient: context.queryClient }), + loader: ({ context }) => context.queryClient.ensureQueryData(postsQuery), + }) + source: 'docs/router/eslint/create-route-property-order.md' + priority: HIGH + status: active + + - mistake: 'Placing TanStackRouter plugin after framework plugin' + mechanism: >- + The TanStackRouter Vite plugin must be listed before the + React/Solid plugin in the Vite config. Wrong order causes + route generation to fail silently. + wrong_pattern: | + plugins: [react(), TanStackRouterVite()] + correct_pattern: | + plugins: [TanStackRouterVite(), react()] + source: 'docs/router/installation/with-vite.md' + priority: HIGH + status: active + + - mistake: 'Using getParentRoute incorrectly in code-based routing' + mechanism: >- + Every non-root route must reference its correct parent via + getParentRoute. Wrong parent breaks type inference for + context, search params, and path params from ancestors. + source: 'docs/router/decisions-on-dx.md' + priority: HIGH + status: active + + compositions: + - library: '@tanstack/eslint-plugin-router' + skill: null + + # ── Navigating ─────────────────────────────────────────────────── + - name: 'Navigation' + slug: 'navigation' + domain: 'navigating' + description: >- + Navigate between routes using Link, useNavigate, Navigate + component, and router.navigate. Configure preloading, active + link states, and navigation blocking. + type: core + packages: + - '@tanstack/react-router' + - '@tanstack/router-core' + covers: + - Link component + - useNavigate hook + - Navigate component + - router.navigate + - ToOptions / NavigateOptions / LinkOptions + - from / to relative navigation + - activeOptions / activeProps / inactiveProps + - preload (intent / viewport / render) + - preloadDelay / defaultPreloadDelay + - useBlocker hook + - Block component + - createLink (custom link components) + - linkOptions helper + - MatchRoute component + - scroll restoration + tasks: + - 'Create type-safe links between routes' + - 'Navigate programmatically after an action' + - 'Set up link preloading on hover intent' + - 'Style active and inactive links' + - 'Block navigation for unsaved changes' + - 'Create custom link components with createLink' + - 'Build reusable navigation options with linkOptions' + - 'Configure scroll restoration' + failure_modes: + - mistake: 'Interpolating params into the to string' + mechanism: >- + Agents build paths like to={`/posts/${id}`} instead of + using the params option. Breaks type safety and param + encoding. + wrong_pattern: | + Post + correct_pattern: | + Post + source: 'docs/router/guide/navigation.md' + priority: CRITICAL + status: active + skills: ['navigation', 'type-safety'] + + - mistake: 'Using useNavigate instead of Link for user-clickable elements' + mechanism: >- + Link renders a real with href, supports cmd/ctrl+click + and preloading. useNavigate produces no accessible element. + source: 'docs/router/guide/navigation.md' + priority: MEDIUM + status: active + + - mistake: 'Not providing from for relative navigation' + mechanism: >- + Without from, the router assumes navigation from root. + Only absolute paths are autocompleted. Relative paths + like ".." resolve from root instead of current route. + wrong_pattern: | + Back + correct_pattern: | + Back + source: 'docs/router/guide/navigation.md' + priority: HIGH + status: active + + - mistake: 'Using search as object instead of function loses existing params' + mechanism: >- + search={{ page: 2 }} replaces all search params. + Must use function form to preserve existing params. + wrong_pattern: | + Page 2 + correct_pattern: | + ({ ...prev, page: 2 })}>Page 2 + source: 'docs/router/how-to/navigate-with-search-params.md' + priority: HIGH + status: active + skills: ['navigation', 'search-params'] + + # ── Managing URL State ─────────────────────────────────────────── + - name: 'Search Params' + slug: 'search-params' + domain: 'managing-url-state' + description: >- + Validate, read, write, and transform search params with + schema validation, adapters, middlewares, and custom + serialization. + type: core + packages: + - '@tanstack/react-router' + - '@tanstack/router-core' + - '@tanstack/zod-adapter' + - '@tanstack/valibot-adapter' + - '@tanstack/arktype-adapter' + covers: + - validateSearch + - useSearch hook + - Route.useSearch + - getRouteApi().useSearch + - search middlewares + - retainSearchParams + - stripSearchParams + - custom serialization (parseSearch / stringifySearch) + - zod adapter / valibot adapter / arktype adapter + - search param inheritance from parent routes + - structural sharing for search params + tasks: + - 'Add validated search params to a route' + - 'Read search params in components' + - 'Update search params while preserving others' + - 'Share search params across parent and child routes' + - 'Set up search param middlewares for retention/stripping' + - 'Configure custom search param serialization' + - 'Handle arrays, objects, and dates in search params' + subsystems: + - name: 'Zod adapter' + package: '@tanstack/zod-adapter' + config_surface: 'zodValidator, fallback() wrapper for .catch/.default type correctness' + - name: 'Valibot adapter' + package: '@tanstack/valibot-adapter' + config_surface: 'Standard Schema compliant, requires valibot 1.0' + - name: 'ArkType adapter' + package: '@tanstack/arktype-adapter' + config_surface: 'Standard Schema compliant, requires arktype 2.0-rc, pass directly to validateSearch' + reference_candidates: + - topic: 'search param validation patterns' + reason: '>10 distinct patterns (basic, optional, arrays, objects, dates, discriminated unions, transforms, conditional, cross-route inheritance, middleware chaining)' + failure_modes: + - mistake: 'Using zod .catch() instead of adapter fallback()' + mechanism: >- + Zod .catch() makes the output type unknown, losing type + safety. The @tanstack/zod-adapter fallback() preserves + the correct inferred type. + wrong_pattern: | + z.object({ page: z.number().catch(1) }) + correct_pattern: | + import { fallback } from '@tanstack/zod-adapter' + z.object({ page: fallback(z.number(), 1) }) + source: 'docs/router/guide/search-params.md' + priority: HIGH + status: active + + - mistake: 'Returning entire search object from loaderDeps' + mechanism: >- + loaderDeps: ({ search }) => search causes loader to + re-run on ANY search param change, not just relevant ones. + Deep equality comparison fires on every unrelated change. + wrong_pattern: | + loaderDeps: ({ search }) => search + correct_pattern: | + loaderDeps: ({ search }) => ({ page: search.page }) + source: 'docs/router/guide/data-loading.md' + priority: HIGH + status: active + skills: ['search-params', 'data-loading'] + + - mistake: 'Passing Date objects in search params' + mechanism: >- + Date objects serialize to [object Object] in URLs. + Must convert to ISO strings for correct serialization. + wrong_pattern: | + + correct_pattern: | + + source: 'docs/router/how-to/arrays-objects-dates-search-params.md' + priority: HIGH + status: active + + - mistake: 'Parent route missing validateSearch blocks inheritance' + mechanism: >- + Child routes cannot access shared search params unless + the parent route defines validateSearch. Without it, + the types and runtime values are not available. + source: 'docs/router/how-to/share-search-params-across-routes.md' + priority: MEDIUM + status: active + + - name: 'Path Params' + slug: 'path-params' + domain: 'managing-url-state' + description: >- + Define and consume dynamic path segments, splat routes, + optional params, and prefix/suffix patterns. + type: core + packages: + - '@tanstack/react-router' + - '@tanstack/router-core' + covers: + - '$paramName dynamic segments' + - '$ splat / _splat' + - '{-$paramName} optional params' + - '{$paramName} prefix/suffix patterns' + - useParams hook + - params.parse / params.stringify + - pathParamsAllowedCharacters + tasks: + - 'Define routes with dynamic path parameters' + - 'Use splat routes for catch-all matching' + - 'Set up optional path parameters for i18n' + - 'Navigate with type-safe path params' + failure_modes: + - mistake: 'Interpolating path params into to string' + mechanism: >- + Same as navigation failure mode — agents build template + literal paths instead of using params option. + source: 'docs/router/guide/navigation.md' + priority: CRITICAL + status: active + skills: ['path-params', 'navigation'] + + - mistake: 'Using * for splat routes instead of $' + mechanism: >- + TanStack Router uses $ for splat routes, not * like some + other routers. The captured value is under _splat key, + not * (though * works in v1 for backwards compat, removed in v2). + source: 'docs/router/routing/routing-concepts.md' + priority: MEDIUM + status: 'fixed-but-legacy-risk' + version_context: '* works in v1 for compat but will be removed in v2' + + - mistake: 'Using curly braces for basic dynamic segments' + mechanism: >- + Basic dynamic segments use $postId (no braces). Curly + braces are only for prefix/suffix patterns like {$postId}.txt + or optional params {-$locale}. + source: 'docs/router/guide/path-params.md' + priority: MEDIUM + status: active + + # ── Loading Data ───────────────────────────────────────────────── + - name: 'Data Loading' + slug: 'data-loading' + domain: 'loading-data' + description: >- + Load data via route loaders with built-in SWR caching, + configure staleTime/gcTime, use loaderDeps for cache keys, + handle pending and error states, and inject dependencies + via router context. + type: core + packages: + - '@tanstack/react-router' + - '@tanstack/router-core' + covers: + - loader option + - loaderDeps + - staleTime / gcTime / defaultPreloadStaleTime + - pendingComponent / pendingMs / pendingMinMs + - errorComponent / onError / onCatch + - beforeLoad + - router context / createRootRouteWithContext + - router.invalidate + - Await component + - deferred data loading + tasks: + - 'Add a data loader to a route' + - 'Configure cache timing (staleTime, gcTime)' + - 'Set up loader dependencies for search-param-driven data' + - 'Show pending UI during slow loads' + - 'Handle loader errors with error boundaries' + - 'Inject dependencies via router context' + - 'Defer non-critical data with unawaited promises' + - 'Invalidate cached data after mutations' + failure_modes: + - mistake: 'Assuming loaders only run on the server' + mechanism: >- + TanStack Router is client-first. Loaders run on the client + by default (and also on the server if using TanStack Start). + Agents trained on Remix/Next assume loaders are server-only + and put server-only code (DB queries, fs access) in them. + source: 'maintainer interview' + priority: CRITICAL + status: active + skills: ['data-loading', 'ssr'] + + - mistake: 'Not understanding staleTime default is 0' + mechanism: >- + Default staleTime is 0, meaning data is always considered + stale and reloads in background on every route re-match. + Agents may not set staleTime, causing excessive refetching. + source: 'docs/router/guide/data-loading.md' + priority: MEDIUM + status: active + + - mistake: 'Using reset() instead of router.invalidate() in error components' + mechanism: >- + For loader errors, reset() only resets the error boundary + but does not re-run the loader. Must use router.invalidate() + to coordinate both reload and boundary reset. + wrong_pattern: | + function PostErrorComponent({ error, reset }) { + return + } + correct_pattern: | + function PostErrorComponent({ error, reset }) { + const router = useRouter() + return + } + source: 'docs/router/guide/data-loading.md' + priority: HIGH + status: active + + - mistake: 'Double parentheses missing on createRootRouteWithContext' + mechanism: >- + createRootRouteWithContext() is a factory — it returns + a function. Must call it twice: createRootRouteWithContext()(routeOptions). + Missing second call is a common error. + wrong_pattern: | + const rootRoute = createRootRouteWithContext<{ auth: AuthState }>({ + component: RootComponent, + }) + correct_pattern: | + const rootRoute = createRootRouteWithContext<{ auth: AuthState }>()({ + component: RootComponent, + }) + source: 'docs/router/guide/data-loading.md' + priority: HIGH + status: active + + - mistake: 'Using React hooks in beforeLoad or loader' + mechanism: >- + beforeLoad and loader are not React components — hooks + cannot be called in them. Must extract hook state in a + wrapper component and pass via router context. + source: 'docs/router/guide/router-context.md' + priority: HIGH + status: active + + compositions: + - library: '@tanstack/react-query' + skill: 'external-data-loading' + + - name: 'External Data Loading' + slug: 'external-data-loading' + domain: 'loading-data' + description: >- + Integrate TanStack Query (or other external caches) with + Router for coordinated data loading, SSR dehydration/hydration, + and streaming. + type: composition + packages: + - '@tanstack/react-router' + - '@tanstack/react-router-ssr-query' + covers: + - External cache coordination pattern + - queryClient.ensureQueryData in loader + - useSuspenseQuery in components + - setupRouterSsrQueryIntegration + - defaultPreloadStaleTime + - dehydrate / hydrate router options + - QueryClient per-request isolation + - Wrap router option for providers + tasks: + - 'Set up TanStack Query with Router' + - 'Preload query data in route loaders' + - 'Configure SSR dehydration/hydration with Query' + - 'Stream query data during SSR' + failure_modes: + - mistake: 'Not setting defaultPreloadStaleTime to 0 with external caches' + mechanism: >- + Router's built-in preload cache (30s default) prevents + external library from controlling freshness. Set + defaultPreloadStaleTime: 0 to let Query manage caching. + wrong_pattern: | + createRouter({ routeTree }) + correct_pattern: | + createRouter({ routeTree, defaultPreloadStaleTime: 0 }) + source: 'docs/router/guide/external-data-loading.md' + priority: HIGH + status: active + + - mistake: 'Creating QueryClient outside createRouter for SSR' + mechanism: >- + For SSR, QueryClient must be created inside createRouter + to ensure per-request isolation. A shared singleton leaks + data between requests. + wrong_pattern: | + const queryClient = new QueryClient() + const router = createRouter({ routeTree, context: { queryClient } }) + correct_pattern: | + function createAppRouter() { + const queryClient = new QueryClient() + return createRouter({ routeTree, context: { queryClient } }) + } + source: 'docs/router/guide/external-data-loading.md' + priority: HIGH + status: active + + - mistake: 'Awaiting prefetchQuery in loader blocks rendering' + mechanism: >- + For streaming/deferred patterns, prefetchQuery should NOT + be awaited in the loader — it starts fetching on server + and streams without blocking. Awaiting defeats the purpose. + source: 'docs/router/integrations/query.md' + priority: MEDIUM + status: active + + # ── Protecting Routes ──────────────────────────────────────────── + - name: 'Auth and Guards' + slug: 'auth-and-guards' + domain: 'protecting-routes' + description: >- + Protect routes with authentication checks, authorization + guards, and RBAC patterns using beforeLoad redirects and + layout routes. + type: core + packages: + - '@tanstack/react-router' + - '@tanstack/router-core' + covers: + - beforeLoad for auth checks + - redirect() / throw redirect() + - isRedirect helper + - Authenticated layout routes (_authenticated) + - Non-redirect auth (inline login) + - RBAC with roles and permissions + - Auth provider integration (Auth0, Clerk, Supabase) + - Router context for auth state + tasks: + - 'Protect routes with authentication checks' + - 'Redirect unauthenticated users to login' + - 'Implement role-based access control' + - 'Integrate third-party auth providers' + - 'Pass auth state through router context' + failure_modes: + - mistake: 'Auth check in component instead of beforeLoad' + mechanism: >- + Component-level auth checks cause protected content to + flash before redirect. beforeLoad runs before any + component rendering and prevents the flash entirely. + wrong_pattern: | + component: () => { + const auth = useAuth() + if (!auth.isAuthenticated) return + return + } + correct_pattern: | + beforeLoad: ({ context }) => { + if (!context.auth.isAuthenticated) { + throw redirect({ to: '/login' }) + } + } + source: 'docs/router/how-to/setup-authentication.md' + priority: HIGH + status: active + + - mistake: 'Not re-throwing redirects in try/catch' + mechanism: >- + redirect() works by throwing. If beforeLoad has a try/catch + for error handling, the redirect gets swallowed unless + re-thrown via isRedirect() check. + wrong_pattern: | + beforeLoad: async ({ context }) => { + try { + await validateSession(context.auth) + } catch (e) { + // swallows redirect! + console.error(e) + } + } + correct_pattern: | + beforeLoad: async ({ context }) => { + try { + await validateSession(context.auth) + } catch (e) { + if (isRedirect(e)) throw e + console.error(e) + } + } + source: 'docs/router/guide/authenticated-routes.md' + priority: HIGH + status: active + + - mistake: 'Trying to conditionally render root route component' + mechanism: >- + Root route is always rendered regardless of auth state. + Use a pathless layout route (_authenticated) with + beforeLoad for conditional rendering. + source: 'docs/router/faq.md' + priority: MEDIUM + status: active + + # ── Rendering and Layout ───────────────────────────────────────── + - name: 'Code Splitting' + slug: 'code-splitting' + domain: 'rendering-and-layout' + description: >- + Split route code into lazy-loaded chunks using automatic + code splitting, .lazy.tsx files, or code-based lazy loading. + type: core + packages: + - '@tanstack/react-router' + - '@tanstack/router-plugin' + covers: + - autoCodeSplitting plugin option + - .lazy.tsx file convention + - createLazyFileRoute + - createLazyRoute + - lazyRouteComponent + - getRouteApi for code-split components + - codeSplitGroupings per-route override + - splitBehavior programmatic config + - Critical vs non-critical route properties + tasks: + - 'Enable automatic code splitting' + - 'Manually split a route with .lazy.tsx' + - 'Access typed hooks from code-split components' + - 'Customize which properties are split per route' + failure_modes: + - mistake: 'Exporting route property functions prevents code splitting' + mechanism: >- + Exported functions from route files are included in the + main bundle even with auto code splitting. Component + functions must NOT be exported. + wrong_pattern: | + export function PostsComponent() { return
Posts
} + correct_pattern: | + function PostsComponent() { return
Posts
} + source: 'docs/router/guide/automatic-code-splitting.md' + priority: HIGH + status: active + + - mistake: 'Trying to code-split the root route' + mechanism: >- + __root.tsx does not support code splitting — it is always + rendered regardless of current route. + source: 'docs/router/guide/code-splitting.md' + priority: MEDIUM + status: active + + - mistake: 'Splitting the loader adds double async cost' + mechanism: >- + Loader is already async. Splitting it requires fetching + the chunk AND then executing the loader, adding latency. + Loaders should stay in the main bundle unless there is + a specific reason to split them. + source: 'docs/router/guide/code-splitting.md' + priority: MEDIUM + status: active + + - mistake: 'Importing Route in code-split files for typed hooks' + mechanism: >- + Importing the Route object in a lazy-loaded file pulls + the route config into the lazy chunk, defeating code + splitting. Use getRouteApi('/path') instead. + wrong_pattern: | + import { Route } from './posts.tsx' + const data = Route.useLoaderData() + correct_pattern: | + import { getRouteApi } from '@tanstack/react-router' + const route = getRouteApi('/posts') + const data = route.useLoaderData() + source: 'docs/router/guide/code-splitting.md' + priority: HIGH + status: active + + - name: 'Not Found and Errors' + slug: 'not-found-and-errors' + domain: 'rendering-and-layout' + description: >- + Handle not-found routes, missing resources, and route-level + errors with notFound(), notFoundComponent, errorComponent, + and CatchBoundary. + type: core + packages: + - '@tanstack/react-router' + - '@tanstack/router-core' + covers: + - notFound() function + - notFoundComponent + - defaultNotFoundComponent + - notFoundMode (fuzzy / root) + - errorComponent + - onError / onCatch + - CatchBoundary component + - NotFoundRoute (deprecated) + tasks: + - 'Add a 404 page for unmatched routes' + - 'Throw not-found for missing resources in loaders' + - 'Configure error boundaries per route' + - 'Target specific routes with notFound()' + failure_modes: + - mistake: 'Using deprecated NotFoundRoute' + mechanism: >- + NotFoundRoute is deprecated. When present, notFound() and + notFoundComponent will NOT work. Must remove NotFoundRoute + and use notFoundComponent instead. + source: 'docs/router/guide/not-found-errors.md' + priority: HIGH + status: 'fixed-but-legacy-risk' + version_context: 'NotFoundRoute deprecated, will be removed in next major' + + - mistake: 'Expecting useLoaderData to work in notFoundComponent' + mechanism: >- + When notFoundComponent renders, the route's loader may + not have completed. useLoaderData() may return undefined. + Use useParams, useSearch, or useRouteContext instead. + source: 'docs/router/guide/not-found-errors.md' + priority: MEDIUM + status: active + + - mistake: 'Leaf route cannot handle not-found errors' + mechanism: >- + Routes without children never render Outlet and therefore + cannot catch not-found errors. Only routes with children + (or the root route) can have notFoundComponent. + source: 'docs/router/guide/not-found-errors.md' + priority: MEDIUM + status: active + + - name: 'Route Masking' + slug: 'route-masking' + domain: 'rendering-and-layout' + description: >- + Mask the browser URL while navigating to a different internal + route, with configurable unmask behavior. + type: core + packages: + - '@tanstack/react-router' + - '@tanstack/router-core' + covers: + - mask option on Link / navigate + - createRouteMask + - routeMasks router option + - unmaskOnReload + - location.maskedLocation + - location.state.__tempLocation + tasks: + - 'Show a modal route with a clean URL' + - 'Configure declarative route masks' + - 'Control unmask behavior on page reload' + failure_modes: + - mistake: 'Expecting masked URLs to survive sharing' + mechanism: >- + Masking data is stored in browser history state. When a + URL is shared or opened in a new tab, masking data is + lost and the visible URL is used directly. + source: 'docs/router/guide/route-masking.md' + priority: MEDIUM + status: active + + # ── Type Safety ────────────────────────────────────────────────── + - name: 'Type Safety' + slug: 'type-safety' + domain: 'type-safety' + description: >- + Leverage TanStack Router's fully inferred type system. + Avoid unnecessary type annotations, narrow routes with from, + and optimize TypeScript performance. + type: core + packages: + - '@tanstack/react-router' + - '@tanstack/router-core' + covers: + - Full type inference philosophy + - Register module declaration + - from narrowing on hooks and Link + - strict false for shared components + - getRouteApi for code-split typed access + - addChildren with object syntax for TS perf + - LinkProps / ValidateLinkOptions type utilities + - as const satisfies pattern + tasks: + - 'Register router types for global type safety' + - 'Narrow route types with from parameter' + - 'Use getRouteApi in code-split components' + - 'Optimize TypeScript performance in large route trees' + - 'Build type-safe link option utilities' + failure_modes: + - mistake: 'Adding type annotations or casts to inferred values' + mechanism: >- + TanStack Router is fully type-safe with full inference. + Adding as Type, generic type parameters, satisfies, or + explicit type annotations is unnecessary and can mask + real type errors or break the inference chain. + wrong_pattern: | + const search = useSearch({ from: '/posts' }) as { page: number } + correct_pattern: | + const search = useSearch({ from: '/posts' }) + // search.page is already typed as number + source: 'maintainer interview' + priority: CRITICAL + status: active + + - mistake: 'Using un-narrowed LinkProps type' + mechanism: >- + LinkProps without generics is an extremely large union type. + Using it as a variable type causes severe TS performance + degradation. Use as const satisfies or narrow with generics. + wrong_pattern: | + const props: LinkProps = { to: '/posts' } + correct_pattern: | + const props = { to: '/posts' } as const satisfies LinkProps + source: 'docs/router/guide/type-safety.md' + priority: HIGH + status: active + + - mistake: 'Not narrowing Link/useNavigate with from' + mechanism: >- + Without from, search resolves to a union of ALL routes + search params. TypeScript check time grows linearly with + route count. Always provide from to narrow. + wrong_pattern: | + + correct_pattern: | + + source: 'docs/router/guide/type-safety.md' + priority: HIGH + status: active + + # ── Server-Side Rendering ──────────────────────────────────────── + - name: 'SSR' + slug: 'ssr' + domain: 'server-side-rendering' + description: >- + Set up server-side rendering with non-streaming or streaming + approaches, manage hydration, and handle document head. + type: core + packages: + - '@tanstack/react-router' + - '@tanstack/router-core' + covers: + - RouterClient / RouterServer + - renderRouterToString / renderRouterToStream + - createRequestHandler + - defaultRenderHandler / defaultStreamHandler + - HeadContent / Scripts components + - head route option (meta, links, styles, scripts) + - ScriptOnce component + - Automatic loader dehydration/hydration + - Memory history on server + - Data serialization (Date, Error, FormData, undefined) + tasks: + - 'Set up non-streaming SSR with Express' + - 'Set up streaming SSR' + - 'Configure document head management' + - 'Handle hydration mismatches' + failure_modes: + - mistake: 'Using browser APIs in loaders without environment check' + mechanism: >- + Loaders run on both client and server when using SSR. + Browser-only APIs (window, document, localStorage) will + throw on the server. + source: 'docs/router/guide/ssr.md' + priority: HIGH + status: active + skills: ['ssr', 'data-loading'] + + - mistake: 'Using hash fragments for server-rendered content' + mechanism: >- + Hash fragments are client-only — browsers never send them + to the server. Using hash for conditional rendering causes + hydration mismatches during SSR. + source: 'docs/router/guide/navigation.md' + priority: MEDIUM + status: active + + - mistake: 'Generating Next.js or Remix SSR patterns' + mechanism: >- + Agents trained on Next.js generate getServerSideProps, + App Router patterns, or server components. Agents trained + on Remix generate server-only loader exports. TanStack + Router has its own SSR API (RouterClient/RouterServer). + source: 'maintainer interview' + priority: CRITICAL + status: active + skills: ['ssr', 'route-setup', 'data-loading'] + + # ── Migration ──────────────────────────────────────────────────── + - name: 'Migrate from React Router' + slug: 'migrate-from-react-router' + domain: 'defining-routes' + description: >- + Step-by-step migration from React Router v7 to TanStack Router, + covering route definitions, navigation, search params, and + data loading. + type: lifecycle + packages: + - '@tanstack/react-router' + - '@tanstack/router-plugin' + covers: + - Migration checklist + - Route definition conversion + - Link / useNavigate API differences + - useSearchParams to validateSearch + useSearch + - useParams with from + - Outlet replacement + - Loader conversion + - Code splitting differences + tasks: + - 'Migrate routes from React Router to TanStack Router' + - 'Convert React Router Links and navigation' + - 'Replace useSearchParams with validated search params' + - 'Convert React Router loaders' + failure_modes: + - mistake: 'Leaving React Router imports alongside TanStack Router' + mechanism: >- + React Router and TanStack Router share similar API names + (Link, useNavigate, Outlet). Leftover React Router imports + cause context errors ("cannot use useNavigate outside of + context"). Uninstall react-router-dom to surface these. + source: 'docs/router/how-to/migrate-from-react-router.md' + priority: HIGH + status: active + + - mistake: 'Using React Router useSearchParams pattern' + mechanism: >- + React Router uses useSearchParams() returning URLSearchParams. + TanStack Router uses validateSearch schema + useSearch() + returning typed objects. Agents may generate the RR pattern. + wrong_pattern: | + const [searchParams, setSearchParams] = useSearchParams() + const page = Number(searchParams.get('page')) + correct_pattern: | + // In route definition: + validateSearch: z.object({ page: fallback(z.number(), 1) }) + // In component: + const { page } = Route.useSearch() + source: 'docs/router/installation/migrate-from-react-router.md' + priority: HIGH + status: active + +tensions: + - name: 'Type safety strictness vs rapid prototyping' + skills: ['type-safety', 'route-setup'] + description: >- + Full type inference requires proper setup (Register, correct + property order, getParentRoute). This upfront cost conflicts + with quick prototyping where developers want to skip setup. + implication: >- + Agents skip Register declaration or use type casts to silence + errors, producing code that compiles but has no type safety. + + - name: 'Client-first loaders vs SSR expectations' + skills: ['data-loading', 'ssr'] + description: >- + Loaders are client-first by design but agents assume they are + server-only (Remix/Next mental model). This tension means + browser APIs work in loaders by default but break under SSR. + implication: >- + Agents either put server-only code in client loaders or avoid + browser APIs unnecessarily, depending on which framework mental + model they default to. + + - name: 'Built-in SWR cache vs external cache coordination' + skills: ['data-loading', 'external-data-loading'] + description: >- + Router has built-in caching with staleTime/gcTime defaults. + When using an external cache like React Query, the built-in + cache must be bypassed (defaultPreloadStaleTime: 0) or it + conflicts with the external library's freshness management. + implication: >- + Agents use both caches simultaneously without disabling the + built-in one, causing stale data or double-fetching. + + - name: 'Code splitting granularity vs loader performance' + skills: ['code-splitting', 'data-loading'] + description: >- + Splitting more aggressively reduces initial bundle but splitting + the loader adds a network round-trip before data can be fetched. + The optimal split point differs per route. + implication: >- + Agents split everything including loaders, adding latency to + data loading without understanding the tradeoff. + +cross_references: + - from: 'route-setup' + to: 'type-safety' + reason: 'Register declaration and property order are prerequisites for type inference' + - from: 'navigation' + to: 'search-params' + reason: 'Link search prop and navigate search option directly interact with search param validation' + - from: 'data-loading' + to: 'search-params' + reason: 'loaderDeps consumes validated search params as cache keys' + - from: 'data-loading' + to: 'external-data-loading' + reason: 'Understanding built-in caching is prerequisite for coordinating with external caches' + - from: 'auth-and-guards' + to: 'data-loading' + reason: 'beforeLoad runs before loader — auth context flows into loader via route context' + - from: 'code-splitting' + to: 'data-loading' + reason: 'Loader splitting decisions affect data loading performance' + - from: 'ssr' + to: 'data-loading' + reason: 'SSR changes where loaders execute — must handle both environments' + - from: 'ssr' + to: 'external-data-loading' + reason: 'SSR dehydration/hydration requires special setup for Query integration' + - from: 'not-found-and-errors' + to: 'data-loading' + reason: 'notFound() thrown in loaders interacts with error boundaries and loader data availability' + - from: 'migrate-from-react-router' + to: 'route-setup' + reason: 'Migration requires understanding TanStack Router route setup to replace RR patterns' + +gaps: [] diff --git a/_artifacts/skill_spec.md b/_artifacts/skill_spec.md new file mode 100644 index 00000000000..35c811a3bc6 --- /dev/null +++ b/_artifacts/skill_spec.md @@ -0,0 +1,193 @@ +# TanStack Router — Skill Spec + +TanStack Router is a type-safe router for React and Solid applications with built-in SWR caching, JSON-first search params, file-based route generation, and end-to-end type inference. It is client-first/isomorphic — loaders run on the client by default and additionally on the server when used with TanStack Start. + +## Domains + +| Domain | Description | Skills | +| --------------------- | ------------------------------------------------------------------- | --------------------------------------------------- | +| Defining Routes | Setting up route trees, configuring the router, registering types | route-setup, migrate-from-react-router | +| Navigating | Links, imperative navigation, preloading, blocking | navigation | +| Managing URL State | Search params and path params with validation and serialization | search-params, path-params | +| Loading Data | Route loaders, SWR caching, external cache coordination, context/DI | data-loading, external-data-loading | +| Protecting Routes | Auth guards, RBAC, beforeLoad redirects | auth-and-guards | +| Rendering and Layout | Code splitting, error/not-found handling, route masking | code-splitting, not-found-and-errors, route-masking | +| Type Safety | Inference patterns, narrowing, TS performance | type-safety | +| Server-Side Rendering | SSR setup, hydration, head management | ssr | + +## Skill Inventory + +| Skill | Type | Domain | What it covers | Failure modes | +| ------------------------- | ----------- | --------------------- | ------------------------------------------------------------------------------------ | ------------- | +| route-setup | core | defining-routes | createRouter, file/code/virtual routing, Register, plugin config, ESLint | 5 | +| navigation | core | navigating | Link, useNavigate, Navigate, preloading, blocking, active states, scroll restoration | 4 | +| search-params | core | managing-url-state | validateSearch, useSearch, middlewares, serialization, adapters | 4 | +| path-params | core | managing-url-state | $params, splats, optional params, prefix/suffix patterns | 3 | +| data-loading | core | loading-data | loader, loaderDeps, staleTime/gcTime, pending/error, context/DI, deferred | 5 | +| external-data-loading | composition | loading-data | TanStack Query integration, SSR dehydration/hydration, streaming | 3 | +| auth-and-guards | core | protecting-routes | beforeLoad redirects, layout auth, RBAC, provider integration | 3 | +| code-splitting | core | rendering-and-layout | autoCodeSplitting, .lazy.tsx, getRouteApi, split groupings | 4 | +| not-found-and-errors | core | rendering-and-layout | notFound(), notFoundComponent, errorComponent, CatchBoundary | 3 | +| route-masking | core | rendering-and-layout | mask option, createRouteMask, unmaskOnReload | 1 | +| type-safety | core | type-safety | Register, from narrowing, TS perf, type utilities | 3 | +| ssr | core | server-side-rendering | RouterClient/Server, streaming, hydration, head management | 3 | +| migrate-from-react-router | lifecycle | defining-routes | Migration checklist, API mapping, search params conversion | 2 | + +## Failure Mode Inventory + +### route-setup (5 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ------------------------------------------------------ | -------- | --------------------------------------- | ------------ | +| 1 | Missing router type registration | CRITICAL | docs/guide/creating-a-router | type-safety | +| 2 | Not committing routeTree.gen.ts | HIGH | docs/faq | — | +| 3 | Wrong route property order breaks type inference | HIGH | docs/eslint/create-route-property-order | — | +| 4 | Placing TanStackRouter plugin after framework plugin | HIGH | docs/installation/with-vite | — | +| 5 | Using getParentRoute incorrectly in code-based routing | HIGH | docs/decisions-on-dx | — | + +### navigation (4 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | -------------------------------------------------------- | -------- | --------------------------------------- | ------------- | +| 1 | Interpolating params into the to string | CRITICAL | docs/guide/navigation | type-safety | +| 2 | Using useNavigate instead of Link for clickable elements | MEDIUM | docs/guide/navigation | — | +| 3 | Not providing from for relative navigation | HIGH | docs/guide/navigation | — | +| 4 | Using search as object instead of function loses params | HIGH | docs/how-to/navigate-with-search-params | search-params | + +### search-params (4 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ------------------------------------------------------ | -------- | -------------------------------- | ------------ | +| 1 | Using zod .catch() instead of adapter fallback() | HIGH | docs/guide/search-params | — | +| 2 | Returning entire search object from loaderDeps | HIGH | docs/guide/data-loading | data-loading | +| 3 | Passing Date objects in search params | HIGH | docs/how-to/arrays-objects-dates | — | +| 4 | Parent route missing validateSearch blocks inheritance | MEDIUM | docs/how-to/share-search-params | — | + +### path-params (3 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --------------------------------------------- | -------- | ----------------------------- | ------------ | +| 1 | Interpolating path params into to string | CRITICAL | docs/guide/navigation | navigation | +| 2 | Using \* for splat routes instead of $ | MEDIUM | docs/routing/routing-concepts | — | +| 3 | Using curly braces for basic dynamic segments | MEDIUM | docs/guide/path-params | — | + +### data-loading (5 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ---------------------------------------------------------------- | -------- | ------------------------- | ------------ | +| 1 | Assuming loaders only run on the server | CRITICAL | maintainer interview | ssr | +| 2 | Not understanding staleTime default is 0 | MEDIUM | docs/guide/data-loading | — | +| 3 | Using reset() instead of router.invalidate() in error components | HIGH | docs/guide/data-loading | — | +| 4 | Double parentheses missing on createRootRouteWithContext | HIGH | docs/guide/data-loading | — | +| 5 | Using React hooks in beforeLoad or loader | HIGH | docs/guide/router-context | — | + +### external-data-loading (3 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ------------------------------------------------- | -------- | -------------------------------- | ------------ | +| 1 | Not setting defaultPreloadStaleTime to 0 | HIGH | docs/guide/external-data-loading | — | +| 2 | Creating QueryClient outside createRouter for SSR | HIGH | docs/guide/external-data-loading | — | +| 3 | Awaiting prefetchQuery in loader blocks rendering | MEDIUM | docs/integrations/query | — | + +### auth-and-guards (3 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --------------------------------------------------- | -------- | -------------------------------- | ------------ | +| 1 | Auth check in component instead of beforeLoad | HIGH | docs/how-to/setup-authentication | — | +| 2 | Not re-throwing redirects in try/catch | HIGH | docs/guide/authenticated-routes | — | +| 3 | Trying to conditionally render root route component | MEDIUM | docs/faq | — | + +### code-splitting (4 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ---------------------------------------------------------- | -------- | ----------------------------------- | ------------ | +| 1 | Exporting route property functions prevents code splitting | HIGH | docs/guide/automatic-code-splitting | — | +| 2 | Trying to code-split the root route | MEDIUM | docs/guide/code-splitting | — | +| 3 | Splitting the loader adds double async cost | MEDIUM | docs/guide/code-splitting | — | +| 4 | Importing Route in code-split files for typed hooks | HIGH | docs/guide/code-splitting | — | + +### not-found-and-errors (3 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ---------------------------------------------------- | -------- | --------------------------- | ------------ | +| 1 | Using deprecated NotFoundRoute | HIGH | docs/guide/not-found-errors | — | +| 2 | Expecting useLoaderData to work in notFoundComponent | MEDIUM | docs/guide/not-found-errors | — | +| 3 | Leaf route cannot handle not-found errors | MEDIUM | docs/guide/not-found-errors | — | + +### route-masking (1 failure mode) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ---------------------------------------- | -------- | ------------------------ | ------------ | +| 1 | Expecting masked URLs to survive sharing | MEDIUM | docs/guide/route-masking | — | + +### type-safety (3 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --------------------------------------------------- | -------- | ---------------------- | ------------ | +| 1 | Adding type annotations or casts to inferred values | CRITICAL | maintainer interview | — | +| 2 | Using un-narrowed LinkProps type | HIGH | docs/guide/type-safety | — | +| 3 | Not narrowing Link/useNavigate with from | HIGH | docs/guide/type-safety | — | + +### ssr (3 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ------------------------------------------------------- | -------- | --------------------- | ------------------------- | +| 1 | Using browser APIs in loaders without environment check | HIGH | docs/guide/ssr | data-loading | +| 2 | Using hash fragments for server-rendered content | MEDIUM | docs/guide/navigation | — | +| 3 | Generating Next.js or Remix SSR patterns | CRITICAL | maintainer interview | route-setup, data-loading | + +### migrate-from-react-router (2 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | ------------------------------------------------------ | -------- | ------------------------------------------- | ------------ | +| 1 | Leaving React Router imports alongside TanStack Router | HIGH | docs/how-to/migrate-from-react-router | — | +| 2 | Using React Router useSearchParams pattern | HIGH | docs/installation/migrate-from-react-router | — | + +## Tensions + +| Tension | Skills | Agent implication | +| ------------------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------- | +| Type safety strictness vs rapid prototyping | type-safety ↔ route-setup | Agents skip Register declaration or use type casts to silence errors | +| Client-first loaders vs SSR expectations | data-loading ↔ ssr | Agents put server-only code in client loaders or avoid browser APIs unnecessarily | +| Built-in SWR cache vs external cache coordination | data-loading ↔ external-data-loading | Agents use both caches simultaneously causing stale data or double-fetching | +| Code splitting granularity vs loader performance | code-splitting ↔ data-loading | Agents split everything including loaders, adding latency | + +## Cross-References + +| From | To | Reason | +| ------------------------- | --------------------- | ---------------------------------------------------------------------------------- | +| route-setup | type-safety | Register declaration and property order are prerequisites for type inference | +| navigation | search-params | Link search prop directly interacts with search param validation | +| data-loading | search-params | loaderDeps consumes validated search params as cache keys | +| data-loading | external-data-loading | Understanding built-in caching is prerequisite for external cache coordination | +| auth-and-guards | data-loading | beforeLoad runs before loader — auth context flows into loader via route context | +| code-splitting | data-loading | Loader splitting decisions affect data loading performance | +| ssr | data-loading | SSR changes where loaders execute — must handle both environments | +| ssr | external-data-loading | SSR dehydration/hydration requires special Query setup | +| not-found-and-errors | data-loading | notFound() in loaders interacts with error boundaries and loader data availability | +| migrate-from-react-router | route-setup | Migration requires understanding TanStack Router route setup | + +## Subsystems & Reference Candidates + +| Skill | Subsystems | Reference candidates | +| ------------- | --------------------------------------------- | -------------------------------------------------------- | +| route-setup | — | file naming conventions (>10 distinct conventions) | +| search-params | Zod adapter, Valibot adapter, ArkType adapter | search param validation patterns (>10 distinct patterns) | +| All others | — | — | + +## Recommended Skill File Structure + +- **Core skills:** route-setup, navigation, search-params, path-params, data-loading, code-splitting, not-found-and-errors, route-masking, type-safety, ssr, auth-and-guards +- **Framework skills:** None needed separately — React and Solid share the same core API surface. Framework-specific notes belong inline. +- **Lifecycle skills:** migrate-from-react-router +- **Composition skills:** external-data-loading (TanStack Query) +- **Reference files:** search-params (validation patterns), route-setup (file naming conventions) + +## Composition Opportunities + +| Library | Integration points | Composition skill needed? | +| --------------------- | --------------------------------------------------------- | ------------------------------------------ | +| @tanstack/react-query | Loader coordination, SSR dehydration/hydration, streaming | Yes — external-data-loading | +| Zod | Search param validation | No — covered as subsystem in search-params | +| Valibot | Search param validation | No — covered as subsystem in search-params | +| ArkType | Search param validation | No — covered as subsystem in search-params | diff --git a/_artifacts/skill_tree.yaml b/_artifacts/skill_tree.yaml new file mode 100644 index 00000000000..b5ce76f5474 --- /dev/null +++ b/_artifacts/skill_tree.yaml @@ -0,0 +1,279 @@ +# skills/_artifacts/skill_tree.yaml +library: + name: '@tanstack/router' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Type-safe router for React and Solid with built-in SWR caching, + JSON-first search params, and end-to-end type inference. +generated_from: + domain_map: '_artifacts/domain_map.yaml' + skill_spec: '_artifacts/skill_spec.md' +generated_at: '2026-03-07' + +skills: + # ── Router Core Skills ─────────────────────────────────────────── + - name: 'Router Core' + slug: 'router-core' + type: 'core' + domain: 'defining-routes' + path: 'skills/router-core/SKILL.md' + package: 'packages/router-core' + description: >- + Framework-agnostic core concepts for TanStack Router: route trees, + createRouter, createRoute, createRootRoute, createRootRouteWithContext, + addChildren, Register type declaration, route matching, route sorting, + file naming conventions. Entry point for all router skills. + sources: + - 'TanStack/router:docs/router/overview.md' + - 'TanStack/router:docs/router/routing/routing-concepts.md' + - 'TanStack/router:docs/router/routing/route-trees.md' + - 'TanStack/router:docs/router/routing/route-matching.md' + - 'TanStack/router:docs/router/guide/creating-a-router.md' + - 'TanStack/router:docs/router/decisions-on-dx.md' + - 'TanStack/router:packages/router-core/src' + + - name: 'Search Params' + slug: 'router-core/search-params' + type: 'sub-skill' + domain: 'managing-url-state' + path: 'skills/router-core/search-params/SKILL.md' + package: 'packages/router-core' + description: >- + validateSearch, search param validation with Zod/Valibot/ArkType adapters, + fallback(), search middlewares (retainSearchParams, stripSearchParams), + custom serialization (parseSearch, stringifySearch), search param + inheritance, loaderDeps for cache keys, reading and writing search params. + requires: + - 'router-core' + sources: + - 'TanStack/router:docs/router/guide/search-params.md' + - 'TanStack/router:docs/router/how-to/setup-basic-search-params.md' + - 'TanStack/router:docs/router/how-to/validate-search-params.md' + - 'TanStack/router:docs/router/how-to/navigate-with-search-params.md' + - 'TanStack/router:docs/router/how-to/share-search-params-across-routes.md' + - 'TanStack/router:docs/router/how-to/arrays-objects-dates-search-params.md' + - 'TanStack/router:docs/router/guide/custom-search-param-serialization.md' + subsystems: + - 'Zod adapter' + - 'Valibot adapter' + - 'ArkType adapter' + references: + - 'references/validation-patterns.md' + + - name: 'Path Params' + slug: 'router-core/path-params' + type: 'sub-skill' + domain: 'managing-url-state' + path: 'skills/router-core/path-params/SKILL.md' + package: 'packages/router-core' + description: >- + Dynamic path segments ($paramName), splat routes ($ / _splat), + optional params ({-$paramName}), prefix/suffix patterns ({$param}.ext), + useParams, params.parse/stringify, pathParamsAllowedCharacters, + i18n locale patterns. + requires: + - 'router-core' + sources: + - 'TanStack/router:docs/router/guide/path-params.md' + - 'TanStack/router:docs/router/guide/internationalization-i18n.md' + - 'TanStack/router:docs/router/routing/routing-concepts.md' + + - name: 'Navigation' + slug: 'router-core/navigation' + type: 'sub-skill' + domain: 'navigating' + path: 'skills/router-core/navigation/SKILL.md' + package: 'packages/router-core' + description: >- + Link component, useNavigate, Navigate component, router.navigate, + ToOptions/NavigateOptions/LinkOptions, from/to relative navigation, + activeOptions/activeProps, preloading (intent/viewport/render), + preloadDelay, navigation blocking (useBlocker, Block), createLink, + linkOptions helper, scroll restoration, MatchRoute. + requires: + - 'router-core' + sources: + - 'TanStack/router:docs/router/guide/navigation.md' + - 'TanStack/router:docs/router/guide/preloading.md' + - 'TanStack/router:docs/router/guide/navigation-blocking.md' + - 'TanStack/router:docs/router/guide/link-options.md' + - 'TanStack/router:docs/router/guide/custom-link.md' + - 'TanStack/router:docs/router/guide/scroll-restoration.md' + + - name: 'Data Loading' + slug: 'router-core/data-loading' + type: 'sub-skill' + domain: 'loading-data' + path: 'skills/router-core/data-loading/SKILL.md' + package: 'packages/router-core' + description: >- + Route loader option, loaderDeps for cache keys, staleTime/gcTime/ + defaultPreloadStaleTime SWR caching, pendingComponent/pendingMs/ + pendingMinMs, errorComponent/onError/onCatch, beforeLoad, router + context and createRootRouteWithContext DI pattern, router.invalidate, + Await component, deferred data loading with unawaited promises. + requires: + - 'router-core' + sources: + - 'TanStack/router:docs/router/guide/data-loading.md' + - 'TanStack/router:docs/router/guide/deferred-data-loading.md' + - 'TanStack/router:docs/router/guide/router-context.md' + - 'TanStack/router:docs/router/guide/data-mutations.md' + + - name: 'Auth and Guards' + slug: 'router-core/auth-and-guards' + type: 'sub-skill' + domain: 'protecting-routes' + path: 'skills/router-core/auth-and-guards/SKILL.md' + package: 'packages/router-core' + description: >- + Route protection with beforeLoad, redirect()/throw redirect(), + isRedirect helper, authenticated layout routes (_authenticated), + non-redirect auth (inline login), RBAC with roles and permissions, + auth provider integration (Auth0, Clerk, Supabase), router context + for auth state. + requires: + - 'router-core' + - 'router-core/data-loading' + sources: + - 'TanStack/router:docs/router/guide/authenticated-routes.md' + - 'TanStack/router:docs/router/how-to/setup-authentication.md' + - 'TanStack/router:docs/router/how-to/setup-auth-providers.md' + - 'TanStack/router:docs/router/how-to/setup-rbac.md' + + - name: 'Code Splitting' + slug: 'router-core/code-splitting' + type: 'sub-skill' + domain: 'rendering-and-layout' + path: 'skills/router-core/code-splitting/SKILL.md' + package: 'packages/router-core' + description: >- + Automatic code splitting (autoCodeSplitting), .lazy.tsx convention, + createLazyFileRoute, createLazyRoute, lazyRouteComponent, getRouteApi + for typed hooks in split files, codeSplitGroupings per-route override, + splitBehavior programmatic config, critical vs non-critical properties. + requires: + - 'router-core' + sources: + - 'TanStack/router:docs/router/guide/code-splitting.md' + - 'TanStack/router:docs/router/guide/automatic-code-splitting.md' + + - name: 'Not Found and Errors' + slug: 'router-core/not-found-and-errors' + type: 'sub-skill' + domain: 'rendering-and-layout' + path: 'skills/router-core/not-found-and-errors/SKILL.md' + package: 'packages/router-core' + description: >- + notFound() function, notFoundComponent, defaultNotFoundComponent, + notFoundMode (fuzzy/root), errorComponent, onError/onCatch, + CatchBoundary, NotFoundRoute (deprecated), route masking (mask + option, createRouteMask, unmaskOnReload). + requires: + - 'router-core' + sources: + - 'TanStack/router:docs/router/guide/not-found-errors.md' + - 'TanStack/router:docs/router/guide/route-masking.md' + + - name: 'Type Safety' + slug: 'router-core/type-safety' + type: 'sub-skill' + domain: 'type-safety' + path: 'skills/router-core/type-safety/SKILL.md' + package: 'packages/router-core' + description: >- + Full type inference philosophy (never cast, never annotate inferred + values), Register module declaration, from narrowing on hooks and + Link, strict:false for shared components, getRouteApi for code-split + typed access, addChildren with object syntax for TS perf, LinkProps + and ValidateLinkOptions type utilities, as const satisfies pattern. + requires: + - 'router-core' + sources: + - 'TanStack/router:docs/router/guide/type-safety.md' + - 'TanStack/router:docs/router/guide/type-utilities.md' + - 'TanStack/router:docs/router/guide/render-optimizations.md' + + - name: 'SSR' + slug: 'router-core/ssr' + type: 'sub-skill' + domain: 'server-side-rendering' + path: 'skills/router-core/ssr/SKILL.md' + package: 'packages/router-core' + description: >- + Non-streaming and streaming SSR, RouterClient/RouterServer, + renderRouterToString/renderRouterToStream, createRequestHandler, + defaultRenderHandler/defaultStreamHandler, HeadContent/Scripts + components, head route option (meta/links/styles/scripts), + ScriptOnce, automatic loader dehydration/hydration, memory + history on server, data serialization, document head management. + requires: + - 'router-core' + - 'router-core/data-loading' + sources: + - 'TanStack/router:docs/router/guide/ssr.md' + - 'TanStack/router:docs/router/guide/document-head-management.md' + - 'TanStack/router:docs/router/how-to/setup-ssr.md' + + # ── React Router Skills ────────────────────────────────────────── + - name: 'React Router' + slug: 'react-router' + type: 'framework' + domain: 'defining-routes' + path: 'skills/react-router/SKILL.md' + package: 'packages/react-router' + description: >- + React bindings for TanStack Router: RouterProvider, useRouter, + useRouterState, useMatch, useMatches, useLocation, useSearch, + useParams, useNavigate, useLoaderData, useLoaderDeps, + useRouteContext, useBlocker, useCanGoBack, Link, Navigate, + Outlet, CatchBoundary, ErrorComponent. React-specific patterns + for hooks, providers, SSR hydration, and createLink with + forwardRef. + requires: + - 'router-core' + sources: + - 'TanStack/router:packages/react-router/src' + - 'TanStack/router:docs/router/guide/creating-a-router.md' + - 'TanStack/router:docs/router/installation/manual.md' + + # ── Composition Skills ─────────────────────────────────────────── + - name: 'External Data Loading (TanStack Query)' + slug: 'compositions/router-query' + type: 'composition' + domain: 'loading-data' + path: 'skills/compositions/router-query/SKILL.md' + package: 'packages/react-router' + description: >- + Integrating TanStack Router with TanStack Query: queryClient + in router context, ensureQueryData/prefetchQuery in loaders, + useSuspenseQuery in components, defaultPreloadStaleTime: 0, + setupRouterSsrQueryIntegration for SSR dehydration/hydration + and streaming, per-request QueryClient isolation. + requires: + - 'router-core' + - 'router-core/data-loading' + - 'react-router' + sources: + - 'TanStack/router:docs/router/guide/external-data-loading.md' + - 'TanStack/router:docs/router/integrations/query.md' + + # ── Lifecycle Skills ───────────────────────────────────────────── + - name: 'Migrate from React Router' + slug: 'lifecycle/migrate-from-react-router' + type: 'lifecycle' + domain: 'defining-routes' + path: 'skills/lifecycle/migrate-from-react-router/SKILL.md' + package: 'packages/react-router' + description: >- + Step-by-step migration from React Router v7 to TanStack Router: + route definition conversion, Link/useNavigate API differences, + useSearchParams to validateSearch + useSearch, useParams with from, + Outlet replacement, loader conversion, code splitting differences. + requires: + - 'router-core' + - 'react-router' + sources: + - 'TanStack/router:docs/router/how-to/migrate-from-react-router.md' + - 'TanStack/router:docs/router/installation/migrate-from-react-router.md' diff --git a/_artifacts/start_domain_map.yaml b/_artifacts/start_domain_map.yaml new file mode 100644 index 00000000000..b5e84d4dfbc --- /dev/null +++ b/_artifacts/start_domain_map.yaml @@ -0,0 +1,482 @@ +# domain_map.yaml +# Generated by skill-domain-discovery +# Library: TanStack Start +# Version: 1.166.2 +# Date: 2026-03-07 +# Status: reviewed + +library: + name: '@tanstack/react-start' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Full-stack React framework built on TanStack Router and Vite. Adds + SSR, streaming, server functions (type-safe RPCs), middleware, + server routes, and universal deployment. Isomorphic by default — + all code runs in both environments unless explicitly constrained. + primary_framework: 'React' + +domains: + - name: 'Project Setup' + slug: 'project-setup' + description: >- + Scaffolding a Start project, configuring Vite plugin, router + setup with getRouter(), root route with document shell, client + and server entry points. + + - name: 'Server Functions' + slug: 'server-functions' + description: >- + Creating type-safe RPCs with createServerFn, input validation, + calling from loaders/components/other server functions, error + handling, streaming responses. + + - name: 'Middleware and Context' + slug: 'middleware-and-context' + description: >- + Request middleware, server function middleware, context passing + with sendContext, global middleware via createStart, middleware + factories, fetch override precedence. + + - name: 'Execution Model' + slug: 'execution-model' + description: >- + Isomorphic code execution, environment functions + (createServerFn, createServerOnlyFn, createClientOnlyFn, + createIsomorphicFn), import protection, dead code elimination, + environment variable safety. + + - name: 'Server Routes' + slug: 'server-routes' + description: >- + Server-side API endpoints defined in routes, HTTP method + handlers, handler middleware, request/response patterns. + + - name: 'Deployment and Rendering' + slug: 'deployment-and-rendering' + description: >- + Hosting providers (Cloudflare, Netlify, Vercel, Node/Docker), + selective SSR, SPA mode, static prerendering, ISR with + Cache-Control headers, SEO and head management. + +skills: + # ── Project Setup ──────────────────────────────────────────────── + - name: 'Start Setup' + slug: 'start-setup' + domain: 'project-setup' + description: >- + Scaffold a TanStack Start project, configure Vite plugin with + tanstackStart(), set up router with getRouter(), create root + route with document shell (HeadContent, Scripts, Outlet), + configure client and server entry points. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-plugin-core' + covers: + - tanstackStart Vite plugin + - getRouter() factory pattern + - Root route document shell + - HeadContent / Scripts / Outlet + - Client entry point (optional) + - Server entry point (optional) + - routeTree.gen.ts + - tsconfig configuration + tasks: + - 'Scaffold a new TanStack Start project' + - 'Configure the Vite plugin' + - 'Set up the router factory' + - 'Customize client/server entry points' + failure_modes: + - mistake: 'React plugin before Start plugin in Vite config' + mechanism: >- + Start's Vite plugin must come before React's plugin. + Wrong order causes route generation and server function + compilation to fail. + wrong_pattern: | + plugins: [react(), tanstackStart()] + correct_pattern: | + plugins: [tanstackStart(), react()] + source: 'docs/start/framework/react/build-from-scratch.md' + priority: CRITICAL + status: active + + - mistake: 'Enabling verbatimModuleSyntax in tsconfig' + mechanism: >- + verbatimModuleSyntax causes server bundles to leak into + client bundles. Must be disabled. + source: 'docs/start/framework/react/build-from-scratch.md' + priority: HIGH + status: active + + - mistake: 'Missing Scripts component in root route' + mechanism: >- + The Scripts component must be rendered in the body of + the root route for proper hydration and functionality. + Without it, client-side JavaScript does not load. + source: 'docs/start/framework/react/guide/routing.md' + priority: HIGH + status: active + + # ── Server Functions ───────────────────────────────────────────── + - name: 'Server Functions' + slug: 'server-functions' + domain: 'server-functions' + description: >- + Create type-safe RPCs with createServerFn, validate inputs + with Zod or plain functions, call from loaders/components/ + event handlers, handle errors/redirects/notFound, stream + responses with ReadableStream or async generators. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-client-core' + - '@tanstack/start-server-core' + covers: + - createServerFn (GET/POST) + - inputValidator (Zod or function) + - useServerFn hook + - Server context utilities (getRequest, getRequestHeader, setResponseHeader, setResponseStatus) + - Error handling (throw errors, redirect, notFound) + - Streaming (ReadableStream, async generators) + - File organization (.functions.ts, .server.ts) + - FormData handling + tasks: + - 'Create a server function for data fetching' + - 'Validate server function inputs' + - 'Call server functions from components' + - 'Stream data from server functions' + - 'Handle errors in server functions' + failure_modes: + - mistake: 'Putting server-only code in loaders instead of server functions' + mechanism: >- + Loaders are ISOMORPHIC — they run on both client and server. + Database queries, file system access, and secret API keys + in loaders will either fail on the client or leak to the + client bundle. Use createServerFn for server-only logic. + wrong_pattern: | + export const Route = createFileRoute('/posts')({ + loader: async () => { + const posts = await db.query('SELECT * FROM posts') + return { posts } + }, + }) + correct_pattern: | + const getPosts = createServerFn({ method: 'GET' }) + .handler(async () => { + const posts = await db.query('SELECT * FROM posts') + return { posts } + }) + + export const Route = createFileRoute('/posts')({ + loader: () => getPosts(), + }) + source: 'maintainer interview' + priority: CRITICAL + status: active + skills: ['server-functions', 'execution-model'] + + - mistake: 'Using dynamic imports for server functions' + mechanism: >- + Dynamic imports of server functions can cause bundler + issues. Static imports are safe — the build process + replaces server implementations with RPC stubs. + wrong_pattern: | + const { getUser } = await import('~/utils/users.functions') + correct_pattern: | + import { getUser } from '~/utils/users.functions' + source: 'docs/start/framework/react/guide/server-functions.md' + priority: HIGH + status: active + + - mistake: 'Generating Next.js or Remix server patterns' + mechanism: >- + Agents generate getServerSideProps, "use server" directives, + or Remix-style loader exports. TanStack Start uses + createServerFn for server-only code. + wrong_pattern: | + 'use server' + export async function getUser() { ... } + correct_pattern: | + const getUser = createServerFn({ method: 'GET' }) + .handler(async () => { ... }) + source: 'maintainer interview' + priority: CRITICAL + status: active + + # ── Middleware and Context ─────────────────────────────────────── + - name: 'Middleware' + slug: 'middleware' + domain: 'middleware-and-context' + description: >- + Request middleware and server function middleware with + createMiddleware, context passing via next(), sendContext + for client-server transfer, global middleware via createStart + in src/start.ts, middleware factories, fetch override + precedence, header merging. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-client-core' + - '@tanstack/start-server-core' + covers: + - createMiddleware + - Request middleware (.server only) + - Server function middleware (.client + .server) + - Context passing via next({ context }) + - sendContext for client-server transfer + - Global middleware (createStart in src/start.ts) + - Middleware factories + - Fetch override precedence + - Header merging + - Method order enforcement (middleware → inputValidator → client → server) + tasks: + - 'Add authentication middleware' + - 'Pass context through middleware chain' + - 'Configure global request middleware' + - 'Create reusable middleware factories' + failure_modes: + - mistake: 'Trusting client context without server validation' + mechanism: >- + Client context via sendContext is NOT validated by default. + Dynamic user-generated data must be validated in server-side + middleware before use. + source: 'docs/start/framework/react/guide/middleware.md' + priority: HIGH + status: active + + - mistake: 'Wrong middleware method order' + mechanism: >- + TypeScript enforces method order: middleware → inputValidator + → client → server. Wrong order causes type errors and + runtime failures. + source: 'docs/start/framework/react/guide/middleware.md' + priority: MEDIUM + status: active + + - mistake: 'Confusing request vs server function middleware' + mechanism: >- + Request middleware runs on ALL server requests (SSR, server + routes, server functions). Server function middleware runs + only for server functions and has .client() method. Using + the wrong type causes unexpected scope. + source: 'docs/start/framework/react/guide/middleware.md' + priority: MEDIUM + status: active + + # ── Execution Model ────────────────────────────────────────────── + - name: 'Execution Model' + slug: 'execution-model' + domain: 'execution-model' + description: >- + Isomorphic code execution model, environment boundary functions + (createServerFn, createServerOnlyFn, createClientOnlyFn, + createIsomorphicFn), import protection, dead code elimination, + environment variable safety (VITE_ prefix), useHydrated hook. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-client-core' + - '@tanstack/start-server-core' + covers: + - Isomorphic-by-default principle + - createServerFn (RPC boundary) + - createServerOnlyFn (throws on client) + - createClientOnlyFn (throws on server) + - createIsomorphicFn (different impl per env) + - ClientOnly component + - useHydrated hook + - Import protection (experimental) + - Environment variables (VITE_ prefix, process.env) + - Dead code elimination / tree shaking + tasks: + - 'Protect server-only code from client bundles' + - 'Use environment-specific implementations' + - 'Handle environment variables safely' + - 'Debug import protection violations' + failure_modes: + - mistake: 'Assuming loaders are server-only' + mechanism: >- + ALL code in TanStack Start is isomorphic by default. + Loaders run on BOTH server and client. Server-only + operations must use createServerFn, createServerOnlyFn, + or server routes. + wrong_pattern: | + export const Route = createFileRoute('/dashboard')({ + loader: async () => { + const secret = process.env.API_SECRET + return fetch(`https://api.example.com/data`, { + headers: { Authorization: secret } + }) + }, + }) + correct_pattern: | + const getData = createServerFn({ method: 'GET' }) + .handler(async () => { + const secret = process.env.API_SECRET + return fetch(`https://api.example.com/data`, { + headers: { Authorization: secret } + }) + }) + + export const Route = createFileRoute('/dashboard')({ + loader: () => getData(), + }) + source: 'docs/start/framework/react/guide/execution-model.md' + priority: CRITICAL + status: active + skills: ['execution-model', 'server-functions'] + + - mistake: 'Exposing secrets via module-level process.env' + mechanism: >- + Module-level process.env access runs in both environments. + The variable value leaks into the client bundle. Access + secrets only inside createServerFn or createServerOnlyFn. + wrong_pattern: | + const apiKey = process.env.SECRET_KEY + export function fetchData() { ... } + correct_pattern: | + const fetchData = createServerFn({ method: 'GET' }) + .handler(async () => { + const apiKey = process.env.SECRET_KEY + return fetch(url, { headers: { Authorization: apiKey } }) + }) + source: 'docs/start/framework/react/guide/execution-model.md' + priority: CRITICAL + status: active + + - mistake: 'Using VITE_ prefix for server secrets' + mechanism: >- + VITE_ prefixed variables are exposed to the client bundle. + Server secrets must NOT have the VITE_ prefix. Access + them via process.env inside server functions only. + source: 'docs/start/framework/react/guide/environment-variables.md' + priority: CRITICAL + status: active + + # ── Server Routes ──────────────────────────────────────────────── + - name: 'Server Routes' + slug: 'server-routes' + domain: 'server-routes' + description: >- + Define server-side API endpoints alongside app routes using + the server property with HTTP method handlers, per-handler + middleware via createHandlers, request/response patterns. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-server-core' + covers: + - server property on createFileRoute + - handlers object (GET, POST, PUT, DELETE) + - createHandlers for per-handler middleware + - Handler context (request, params, context) + - Request body parsing (json, text, formData) + - Response helpers (Response.json) + - File naming for API routes + tasks: + - 'Create a REST API endpoint' + - 'Add middleware to server route handlers' + - 'Handle different HTTP methods' + failure_modes: + - mistake: 'Duplicate path resolution for server routes' + mechanism: >- + Each route can only have a single handler file. Having + both users.ts and users/index.ts causes errors. + source: 'docs/start/framework/react/guide/server-routes.md' + priority: MEDIUM + status: active + + # ── Deployment and Rendering ───────────────────────────────────── + - name: 'Deployment' + slug: 'deployment' + domain: 'deployment-and-rendering' + description: >- + Deploy TanStack Start to Cloudflare Workers, Netlify, Vercel, + Node.js/Docker, Bun, Railway. Configure selective SSR, SPA + mode, static prerendering, ISR with Cache-Control headers. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-plugin-core' + covers: + - Cloudflare Workers deployment + - Netlify deployment + - Vercel / Railway deployment + - Node.js / Docker deployment + - Bun deployment + - Selective SSR (ssr option per route) + - SPA mode configuration + - Static prerendering (prerender option) + - ISR with Cache-Control headers + - SEO (head property, structured data) + tasks: + - 'Deploy to Cloudflare Workers' + - 'Deploy to Netlify' + - 'Configure selective SSR per route' + - 'Enable SPA mode' + - 'Set up static prerendering' + - 'Configure ISR with cache headers' + failure_modes: + - mistake: 'Bun deployment with React 18' + mechanism: >- + Bun-specific deployment only works with React 19. + For React 18, use Node.js deployment guidelines. + source: 'docs/start/framework/react/guide/hosting.md' + priority: MEDIUM + status: active + + - mistake: 'Missing nodejs_compat flag for Cloudflare Workers' + mechanism: >- + Cloudflare Workers requires compatibility_flags: + ["nodejs_compat"] in wrangler config. Without it, + Node.js APIs used by Start fail at runtime. + source: 'docs/start/framework/react/guide/hosting.md' + priority: HIGH + status: active + + - mistake: 'Child route loosening parent SSR config' + mechanism: >- + SSR config inherits from parent and can only become MORE + restrictive (true → data-only → false). A child cannot + set ssr: true if parent has ssr: false. + source: 'docs/start/framework/react/guide/selective-ssr.md' + priority: MEDIUM + status: active + +tensions: + - name: 'Isomorphic defaults vs server-only expectations' + skills: ['execution-model', 'server-functions'] + description: >- + All code runs everywhere by default. Agents trained on + Next.js/Remix assume loaders and route code are server-only. + implication: >- + Agents put secrets, DB queries, and file system access in + loaders instead of server functions, causing client-side + failures or security leaks. + + - name: 'Simplicity of isomorphic code vs security boundaries' + skills: ['execution-model', 'middleware'] + description: >- + The isomorphic model makes code easy to write but requires + explicit boundaries for security. Agents don't realize they + need to actively constrain execution environment. + implication: >- + Agents expose secrets via module-level process.env or + forget to validate sendContext data in middleware. + +cross_references: + - from: 'server-functions' + to: 'execution-model' + reason: 'Server functions ARE the isomorphic boundary — understanding the execution model is prerequisite' + - from: 'server-functions' + to: 'middleware' + reason: 'Server function middleware chains compose with server functions' + - from: 'middleware' + to: 'server-routes' + reason: 'Server routes use the same middleware system' + - from: 'deployment' + to: 'execution-model' + reason: 'Deployment target affects which environment code runs in' + +gaps: [] diff --git a/_artifacts/start_skill_tree.yaml b/_artifacts/start_skill_tree.yaml new file mode 100644 index 00000000000..cf743b067ca --- /dev/null +++ b/_artifacts/start_skill_tree.yaml @@ -0,0 +1,158 @@ +# skills/_artifacts/start_skill_tree.yaml +library: + name: '@tanstack/react-start' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Full-stack React framework built on TanStack Router and Vite. + Adds SSR, streaming, server functions (type-safe RPCs), middleware, + server routes, and universal deployment. Isomorphic by default — + all code runs in both environments unless explicitly constrained. +generated_from: + domain_map: '_artifacts/start_domain_map.yaml' +generated_at: '2026-03-07' + +skills: + # ── Start Core Skills ─────────────────────────────────────────── + - name: 'Start Core' + slug: 'start-core' + type: 'core' + domain: 'project-setup' + path: 'skills/start-core/SKILL.md' + package: 'packages/start-client-core' + description: >- + Core overview for TanStack Start: tanstackStart() Vite plugin, + getRouter() factory, root route document shell (HeadContent, + Scripts, Outlet), client/server entry points, routeTree.gen.ts, + tsconfig configuration. Entry point for all Start skills. + sources: + - 'TanStack/router:docs/start/framework/react/build-from-scratch.md' + - 'TanStack/router:docs/start/framework/react/quick-start.md' + - 'TanStack/router:docs/start/framework/react/guide/routing.md' + + - name: 'Server Functions' + slug: 'start-core/server-functions' + type: 'sub-skill' + domain: 'server-functions' + path: 'skills/start-core/server-functions/SKILL.md' + package: 'packages/start-client-core' + description: >- + createServerFn (GET/POST), inputValidator (Zod or function), + useServerFn hook, server context utilities (getRequest, + getRequestHeader, setResponseHeader, setResponseStatus), error + handling (throw errors, redirect, notFound), streaming + (ReadableStream, async generators), FormData handling, file + organization (.functions.ts, .server.ts). + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/server-functions.md' + + - name: 'Middleware' + slug: 'start-core/middleware' + type: 'sub-skill' + domain: 'middleware-and-context' + path: 'skills/start-core/middleware/SKILL.md' + package: 'packages/start-client-core' + description: >- + createMiddleware, request middleware (.server only), server + function middleware (.client + .server), context passing via + next({ context }), sendContext for client-server transfer, + global middleware via createStart in src/start.ts, middleware + factories, method order enforcement. + requires: + - 'start-core' + - 'start-core/server-functions' + sources: + - 'TanStack/router:docs/start/framework/react/guide/middleware.md' + + - name: 'Execution Model' + slug: 'start-core/execution-model' + type: 'sub-skill' + domain: 'execution-model' + path: 'skills/start-core/execution-model/SKILL.md' + package: 'packages/start-client-core' + description: >- + Isomorphic-by-default principle, environment boundary functions + (createServerFn, createServerOnlyFn, createClientOnlyFn, + createIsomorphicFn), ClientOnly component, useHydrated hook, + import protection, dead code elimination, environment variable + safety (VITE_ prefix, process.env). + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/execution-model.md' + - 'TanStack/router:docs/start/framework/react/guide/environment-variables.md' + + - name: 'Server Routes' + slug: 'start-core/server-routes' + type: 'sub-skill' + domain: 'server-routes' + path: 'skills/start-core/server-routes/SKILL.md' + package: 'packages/start-client-core' + description: >- + Server-side API endpoints using the server property on + createFileRoute, HTTP method handlers (GET, POST, PUT, DELETE), + createHandlers for per-handler middleware, handler context + (request, params, context), request body parsing, response + helpers, file naming for API routes. + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/server-routes.md' + + - name: 'Deployment' + slug: 'start-core/deployment' + type: 'sub-skill' + domain: 'deployment-and-rendering' + path: 'skills/start-core/deployment/SKILL.md' + package: 'packages/start-client-core' + description: >- + Deploy to Cloudflare Workers, Netlify, Vercel, Node.js/Docker, + Bun, Railway. Selective SSR (ssr option per route), SPA mode, + static prerendering, ISR with Cache-Control headers, SEO and + head management. + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/hosting.md' + - 'TanStack/router:docs/start/framework/react/guide/selective-ssr.md' + - 'TanStack/router:docs/start/framework/react/guide/static-prerendering.md' + - 'TanStack/router:docs/start/framework/react/guide/full-stack-seo.md' + + # ── React Start Skills ────────────────────────────────────────── + - name: 'React Start' + slug: 'react-start' + type: 'framework' + domain: 'project-setup' + path: 'skills/react-start/SKILL.md' + package: 'packages/react-start' + description: >- + React bindings for TanStack Start: createStart, StartClient, + StartServer, React-specific imports, re-exports from + @tanstack/react-router, full project setup with React. + requires: + - 'start-core' + sources: + - 'TanStack/router:packages/react-start/src' + - 'TanStack/router:docs/start/framework/react/build-from-scratch.md' + + # ── Lifecycle Skills ──────────────────────────────────────────── + - name: 'Migrate from Next.js' + slug: 'lifecycle/migrate-from-nextjs' + type: 'lifecycle' + domain: 'project-setup' + path: 'skills/lifecycle/migrate-from-nextjs/SKILL.md' + package: 'packages/react-start' + description: >- + Step-by-step migration from Next.js App Router to TanStack + Start: route definition conversion, API mapping, server + function conversion from Server Actions, middleware conversion, + data fetching pattern changes. + requires: + - 'start-core' + - 'react-start' + sources: + - 'TanStack/router:docs/start/framework/react/guide/server-functions.md' + - 'TanStack/router:docs/start/framework/react/guide/middleware.md' + - 'TanStack/router:docs/start/framework/react/guide/execution-model.md' diff --git a/packages/react-router/bin/intent.js b/packages/react-router/bin/intent.js new file mode 100755 index 00000000000..2cf2efab472 --- /dev/null +++ b/packages/react-router/bin/intent.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// Auto-generated by @tanstack/intent setup +// Exposes the intent end-user CLI for consumers of this library. +// Commit this file, then add to your package.json: +// "bin": { "intent": "./bin/intent.js" } +try { + await import('@tanstack/intent/intent-library') +} catch (e) { + if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + console.error('@tanstack/intent is not installed.') + console.error('') + console.error('Install it as a dev dependency:') + console.error(' npm add -D @tanstack/intent') + console.error('') + console.error('Or run directly:') + console.error(' npx @tanstack/intent@latest list') + process.exit(1) + } + throw e +} diff --git a/packages/react-router/package.json b/packages/react-router/package.json index ca3fc012e33..2af9103d163 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -90,7 +90,10 @@ "sideEffects": false, "files": [ "dist", - "src" + "src", + "skills", + "bin", + "!skills/_artifacts" ], "engines": { "node": ">=20.19" @@ -104,6 +107,7 @@ "tiny-warning": "^1.0.3" }, "devDependencies": { + "@tanstack/intent": "^0.0.14", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@vitejs/plugin-react": "^4.3.4", @@ -117,5 +121,8 @@ "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" + }, + "bin": { + "intent": "./bin/intent.js" } } diff --git a/packages/react-router/skills/compositions/router-query/SKILL.md b/packages/react-router/skills/compositions/router-query/SKILL.md new file mode 100644 index 00000000000..5be24645b9c --- /dev/null +++ b/packages/react-router/skills/compositions/router-query/SKILL.md @@ -0,0 +1,408 @@ +--- +name: compositions/router-query +description: >- + Integrating TanStack Router with TanStack Query: queryClient + in router context, ensureQueryData/prefetchQuery in loaders, + useSuspenseQuery in components, defaultPreloadStaleTime: 0, + setupRouterSsrQueryIntegration for SSR dehydration/hydration + and streaming, per-request QueryClient isolation. +type: composition +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core + - router-core/data-loading + - react-router +sources: + - TanStack/router:docs/router/guide/external-data-loading.md + - TanStack/router:docs/router/integrations/query.md +--- + +# TanStack Router + TanStack Query Integration + +This skill requires familiarity with both TanStack Router and TanStack Query. Read [router-core](../../../../router-core/skills/router-core/SKILL.md) and [react-router](../../react-router/SKILL.md) first. + +This skill covers coordinating TanStack Query as an external data cache with TanStack Router's loader system. The router acts as a **coordinator** — it triggers data fetching during navigation, while Query manages caching, background refetching, and data lifecycle. + +> **CRITICAL**: Set `defaultPreloadStaleTime: 0` when using TanStack Query. Without this, Router's built-in preload cache (30s default) prevents Query from controlling data freshness. + +> **CRITICAL**: For SSR, create `QueryClient` inside the `createRouter` factory function. A module-level singleton leaks data between server requests. + +## Setup: QueryClient in Router Context + +### Basic (Client-Only) + +```tsx +// src/main.tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { + RouterProvider, + createRouter, + createRootRouteWithContext, +} from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +// Root route declares that router context includes queryClient +// (root route file creates it with createRootRouteWithContext — see below) + +const queryClient = new QueryClient() + +const router = createRouter({ + routeTree, + defaultPreloadStaleTime: 0, // Let Query manage caching + context: { queryClient }, + Wrap: ({ children }) => ( + {children} + ), +}) + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} + +function App() { + return +} +``` + +### Root Route with Context + +```tsx +// src/routes/__root.tsx +import { createRootRouteWithContext, Outlet } from '@tanstack/react-router' +import type { QueryClient } from '@tanstack/react-query' + +// Double parentheses: factory pattern +export const Route = createRootRouteWithContext<{ + queryClient: QueryClient +}>()({ + component: () => , +}) +``` + +### SSR-Safe Setup + +```tsx +// src/router.tsx +import { QueryClient } from '@tanstack/react-query' +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function createAppRouter() { + // Fresh QueryClient per request — prevents data leaking between SSR requests + const queryClient = new QueryClient() + + return createRouter({ + routeTree, + defaultPreloadStaleTime: 0, + context: { queryClient }, + Wrap: ({ children }) => ( + {children} + ), + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} +``` + +### SSR with `setupRouterSsrQueryIntegration` + +For automatic SSR dehydration/hydration and streaming: + +```bash +npm install @tanstack/react-router-ssr-query +``` + +```tsx +// src/router.tsx +import { QueryClient } from '@tanstack/react-query' +import { createRouter } from '@tanstack/react-router' +import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query' +import { routeTree } from './routeTree.gen' + +export function createAppRouter() { + const queryClient = new QueryClient() + + const router = createRouter({ + routeTree, + defaultPreloadStaleTime: 0, + context: { queryClient }, + }) + + setupRouterSsrQueryIntegration({ + router, + queryClient, + // wrapQueryClient: true (default — wraps with QueryClientProvider) + // handleRedirects: true (default — handles redirect() from queries) + }) + + return router +} +``` + +The integration: + +- Dehydrates query state on the server and hydrates on the client automatically +- Streams queries that resolve during server render to the client +- Handles `redirect()` thrown from queries/mutations + +### Manual SSR Dehydration/Hydration (Without SSR Query Package) + +```tsx +// src/router.tsx +import { QueryClient, dehydrate, hydrate } from '@tanstack/react-query' +import { QueryClientProvider } from '@tanstack/react-query' +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function createAppRouter() { + const queryClient = new QueryClient() + + return createRouter({ + routeTree, + defaultPreloadStaleTime: 0, + context: { queryClient }, + dehydrate: () => ({ + queryClientState: dehydrate(queryClient), + }), + hydrate: (dehydrated) => { + hydrate(queryClient, dehydrated.queryClientState) + }, + Wrap: ({ children }) => ( + {children} + ), + }) +} +``` + +## Core Pattern: `ensureQueryData` in Loader + `useSuspenseQuery` in Component + +This is the recommended pattern. The loader ensures data is in the cache before render (no loading flash). The component subscribes to the cache for updates. + +```tsx +// src/routes/posts.tsx +import { queryOptions, useSuspenseQuery } from '@tanstack/react-query' +import { createFileRoute } from '@tanstack/react-router' + +const postsQueryOptions = queryOptions({ + queryKey: ['posts'], + queryFn: () => fetch('/api/posts').then((r) => r.json()), +}) + +export const Route = createFileRoute('/posts')({ + loader: ({ context }) => { + // ensureQueryData returns cached data if fresh, fetches if stale + return context.queryClient.ensureQueryData(postsQueryOptions) + }, + component: PostsPage, +}) + +function PostsPage() { + // useSuspenseQuery subscribes to cache — gets background updates + const { data: posts } = useSuspenseQuery(postsQueryOptions) + + return ( +
    + {posts.map((post: any) => ( +
  • {post.title}
  • + ))} +
+ ) +} +``` + +### With Dynamic Params + +```tsx +// src/routes/posts/$postId.tsx +import { queryOptions, useSuspenseQuery } from '@tanstack/react-query' +import { createFileRoute } from '@tanstack/react-router' + +const postQueryOptions = (postId: string) => + queryOptions({ + queryKey: ['posts', postId], + queryFn: () => fetch(`/api/posts/${postId}`).then((r) => r.json()), + }) + +export const Route = createFileRoute('/posts/$postId')({ + loader: ({ context, params }) => { + return context.queryClient.ensureQueryData(postQueryOptions(params.postId)) + }, + component: PostPage, +}) + +function PostPage() { + const { postId } = Route.useParams() + const { data: post } = useSuspenseQuery(postQueryOptions(postId)) + + return
{post.title}
+} +``` + +## Streaming Pattern: `prefetchQuery` (Not Awaited) + +For non-critical data, start the fetch without blocking navigation: + +```tsx +export const Route = createFileRoute('/dashboard')({ + loader: ({ context }) => { + // Await critical data + const user = context.queryClient.ensureQueryData(userQueryOptions) + + // Start non-critical fetch without awaiting — streams during SSR + context.queryClient.prefetchQuery(analyticsQueryOptions) + + return user + }, + component: Dashboard, +}) + +function Dashboard() { + // Critical: suspense (data ready immediately) + const { data: user } = useSuspenseQuery(userQueryOptions) + + // Non-critical: regular query (shows loading state) + const { data: analytics, isLoading } = useQuery(analyticsQueryOptions) + + return ( +
+

Welcome {user.name}

+ {isLoading ? : } +
+ ) +} +``` + +## Error Handling with `useQueryErrorResetBoundary` + +```tsx +import { useEffect } from 'react' +import { useQueryErrorResetBoundary } from '@tanstack/react-query' +import { useRouter } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts')({ + loader: ({ context }) => + context.queryClient.ensureQueryData(postsQueryOptions), + errorComponent: PostsErrorComponent, + component: PostsPage, +}) + +function PostsErrorComponent({ + error, + reset, +}: { + error: Error + reset: () => void +}) { + const router = useRouter() + const queryErrorResetBoundary = useQueryErrorResetBoundary() + + useEffect(() => { + queryErrorResetBoundary.reset() + }, [queryErrorResetBoundary]) + + return ( +
+

{error.message}

+ +
+ ) +} +``` + +## Common Mistakes + +### 1. HIGH: Not setting `defaultPreloadStaleTime` to 0 + +Router has a built-in preload cache (default `staleTime` for preloads is 30s). This prevents Query from controlling data freshness during preloading. + +```tsx +// WRONG — Router's preload cache serves stale data, Query never refetches +const router = createRouter({ routeTree }) + +// CORRECT — disable Router's preload cache, let Query manage freshness +const router = createRouter({ + routeTree, + defaultPreloadStaleTime: 0, +}) +``` + +### 2. HIGH: Creating QueryClient outside `createRouter` for SSR + +A module-level singleton `QueryClient` is shared across all server requests, leaking user data between requests. + +```tsx +// WRONG — shared across SSR requests +const queryClient = new QueryClient() +export function createRouter() { + return createTanstackRouter({ + routeTree, + context: { queryClient }, + }) +} + +// CORRECT — new QueryClient per createRouter call +export function createRouter() { + const queryClient = new QueryClient() + return createTanstackRouter({ + routeTree, + context: { queryClient }, + }) +} +``` + +### 3. MEDIUM: Awaiting `prefetchQuery` in loader blocks rendering + +`prefetchQuery` is designed to fire-and-forget. Awaiting it blocks the navigation transition until the data resolves, defeating the purpose of streaming. + +```tsx +// WRONG — blocks navigation, no streaming benefit +loader: async ({ context }) => { + await context.queryClient.prefetchQuery(analyticsQueryOptions) +} + +// CORRECT — fire and forget for streaming +loader: ({ context }) => { + context.queryClient.prefetchQuery(analyticsQueryOptions) +} + +// If you need to block (critical data), use ensureQueryData instead: +loader: ({ context }) => { + return context.queryClient.ensureQueryData(criticalQueryOptions) +} +``` + +### 4. HIGH: Missing double parentheses on `createRootRouteWithContext` + +`createRootRouteWithContext()` is a factory — it returns a function. The second call passes route options. + +```tsx +// WRONG — passing options to the factory, not the returned function +const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>({ + component: RootComponent, +}) + +// CORRECT — double call: factory()({options}) +const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>()({ + component: RootComponent, +}) +``` + +## Tension: Built-In SWR Cache vs External Cache + +TanStack Router has its own SWR cache (`staleTime`, `gcTime`, `defaultPreloadStaleTime`). When using Query as an external cache: + +- Set `defaultPreloadStaleTime: 0` to prevent Router's cache from short-circuiting Query's freshness logic +- Router's `staleTime`/`gcTime` still apply to the loader return value. For pure Query patterns, return nothing from the loader (just `ensureQueryData` for the side effect) and read data exclusively from `useSuspenseQuery` +- `router.invalidate()` re-runs loaders (which call `ensureQueryData`), but Query decides whether to actually refetch based on its own `staleTime` + +## Cross-References + +- [router-core/data-loading](../../../../router-core/skills/router-core/data-loading/SKILL.md) — built-in loader caching fundamentals +- [router-core/ssr](../../../../router-core/skills/router-core/ssr/SKILL.md) — SSR setup for dehydration/hydration diff --git a/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md b/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md new file mode 100644 index 00000000000..6ef7d11be1b --- /dev/null +++ b/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md @@ -0,0 +1,486 @@ +--- +name: lifecycle/migrate-from-react-router +description: >- + Step-by-step migration from React Router v7 to TanStack Router: + route definition conversion, Link/useNavigate API differences, + useSearchParams to validateSearch + useSearch, useParams with from, + Outlet replacement, loader conversion, code splitting differences. +type: lifecycle +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core + - react-router +sources: + - TanStack/router:docs/router/how-to/migrate-from-react-router.md + - TanStack/router:docs/router/installation/migrate-from-react-router.md +--- + +# Migrate from React Router v7 to TanStack Router + +This is a step-by-step migration checklist. Each check covers one conversion task. Complete them in order. + +> **CRITICAL**: If your UI is blank after migration, open the console. Errors like "cannot use useNavigate outside of context" mean React Router imports remain alongside TanStack Router imports. Uninstall `react-router` to surface them as TypeScript errors. + +> **CRITICAL**: TanStack Router uses `to` + `params` for navigation, NOT template literal paths. Never interpolate params into the `to` string. + +## Pre-Migration + +- [ ] **Create a migration branch** + +```bash +git checkout -b migrate-to-tanstack-router +``` + +- [ ] **Install TanStack Router alongside React Router temporarily** + +```bash +npm install @tanstack/react-router +npm install -D @tanstack/router-plugin @tanstack/react-router-devtools +``` + +- [ ] **Configure bundler plugin (Vite example)** + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +export default defineConfig({ + plugins: [ + tanstackRouter({ target: 'react', autoCodeSplitting: true }), + react(), + ], +}) +``` + +- [ ] **Create routes directory** + +```bash +mkdir src/routes +``` + +## Route Definitions + +- [ ] **Create root route** + +React Router: `` or `createBrowserRouter([{ element: , children: [...] }])` + +TanStack Router: `src/routes/__root.tsx` + +```tsx +// src/routes/__root.tsx +import { createRootRoute, Link, Outlet } from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' + +export const Route = createRootRoute({ + component: () => ( + <> + + + + + ), +}) +``` + +- [ ] **Create router instance with type registration** + +```tsx +// src/main.tsx +import { StrictMode } from 'react' +import ReactDOM from 'react-dom/client' +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +const router = createRouter({ routeTree }) + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('root')! +if (!rootElement.innerHTML) { + ReactDOM.createRoot(rootElement).render( + + + , + ) +} +``` + +- [ ] **Convert each route file to `createFileRoute`** + +React Router: + +```tsx +// Defined in route config array +{ path: '/posts', element: , loader: postsLoader } +``` + +TanStack Router: + +```tsx +// src/routes/posts.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts')({ + loader: async () => { + const posts = await fetchPosts() + return { posts } + }, + component: PostsPage, +}) + +function PostsPage() { + const { posts } = Route.useLoaderData() + return ( +
    + {posts.map((p) => ( +
  • {p.title}
  • + ))} +
+ ) +} +``` + +- [ ] **Convert dynamic routes** + +React Router: `/posts/:postId` (colon syntax) + +TanStack Router: `/posts/$postId` (dollar syntax) + +```tsx +// src/routes/posts/$postId.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params }) => { + const post = await fetchPost(params.postId) + return { post } + }, + component: PostPage, +}) + +function PostPage() { + const { post } = Route.useLoaderData() + return
{post.title}
+} +``` + +## Navigation + +- [ ] **Convert all `` components** + +React Router: + +```tsx +import { Link } from 'react-router' +;View Post +``` + +TanStack Router: + +```tsx +import { Link } from '@tanstack/react-router' +; + View Post + +``` + +Key differences: + +- `to` is a route path pattern, NOT an interpolated string +- `params` is a separate prop with typed values +- Active class: `className="[&.active]:font-bold"` (automatic `active` data attribute) + +- [ ] **Convert all `useNavigate` calls** + +React Router: + +```tsx +import { useNavigate } from 'react-router' +const navigate = useNavigate() +navigate(`/posts/${postId}`) +``` + +TanStack Router: + +```tsx +import { useNavigate } from '@tanstack/react-router' +const navigate = useNavigate() +navigate({ to: '/posts/$postId', params: { postId } }) +``` + +## Search Params + +- [ ] **Replace `useSearchParams` with `validateSearch` + `useSearch`** + +React Router: + +```tsx +import { useSearchParams } from 'react-router' + +function Posts() { + const [searchParams, setSearchParams] = useSearchParams() + const page = Number(searchParams.get('page')) || 1 + + const goToPage = (p: number) => setSearchParams({ page: String(p) }) +} +``` + +TanStack Router: + +```tsx +// In the route definition: +import { createFileRoute } from '@tanstack/react-router' +import { z } from 'zod' +import { zodValidator, fallback } from '@tanstack/zod-adapter' + +export const Route = createFileRoute('/posts')({ + validateSearch: zodValidator( + z.object({ + page: fallback(z.number(), 1).default(1), + }), + ), + component: Posts, +}) + +// In the component: +import { useNavigate } from '@tanstack/react-router' + +function Posts() { + const { page } = Route.useSearch() + const navigate = useNavigate({ from: '/posts' }) + + const goToPage = (p: number) => { + navigate({ search: (prev) => ({ ...prev, page: p }) }) + } +} +``` + +Key differences: + +- Search params are validated and typed at the route level +- `useSearch()` returns typed objects, not `URLSearchParams` +- Update with function form to preserve other params + +## Path Params + +- [ ] **Update `useParams` with `from` property** + +React Router: + +```tsx +import { useParams } from 'react-router' +const { postId } = useParams() +``` + +TanStack Router: + +```tsx +import { useParams } from '@tanstack/react-router' +const { postId } = useParams({ from: '/posts/$postId' }) +``` + +Or from within the route component: + +```tsx +const { postId } = Route.useParams() +``` + +## Outlet + +- [ ] **Replace React Router `Outlet` with TanStack Router `Outlet`** + +The API is identical — just change the import: + +```tsx +// Before +import { Outlet } from 'react-router' + +// After +import { Outlet } from '@tanstack/react-router' +``` + +## Loaders + +- [ ] **Convert React Router loaders** + +React Router (v7): + +```tsx +export async function loader({ params }) { + const post = await fetchPost(params.postId) + return { post } +} + +export default function Post() { + const { post } = useLoaderData() + return
{post.title}
+} +``` + +TanStack Router: + +```tsx +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params }) => { + const post = await fetchPost(params.postId) + return { post } + }, + component: Post, +}) + +function Post() { + const { post } = Route.useLoaderData() + return
{post.title}
+} +``` + +Key differences: + +- Loader is a route option, not a separate export +- `useLoaderData()` is called via `Route.useLoaderData()` (or `useLoaderData({ from })`) +- TanStack Router loaders run on the CLIENT by default (not server-only) +- No `json()` wrapper needed — return plain objects + +## Code Splitting + +- [ ] **Convert lazy route imports** + +React Router: + +```tsx +const LazyPage = lazy(() => import('./pages/LazyPage')) +{ path: '/lazy', element: } +``` + +TanStack Router (with `autoCodeSplitting: true` in plugin config, this is automatic). For manual splitting: + +```tsx +// src/routes/lazy-page.lazy.tsx +import { createLazyFileRoute } from '@tanstack/react-router' + +export const Route = createLazyFileRoute('/lazy-page')({ + component: LazyPage, +}) + +function LazyPage() { + return
Lazy loaded
+} +``` + +## Cleanup + +- [ ] **Remove React Router** + +```bash +npm uninstall react-router react-router-dom +``` + +- [ ] **Search for remaining React Router imports** + +```bash +grep -r "from 'react-router" src/ +grep -r 'from "react-router' src/ +``` + +Any remaining imports will now produce TypeScript errors after uninstalling. + +- [ ] **Verify TypeScript compiles cleanly** + +```bash +npx tsc --noEmit +``` + +- [ ] **Test all routes manually** + +Verify: + +- All routes render +- Navigation works (including browser back/forward) +- Search params persist and validate +- Dynamic route params resolve +- Loaders execute and data displays + +## Common Mistakes + +### 1. HIGH: Leaving React Router imports alongside TanStack Router + +Both libraries export `Link`, `useNavigate`, `Outlet`, etc. Leftover React Router imports cause "cannot use useNavigate outside of context" errors because the wrong context provider is used. + +```tsx +// WRONG — mixed imports +import { Link } from '@tanstack/react-router' +import { useNavigate } from 'react-router' // <- still React Router! + +// CORRECT — all from TanStack Router +import { Link, useNavigate } from '@tanstack/react-router' +``` + +**Fix**: Uninstall `react-router`/`react-router-dom` completely. TypeScript will flag every stale import. + +### 2. HIGH: Using React Router `useSearchParams` pattern + +```tsx +// WRONG — React Router pattern, returns URLSearchParams +const [searchParams, setSearchParams] = useSearchParams() +const page = Number(searchParams.get('page')) + +// CORRECT — TanStack Router pattern, returns typed object +// Route definition: +validateSearch: zodValidator( + z.object({ + page: fallback(z.number(), 1).default(1), + }), +) + +// Component: +const { page } = Route.useSearch() +// page is already typed as number — no casting needed +``` + +### 3. HIGH: Interpolating params into `to` string + +```tsx +// WRONG — React Router habit +Post + +// CORRECT — TanStack Router: path pattern + params prop +Post +``` + +### 4. MEDIUM: Using `:param` syntax instead of `$param` + +``` +React Router: /posts/:postId +TanStack Router: /posts/$postId +``` + +File naming also uses `$`: `src/routes/posts/$postId.tsx` + +## Quick Reference: API Mapping + +| React Router v7 | TanStack Router | +| ---------------------------- | ---------------------------------------------------- | +| `` | `` | +| `` / `` | File-based: `src/routes/*.tsx` | +| `` | `` | +| `` | `` | +| `useNavigate()('/path')` | `navigate({ to: '/path' })` | +| `useParams()` | `useParams({ from: '/route/$param' })` | +| `useSearchParams()` | `validateSearch` + `useSearch({ from })` | +| `useLoaderData()` | `Route.useLoaderData()` | +| `` | `` | +| `loader({ params })` | `loader: ({ params }) => ...` (route option) | +| `action({ request })` | Use mutations / form libraries | +| `lazy(() => import(...))` | `autoCodeSplitting` or `.lazy.tsx` files | +| `:paramName` | `$paramName` | +| `*` (splat) | `$` (splat, accessed via `_splat`) | diff --git a/packages/react-router/skills/react-router/SKILL.md b/packages/react-router/skills/react-router/SKILL.md new file mode 100644 index 00000000000..1ad81095dba --- /dev/null +++ b/packages/react-router/skills/react-router/SKILL.md @@ -0,0 +1,493 @@ +--- +name: react-router +description: >- + React bindings for TanStack Router: RouterProvider, useRouter, + useRouterState, useMatch, useMatches, useLocation, useSearch, + useParams, useNavigate, useLoaderData, useLoaderDeps, + useRouteContext, useBlocker, useCanGoBack, Link, Navigate, + Outlet, CatchBoundary, ErrorComponent. React-specific patterns + for hooks, providers, SSR hydration, and createLink with + forwardRef. +type: framework +library: tanstack-router +library_version: '1.166.2' +framework: react +requires: + - router-core +sources: + - TanStack/router:packages/react-router/src + - TanStack/router:docs/router/guide/creating-a-router.md + - TanStack/router:docs/router/installation/manual.md +--- + +# React Router (`@tanstack/react-router`) + +This skill builds on router-core. Read [router-core](../../../router-core/skills/router-core/SKILL.md) first for foundational concepts. + +This skill covers the React-specific bindings, components, hooks, and setup for TanStack Router. + +> **CRITICAL**: TanStack Router types are FULLY INFERRED. Never cast, never annotate inferred values. + +> **CRITICAL**: TanStack Router is CLIENT-FIRST. Loaders run on the client by default, not on the server. + +> **CRITICAL**: Do not confuse `@tanstack/react-router` with `react-router-dom`/`react-router`. They are completely different libraries with different APIs. + +## Full Setup: File-Based Routing with Vite + +### 1. Install Dependencies + +```bash +npm install @tanstack/react-router +npm install -D @tanstack/router-plugin @tanstack/react-router-devtools +``` + +### 2. Configure Vite Plugin + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +export default defineConfig({ + plugins: [ + // MUST come before react() + tanstackRouter({ + target: 'react', + autoCodeSplitting: true, + }), + react(), + ], +}) +``` + +### 3. Create Root Route + +```tsx +// src/routes/__root.tsx +import { createRootRoute, Link, Outlet } from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' + +export const Route = createRootRoute({ + component: RootLayout, +}) + +function RootLayout() { + return ( + <> + +
+ + + + ) +} +``` + +### 4. Create Route Files + +```tsx +// src/routes/index.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return

Welcome Home

+} +``` + +```tsx +// src/routes/about.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return

About

+} +``` + +### 5. Create Router Instance and Register Types + +```tsx +// src/main.tsx +import { StrictMode } from 'react' +import ReactDOM from 'react-dom/client' +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +const router = createRouter({ routeTree }) + +// REQUIRED — without this, Link/useNavigate/useSearch have no type safety +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('root')! +if (!rootElement.innerHTML) { + const root = ReactDOM.createRoot(rootElement) + root.render( + + + , + ) +} +``` + +## Hooks Reference + +All hooks are imported from `@tanstack/react-router`. + +### `useRouter()` + +Access the router instance directly: + +```tsx +import { useRouter } from '@tanstack/react-router' + +function InvalidateButton() { + const router = useRouter() + return +} +``` + +### `useRouterState()` + +Subscribe to router state changes: + +```tsx +import { useRouterState } from '@tanstack/react-router' + +function LoadingIndicator() { + const isLoading = useRouterState({ select: (s) => s.isLoading }) + return isLoading ?
Loading...
: null +} +``` + +### `useNavigate()` + +Programmatic navigation (prefer `` for user-clickable elements): + +```tsx +import { useNavigate } from '@tanstack/react-router' + +function AfterSubmit() { + const navigate = useNavigate() + + const handleSubmit = async () => { + await saveData() + navigate({ to: '/posts/$postId', params: { postId: '123' } }) + } + + return +} +``` + +### `useSearch({ from })` + +Read validated search params: + +```tsx +import { useSearch } from '@tanstack/react-router' + +function Pagination() { + const { page } = useSearch({ from: '/products' }) + return Page {page} +} +``` + +### `useParams({ from })` + +Read path params: + +```tsx +import { useParams } from '@tanstack/react-router' + +function PostHeader() { + const { postId } = useParams({ from: '/posts/$postId' }) + return

Post {postId}

+} +``` + +### `useLoaderData({ from })` + +Read data returned from the route loader: + +```tsx +import { useLoaderData } from '@tanstack/react-router' + +function PostContent() { + const { post } = useLoaderData({ from: '/posts/$postId' }) + return
{post.content}
+} +``` + +### `useMatch({ from })` + +Access the full route match (params, search, loader data, context): + +```tsx +import { useMatch } from '@tanstack/react-router' + +function PostDetails() { + const match = useMatch({ from: '/posts/$postId' }) + return
{match.loaderData.post.title}
+} +``` + +### Other Hooks + +All imported from `@tanstack/react-router`: + +- **`useMatches()`** — array of all active route matches (useful for breadcrumbs) +- **`useRouteContext({ from })`** — read context from `beforeLoad` or parent routes +- **`useBlocker({ shouldBlockFn })`** — block navigation for unsaved changes +- **`useCanGoBack()`** — returns `boolean`, check if history has entries to go back to +- **`useLocation()`** — current parsed location (`pathname`, `search`, `hash`) +- **`useLinkProps({ to, params?, search? })`** — get `
` props for custom link elements +- **`useMatchRoute()`** — returns a function: `matchRoute({ to }) => match | false` + +## Components Reference + +### `RouterProvider` + +Mount the router at the top of your React tree: + +```tsx + +``` + +### `Link` + +Type-safe navigation link with `` semantics: + +```tsx + + View Post + +``` + +### `Outlet` + +Renders the matched child route component: + +```tsx +function Layout() { + return ( +
+ +
+ +
+
+ ) +} +``` + +### `Navigate` + +Declarative redirect component: + +```tsx +import { Navigate } from '@tanstack/react-router' + +function OldPage() { + return +} +``` + +### `Await` + +Renders deferred data from unawaited loader promises with Suspense: + +```tsx +import { Await } from '@tanstack/react-router' +import { Suspense } from 'react' + +function PostWithComments() { + const { deferredComments } = Route.useLoaderData() + return ( +
+

Post

+ Loading comments...
}> + + {(comments) => ( +
    + {comments.map((c) => ( +
  • {c.text}
  • + ))} +
+ )} +
+ + + ) +} +``` + +### `CatchBoundary` + +Error boundary for component-level error handling (route-level errors use `errorComponent` route option): + +```tsx +import { CatchBoundary } from '@tanstack/react-router' +; 'widget'} + onCatch={(error) => console.error(error)} + errorComponent={({ error }) =>
Error: {error.message}
} +> + +
+``` + +## React-Specific Patterns + +### Custom Link Component with `createLink` + +Wrap `Link` in a custom component while preserving type safety: + +```tsx +import { createLink } from '@tanstack/react-router' +import { forwardRef } from 'react' + +const StyledLinkComponent = forwardRef< + HTMLAnchorElement, + React.ComponentPropsWithoutRef<'a'> +>((props, ref) => ( +
+)) + +const StyledLink = createLink(StyledLinkComponent) + +// Usage — same type-safe props as Link +function Nav() { + return ( + + Post + + ) +} +``` + +### Auth Provider Must Wrap RouterProvider + +If routes use auth context (via `createRootRouteWithContext`), the auth provider must be an ancestor of `RouterProvider`: + +```tsx +// CORRECT — AuthProvider wraps RouterProvider +function App() { + return ( + + + + ) +} + +// WRONG — RouterProvider outside auth provider +function App() { + return ( + + {/* ... */} + + ) +} +``` + +Or use the `Wrap` router option to provide context without wrapping externally: + +```tsx +const router = createRouter({ + routeTree, + Wrap: ({ children }) => ( + {children} + ), +}) +``` + +## Common Mistakes + +### 1. HIGH: Using React hooks in `beforeLoad` or `loader` + +`beforeLoad` and `loader` are NOT React components — they are plain async functions called by the router. React hooks cannot be used in them. + +```tsx +// WRONG — useAuth is a React hook, cannot be called here +beforeLoad: () => { + const auth = useAuth() + if (!auth.user) throw redirect({ to: '/login' }) +} + +// CORRECT — pass auth state via router context +const rootRoute = createRootRouteWithContext<{ auth: AuthState }>()({ + component: RootComponent, +}) + +// In the component that creates the router: +const router = createRouter({ + routeTree, + context: { auth: getAuthState() }, +}) + +// Then in a route: +beforeLoad: ({ context }) => { + if (!context.auth.isAuthenticated) { + throw redirect({ to: '/login' }) + } +} +``` + +### 2. HIGH: Wrapping RouterProvider inside an auth provider incorrectly + +If you use `createRootRouteWithContext<{ auth: AuthState }>()`, the auth state must be available when the router is created — not injected after. + +```tsx +// WRONG — router created before auth is available +const router = createRouter({ routeTree, context: {} }) + +function App() { + const auth = useAuth() // too late + return +} + +// CORRECT — provide context at creation time +function App() { + const auth = useAuth() + const router = useMemo( + () => createRouter({ routeTree, context: { auth } }), + [auth], + ) + return +} +``` + +### 3. MEDIUM: Missing Suspense boundary for `Await`/deferred data + +`Await` requires a `` ancestor. Without it, the deferred promise has no fallback UI and throws. + +```tsx +// WRONG — no Suspense boundary +{(data) =>
{data}
}
+ +// CORRECT — wrap in Suspense +Loading...}> + {(data) =>
{data}
}
+
+``` + +## Cross-References + +- [router-core/SKILL.md](../../../router-core/skills/router-core/SKILL.md) — all sub-skills for domain-specific patterns (search params, data loading, navigation, auth, SSR, etc.) diff --git a/packages/react-start/bin/intent.js b/packages/react-start/bin/intent.js new file mode 100755 index 00000000000..2cf2efab472 --- /dev/null +++ b/packages/react-start/bin/intent.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// Auto-generated by @tanstack/intent setup +// Exposes the intent end-user CLI for consumers of this library. +// Commit this file, then add to your package.json: +// "bin": { "intent": "./bin/intent.js" } +try { + await import('@tanstack/intent/intent-library') +} catch (e) { + if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + console.error('@tanstack/intent is not installed.') + console.error('') + console.error('Install it as a dev dependency:') + console.error(' npm add -D @tanstack/intent') + console.error('') + console.error('Or run directly:') + console.error(' npx @tanstack/intent@latest list') + process.exit(1) + } + throw e +} diff --git a/packages/react-start/package.json b/packages/react-start/package.json index 70cf1f9e757..65c5ce3013b 100644 --- a/packages/react-start/package.json +++ b/packages/react-start/package.json @@ -97,7 +97,10 @@ "sideEffects": false, "files": [ "dist", - "src" + "src", + "skills", + "bin", + "!skills/_artifacts" ], "engines": { "node": ">=22.12.0" @@ -116,5 +119,11 @@ "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" + }, + "devDependencies": { + "@tanstack/intent": "^0.0.14" + }, + "bin": { + "intent": "./bin/intent.js" } } diff --git a/packages/react-start/skills/_artifacts/domain_map.yaml b/packages/react-start/skills/_artifacts/domain_map.yaml new file mode 100644 index 00000000000..b5e84d4dfbc --- /dev/null +++ b/packages/react-start/skills/_artifacts/domain_map.yaml @@ -0,0 +1,482 @@ +# domain_map.yaml +# Generated by skill-domain-discovery +# Library: TanStack Start +# Version: 1.166.2 +# Date: 2026-03-07 +# Status: reviewed + +library: + name: '@tanstack/react-start' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Full-stack React framework built on TanStack Router and Vite. Adds + SSR, streaming, server functions (type-safe RPCs), middleware, + server routes, and universal deployment. Isomorphic by default — + all code runs in both environments unless explicitly constrained. + primary_framework: 'React' + +domains: + - name: 'Project Setup' + slug: 'project-setup' + description: >- + Scaffolding a Start project, configuring Vite plugin, router + setup with getRouter(), root route with document shell, client + and server entry points. + + - name: 'Server Functions' + slug: 'server-functions' + description: >- + Creating type-safe RPCs with createServerFn, input validation, + calling from loaders/components/other server functions, error + handling, streaming responses. + + - name: 'Middleware and Context' + slug: 'middleware-and-context' + description: >- + Request middleware, server function middleware, context passing + with sendContext, global middleware via createStart, middleware + factories, fetch override precedence. + + - name: 'Execution Model' + slug: 'execution-model' + description: >- + Isomorphic code execution, environment functions + (createServerFn, createServerOnlyFn, createClientOnlyFn, + createIsomorphicFn), import protection, dead code elimination, + environment variable safety. + + - name: 'Server Routes' + slug: 'server-routes' + description: >- + Server-side API endpoints defined in routes, HTTP method + handlers, handler middleware, request/response patterns. + + - name: 'Deployment and Rendering' + slug: 'deployment-and-rendering' + description: >- + Hosting providers (Cloudflare, Netlify, Vercel, Node/Docker), + selective SSR, SPA mode, static prerendering, ISR with + Cache-Control headers, SEO and head management. + +skills: + # ── Project Setup ──────────────────────────────────────────────── + - name: 'Start Setup' + slug: 'start-setup' + domain: 'project-setup' + description: >- + Scaffold a TanStack Start project, configure Vite plugin with + tanstackStart(), set up router with getRouter(), create root + route with document shell (HeadContent, Scripts, Outlet), + configure client and server entry points. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-plugin-core' + covers: + - tanstackStart Vite plugin + - getRouter() factory pattern + - Root route document shell + - HeadContent / Scripts / Outlet + - Client entry point (optional) + - Server entry point (optional) + - routeTree.gen.ts + - tsconfig configuration + tasks: + - 'Scaffold a new TanStack Start project' + - 'Configure the Vite plugin' + - 'Set up the router factory' + - 'Customize client/server entry points' + failure_modes: + - mistake: 'React plugin before Start plugin in Vite config' + mechanism: >- + Start's Vite plugin must come before React's plugin. + Wrong order causes route generation and server function + compilation to fail. + wrong_pattern: | + plugins: [react(), tanstackStart()] + correct_pattern: | + plugins: [tanstackStart(), react()] + source: 'docs/start/framework/react/build-from-scratch.md' + priority: CRITICAL + status: active + + - mistake: 'Enabling verbatimModuleSyntax in tsconfig' + mechanism: >- + verbatimModuleSyntax causes server bundles to leak into + client bundles. Must be disabled. + source: 'docs/start/framework/react/build-from-scratch.md' + priority: HIGH + status: active + + - mistake: 'Missing Scripts component in root route' + mechanism: >- + The Scripts component must be rendered in the body of + the root route for proper hydration and functionality. + Without it, client-side JavaScript does not load. + source: 'docs/start/framework/react/guide/routing.md' + priority: HIGH + status: active + + # ── Server Functions ───────────────────────────────────────────── + - name: 'Server Functions' + slug: 'server-functions' + domain: 'server-functions' + description: >- + Create type-safe RPCs with createServerFn, validate inputs + with Zod or plain functions, call from loaders/components/ + event handlers, handle errors/redirects/notFound, stream + responses with ReadableStream or async generators. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-client-core' + - '@tanstack/start-server-core' + covers: + - createServerFn (GET/POST) + - inputValidator (Zod or function) + - useServerFn hook + - Server context utilities (getRequest, getRequestHeader, setResponseHeader, setResponseStatus) + - Error handling (throw errors, redirect, notFound) + - Streaming (ReadableStream, async generators) + - File organization (.functions.ts, .server.ts) + - FormData handling + tasks: + - 'Create a server function for data fetching' + - 'Validate server function inputs' + - 'Call server functions from components' + - 'Stream data from server functions' + - 'Handle errors in server functions' + failure_modes: + - mistake: 'Putting server-only code in loaders instead of server functions' + mechanism: >- + Loaders are ISOMORPHIC — they run on both client and server. + Database queries, file system access, and secret API keys + in loaders will either fail on the client or leak to the + client bundle. Use createServerFn for server-only logic. + wrong_pattern: | + export const Route = createFileRoute('/posts')({ + loader: async () => { + const posts = await db.query('SELECT * FROM posts') + return { posts } + }, + }) + correct_pattern: | + const getPosts = createServerFn({ method: 'GET' }) + .handler(async () => { + const posts = await db.query('SELECT * FROM posts') + return { posts } + }) + + export const Route = createFileRoute('/posts')({ + loader: () => getPosts(), + }) + source: 'maintainer interview' + priority: CRITICAL + status: active + skills: ['server-functions', 'execution-model'] + + - mistake: 'Using dynamic imports for server functions' + mechanism: >- + Dynamic imports of server functions can cause bundler + issues. Static imports are safe — the build process + replaces server implementations with RPC stubs. + wrong_pattern: | + const { getUser } = await import('~/utils/users.functions') + correct_pattern: | + import { getUser } from '~/utils/users.functions' + source: 'docs/start/framework/react/guide/server-functions.md' + priority: HIGH + status: active + + - mistake: 'Generating Next.js or Remix server patterns' + mechanism: >- + Agents generate getServerSideProps, "use server" directives, + or Remix-style loader exports. TanStack Start uses + createServerFn for server-only code. + wrong_pattern: | + 'use server' + export async function getUser() { ... } + correct_pattern: | + const getUser = createServerFn({ method: 'GET' }) + .handler(async () => { ... }) + source: 'maintainer interview' + priority: CRITICAL + status: active + + # ── Middleware and Context ─────────────────────────────────────── + - name: 'Middleware' + slug: 'middleware' + domain: 'middleware-and-context' + description: >- + Request middleware and server function middleware with + createMiddleware, context passing via next(), sendContext + for client-server transfer, global middleware via createStart + in src/start.ts, middleware factories, fetch override + precedence, header merging. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-client-core' + - '@tanstack/start-server-core' + covers: + - createMiddleware + - Request middleware (.server only) + - Server function middleware (.client + .server) + - Context passing via next({ context }) + - sendContext for client-server transfer + - Global middleware (createStart in src/start.ts) + - Middleware factories + - Fetch override precedence + - Header merging + - Method order enforcement (middleware → inputValidator → client → server) + tasks: + - 'Add authentication middleware' + - 'Pass context through middleware chain' + - 'Configure global request middleware' + - 'Create reusable middleware factories' + failure_modes: + - mistake: 'Trusting client context without server validation' + mechanism: >- + Client context via sendContext is NOT validated by default. + Dynamic user-generated data must be validated in server-side + middleware before use. + source: 'docs/start/framework/react/guide/middleware.md' + priority: HIGH + status: active + + - mistake: 'Wrong middleware method order' + mechanism: >- + TypeScript enforces method order: middleware → inputValidator + → client → server. Wrong order causes type errors and + runtime failures. + source: 'docs/start/framework/react/guide/middleware.md' + priority: MEDIUM + status: active + + - mistake: 'Confusing request vs server function middleware' + mechanism: >- + Request middleware runs on ALL server requests (SSR, server + routes, server functions). Server function middleware runs + only for server functions and has .client() method. Using + the wrong type causes unexpected scope. + source: 'docs/start/framework/react/guide/middleware.md' + priority: MEDIUM + status: active + + # ── Execution Model ────────────────────────────────────────────── + - name: 'Execution Model' + slug: 'execution-model' + domain: 'execution-model' + description: >- + Isomorphic code execution model, environment boundary functions + (createServerFn, createServerOnlyFn, createClientOnlyFn, + createIsomorphicFn), import protection, dead code elimination, + environment variable safety (VITE_ prefix), useHydrated hook. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-client-core' + - '@tanstack/start-server-core' + covers: + - Isomorphic-by-default principle + - createServerFn (RPC boundary) + - createServerOnlyFn (throws on client) + - createClientOnlyFn (throws on server) + - createIsomorphicFn (different impl per env) + - ClientOnly component + - useHydrated hook + - Import protection (experimental) + - Environment variables (VITE_ prefix, process.env) + - Dead code elimination / tree shaking + tasks: + - 'Protect server-only code from client bundles' + - 'Use environment-specific implementations' + - 'Handle environment variables safely' + - 'Debug import protection violations' + failure_modes: + - mistake: 'Assuming loaders are server-only' + mechanism: >- + ALL code in TanStack Start is isomorphic by default. + Loaders run on BOTH server and client. Server-only + operations must use createServerFn, createServerOnlyFn, + or server routes. + wrong_pattern: | + export const Route = createFileRoute('/dashboard')({ + loader: async () => { + const secret = process.env.API_SECRET + return fetch(`https://api.example.com/data`, { + headers: { Authorization: secret } + }) + }, + }) + correct_pattern: | + const getData = createServerFn({ method: 'GET' }) + .handler(async () => { + const secret = process.env.API_SECRET + return fetch(`https://api.example.com/data`, { + headers: { Authorization: secret } + }) + }) + + export const Route = createFileRoute('/dashboard')({ + loader: () => getData(), + }) + source: 'docs/start/framework/react/guide/execution-model.md' + priority: CRITICAL + status: active + skills: ['execution-model', 'server-functions'] + + - mistake: 'Exposing secrets via module-level process.env' + mechanism: >- + Module-level process.env access runs in both environments. + The variable value leaks into the client bundle. Access + secrets only inside createServerFn or createServerOnlyFn. + wrong_pattern: | + const apiKey = process.env.SECRET_KEY + export function fetchData() { ... } + correct_pattern: | + const fetchData = createServerFn({ method: 'GET' }) + .handler(async () => { + const apiKey = process.env.SECRET_KEY + return fetch(url, { headers: { Authorization: apiKey } }) + }) + source: 'docs/start/framework/react/guide/execution-model.md' + priority: CRITICAL + status: active + + - mistake: 'Using VITE_ prefix for server secrets' + mechanism: >- + VITE_ prefixed variables are exposed to the client bundle. + Server secrets must NOT have the VITE_ prefix. Access + them via process.env inside server functions only. + source: 'docs/start/framework/react/guide/environment-variables.md' + priority: CRITICAL + status: active + + # ── Server Routes ──────────────────────────────────────────────── + - name: 'Server Routes' + slug: 'server-routes' + domain: 'server-routes' + description: >- + Define server-side API endpoints alongside app routes using + the server property with HTTP method handlers, per-handler + middleware via createHandlers, request/response patterns. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-server-core' + covers: + - server property on createFileRoute + - handlers object (GET, POST, PUT, DELETE) + - createHandlers for per-handler middleware + - Handler context (request, params, context) + - Request body parsing (json, text, formData) + - Response helpers (Response.json) + - File naming for API routes + tasks: + - 'Create a REST API endpoint' + - 'Add middleware to server route handlers' + - 'Handle different HTTP methods' + failure_modes: + - mistake: 'Duplicate path resolution for server routes' + mechanism: >- + Each route can only have a single handler file. Having + both users.ts and users/index.ts causes errors. + source: 'docs/start/framework/react/guide/server-routes.md' + priority: MEDIUM + status: active + + # ── Deployment and Rendering ───────────────────────────────────── + - name: 'Deployment' + slug: 'deployment' + domain: 'deployment-and-rendering' + description: >- + Deploy TanStack Start to Cloudflare Workers, Netlify, Vercel, + Node.js/Docker, Bun, Railway. Configure selective SSR, SPA + mode, static prerendering, ISR with Cache-Control headers. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-plugin-core' + covers: + - Cloudflare Workers deployment + - Netlify deployment + - Vercel / Railway deployment + - Node.js / Docker deployment + - Bun deployment + - Selective SSR (ssr option per route) + - SPA mode configuration + - Static prerendering (prerender option) + - ISR with Cache-Control headers + - SEO (head property, structured data) + tasks: + - 'Deploy to Cloudflare Workers' + - 'Deploy to Netlify' + - 'Configure selective SSR per route' + - 'Enable SPA mode' + - 'Set up static prerendering' + - 'Configure ISR with cache headers' + failure_modes: + - mistake: 'Bun deployment with React 18' + mechanism: >- + Bun-specific deployment only works with React 19. + For React 18, use Node.js deployment guidelines. + source: 'docs/start/framework/react/guide/hosting.md' + priority: MEDIUM + status: active + + - mistake: 'Missing nodejs_compat flag for Cloudflare Workers' + mechanism: >- + Cloudflare Workers requires compatibility_flags: + ["nodejs_compat"] in wrangler config. Without it, + Node.js APIs used by Start fail at runtime. + source: 'docs/start/framework/react/guide/hosting.md' + priority: HIGH + status: active + + - mistake: 'Child route loosening parent SSR config' + mechanism: >- + SSR config inherits from parent and can only become MORE + restrictive (true → data-only → false). A child cannot + set ssr: true if parent has ssr: false. + source: 'docs/start/framework/react/guide/selective-ssr.md' + priority: MEDIUM + status: active + +tensions: + - name: 'Isomorphic defaults vs server-only expectations' + skills: ['execution-model', 'server-functions'] + description: >- + All code runs everywhere by default. Agents trained on + Next.js/Remix assume loaders and route code are server-only. + implication: >- + Agents put secrets, DB queries, and file system access in + loaders instead of server functions, causing client-side + failures or security leaks. + + - name: 'Simplicity of isomorphic code vs security boundaries' + skills: ['execution-model', 'middleware'] + description: >- + The isomorphic model makes code easy to write but requires + explicit boundaries for security. Agents don't realize they + need to actively constrain execution environment. + implication: >- + Agents expose secrets via module-level process.env or + forget to validate sendContext data in middleware. + +cross_references: + - from: 'server-functions' + to: 'execution-model' + reason: 'Server functions ARE the isomorphic boundary — understanding the execution model is prerequisite' + - from: 'server-functions' + to: 'middleware' + reason: 'Server function middleware chains compose with server functions' + - from: 'middleware' + to: 'server-routes' + reason: 'Server routes use the same middleware system' + - from: 'deployment' + to: 'execution-model' + reason: 'Deployment target affects which environment code runs in' + +gaps: [] diff --git a/packages/react-start/skills/_artifacts/skill_spec.md b/packages/react-start/skills/_artifacts/skill_spec.md new file mode 100644 index 00000000000..11b6843bb01 --- /dev/null +++ b/packages/react-start/skills/_artifacts/skill_spec.md @@ -0,0 +1,94 @@ +# TanStack Start — Skill Spec + +TanStack Start is a full-stack React framework built on TanStack Router and Vite. It adds SSR, streaming, server functions (type-safe RPCs), middleware, server routes, and universal deployment. All code is isomorphic by default — it runs in both server and client environments unless explicitly constrained. + +## Domains + +| Domain | Description | Skills | +| ------------------------ | ------------------------------------------------------------- | ---------------- | +| Project Setup | Scaffolding, Vite plugin, router factory, root route, entries | start-setup | +| Server Functions | Type-safe RPCs with createServerFn, validation, streaming | server-functions | +| Middleware and Context | Request/function middleware, context, global middleware | middleware | +| Execution Model | Isomorphic defaults, environment boundaries, env vars | execution-model | +| Server Routes | API endpoints, HTTP handlers, handler middleware | server-routes | +| Deployment and Rendering | Hosting, selective SSR, prerendering, SEO | deployment | + +## Skill Inventory + +| Skill | Type | Domain | What it covers | Failure modes | +| ------------------- | --------- | ------------------------ | ------------------------------------------------------- | ------------- | +| start-setup | core | project-setup | tanstackStart(), getRouter(), root route, entries | 3 | +| server-functions | core | server-functions | createServerFn, validation, useServerFn, streaming | 4 | +| middleware | core | middleware-and-context | createMiddleware, context, global middleware, factories | 3 | +| execution-model | core | execution-model | Isomorphic defaults, environment functions, env vars | 4 | +| server-routes | core | server-routes | server property, HTTP handlers, createHandlers | 2 | +| deployment | core | deployment-and-rendering | Hosting, SSR modes, prerendering, SEO | 3 | +| react-start | framework | project-setup | React bindings, useServerFn, full setup | 3 | +| migrate-from-nextjs | lifecycle | project-setup | Next.js App Router migration checklist | 3 | + +## Failure Mode Inventory + +### start-setup (3 failure modes) + +| # | Mistake | Priority | Source | +| --- | ----------------------------------------------- | -------- | ----------------------- | +| 1 | React plugin before Start plugin in Vite config | CRITICAL | docs/build-from-scratch | +| 2 | Enabling verbatimModuleSyntax in tsconfig | HIGH | docs/build-from-scratch | +| 3 | Missing Scripts component in root route | HIGH | docs/guide/routing | + +### server-functions (4 failure modes) + +| # | Mistake | Priority | Source | +| --- | --------------------------------------------------------------------------- | -------- | --------------------------- | +| 1 | Putting server-only code in loaders instead of server functions | CRITICAL | maintainer interview | +| 2 | Generating Next.js/Remix server patterns ("use server", getServerSideProps) | CRITICAL | maintainer interview | +| 3 | Using dynamic imports for server functions | HIGH | docs/guide/server-functions | +| 4 | Not using useServerFn for component calls | MEDIUM | docs/guide/server-functions | + +### middleware (3 failure modes) + +| # | Mistake | Priority | Source | +| --- | ----------------------------------------------------- | -------- | --------------------- | +| 1 | Trusting client sendContext without server validation | HIGH | docs/guide/middleware | +| 2 | Confusing request vs server function middleware | MEDIUM | docs/guide/middleware | +| 3 | Wrong middleware method order | MEDIUM | docs/guide/middleware | + +### execution-model (4 failure modes) + +| # | Mistake | Priority | Source | +| --- | ------------------------------------------------- | -------- | -------------------------------- | +| 1 | Assuming loaders are server-only | CRITICAL | docs/guide/execution-model | +| 2 | Exposing secrets via module-level process.env | CRITICAL | docs/guide/execution-model | +| 3 | Using VITE\_ prefix for server secrets | CRITICAL | docs/guide/environment-variables | +| 4 | Hydration mismatches from env-dependent rendering | HIGH | docs/guide/execution-model | + +### server-routes (2 failure modes) + +| # | Mistake | Priority | Source | +| --- | ---------------------------------------- | -------- | ------------------------ | +| 1 | Duplicate route path resolution | MEDIUM | docs/guide/server-routes | +| 2 | Forgetting to await request body methods | MEDIUM | docs/guide/server-routes | + +### deployment (3 failure modes) + +| # | Mistake | Priority | Source | +| --- | ------------------------------------------------- | -------- | ------------------------ | +| 1 | Missing nodejs_compat flag for Cloudflare Workers | HIGH | docs/guide/hosting | +| 2 | Bun deployment with React 18 | MEDIUM | docs/guide/hosting | +| 3 | Child route loosening parent SSR config | MEDIUM | docs/guide/selective-ssr | + +## Tensions + +| Tension | Skills | Agent implication | +| ---------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------- | +| Isomorphic defaults vs server-only expectations | execution-model ↔ server-functions | Agents put secrets/DB queries in loaders instead of server functions | +| Simplicity of isomorphic code vs security boundaries | execution-model ↔ middleware | Agents expose secrets via module-level process.env or skip context validation | + +## Cross-References + +| From | To | Reason | +| ---------------- | --------------- | ----------------------------------------------- | +| server-functions | execution-model | Server functions ARE the isomorphic boundary | +| server-functions | middleware | Middleware chains compose with server functions | +| middleware | server-routes | Server routes use the same middleware system | +| deployment | execution-model | Deployment target affects where code runs | diff --git a/packages/react-start/skills/_artifacts/skill_tree.yaml b/packages/react-start/skills/_artifacts/skill_tree.yaml new file mode 100644 index 00000000000..cf743b067ca --- /dev/null +++ b/packages/react-start/skills/_artifacts/skill_tree.yaml @@ -0,0 +1,158 @@ +# skills/_artifacts/start_skill_tree.yaml +library: + name: '@tanstack/react-start' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Full-stack React framework built on TanStack Router and Vite. + Adds SSR, streaming, server functions (type-safe RPCs), middleware, + server routes, and universal deployment. Isomorphic by default — + all code runs in both environments unless explicitly constrained. +generated_from: + domain_map: '_artifacts/start_domain_map.yaml' +generated_at: '2026-03-07' + +skills: + # ── Start Core Skills ─────────────────────────────────────────── + - name: 'Start Core' + slug: 'start-core' + type: 'core' + domain: 'project-setup' + path: 'skills/start-core/SKILL.md' + package: 'packages/start-client-core' + description: >- + Core overview for TanStack Start: tanstackStart() Vite plugin, + getRouter() factory, root route document shell (HeadContent, + Scripts, Outlet), client/server entry points, routeTree.gen.ts, + tsconfig configuration. Entry point for all Start skills. + sources: + - 'TanStack/router:docs/start/framework/react/build-from-scratch.md' + - 'TanStack/router:docs/start/framework/react/quick-start.md' + - 'TanStack/router:docs/start/framework/react/guide/routing.md' + + - name: 'Server Functions' + slug: 'start-core/server-functions' + type: 'sub-skill' + domain: 'server-functions' + path: 'skills/start-core/server-functions/SKILL.md' + package: 'packages/start-client-core' + description: >- + createServerFn (GET/POST), inputValidator (Zod or function), + useServerFn hook, server context utilities (getRequest, + getRequestHeader, setResponseHeader, setResponseStatus), error + handling (throw errors, redirect, notFound), streaming + (ReadableStream, async generators), FormData handling, file + organization (.functions.ts, .server.ts). + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/server-functions.md' + + - name: 'Middleware' + slug: 'start-core/middleware' + type: 'sub-skill' + domain: 'middleware-and-context' + path: 'skills/start-core/middleware/SKILL.md' + package: 'packages/start-client-core' + description: >- + createMiddleware, request middleware (.server only), server + function middleware (.client + .server), context passing via + next({ context }), sendContext for client-server transfer, + global middleware via createStart in src/start.ts, middleware + factories, method order enforcement. + requires: + - 'start-core' + - 'start-core/server-functions' + sources: + - 'TanStack/router:docs/start/framework/react/guide/middleware.md' + + - name: 'Execution Model' + slug: 'start-core/execution-model' + type: 'sub-skill' + domain: 'execution-model' + path: 'skills/start-core/execution-model/SKILL.md' + package: 'packages/start-client-core' + description: >- + Isomorphic-by-default principle, environment boundary functions + (createServerFn, createServerOnlyFn, createClientOnlyFn, + createIsomorphicFn), ClientOnly component, useHydrated hook, + import protection, dead code elimination, environment variable + safety (VITE_ prefix, process.env). + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/execution-model.md' + - 'TanStack/router:docs/start/framework/react/guide/environment-variables.md' + + - name: 'Server Routes' + slug: 'start-core/server-routes' + type: 'sub-skill' + domain: 'server-routes' + path: 'skills/start-core/server-routes/SKILL.md' + package: 'packages/start-client-core' + description: >- + Server-side API endpoints using the server property on + createFileRoute, HTTP method handlers (GET, POST, PUT, DELETE), + createHandlers for per-handler middleware, handler context + (request, params, context), request body parsing, response + helpers, file naming for API routes. + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/server-routes.md' + + - name: 'Deployment' + slug: 'start-core/deployment' + type: 'sub-skill' + domain: 'deployment-and-rendering' + path: 'skills/start-core/deployment/SKILL.md' + package: 'packages/start-client-core' + description: >- + Deploy to Cloudflare Workers, Netlify, Vercel, Node.js/Docker, + Bun, Railway. Selective SSR (ssr option per route), SPA mode, + static prerendering, ISR with Cache-Control headers, SEO and + head management. + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/hosting.md' + - 'TanStack/router:docs/start/framework/react/guide/selective-ssr.md' + - 'TanStack/router:docs/start/framework/react/guide/static-prerendering.md' + - 'TanStack/router:docs/start/framework/react/guide/full-stack-seo.md' + + # ── React Start Skills ────────────────────────────────────────── + - name: 'React Start' + slug: 'react-start' + type: 'framework' + domain: 'project-setup' + path: 'skills/react-start/SKILL.md' + package: 'packages/react-start' + description: >- + React bindings for TanStack Start: createStart, StartClient, + StartServer, React-specific imports, re-exports from + @tanstack/react-router, full project setup with React. + requires: + - 'start-core' + sources: + - 'TanStack/router:packages/react-start/src' + - 'TanStack/router:docs/start/framework/react/build-from-scratch.md' + + # ── Lifecycle Skills ──────────────────────────────────────────── + - name: 'Migrate from Next.js' + slug: 'lifecycle/migrate-from-nextjs' + type: 'lifecycle' + domain: 'project-setup' + path: 'skills/lifecycle/migrate-from-nextjs/SKILL.md' + package: 'packages/react-start' + description: >- + Step-by-step migration from Next.js App Router to TanStack + Start: route definition conversion, API mapping, server + function conversion from Server Actions, middleware conversion, + data fetching pattern changes. + requires: + - 'start-core' + - 'react-start' + sources: + - 'TanStack/router:docs/start/framework/react/guide/server-functions.md' + - 'TanStack/router:docs/start/framework/react/guide/middleware.md' + - 'TanStack/router:docs/start/framework/react/guide/execution-model.md' diff --git a/packages/react-start/skills/lifecycle/migrate-from-nextjs/SKILL.md b/packages/react-start/skills/lifecycle/migrate-from-nextjs/SKILL.md new file mode 100644 index 00000000000..2206a2a71bc --- /dev/null +++ b/packages/react-start/skills/lifecycle/migrate-from-nextjs/SKILL.md @@ -0,0 +1,434 @@ +--- +name: lifecycle/migrate-from-nextjs +description: >- + Step-by-step migration from Next.js App Router to TanStack Start: + route definition conversion, API mapping, server function + conversion from Server Actions, middleware conversion, data + fetching pattern changes. +type: lifecycle +library: tanstack-start +library_version: '1.166.2' +requires: + - start-core + - react-start +sources: + - TanStack/router:docs/start/framework/react/guide/server-functions.md + - TanStack/router:docs/start/framework/react/guide/middleware.md + - TanStack/router:docs/start/framework/react/guide/execution-model.md +--- + +# Migrate from Next.js App Router to TanStack Start + +This is a step-by-step migration checklist. Complete tasks in order. + +> **CRITICAL**: TanStack Start is isomorphic by default. ALL code runs in both environments unless you use `createServerFn`. This is the opposite of Next.js Server Components, where code is server-only by default. + +> **CRITICAL**: TanStack Start uses `createServerFn`, NOT `"use server"` directives. Do not carry over any `"use server"` or `"use client"` directives. + +> **CRITICAL**: Types are FULLY INFERRED in TanStack Router/Start. Never cast, never annotate inferred values. + +## Pre-Migration + +- [ ] **Create a migration branch** + +```bash +git checkout -b migrate-to-tanstack-start +``` + +- [ ] **Install TanStack Start** + +```bash +npm i @tanstack/react-start @tanstack/react-router +npm i -D vite @vitejs/plugin-react +``` + +- [ ] **Remove Next.js** + +```bash +npm uninstall next @next/font @next/image +``` + +## Concept Mapping + +| Next.js App Router | TanStack Start | +| -------------------------------- | ------------------------------------------------------------------------- | +| `app/page.tsx` | `src/routes/index.tsx` | +| `app/layout.tsx` | `src/routes/__root.tsx` | +| `app/posts/[id]/page.tsx` | `src/routes/posts/$postId.tsx` | +| `app/api/users/route.ts` | `src/routes/api/users.ts` (server property) | +| `"use server"` + Server Actions | `createServerFn()` | +| `"use client"` | Not needed (everything is isomorphic) | +| Server Components (default) | All components are isomorphic; use `createServerFn` for server-only logic | +| `next/navigation` `useRouter` | `useRouter()` from `@tanstack/react-router` | +| `next/link` `Link` | `` from `@tanstack/react-router` | +| `next/head` or `metadata` export | `head` property on route | +| `middleware.ts` (edge) | `createMiddleware()` in `src/start.ts` | +| `next.config.js` | `vite.config.ts` with `tanstackStart()` | +| `generateStaticParams` | `prerender` config in `vite.config.ts` | + +## Step 1: Vite Configuration + +Replace `next.config.js` with: + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import viteReact from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [ + tanstackStart(), // MUST come before react() + viteReact(), + ], +}) +``` + +Update `package.json`: + +```json +{ + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "start": "node .output/server/index.mjs" + } +} +``` + +## Step 2: Router Factory + +```tsx +// src/router.tsx +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + const router = createRouter({ + routeTree, + scrollRestoration: true, + }) + return router +} +``` + +## Step 3: Convert Layout → Root Route + +Next.js: + +```tsx +// app/layout.tsx +export const metadata = { title: 'My App' } +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} +``` + +TanStack Start: + +```tsx +// src/routes/__root.tsx +import type { ReactNode } from 'react' +import { + Outlet, + createRootRoute, + HeadContent, + Scripts, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: 'My App' }, + ], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} +``` + +## Step 4: Convert Pages → File Routes + +Next.js: + +```tsx +// app/posts/[id]/page.tsx +export default function PostPage({ params }: { params: { id: string } }) { + // ... +} +``` + +TanStack Start: + +```tsx +// src/routes/posts/$postId.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/$postId')({ + component: PostPage, +}) + +function PostPage() { + const { postId } = Route.useParams() + // ... +} +``` + +Key differences: + +- Dynamic segments use `$param` not `[param]` +- Params accessed via `Route.useParams()` not component props +- Route path in filename uses `.` or `/` separators + +## Step 5: Convert Server Actions → Server Functions + +Next.js: + +```tsx +// app/actions.ts +'use server' +export async function createPost(formData: FormData) { + const title = formData.get('title') as string + await db.posts.create({ title }) +} +``` + +TanStack Start: + +```tsx +// src/utils/posts.functions.ts +import { createServerFn } from '@tanstack/react-start' + +export const createPost = createServerFn({ method: 'POST' }) + .inputValidator((data) => { + if (!(data instanceof FormData)) throw new Error('Expected FormData') + return { title: data.get('title')?.toString() || '' } + }) + .handler(async ({ data }) => { + await db.posts.create({ title: data.title }) + return { success: true } + }) +``` + +## Step 6: Convert Data Fetching + +Next.js Server Component: + +```tsx +// app/posts/page.tsx (Server Component — server-only by default) +export default async function PostsPage() { + const posts = await db.posts.findMany() + return +} +``` + +TanStack Start: + +```tsx +// src/routes/posts.tsx +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-start' + +const getPosts = createServerFn({ method: 'GET' }).handler(async () => { + return db.posts.findMany() +}) + +export const Route = createFileRoute('/posts')({ + loader: () => getPosts(), // loader is isomorphic, getPosts runs on server + component: PostsPage, +}) + +function PostsPage() { + const posts = Route.useLoaderData() + return +} +``` + +## Step 7: Convert API Routes → Server Routes + +Next.js: + +```ts +// app/api/users/route.ts +export async function GET() { + const users = await db.users.findMany() + return Response.json(users) +} +``` + +TanStack Start: + +```ts +// src/routes/api/users.ts +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/api/users')({ + server: { + handlers: { + GET: async () => { + const users = await db.users.findMany() + return Response.json(users) + }, + }, + }, +}) +``` + +## Step 8: Convert Navigation + +Next.js: + +```tsx +import Link from 'next/link' +;View Post +``` + +TanStack Start: + +```tsx +import { Link } from '@tanstack/react-router' +; + View Post + +``` + +Never interpolate params into the `to` string. Use `params` prop. + +## Step 9: Convert Middleware + +Next.js: + +```ts +// middleware.ts +export function middleware(request: NextRequest) { + const token = request.cookies.get('session') + if (!token) return NextResponse.redirect(new URL('/login', request.url)) +} +export const config = { matcher: ['/dashboard/:path*'] } +``` + +TanStack Start: + +```tsx +// src/start.ts +import { createStart, createMiddleware } from '@tanstack/react-start' + +const authMiddleware = createMiddleware().server(async ({ next, request }) => { + const cookie = request.headers.get('cookie') + if (!cookie?.includes('session=')) { + throw redirect({ to: '/login' }) + } + return next() +}) + +export const startInstance = createStart(() => ({ + requestMiddleware: [authMiddleware], +})) +``` + +## Step 10: Convert Metadata/SEO + +Next.js: + +```tsx +export const metadata = { + title: 'Post Title', + description: 'Post description', +} +``` + +TanStack Start: + +```tsx +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params }) => fetchPost(params.postId), + head: ({ loaderData }) => ({ + meta: [ + { title: loaderData.title }, + { name: 'description', content: loaderData.excerpt }, + { property: 'og:title', content: loaderData.title }, + ], + }), +}) +``` + +## Post-Migration Checklist + +- [ ] Remove all `"use server"` and `"use client"` directives +- [ ] Remove `next.config.js` / `next.config.ts` +- [ ] Remove `app/` directory (replaced by `src/routes/`) +- [ ] Remove `middleware.ts` (replaced by `src/start.ts`) +- [ ] Verify no `next/*` imports remain +- [ ] Run `npm run dev` and check all routes +- [ ] Verify server-only code is inside `createServerFn` (not bare in components/loaders) +- [ ] Check that `` is in the root route `` + +## Common Mistakes + +### 1. CRITICAL: Keeping Server Component mental model + +```tsx +// WRONG — treating component as server-only (Next.js habit) +function PostsPage() { + const posts = await db.posts.findMany() // fails on client + return
{posts.map(...)}
+} + +// CORRECT — use server function + loader +const getPosts = createServerFn({ method: 'GET' }).handler(async () => { + return db.posts.findMany() +}) + +export const Route = createFileRoute('/posts')({ + loader: () => getPosts(), + component: PostsPage, +}) +``` + +### 2. CRITICAL: Using "use server" directive + +```tsx +// WRONG — "use server" is Next.js/React pattern +'use server' +export async function myAction() { ... } + +// CORRECT — use createServerFn +export const myAction = createServerFn({ method: 'POST' }) + .handler(async () => { ... }) +``` + +### 3. HIGH: Interpolating params into Link href + +```tsx +// WRONG — Next.js pattern +View + +// CORRECT — TanStack Router pattern +View +``` + +## Cross-References + +- [react-start](../../react-start/SKILL.md) — full React Start setup +- [start-core/server-functions](../../../../start-client-core/skills/start-core/server-functions/SKILL.md) — server function patterns +- [start-core/execution-model](../../../../start-client-core/skills/start-core/execution-model/SKILL.md) — isomorphic execution diff --git a/packages/react-start/skills/react-start/SKILL.md b/packages/react-start/skills/react-start/SKILL.md new file mode 100644 index 00000000000..e3067e9e055 --- /dev/null +++ b/packages/react-start/skills/react-start/SKILL.md @@ -0,0 +1,285 @@ +--- +name: react-start +description: >- + React bindings for TanStack Start: createStart, StartClient, + StartServer, React-specific imports, re-exports from + @tanstack/react-router, full project setup with React, useServerFn + hook. +type: framework +library: tanstack-start +library_version: '1.166.2' +framework: react +requires: + - start-core +sources: + - TanStack/router:packages/react-start/src + - TanStack/router:docs/start/framework/react/build-from-scratch.md +--- + +# React Start (`@tanstack/react-start`) + +This skill builds on start-core. Read [start-core](../../../start-client-core/skills/start-core/SKILL.md) first for foundational concepts. + +This skill covers the React-specific bindings, setup, and patterns for TanStack Start. + +> **CRITICAL**: All code is ISOMORPHIC by default. Loaders run on BOTH server and client. Use `createServerFn` for server-only logic. + +> **CRITICAL**: Do not confuse `@tanstack/react-start` with Next.js or Remix. They are completely different frameworks with different APIs. + +> **CRITICAL**: Types are FULLY INFERRED. Never cast, never annotate inferred values. + +## Package API Surface + +`@tanstack/react-start` re-exports everything from `@tanstack/start-client-core` plus: + +- `useServerFn` — React hook for calling server functions from components + +All core APIs (`createServerFn`, `createMiddleware`, `createStart`, `createIsomorphicFn`, `createServerOnlyFn`, `createClientOnlyFn`) are available from `@tanstack/react-start`. + +Server utilities (`getRequest`, `getRequestHeader`, `setResponseHeader`, `setResponseHeaders`, `setResponseStatus`) are imported from `@tanstack/react-start/server`. + +## Full Project Setup + +### 1. Install Dependencies + +```bash +npm i @tanstack/react-start @tanstack/react-router react react-dom +npm i -D vite @vitejs/plugin-react typescript @types/react @types/react-dom +``` + +### 2. package.json + +```json +{ + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "start": "node .output/server/index.mjs" + } +} +``` + +### 3. tsconfig.json + +```json +{ + "compilerOptions": { + "jsx": "react-jsx", + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ES2022", + "skipLibCheck": true, + "strictNullChecks": true + } +} +``` + +### 4. vite.config.ts + +```ts +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import viteReact from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [ + tanstackStart(), // MUST come before react() + viteReact(), + ], +}) +``` + +### 5. Router Factory (src/router.tsx) + +```tsx +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + const router = createRouter({ + routeTree, + scrollRestoration: true, + }) + return router +} +``` + +### 6. Root Route (src/routes/\_\_root.tsx) + +```tsx +import type { ReactNode } from 'react' +import { + Outlet, + createRootRoute, + HeadContent, + Scripts, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: 'My TanStack Start App' }, + ], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + ) +} + +function RootDocument({ children }: Readonly<{ children: ReactNode }>) { + return ( + + + + + + {children} + + + + ) +} +``` + +### 7. Index Route (src/routes/index.tsx) + +```tsx +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-start' + +const getGreeting = createServerFn({ method: 'GET' }).handler(async () => { + return 'Hello from TanStack Start!' +}) + +export const Route = createFileRoute('/')({ + loader: () => getGreeting(), + component: HomePage, +}) + +function HomePage() { + const greeting = Route.useLoaderData() + return

{greeting}

+} +``` + +## useServerFn Hook + +Use `useServerFn` to call server functions from React components with proper integration: + +```tsx +import { createServerFn, useServerFn } from '@tanstack/react-start' + +const updatePost = createServerFn({ method: 'POST' }) + .inputValidator((data: { id: string; title: string }) => data) + .handler(async ({ data }) => { + await db.posts.update(data.id, { title: data.title }) + return { success: true } + }) + +function EditPostForm({ postId }: { postId: string }) { + const updatePostFn = useServerFn(updatePost) + const [title, setTitle] = useState('') + + return ( +
{ + e.preventDefault() + await updatePostFn({ data: { id: postId, title } }) + }} + > + setTitle(e.target.value)} /> + +
+ ) +} +``` + +## Global Start Configuration (src/start.ts) + +```tsx +import { createStart, createMiddleware } from '@tanstack/react-start' + +const requestLogger = createMiddleware().server(async ({ next, request }) => { + console.log(`${request.method} ${request.url}`) + return next() +}) + +export const startInstance = createStart(() => ({ + requestMiddleware: [requestLogger], +})) +``` + +## React-Specific Components + +All routing components from `@tanstack/react-router` work in Start: + +- `` — not needed in Start (handled automatically) +- `` — renders matched child route +- `` — type-safe navigation +- `` — declarative redirect +- `` — renders head tags (must be in ``) +- `` — renders body scripts (must be in ``) +- `` — renders deferred data with Suspense +- `` — renders children only after hydration +- `` — error boundary + +## Hooks Reference + +All hooks from `@tanstack/react-router` work in Start: + +- `useRouter()` — router instance +- `useRouterState()` — subscribe to router state +- `useNavigate()` — programmatic navigation +- `useSearch({ from })` — validated search params +- `useParams({ from })` — path params +- `useLoaderData({ from })` — loader data +- `useMatch({ from })` — full route match +- `useRouteContext({ from })` — route context +- `Route.useLoaderData()` — typed loader data (preferred in route files) +- `Route.useSearch()` — typed search params (preferred in route files) + +## Common Mistakes + +### 1. CRITICAL: Importing from wrong package + +```tsx +// WRONG — this is the SPA router, NOT Start +import { createServerFn } from '@tanstack/react-router' + +// CORRECT — server functions come from react-start +import { createServerFn } from '@tanstack/react-start' + +// CORRECT — routing APIs come from react-router (re-exported by Start too) +import { createFileRoute, Link } from '@tanstack/react-router' +``` + +### 2. HIGH: Using React hooks in beforeLoad or loader + +```tsx +// WRONG — beforeLoad/loader are NOT React components +beforeLoad: () => { + const auth = useAuth() // React hook, cannot be used here +} + +// CORRECT — pass state via router context +const rootRoute = createRootRouteWithContext<{ auth: AuthState }>()({}) +``` + +### 3. HIGH: Missing Scripts component + +Without `` in the root route's ``, client JavaScript doesn't load and the app won't hydrate. + +## Cross-References + +- [start-core](../../../start-client-core/skills/start-core/SKILL.md) — core Start concepts +- [router-core](../../../router-core/skills/router-core/SKILL.md) — routing fundamentals +- [react-router](../../../react-router/skills/react-router/SKILL.md) — React Router hooks and components diff --git a/packages/router-core/bin/intent.js b/packages/router-core/bin/intent.js new file mode 100755 index 00000000000..2cf2efab472 --- /dev/null +++ b/packages/router-core/bin/intent.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// Auto-generated by @tanstack/intent setup +// Exposes the intent end-user CLI for consumers of this library. +// Commit this file, then add to your package.json: +// "bin": { "intent": "./bin/intent.js" } +try { + await import('@tanstack/intent/intent-library') +} catch (e) { + if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + console.error('@tanstack/intent is not installed.') + console.error('') + console.error('Install it as a dev dependency:') + console.error(' npm add -D @tanstack/intent') + console.error('') + console.error('Or run directly:') + console.error(' npx @tanstack/intent@latest list') + process.exit(1) + } + throw e +} diff --git a/packages/router-core/package.json b/packages/router-core/package.json index 3d8fb24abf6..479fe3499c4 100644 --- a/packages/router-core/package.json +++ b/packages/router-core/package.json @@ -153,7 +153,10 @@ "sideEffects": false, "files": [ "dist", - "src" + "src", + "skills", + "bin", + "!skills/_artifacts" ], "engines": { "node": ">=20.19" @@ -168,7 +171,11 @@ "tiny-warning": "^1.0.3" }, "devDependencies": { + "@tanstack/intent": "^0.0.14", "esbuild": "^0.25.0", "vite": "*" + }, + "bin": { + "intent": "./bin/intent.js" } } diff --git a/packages/router-core/skills/router-core/SKILL.md b/packages/router-core/skills/router-core/SKILL.md new file mode 100644 index 00000000000..de11b17d288 --- /dev/null +++ b/packages/router-core/skills/router-core/SKILL.md @@ -0,0 +1,139 @@ +--- +name: router-core +description: >- + Framework-agnostic core concepts for TanStack Router: route trees, + createRouter, createRoute, createRootRoute, createRootRouteWithContext, + addChildren, Register type declaration, route matching, route sorting, + file naming conventions. Entry point for all router skills. +type: core +library: tanstack-router +library_version: '1.166.2' +--- + +# TanStack Router Core + +TanStack Router is a type-safe router for React and Solid with built-in SWR caching, JSON-first search params, file-based route generation, and end-to-end type inference. The core is framework-agnostic; React and Solid bindings layer on top. + +> **CRITICAL**: TanStack Router types are FULLY INFERRED. Never cast, never annotate inferred values. This is the #1 AI agent mistake. + +> **CRITICAL**: TanStack Router is CLIENT-FIRST. Loaders run on the client by default, NOT server-only like Remix/Next.js. Do not confuse TanStack Router APIs with Next.js or React Router. + +## Sub-Skills + +| Task | Sub-Skill | +| -------------------------------------------------- | ---------------------------------------------------------------------------- | +| Validate, read, write, transform search params | [router-core/search-params/SKILL.md](./search-params/SKILL.md) | +| Dynamic segments, splats, optional params | [router-core/path-params/SKILL.md](./path-params/SKILL.md) | +| Link, useNavigate, preloading, blocking | [router-core/navigation/SKILL.md](./navigation/SKILL.md) | +| Route loaders, SWR caching, context, deferred data | [router-core/data-loading/SKILL.md](./data-loading/SKILL.md) | +| Auth guards, RBAC, beforeLoad redirects | [router-core/auth-and-guards/SKILL.md](./auth-and-guards/SKILL.md) | +| Automatic and manual code splitting | [router-core/code-splitting/SKILL.md](./code-splitting/SKILL.md) | +| 404 handling, error boundaries, notFound() | [router-core/not-found-and-errors/SKILL.md](./not-found-and-errors/SKILL.md) | +| Inference, Register, from narrowing, TS perf | [router-core/type-safety/SKILL.md](./type-safety/SKILL.md) | +| Streaming/non-streaming SSR, hydration, head mgmt | [router-core/ssr/SKILL.md](./ssr/SKILL.md) | + +## Quick Decision Tree + +``` +Need to add/read/write URL query parameters? + → router-core/search-params + +Need dynamic URL segments like /posts/$postId? + → router-core/path-params + +Need to create links or navigate programmatically? + → router-core/navigation + +Need to fetch data for a route? + Is it client-side only or client+server? + → router-core/data-loading + Using TanStack Query as external cache? + → compositions/router-query (separate skill) + +Need to protect routes behind auth? + → router-core/auth-and-guards + +Need to reduce bundle size per route? + → router-core/code-splitting + +Need custom 404 or error handling? + → router-core/not-found-and-errors + +Having TypeScript issues or performance problems? + → router-core/type-safety + +Need server-side rendering? + → router-core/ssr +``` + +## Minimal Working Example + +```tsx +// src/routes/__root.tsx +import { createRootRoute, Outlet } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: () => , +}) +``` + +```tsx +// src/routes/index.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: () =>

Home

, +}) +``` + +```tsx +// src/router.tsx +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +const router = createRouter({ routeTree }) + +// REQUIRED for type safety — without this, Link/useNavigate have no autocomplete +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} + +export default router +``` + +```tsx +// src/main.tsx +import { RouterProvider } from '@tanstack/react-router' +import router from './router' + +function App() { + return +} +``` + +## Common Mistakes + +### HIGH: createFileRoute path string must match the file path + +The Vite plugin manages the path string in `createFileRoute`. Do not change it manually — it must match the file's location under `src/routes/`: + +```tsx +// File: src/routes/posts/$postId.tsx +export const Route = createFileRoute('/posts/$postId')({ + // ✅ matches file path + component: PostPage, +}) + +export const Route = createFileRoute('/post/$postId')({ + // ❌ silent mismatch + component: PostPage, +}) +``` + +The plugin auto-generates this string. If you rename a route file, the plugin updates it. Never edit the path string by hand. + +## Version Note + +This skill targets `@tanstack/router-core` v1.166.2 and `@tanstack/react-router` v1.166.2. APIs are stable. Splat routes use `$` (not `*`); the `*` compat alias will be removed in v2. diff --git a/packages/router-core/skills/router-core/auth-and-guards/SKILL.md b/packages/router-core/skills/router-core/auth-and-guards/SKILL.md new file mode 100644 index 00000000000..74d26bb199c --- /dev/null +++ b/packages/router-core/skills/router-core/auth-and-guards/SKILL.md @@ -0,0 +1,454 @@ +--- +name: router-core/auth-and-guards +description: >- + Route protection with beforeLoad, redirect()/throw redirect(), + isRedirect helper, authenticated layout routes (_authenticated), + non-redirect auth (inline login), RBAC with roles and permissions, + auth provider integration (Auth0, Clerk, Supabase), router context + for auth state. +type: sub-skill +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core + - router-core/data-loading +sources: + - TanStack/router:docs/router/guide/authenticated-routes.md + - TanStack/router:docs/router/how-to/setup-authentication.md + - TanStack/router:docs/router/how-to/setup-auth-providers.md + - TanStack/router:docs/router/how-to/setup-rbac.md +--- + +# Auth and Guards + +## Setup + +Protect routes with `beforeLoad` + `redirect()` in a pathless layout route (`_authenticated`): + +```tsx +// src/routes/_authenticated.tsx +import { createFileRoute, redirect, Outlet } from '@tanstack/react-router' + +export const Route = createFileRoute('/_authenticated')({ + beforeLoad: ({ context, location }) => { + if (!context.auth.isAuthenticated) { + throw redirect({ + to: '/login', + search: { + redirect: location.href, + }, + }) + } + }, + component: () => , +}) +``` + +Any route file placed under `src/routes/_authenticated/` is automatically protected: + +```tsx +// src/routes/_authenticated/dashboard.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_authenticated/dashboard')({ + component: DashboardComponent, +}) + +function DashboardComponent() { + const { auth } = Route.useRouteContext() + return
Welcome, {auth.user?.username}
+} +``` + +## Core Patterns + +### Router Context for Auth State + +Auth state flows into the router via `createRootRouteWithContext` and `RouterProvider`'s `context` prop: + +```tsx +// src/routes/__root.tsx +import { createRootRouteWithContext, Outlet } from '@tanstack/react-router' + +interface AuthState { + isAuthenticated: boolean + user: { id: string; username: string; email: string } | null + login: (username: string, password: string) => Promise + logout: () => void +} + +interface MyRouterContext { + auth: AuthState +} + +export const Route = createRootRouteWithContext()({ + component: () => , +}) +``` + +```tsx +// src/router.tsx +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export const router = createRouter({ + routeTree, + context: { + auth: undefined!, // will be set by RouterProvider context prop + }, +}) + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} +``` + +```tsx +// src/App.tsx +import { RouterProvider } from '@tanstack/react-router' +import { AuthProvider, useAuth } from './auth' +import { router } from './router' + +function InnerApp() { + const auth = useAuth() + return +} + +function App() { + return ( + + + + ) +} +``` + +The auth hook is called inside a React component (`InnerApp`), satisfying the Rules of Hooks. The returned value is injected into the router context via `RouterProvider`'s `context` prop. + +### Redirect-Based Auth with Redirect-Back + +Save the current location in search params so you can redirect back after login: + +```tsx +// src/routes/_authenticated.tsx +import { createFileRoute, redirect, Outlet } from '@tanstack/react-router' + +export const Route = createFileRoute('/_authenticated')({ + beforeLoad: ({ context, location }) => { + if (!context.auth.isAuthenticated) { + throw redirect({ + to: '/login', + search: { redirect: location.href }, + }) + } + }, + component: () => , +}) +``` + +```tsx +// src/routes/login.tsx +import { createFileRoute, redirect } from '@tanstack/react-router' +import { useState } from 'react' + +export const Route = createFileRoute('/login')({ + validateSearch: (search) => ({ + redirect: (search.redirect as string) || '/', + }), + beforeLoad: ({ context, search }) => { + if (context.auth.isAuthenticated) { + throw redirect({ to: search.redirect }) + } + }, + component: LoginComponent, +}) + +function LoginComponent() { + const { auth } = Route.useRouteContext() + const search = Route.useSearch() + const navigate = Route.useNavigate() + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState('') + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + try { + await auth.login(username, password) + navigate({ to: search.redirect }) + } catch { + setError('Invalid credentials') + } + } + + return ( +
+ {error &&
{error}
} + setUsername(e.target.value)} /> + setPassword(e.target.value)} + /> + +
+ ) +} +``` + +### Non-Redirect Auth (Inline Login) + +Instead of redirecting, show a login form in place of the `Outlet`: + +```tsx +// src/routes/_authenticated.tsx +import { createFileRoute, Outlet } from '@tanstack/react-router' + +export const Route = createFileRoute('/_authenticated')({ + component: AuthenticatedLayout, +}) + +function AuthenticatedLayout() { + const { auth } = Route.useRouteContext() + + if (!auth.isAuthenticated) { + return + } + + return +} +``` + +This keeps the URL unchanged — the user stays on the same page and sees a login form instead of protected content. After authentication, `` renders and child routes appear. + +### RBAC with Roles and Permissions + +Extend auth state with role/permission helpers, then check in `beforeLoad`: + +```tsx +// src/auth.tsx +interface User { + id: string + username: string + email: string + roles: string[] + permissions: string[] +} + +interface AuthState { + isAuthenticated: boolean + user: User | null + hasRole: (role: string) => boolean + hasAnyRole: (roles: string[]) => boolean + hasPermission: (permission: string) => boolean + hasAnyPermission: (permissions: string[]) => boolean + login: (username: string, password: string) => Promise + logout: () => void +} +``` + +Admin-only layout route: + +```tsx +// src/routes/_authenticated/_admin.tsx +import { createFileRoute, redirect, Outlet } from '@tanstack/react-router' + +export const Route = createFileRoute('/_authenticated/_admin')({ + beforeLoad: ({ context, location }) => { + if (!context.auth.hasRole('admin')) { + throw redirect({ + to: '/unauthorized', + search: { redirect: location.href }, + }) + } + }, + component: () => , +}) +``` + +Multi-role access: + +```tsx +// src/routes/_authenticated/_moderator.tsx +import { createFileRoute, redirect, Outlet } from '@tanstack/react-router' + +export const Route = createFileRoute('/_authenticated/_moderator')({ + beforeLoad: ({ context, location }) => { + if (!context.auth.hasAnyRole(['admin', 'moderator'])) { + throw redirect({ + to: '/unauthorized', + search: { redirect: location.href }, + }) + } + }, + component: () => , +}) +``` + +Permission-based: + +```tsx +// src/routes/_authenticated/_users.tsx +import { createFileRoute, redirect, Outlet } from '@tanstack/react-router' + +export const Route = createFileRoute('/_authenticated/_users')({ + beforeLoad: ({ context, location }) => { + if (!context.auth.hasAnyPermission(['users:read', 'users:write'])) { + throw redirect({ + to: '/unauthorized', + search: { redirect: location.href }, + }) + } + }, + component: () => , +}) +``` + +Page-level permission check (nested under an already-role-protected layout): + +```tsx +// src/routes/_authenticated/_users/manage.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_authenticated/_users/manage')({ + beforeLoad: ({ context }) => { + if (!context.auth.hasPermission('users:write')) { + throw new Error('Write permission required') + } + }, + component: UserManagement, +}) + +function UserManagement() { + const { auth } = Route.useRouteContext() + const canDelete = auth.hasPermission('users:delete') + + return ( +
+

User Management

+ {canDelete && } +
+ ) +} +``` + +### Handling Auth Check Failures (isRedirect) + +When `beforeLoad` has a try/catch, redirects (which work by throwing) can get swallowed. Use `isRedirect` to re-throw: + +```tsx +import { createFileRoute, redirect, isRedirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/_authenticated')({ + beforeLoad: async ({ context, location }) => { + try { + const user = await verifySession(context.auth) + if (!user) { + throw redirect({ + to: '/login', + search: { redirect: location.href }, + }) + } + return { user } + } catch (error) { + if (isRedirect(error)) throw error // re-throw redirect, don't swallow it + // Actual error — redirect to login + throw redirect({ + to: '/login', + search: { redirect: location.href }, + }) + } + }, +}) +``` + +## Common Mistakes + +### HIGH: Auth check in component instead of beforeLoad + +Component-level auth checks cause a **flash of protected content** before the redirect: + +```tsx +// WRONG — protected content renders briefly before redirect +export const Route = createFileRoute('/_authenticated/dashboard')({ + component: () => { + const auth = useAuth() + if (!auth.isAuthenticated) return + return + }, +}) + +// CORRECT — beforeLoad runs before any rendering +export const Route = createFileRoute('/_authenticated/dashboard')({ + beforeLoad: ({ context }) => { + if (!context.auth.isAuthenticated) { + throw redirect({ to: '/login' }) + } + }, + component: Dashboard, +}) +``` + +`beforeLoad` runs before any component rendering and before the loader. It completely prevents the flash. + +### HIGH: Not re-throwing redirects in try/catch + +`redirect()` works by throwing. If `beforeLoad` has a try/catch, the redirect gets swallowed: + +```tsx +// WRONG — redirect is caught and swallowed +beforeLoad: async ({ context }) => { + try { + await validateSession(context.auth) + } catch (e) { + console.error(e) // swallows the redirect! + } +} + +// CORRECT — use isRedirect to distinguish intentional redirects from errors +import { isRedirect } from '@tanstack/react-router' + +beforeLoad: async ({ context }) => { + try { + await validateSession(context.auth) + } catch (e) { + if (isRedirect(e)) throw e + console.error(e) + } +} +``` + +### MEDIUM: Conditionally rendering root route component + +The root route always renders regardless of auth state. You cannot conditionally render its component: + +```tsx +// WRONG — root route always renders, this doesn't protect anything +export const Route = createRootRoute({ + component: () => { + if (!isAuthenticated()) return + return + }, +}) + +// CORRECT — use a pathless layout route for auth boundaries +// src/routes/_authenticated.tsx +export const Route = createFileRoute('/_authenticated')({ + beforeLoad: ({ context }) => { + if (!context.auth.isAuthenticated) { + throw redirect({ to: '/login' }) + } + }, + component: () => , +}) +``` + +Place protected routes as children of the `_authenticated` layout route. Public routes (login, home, etc.) live outside it. + +--- + +## Cross-References + +- See also: **router-core/data-loading/SKILL.md** — `beforeLoad` runs before `loader`; auth context flows into loader via route context diff --git a/packages/router-core/skills/router-core/code-splitting/SKILL.md b/packages/router-core/skills/router-core/code-splitting/SKILL.md new file mode 100644 index 00000000000..8d1ba4e7a5a --- /dev/null +++ b/packages/router-core/skills/router-core/code-splitting/SKILL.md @@ -0,0 +1,322 @@ +--- +name: router-core/code-splitting +description: >- + Automatic code splitting (autoCodeSplitting), .lazy.tsx convention, + createLazyFileRoute, createLazyRoute, lazyRouteComponent, getRouteApi + for typed hooks in split files, codeSplitGroupings per-route override, + splitBehavior programmatic config, critical vs non-critical properties. +type: sub-skill +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core +sources: + - TanStack/router:docs/router/guide/code-splitting.md + - TanStack/router:docs/router/guide/automatic-code-splitting.md +--- + +# Code Splitting + +TanStack Router separates route code into **critical** (required to match and start loading) and **non-critical** (can be lazy-loaded). The bundler plugin can split automatically, or you can split manually with `.lazy.tsx` files. + +> **CRITICAL**: Never `export` component functions from route files — exported functions are included in the main bundle and bypass code splitting entirely. + +> **CRITICAL**: Use `getRouteApi('/path')` in code-split files, NOT `import { Route } from './route'`. Importing Route defeats code splitting. + +## What Stays in the Main Bundle (Critical) + +- Path parsing/serialization +- `validateSearch` +- `loader`, `beforeLoad` +- Route context, static data +- Links, scripts, styles + +## What Gets Split (Non-Critical) + +- `component` +- `errorComponent` +- `pendingComponent` +- `notFoundComponent` + +> The `loader` is NOT split by default. It is already async, so splitting it adds a double async cost: fetch the chunk, then execute the loader. Only split the loader if you have a specific reason. + +## Setup: Automatic Code Splitting + +Enable `autoCodeSplitting: true` in the bundler plugin. This is the recommended approach. + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +export default defineConfig({ + plugins: [ + // TanStack Router plugin MUST come before the framework plugin + tanstackRouter({ + autoCodeSplitting: true, + }), + react(), + ], +}) +``` + +With this enabled, route files are automatically transformed. Components are split into separate chunks; loaders stay in the main bundle. No `.lazy.tsx` files needed. + +```tsx +// src/routes/posts.tsx — everything in one file, splitting is automatic +import { createFileRoute } from '@tanstack/react-router' +import { fetchPosts } from '../api' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, + component: PostsComponent, +}) + +// NOT exported — this is critical for automatic code splitting to work +function PostsComponent() { + const posts = Route.useLoaderData() + return ( +
    + {posts.map((post) => ( +
  • {post.title}
  • + ))} +
+ ) +} +``` + +## Manual Splitting with `.lazy.tsx` + +If you cannot use automatic code splitting (e.g. CLI-only, no bundler plugin), split manually into two files: + +```tsx +// src/routes/posts.tsx — critical route config only +import { createFileRoute } from '@tanstack/react-router' +import { fetchPosts } from '../api' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, +}) +``` + +```tsx +// src/routes/posts.lazy.tsx — non-critical (lazy-loaded) +import { createLazyFileRoute } from '@tanstack/react-router' + +export const Route = createLazyFileRoute('/posts')({ + component: PostsComponent, +}) + +function PostsComponent() { + // Use getRouteApi to access typed hooks without importing Route + return
Posts
+} +``` + +`createLazyFileRoute` supports only: `component`, `errorComponent`, `pendingComponent`, `notFoundComponent`. + +## Virtual Routes + +If splitting leaves the critical route file empty, delete it entirely. A virtual route is auto-generated in `routeTree.gen.ts`: + +```tsx +// src/routes/about.lazy.tsx — no about.tsx needed +import { createLazyFileRoute } from '@tanstack/react-router' + +export const Route = createLazyFileRoute('/about')({ + component: () =>

About Us

, +}) +``` + +## Code-Based Splitting + +For code-based (non-file-based) routing, use `createLazyRoute` and the `.lazy()` method: + +```tsx +// src/posts.lazy.tsx +import { createLazyRoute } from '@tanstack/react-router' + +export const Route = createLazyRoute('/posts')({ + component: PostsComponent, +}) + +function PostsComponent() { + return
Posts
+} +``` + +```tsx +// src/app.tsx +import { createRoute } from '@tanstack/react-router' + +const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/posts', +}).lazy(() => import('./posts.lazy').then((d) => d.Route)) +``` + +## Accessing Typed Hooks in Split Files: `getRouteApi` + +When your component lives in a separate file, use `getRouteApi` to get typed access to route hooks without importing the Route object: + +```tsx +// src/routes/posts.lazy.tsx +import { createLazyFileRoute, getRouteApi } from '@tanstack/react-router' + +const routeApi = getRouteApi('/posts') + +export const Route = createLazyFileRoute('/posts')({ + component: PostsComponent, +}) + +function PostsComponent() { + const posts = routeApi.useLoaderData() + const { page } = routeApi.useSearch() + const params = routeApi.useParams() + const context = routeApi.useRouteContext() + return
Posts page {page}
+} +``` + +`getRouteApi` provides: `useLoaderData`, `useLoaderDeps`, `useMatch`, `useParams`, `useRouteContext`, `useSearch`. + +## Per-Route Split Overrides: `codeSplitGroupings` + +Override split behavior for a specific route by adding `codeSplitGroupings` directly in the route file: + +```tsx +// src/routes/posts.tsx +import { createFileRoute } from '@tanstack/react-router' +import { loadPostsData } from './-heavy-posts-utils' + +export const Route = createFileRoute('/posts')({ + // Bundle loader and component together for this route + codeSplitGroupings: [['loader', 'component']], + loader: () => loadPostsData(), + component: PostsComponent, +}) + +function PostsComponent() { + const data = Route.useLoaderData() + return
{data.title}
+} +``` + +## Global Split Configuration + +### `defaultBehavior` — Change Default Groupings + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +export default defineConfig({ + plugins: [ + tanstackRouter({ + autoCodeSplitting: true, + codeSplittingOptions: { + defaultBehavior: [ + // Bundle all UI components into one chunk + [ + 'component', + 'pendingComponent', + 'errorComponent', + 'notFoundComponent', + ], + ], + }, + }), + ], +}) +``` + +### `splitBehavior` — Programmatic Per-Route Logic + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +export default defineConfig({ + plugins: [ + tanstackRouter({ + autoCodeSplitting: true, + codeSplittingOptions: { + splitBehavior: ({ routeId }) => { + if (routeId.startsWith('/posts')) { + return [['loader', 'component']] + } + // All other routes use defaultBehavior + }, + }, + }), + ], +}) +``` + +### Precedence Order + +1. Per-route `codeSplitGroupings` (highest) +2. `splitBehavior` function +3. `defaultBehavior` option (lowest) + +## Common Mistakes + +### 1. HIGH: Exporting component functions prevents code splitting + +```tsx +// WRONG — export puts PostsComponent in the main bundle +export function PostsComponent() { + return
Posts
+} + +// CORRECT — no export, function stays in the split chunk +function PostsComponent() { + return
Posts
+} +``` + +### 2. MEDIUM: Trying to code-split the root route + +`__root.tsx` does not support code splitting. It is always rendered regardless of the current route. Do not create `__root.lazy.tsx`. + +### 3. MEDIUM: Splitting the loader adds double async cost + +```tsx +// AVOID unless you have a specific reason +codeSplittingOptions: { + defaultBehavior: [ + ['loader'], // Fetch chunk THEN execute loader = two network waterfalls + ['component'], + ], +} + +// PREFERRED — loader stays in main bundle (default behavior) +codeSplittingOptions: { + defaultBehavior: [ + ['component'], + ['errorComponent'], + ['notFoundComponent'], + ], +} +``` + +### 4. HIGH: Importing Route in code-split files for typed hooks + +```tsx +// WRONG — importing Route pulls route config into the lazy chunk +import { Route } from './posts.tsx' +const data = Route.useLoaderData() + +// CORRECT — getRouteApi gives typed hooks without pulling in the route +import { getRouteApi } from '@tanstack/react-router' +const routeApi = getRouteApi('/posts') +const data = routeApi.useLoaderData() +``` + +## Cross-References + +- **router-core/data-loading** — Loader splitting decisions affect data loading performance. Splitting the loader adds latency before data can be fetched. +- **router-core/type-safety** — `getRouteApi` is the type-safe way to access hooks from split files. diff --git a/packages/router-core/skills/router-core/data-loading/SKILL.md b/packages/router-core/skills/router-core/data-loading/SKILL.md new file mode 100644 index 00000000000..9bd266fd577 --- /dev/null +++ b/packages/router-core/skills/router-core/data-loading/SKILL.md @@ -0,0 +1,484 @@ +--- +name: router-core/data-loading +description: >- + Route loader option, loaderDeps for cache keys, staleTime/gcTime/ + defaultPreloadStaleTime SWR caching, pendingComponent/pendingMs/ + pendingMinMs, errorComponent/onError/onCatch, beforeLoad, router + context and createRootRouteWithContext DI pattern, router.invalidate, + Await component, deferred data loading with unawaited promises. +type: sub-skill +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core +sources: + - TanStack/router:docs/router/guide/data-loading.md + - TanStack/router:docs/router/guide/deferred-data-loading.md + - TanStack/router:docs/router/guide/router-context.md + - TanStack/router:docs/router/guide/data-mutations.md +--- + +# Data Loading + +## Setup + +Basic loader returning data, consumed via `useLoaderData`: + +```tsx +// src/routes/posts.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts')({ + loader: () => fetchPosts(), + component: PostsComponent, +}) + +function PostsComponent() { + const posts = Route.useLoaderData() + return ( +
    + {posts.map((post) => ( +
  • {post.title}
  • + ))} +
+ ) +} +``` + +In code-split components, use `getRouteApi` instead of importing Route: + +```tsx +import { getRouteApi } from '@tanstack/react-router' + +const routeApi = getRouteApi('/posts') + +function PostsComponent() { + const posts = routeApi.useLoaderData() + return
    {/* ... */}
+} +``` + +## Route Loading Lifecycle + +The router executes this sequence on every URL/history update: + +1. **Route Matching** (top-down) + - `route.params.parse` + - `route.validateSearch` +2. **Route Pre-Loading** (serial) + - `route.beforeLoad` + - `route.onError` → `route.errorComponent` +3. **Route Loading** (parallel) + - `route.component.preload?` + - `route.loader` + - `route.pendingComponent` (optional) + - `route.component` + - `route.onError` → `route.errorComponent` + +Key: `beforeLoad` runs before `loader`. `beforeLoad` for a parent runs before its children's `beforeLoad`. Throwing in `beforeLoad` prevents all children from loading. + +## Core Patterns + +### loaderDeps for Search-Param-Driven Cache Keys + +Loaders don't receive search params directly. Use `loaderDeps` to declare which search params affect the cache key: + +```tsx +// src/routes/posts.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts')({ + validateSearch: (search) => ({ + offset: Number(search.offset) || 0, + limit: Number(search.limit) || 10, + }), + loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }), + loader: ({ deps: { offset, limit } }) => fetchPosts({ offset, limit }), +}) +``` + +When deps change, the route reloads regardless of `staleTime`. + +### SWR Caching Configuration + +TanStack Router has built-in Stale-While-Revalidate caching keyed on the route's parsed pathname + `loaderDeps`. + +Defaults: + +- **`staleTime`: 0** — data is always considered stale, reloads in background on re-match +- **`preloadStaleTime`: 30 seconds** — preloaded data stays fresh for 30s +- **`gcTime`: 30 minutes** — unused cache entries garbage collected after 30min + +```tsx +export const Route = createFileRoute('/posts')({ + loader: () => fetchPosts(), + staleTime: 10_000, // 10s: data considered fresh for 10 seconds + gcTime: 5 * 60 * 1000, // 5min: garbage collect after 5 minutes +}) +``` + +Disable SWR caching entirely: + +```tsx +export const Route = createFileRoute('/posts')({ + loader: () => fetchPosts(), + staleTime: Infinity, +}) +``` + +Globally: + +```tsx +const router = createRouter({ + routeTree, + defaultStaleTime: Infinity, +}) +``` + +### Pending States (pendingComponent / pendingMs / pendingMinMs) + +By default, a pending component shows after 1 second (`pendingMs: 1000`) and stays for at least 500ms (`pendingMinMs: 500`) to avoid flash. + +```tsx +export const Route = createFileRoute('/posts')({ + loader: () => fetchPosts(), + pendingMs: 500, + pendingMinMs: 300, + pendingComponent: () =>
Loading posts...
, + component: PostsComponent, +}) +``` + +### Router Context with createRootRouteWithContext (Factory Pattern) + +`createRootRouteWithContext` is a factory that returns a function. You must call it twice — the first call passes the generic type, the second passes route options: + +```tsx +// src/routes/__root.tsx +import { createRootRouteWithContext, Outlet } from '@tanstack/react-router' + +interface MyRouterContext { + auth: { userId: string } + fetchPosts: () => Promise +} + +// NOTE: double call — createRootRouteWithContext()({...}) +export const Route = createRootRouteWithContext()({ + component: () => , +}) +``` + +Supply the context when creating the router: + +```tsx +// src/router.tsx +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +const router = createRouter({ + routeTree, + context: { + auth: { userId: '123' }, + fetchPosts, + }, +}) +``` + +Consume in loaders and beforeLoad: + +```tsx +// src/routes/posts.tsx +export const Route = createFileRoute('/posts')({ + loader: ({ context: { fetchPosts } }) => fetchPosts(), +}) +``` + +To pass React hook values into the router context, call the hook above `RouterProvider` and inject via the `context` prop: + +```tsx +import { RouterProvider } from '@tanstack/react-router' + +function InnerApp() { + const auth = useAuth() + return +} + +function App() { + return ( + + + + ) +} +``` + +Route-level context via `beforeLoad`: + +```tsx +export const Route = createFileRoute('/posts')({ + beforeLoad: () => ({ + fetchPosts: () => fetch('/api/posts').then((r) => r.json()), + }), + loader: ({ context: { fetchPosts } }) => fetchPosts(), +}) +``` + +### Deferred Data Loading + +Return unawaited promises from the loader for non-critical data. Use the `Await` component to render them: + +```tsx +import { createFileRoute, Await } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => { + // Slow data — do NOT await + const slowDataPromise = fetchComments(postId) + // Fast data — await + const post = await fetchPost(postId) + + return { post, deferredComments: slowDataPromise } + }, + component: PostComponent, +}) + +function PostComponent() { + const { post, deferredComments } = Route.useLoaderData() + + return ( +
+

{post.title}

+ Loading comments...
} + > + {(comments) => ( +
    + {comments.map((c) => ( +
  • {c.body}
  • + ))} +
+ )} +
+ + ) +} +``` + +### Invalidation After Mutations + +`router.invalidate()` forces all active route loaders to re-run and marks all cached data as stale: + +```tsx +import { useRouter } from '@tanstack/react-router' + +function AddPostButton() { + const router = useRouter() + + const handleAdd = async () => { + await fetch('/api/posts', { method: 'POST', body: '...' }) + router.invalidate() + } + + return +} +``` + +For synchronous invalidation (wait until loaders finish): + +```tsx +await router.invalidate({ sync: true }) +``` + +### Error Handling + +```tsx +import { + createFileRoute, + ErrorComponent, + useRouter, +} from '@tanstack/react-router' + +export const Route = createFileRoute('/posts')({ + loader: () => fetchPosts(), + errorComponent: ({ error, reset }) => { + const router = useRouter() + + if (error instanceof CustomError) { + return
{error.message}
+ } + + return ( +
+ + +
+ ) + }, +}) +``` + +### Loader Parameters + +The `loader` function receives: + +- `params` — parsed path params +- `deps` — object from `loaderDeps` +- `context` — merged parent + beforeLoad context +- `abortController` — cancelled when route unloads or becomes stale +- `cause` — `'enter'`, `'stay'`, or `'preload'` +- `preload` — `true` during preloading +- `location` — current location object +- `parentMatchPromise` — promise of parent route match + +```tsx +export const Route = createFileRoute('/posts/$postId')({ + loader: ({ params: { postId }, abortController }) => + fetchPost(postId, { signal: abortController.signal }), +}) +``` + +## Common Mistakes + +### CRITICAL: Assuming loaders only run on the server + +TanStack Router is **client-first**. Loaders run on the **client** by default. They also run on the server when using TanStack Start for SSR, but the default mental model is client-side execution. + +```tsx +// WRONG — this will crash in the browser +export const Route = createFileRoute('/posts')({ + loader: async () => { + const fs = await import('fs') // Node.js only! + return JSON.parse(fs.readFileSync('...')) // fails in browser + }, +}) + +// CORRECT — loaders run in the browser, use fetch or API calls +export const Route = createFileRoute('/posts')({ + loader: async () => { + const res = await fetch('/api/posts') + return res.json() + }, +}) +``` + +Do NOT put database queries, filesystem access, or server-only code in loaders unless you are using TanStack Start server functions. + +### MEDIUM: Not understanding staleTime default is 0 + +Default `staleTime` is `0`. This means data reloads in the background on every route re-match. This is intentional — it ensures fresh data. But if your data is expensive or static, set `staleTime`: + +```tsx +export const Route = createFileRoute('/posts')({ + loader: () => fetchPosts(), + staleTime: 60_000, // Consider fresh for 1 minute +}) +``` + +### HIGH: Using reset() instead of router.invalidate() in error components + +`reset()` only resets the error boundary UI. It does NOT re-run the loader. For loader errors, use `router.invalidate()`: + +```tsx +// WRONG — resets boundary but loader still has stale error +function PostErrorComponent({ error, reset }) { + return +} + +// CORRECT — re-runs loader and resets boundary +function PostErrorComponent({ error, reset }) { + const router = useRouter() + return +} +``` + +### HIGH: Missing double parentheses on createRootRouteWithContext + +`createRootRouteWithContext()` is a factory — it returns a function. Must call twice: + +```tsx +// WRONG — missing second call, passes options to the factory +const rootRoute = createRootRouteWithContext<{ auth: AuthState }>({ + component: RootComponent, +}) + +// CORRECT — factory()({options}) +const rootRoute = createRootRouteWithContext<{ auth: AuthState }>()({ + component: RootComponent, +}) +``` + +### HIGH: Using React hooks in beforeLoad or loader + +`beforeLoad` and `loader` are NOT React components. You cannot call hooks inside them. Use router context to inject values from hooks: + +```tsx +// WRONG — hooks cannot be called outside React components +export const Route = createFileRoute('/posts')({ + loader: () => { + const auth = useAuth() // This will crash! + return fetchPosts(auth.userId) + }, +}) + +// CORRECT — inject hook values via router context +// In your App component: +function InnerApp() { + const auth = useAuth() + return +} + +// In your route: +export const Route = createFileRoute('/posts')({ + loader: ({ context: { auth } }) => fetchPosts(auth.userId), +}) +``` + +### HIGH: Property order affects TypeScript inference + +Router infers types from earlier properties into later ones. Declaring `beforeLoad` after `loader` means context from `beforeLoad` is unknown in the loader: + +```tsx +// WRONG — context.user is unknown because beforeLoad declared after loader +export const Route = createFileRoute('/admin')({ + loader: ({ context }) => fetchData(context.user), + beforeLoad: () => ({ user: getUser() }), +}) + +// CORRECT — validateSearch → loaderDeps → beforeLoad → loader +export const Route = createFileRoute('/admin')({ + beforeLoad: () => ({ user: getUser() }), + loader: ({ context }) => fetchData(context.user), +}) +``` + +### HIGH: Returning entire search object from loaderDeps + +```tsx +// WRONG — loader re-runs on ANY search param change +loaderDeps: ({ search }) => search + +// CORRECT — only re-run when page changes +loaderDeps: ({ search }) => ({ page: search.page }) +``` + +Returning the whole `search` object means unrelated param changes (e.g., `sortDirection`, `viewMode`) trigger unnecessary reloads because deep equality fails on the entire object. + +## Tensions + +- **Client-first loaders vs SSR expectations**: Loaders run on the client by default. When using SSR (TanStack Start), they run on both client and server. Browser-only APIs work by default but break under SSR. Server-only APIs (fs, db) break by default but work under Start server functions. See **router-core/ssr/SKILL.md**. +- **Built-in SWR cache vs external cache coordination**: Router has built-in caching. When using TanStack Query, set `defaultPreloadStaleTime: 0` to avoid double-caching. See **compositions/router-query/SKILL.md**. + +--- + +## Cross-References + +- See also: **router-core/search-params/SKILL.md** — `loaderDeps` consumes validated search params as cache keys +- See also: **compositions/router-query/SKILL.md** — for external cache coordination with TanStack Query diff --git a/packages/router-core/skills/router-core/navigation/SKILL.md b/packages/router-core/skills/router-core/navigation/SKILL.md new file mode 100644 index 00000000000..3f21161e467 --- /dev/null +++ b/packages/router-core/skills/router-core/navigation/SKILL.md @@ -0,0 +1,448 @@ +--- +name: router-core/navigation +description: >- + Link component, useNavigate, Navigate component, router.navigate, + ToOptions/NavigateOptions/LinkOptions, from/to relative navigation, + activeOptions/activeProps, preloading (intent/viewport/render), + preloadDelay, navigation blocking (useBlocker, Block), createLink, + linkOptions helper, scroll restoration, MatchRoute. +type: sub-skill +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core +sources: + - TanStack/router:docs/router/guide/navigation.md + - TanStack/router:docs/router/guide/preloading.md + - TanStack/router:docs/router/guide/navigation-blocking.md + - TanStack/router:docs/router/guide/link-options.md + - TanStack/router:docs/router/guide/custom-link.md + - TanStack/router:docs/router/guide/scroll-restoration.md +--- + +# Navigation + +## Setup + +Basic type-safe `Link` with `to` and `params`: + +```tsx +import { Link } from '@tanstack/react-router' + +function PostLink({ postId }: { postId: string }) { + return ( + + View Post + + ) +} +``` + +## Core Patterns + +### Link with Active States + +```tsx +import { Link } from '@tanstack/react-router' + +function NavLink() { + return ( + + Posts + + ) +} +``` + +The `data-status` attribute is also set to `"active"` on active links for CSS-based styling. + +`activeOptions` controls matching behavior: + +- `exact` (default `false`) — when `true`, only matches the exact path (not children) +- `includeHash` (default `false`) — include hash in active matching +- `includeSearch` (default `true`) — include search params in active matching + +Children can receive `isActive` as a render function: + +```tsx + + {({ isActive }) => Posts} + +``` + +### Relative Navigation with `from` + +Without `from`, navigation resolves from root `/`. To use relative paths like `..`, provide `from`: + +```tsx +import { createFileRoute, Link } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/$postId')({ + component: PostComponent, +}) + +function PostComponent() { + return ( +
+ {/* Relative to current route */} + + Back to Posts + + + {/* "." reloads the current route */} + + Reload + +
+ ) +} +``` + +### useNavigate for Programmatic Navigation + +Use `useNavigate` only for side-effect-driven navigation (e.g., after a form submission). For anything the user clicks, prefer `Link`. + +```tsx +import { useNavigate } from '@tanstack/react-router' + +function CreatePostForm() { + const navigate = useNavigate({ from: '/posts' }) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + const response = await fetch('/api/posts', { method: 'POST', body: '...' }) + const { id: postId } = await response.json() + + if (response.ok) { + navigate({ to: '/posts/$postId', params: { postId } }) + } + } + + return
{/* ... */}
+} +``` + +The `Navigate` component performs an immediate client-side navigation on mount: + +```tsx +import { Navigate } from '@tanstack/react-router' + +function LegacyRedirect() { + return +} +``` + +`router.navigate` is available anywhere you have the router instance, including outside of React. + +### Preloading + +Strategies: `intent` (hover/touchstart), `viewport` (intersection observer), `render` (on mount). + +Set globally: + +```tsx +import { createRouter } from '@tanstack/react-router' + +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultPreloadDelay: 50, // ms, default is 50 +}) +``` + +Or per-link: + +```tsx + + View Post + +``` + +Preloaded data is cached for 30 seconds by default (`defaultPreloadStaleTime`). When using an external cache like TanStack Query, set `defaultPreloadStaleTime: 0` to let the external library control freshness. + +Manual preloading via the router instance: + +```tsx +import { useRouter } from '@tanstack/react-router' + +function Component() { + const router = useRouter() + + useEffect(() => { + router.preloadRoute({ to: '/posts/$postId', params: { postId: '1' } }) + }, [router]) + + return
+} +``` + +### Navigation Blocking + +Use `useBlocker` to prevent navigation when a form has unsaved changes: + +```tsx +import { useBlocker } from '@tanstack/react-router' +import { useState } from 'react' + +function EditForm() { + const [formIsDirty, setFormIsDirty] = useState(false) + + useBlocker({ + shouldBlockFn: () => { + if (!formIsDirty) return false + const shouldLeave = confirm('Are you sure you want to leave?') + return !shouldLeave + }, + }) + + return
{/* ... */}
+} +``` + +With custom UI using `withResolver`: + +```tsx +import { useBlocker } from '@tanstack/react-router' +import { useState } from 'react' + +function EditForm() { + const [formIsDirty, setFormIsDirty] = useState(false) + + const { proceed, reset, status } = useBlocker({ + shouldBlockFn: () => formIsDirty, + withResolver: true, + }) + + return ( + <> +
{/* ... */}
+ {status === 'blocked' && ( +
+

Are you sure you want to leave?

+ + +
+ )} + + ) +} +``` + +Control `beforeunload` separately: + +```tsx +useBlocker({ + shouldBlockFn: () => formIsDirty, + enableBeforeUnload: formIsDirty, +}) +``` + +### linkOptions for Reusable Navigation Options + +`linkOptions` provides eager type-checking on navigation options objects, so errors surface at definition, not at spread-site: + +```tsx +import { + linkOptions, + Link, + useNavigate, + redirect, +} from '@tanstack/react-router' + +const dashboardLinkOptions = linkOptions({ + to: '/dashboard', + search: { search: '' }, +}) + +// Use anywhere: Link, navigate, redirect +function Nav() { + const navigate = useNavigate() + + return ( +
+ Dashboard + +
+ ) +} + +// Also works in an array for navigation bars +const navOptions = linkOptions([ + { to: '/dashboard', label: 'Summary', activeOptions: { exact: true } }, + { to: '/dashboard/invoices', label: 'Invoices' }, + { to: '/dashboard/users', label: 'Users' }, +]) + +function NavBar() { + return ( + + ) +} +``` + +### createLink for Custom Components + +Wraps any component with TanStack Router's type-safe navigation: + +```tsx +import * as React from 'react' +import { createLink, LinkComponent } from '@tanstack/react-router' + +interface BasicLinkProps extends React.AnchorHTMLAttributes {} + +const BasicLinkComponent = React.forwardRef( + (props, ref) => { + return
+ }, +) + +const CreatedLinkComponent = createLink(BasicLinkComponent) + +export const CustomLink: LinkComponent = (props) => { + return +} +``` + +Usage retains full type safety: + +```tsx + +``` + +### Scroll Restoration + +Enable globally on the router: + +```tsx +const router = createRouter({ + routeTree, + scrollRestoration: true, +}) +``` + +For nested scrollable areas: + +```tsx +const router = createRouter({ + routeTree, + scrollRestoration: true, + scrollToTopSelectors: ['#main-scrollable-area'], +}) +``` + +Custom cache keys: + +```tsx +const router = createRouter({ + routeTree, + scrollRestoration: true, + getScrollRestorationKey: (location) => location.pathname, +}) +``` + +Prevent scroll reset for a specific navigation: + +```tsx + + Posts + +``` + +### MatchRoute for Pending UI + +```tsx +import { Link, MatchRoute } from '@tanstack/react-router' + +function Nav() { + return ( + + Users + + + + + ) +} +``` + +## Common Mistakes + +### CRITICAL: Interpolating params into the `to` string + +```tsx +// WRONG — breaks type safety and param encoding +Post + +// CORRECT — use the params option +Post +``` + +Dynamic segments are declared with `$` in the route path. Always pass them via `params`. This applies to `Link`, `useNavigate`, `Navigate`, and `router.navigate`. + +### MEDIUM: Using useNavigate for clickable elements + +```tsx +// WRONG — no href, no cmd+click, no preloading, no accessibility +function BadNav() { + const navigate = useNavigate() + return +} + +// CORRECT — real tag with href, accessible, preloadable +function GoodNav() { + return Posts +} +``` + +Use `useNavigate` only for programmatic side-effect navigation (after form submit, async action, etc). + +### HIGH: Not providing `from` for relative navigation + +```tsx +// WRONG — without from, ".." resolves from root +Back + +// CORRECT — provide from for relative resolution +Back +``` + +Without `from`, only absolute paths are autocompleted and type-safe. Relative paths like `..` resolve from root instead of the current route. + +### HIGH: Using search as object instead of function loses existing params + +```tsx +// WRONG — replaces ALL search params with just { page: 2 } +Page 2 + +// CORRECT — preserves existing search params, updates page + ({ ...prev, page: 2 })}>Page 2 +``` + +When you pass `search` as a plain object, it replaces all search params. Use the function form to spread previous params and selectively update. + +--- + +## Cross-References + +- See also: **router-core/search-params/SKILL.md** — Link `search` prop interacts with search param validation +- See also: **router-core/type-safety/SKILL.md** — `from` narrowing improves type inference on Link diff --git a/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md b/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md new file mode 100644 index 00000000000..f234702308a --- /dev/null +++ b/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md @@ -0,0 +1,439 @@ +--- +name: router-core/not-found-and-errors +description: >- + notFound() function, notFoundComponent, defaultNotFoundComponent, + notFoundMode (fuzzy/root), errorComponent, onError/onCatch, + CatchBoundary, NotFoundRoute (deprecated), route masking (mask + option, createRouteMask, unmaskOnReload). +type: sub-skill +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core +sources: + - TanStack/router:docs/router/guide/not-found-errors.md + - TanStack/router:docs/router/guide/route-masking.md +--- + +# Not Found and Errors + +TanStack Router handles two categories of "not found": unmatched URL paths (automatic) and missing resources like a post that doesn't exist (manual via `notFound()`). Error boundaries are configured per-route via `errorComponent`. + +> **CRITICAL**: Do NOT use the deprecated `NotFoundRoute`. When present, `notFound()` and `notFoundComponent` will NOT work. Remove it and use `notFoundComponent` instead. + +> **CRITICAL**: `useLoaderData` may be undefined inside `notFoundComponent`. Use `useParams`, `useSearch`, or `useRouteContext` instead. + +## Not Found Handling + +### Global 404: `notFoundComponent` on Root Route + +```tsx +// src/routes/__root.tsx +import { createRootRoute, Outlet, Link } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: () => , + notFoundComponent: () => { + return ( +
+

404 — Page Not Found

+ Go Home +
+ ) + }, +}) +``` + +### Router-Wide Default: `defaultNotFoundComponent` + +```tsx +// src/router.tsx +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +const router = createRouter({ + routeTree, + defaultNotFoundComponent: () => { + return ( +
+

Not found!

+ Go home +
+ ) + }, +}) +``` + +### Per-Route 404: Missing Resources with `notFound()` + +Throw `notFound()` in `loader` or `beforeLoad` when a resource doesn't exist. It works like `redirect()` — throw it to trigger the not-found boundary. + +```tsx +// src/routes/posts.$postId.tsx +import { createFileRoute, notFound } from '@tanstack/react-router' +import { getPost } from '../api' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => { + const post = await getPost(postId) + if (!post) throw notFound() + return { post } + }, + component: PostComponent, + notFoundComponent: ({ data }) => { + const { postId } = Route.useParams() + return

Post "{postId}" not found

+ }, +}) + +function PostComponent() { + const { post } = Route.useLoaderData() + return

{post.title}

+} +``` + +### Targeting a Specific Route with `notFound({ routeId })` + +You can force a specific parent route to handle the not-found error: + +```tsx +// src/routes/_layout/posts.$postId.tsx +import { createFileRoute, notFound } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout/posts/$postId')({ + loader: async ({ params: { postId } }) => { + const post = await getPost(postId) + if (!post) throw notFound({ routeId: '/_layout' }) + return { post } + }, +}) +``` + +### Targeting Root Route with `rootRouteId` + +```tsx +import { createFileRoute, notFound, rootRouteId } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => { + const post = await getPost(postId) + if (!post) throw notFound({ routeId: rootRouteId }) + return { post } + }, +}) +``` + +## `notFoundMode`: Fuzzy vs Root + +### `fuzzy` (default) + +The router finds the nearest parent route with children and a `notFoundComponent`. Preserves as much parent layout as possible. + +Given routes: `__root__` → `posts` → `$postId`, accessing `/posts/1/edit`: + +- `` renders +- `` renders +- `` renders (nearest parent with children + notFoundComponent) + +### `root` + +All not-found errors go to the root route's `notFoundComponent`, regardless of matching: + +```tsx +const router = createRouter({ + routeTree, + notFoundMode: 'root', +}) +``` + +## Error Handling + +### `errorComponent` Per Route + +`errorComponent` receives `error` and `reset` props. For loader errors, use `router.invalidate()` to re-run the loader, then call `reset()` to clear the error boundary. + +```tsx +// src/routes/posts.$postId.tsx +import { createFileRoute, useRouter } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => { + const res = await fetch(`/api/posts/${postId}`) + if (!res.ok) throw new Error('Failed to load post') + return res.json() + }, + component: PostComponent, + errorComponent: PostErrorComponent, +}) + +function PostErrorComponent({ + error, + reset, +}: { + error: Error + reset: () => void +}) { + const router = useRouter() + + return ( +
+

Error: {error.message}

+ +
+ ) +} + +function PostComponent() { + const data = Route.useLoaderData() + return

{data.title}

+} +``` + +### Router-Wide Default Error Component + +```tsx +const router = createRouter({ + routeTree, + defaultErrorComponent: ({ error, reset }) => { + const router = useRouter() + return ( +
+

Something went wrong: {error.message}

+ +
+ ) + }, +}) +``` + +## Data in `notFoundComponent` + +`notFoundComponent` cannot reliably use `useLoaderData` because the loader may not have completed. Safe hooks: + +```tsx +notFoundComponent: ({ data }) => { + // SAFE — always available: + const params = Route.useParams() + const search = Route.useSearch() + const context = Route.useRouteContext() + + // UNSAFE — may be undefined: + // const loaderData = Route.useLoaderData() + + return

Item {params.id} not found

+} +``` + +To forward partial data, use the `data` option on `notFound()`: + +```tsx +loader: async ({ params }) => { + const partialData = await getPartialData(params.id) + if (!partialData.fullResource) { + throw notFound({ data: { name: partialData.name } }) + } + return partialData +}, +notFoundComponent: ({ data }) => { + // data is typed as unknown — validate it + const info = data as { name: string } | undefined + return

{info?.name ?? 'Resource'} not found

+}, +``` + +## Route Masking + +Route masking shows a different URL in the browser bar than the actual route being rendered. Masking data is stored in `location.state` and is lost when the URL is shared or opened in a new tab. + +### Imperative Masking on `` + +```tsx +import { Link } from '@tanstack/react-router' + +function PhotoGrid({ photoId }: { photoId: string }) { + return ( + + Open Photo + + ) +} +``` + +### Imperative Masking with `useNavigate` + +```tsx +import { useNavigate } from '@tanstack/react-router' + +function OpenPhotoButton({ photoId }: { photoId: string }) { + const navigate = useNavigate() + + return ( + + ) +} +``` + +### Declarative Masking with `createRouteMask` + +```tsx +import { createRouter, createRouteMask } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +const photoModalMask = createRouteMask({ + routeTree, + from: '/photos/$photoId/modal', + to: '/photos/$photoId', + params: (prev) => ({ photoId: prev.photoId }), +}) + +const router = createRouter({ + routeTree, + routeMasks: [photoModalMask], +}) +``` + +### Unmasking on Reload + +By default, masks survive local page reloads. To unmask on reload: + +```tsx +// Per-mask +const mask = createRouteMask({ + routeTree, + from: '/photos/$photoId/modal', + to: '/photos/$photoId', + params: (prev) => ({ photoId: prev.photoId }), + unmaskOnReload: true, +}) + +// Per-link + + Open Photo + + +// Router-wide default +const router = createRouter({ + routeTree, + unmaskOnReload: true, +}) +``` + +## Common Mistakes + +### 1. HIGH: Using deprecated `NotFoundRoute` + +```tsx +// WRONG — NotFoundRoute blocks notFound() and notFoundComponent from working +import { NotFoundRoute } from '@tanstack/react-router' +const notFoundRoute = new NotFoundRoute({ component: () =>

404

}) +const router = createRouter({ routeTree, notFoundRoute }) + +// CORRECT — use notFoundComponent on root route +export const Route = createRootRoute({ + component: () => , + notFoundComponent: () =>

404

, +}) +``` + +### 2. MEDIUM: Expecting `useLoaderData` in `notFoundComponent` + +```tsx +// WRONG — loader may not have completed +notFoundComponent: () => { + const data = Route.useLoaderData() // may be undefined! + return

{data.title} not found

+} + +// CORRECT — use safe hooks +notFoundComponent: () => { + const { postId } = Route.useParams() + return

Post {postId} not found

+} +``` + +### 3. MEDIUM: Leaf routes cannot handle not-found errors + +Only routes with children (and therefore an ``) can render `notFoundComponent`. Leaf routes (routes without children) will never catch not-found errors — the error bubbles up to the nearest parent with children. + +```tsx +// This route has NO children — notFoundComponent here will not catch +// unmatched child paths (there are no child paths to unmatch) +export const Route = createFileRoute('/posts/$postId')({ + // notFoundComponent here only works for notFound() thrown in THIS route's loader + // It does NOT catch path-based not-founds + notFoundComponent: () =>

Not found

, +}) +``` + +### 4. MEDIUM: Expecting masked URLs to survive sharing + +Masking data lives in `location.state` (browser history). When a masked URL is copied, shared, or opened in a new tab, the masking data is lost. The browser navigates to the visible (masked) URL directly. + +### 5. HIGH (cross-skill): Using `reset()` alone instead of `router.invalidate()` + `reset()` + +```tsx +// WRONG — reset() clears the error boundary but does NOT re-run the loader +function ErrorFallback({ error, reset }: { error: Error; reset: () => void }) { + return +} + +// CORRECT — invalidate re-runs loaders, reset clears the boundary +function ErrorFallback({ error, reset }: { error: Error; reset: () => void }) { + const router = useRouter() + return ( + + ) +} +``` + +## Cross-References + +- **router-core/data-loading** — `notFound()` thrown in loaders interacts with error boundaries and loader data availability. `errorComponent` retry requires `router.invalidate()`. +- **router-core/type-safety** — `notFoundComponent` data is typed as `unknown`; validate before use. diff --git a/packages/router-core/skills/router-core/path-params/SKILL.md b/packages/router-core/skills/router-core/path-params/SKILL.md new file mode 100644 index 00000000000..32964d06ccc --- /dev/null +++ b/packages/router-core/skills/router-core/path-params/SKILL.md @@ -0,0 +1,382 @@ +--- +name: router-core/path-params +description: >- + Dynamic path segments ($paramName), splat routes ($ / _splat), + optional params ({-$paramName}), prefix/suffix patterns ({$param}.ext), + useParams, params.parse/stringify, pathParamsAllowedCharacters, + i18n locale patterns. +type: sub-skill +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core +sources: + - TanStack/router:docs/router/guide/path-params.md + - TanStack/router:docs/router/routing/routing-concepts.md + - TanStack/router:docs/router/guide/internationalization-i18n.md +--- + +# Path Params + +Path params capture dynamic URL segments into named variables. They are defined with a `$` prefix in the route path. + +> **CRITICAL**: Never interpolate params into the `to` string. Always use the `params` prop. This is the most common agent mistake for path params. + +> **CRITICAL**: Types are fully inferred. Never annotate the return of `useParams()`. + +## Dynamic Segments + +A segment prefixed with `$` captures text until the next `/`. + +```tsx +// src/routes/posts.$postId.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params }) => { + // params.postId is string — fully inferred, do not annotate + return fetchPost(params.postId) + }, + component: PostComponent, +}) + +function PostComponent() { + const { postId } = Route.useParams() + const data = Route.useLoaderData() + return ( +

+ Post {postId}: {data.title} +

+ ) +} +``` + +Multiple dynamic segments work across path levels: + +```tsx +// src/routes/teams.$teamId.members.$memberId.tsx +export const Route = createFileRoute('/teams/$teamId/members/$memberId')({ + component: MemberComponent, +}) + +function MemberComponent() { + const { teamId, memberId } = Route.useParams() + return ( +
+ Team {teamId}, Member {memberId} +
+ ) +} +``` + +## Splat / Catch-All Routes + +A route with a path ending in `$` (bare dollar sign) captures everything after it. The value is available under the `_splat` key. + +```tsx +// src/routes/files.$.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/files/$')({ + component: FileViewer, +}) + +function FileViewer() { + const { _splat } = Route.useParams() + // URL: /files/documents/report.pdf → _splat = "documents/report.pdf" + return
File path: {_splat}
+} +``` + +## Optional Params + +Optional params use `{-$paramName}` syntax. The segment may or may not be present. When absent, the value is `undefined`. + +```tsx +// src/routes/posts.{-$category}.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/{-$category}')({ + component: PostsComponent, +}) + +function PostsComponent() { + const { category } = Route.useParams() + // URL: /posts → category is undefined + // URL: /posts/tech → category is "tech" + return
{category ? `Posts in ${category}` : 'All Posts'}
+} +``` + +Multiple optional params: + +```tsx +// Matches: /posts, /posts/tech, /posts/tech/hello-world +export const Route = createFileRoute('/posts/{-$category}/{-$slug}')({ + component: PostComponent, +}) +``` + +### i18n with Optional Locale + +```tsx +// src/routes/{-$locale}/about.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/{-$locale}/about')({ + component: AboutComponent, +}) + +function AboutComponent() { + const { locale } = Route.useParams() + const currentLocale = locale || 'en' + return

{currentLocale === 'fr' ? 'À Propos' : 'About Us'}

+} +// Matches: /about, /en/about, /fr/about +``` + +## Prefix and Suffix Patterns + +Curly braces `{}` around `$paramName` allow text before or after the dynamic part within a single segment. + +### Prefix + +```tsx +// src/routes/posts/post-{$postId}.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/post-{$postId}')({ + component: PostComponent, +}) + +function PostComponent() { + const { postId } = Route.useParams() + // URL: /posts/post-123 → postId = "123" + return
Post ID: {postId}
+} +``` + +### Suffix + +```tsx +// src/routes/files/{$fileName}[.]txt.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/files/{$fileName}.txt')({ + component: FileComponent, +}) + +function FileComponent() { + const { fileName } = Route.useParams() + // URL: /files/readme.txt → fileName = "readme" + return
File: {fileName}.txt
+} +``` + +### Combined Prefix + Suffix + +```tsx +// URL: /users/user-456.json → userId = "456" +export const Route = createFileRoute('/users/user-{$userId}.json')({ + component: UserComponent, +}) + +function UserComponent() { + const { userId } = Route.useParams() + return
User: {userId}
+} +``` + +## Navigating with Path Params + +### Object Form + +```tsx +import { Link } from '@tanstack/react-router' + +function PostLink({ postId }: { postId: string }) { + return ( + + View Post + + ) +} +``` + +### Function Form (Preserves Other Params) + +```tsx +function PostLink({ postId }: { postId: string }) { + return ( + ({ ...prev, postId })}> + View Post + + ) +} +``` + +### Programmatic Navigation + +```tsx +import { useNavigate } from '@tanstack/react-router' + +function GoToPost({ postId }: { postId: string }) { + const navigate = useNavigate() + + return ( + + ) +} +``` + +### Navigating with Optional Params + +```tsx +// Include the optional param + + Tech Posts + + +// Omit the optional param (renders /posts) + + All Posts + +``` + +## Reading Params Outside Route Components + +### `useParams` with `from` + +```tsx +import { useParams } from '@tanstack/react-router' + +function PostHeader() { + const { postId } = useParams({ from: '/posts/$postId' }) + return

Post {postId}

+} +``` + +### `useParams` with `strict: false` + +```tsx +function GenericBreadcrumb() { + const params = useParams({ strict: false }) + // params is a union of all possible route params + return {params.postId ?? 'Home'} +} +``` + +## Params in Loaders and `beforeLoad` + +```tsx +export const Route = createFileRoute('/posts/$postId')({ + beforeLoad: async ({ params }) => { + // params.postId available here + const canView = await checkPermission(params.postId) + if (!canView) throw redirect({ to: '/unauthorized' }) + }, + loader: async ({ params }) => { + return fetchPost(params.postId) + }, +}) +``` + +## Allowed Characters + +By default, params are encoded with `encodeURIComponent`. Allow extra characters via router config: + +```tsx +import { createRouter } from '@tanstack/react-router' + +const router = createRouter({ + routeTree, + pathParamsAllowedCharacters: ['@', '+'], +}) +``` + +Allowed characters: `;`, `:`, `@`, `&`, `=`, `+`, `$`, `,`. + +## Common Mistakes + +### 1. CRITICAL (cross-skill): Interpolating path params into `to` string + +```tsx +// WRONG — breaks type safety and param encoding +Post + +// CORRECT — use params prop +Post +``` + +### 2. MEDIUM: Using `*` for splat routes instead of `$` + +TanStack Router uses `$` for splat routes. The captured value is under `_splat`, not `*`. + +```tsx +// WRONG (React Router / other frameworks) +// + +// CORRECT (TanStack Router) +// File: src/routes/files.$.tsx +export const Route = createFileRoute('/files/$')({ + component: () => { + const { _splat } = Route.useParams() + return
{_splat}
+ }, +}) +``` + +> Note: `*` works in v1 for backwards compatibility but will be removed in v2. Always use `_splat`. + +### 3. MEDIUM: Using curly braces for basic dynamic segments + +Curly braces are ONLY for prefix/suffix patterns and optional params. Basic dynamic segments use bare `$`. + +```tsx +// WRONG — braces not needed for basic params +createFileRoute('/posts/{$postId}') + +// CORRECT — bare $ for basic dynamic segments +createFileRoute('/posts/$postId') + +// CORRECT — braces for prefix pattern +createFileRoute('/posts/post-{$postId}') + +// CORRECT — braces for optional param +createFileRoute('/posts/{-$category}') +``` + +### 4. Params are always strings + +Path params are always parsed as strings. If you need a number, parse in the loader or component: + +```tsx +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params }) => { + const id = parseInt(params.postId, 10) + if (isNaN(id)) throw notFound() + return fetchPost(id) + }, +}) +``` + +You can also use `params.parse` and `params.stringify` on the route for bidirectional transformation: + +```tsx +export const Route = createFileRoute('/posts/$postId')({ + params: { + parse: (raw) => ({ postId: parseInt(raw.postId, 10) }), + stringify: (parsed) => ({ postId: String(parsed.postId) }), + }, + loader: async ({ params }) => { + // params.postId is now number + return fetchPost(params.postId) + }, +}) +``` diff --git a/packages/router-core/skills/router-core/search-params/SKILL.md b/packages/router-core/skills/router-core/search-params/SKILL.md new file mode 100644 index 00000000000..95b5fecc66d --- /dev/null +++ b/packages/router-core/skills/router-core/search-params/SKILL.md @@ -0,0 +1,355 @@ +--- +name: router-core/search-params +description: >- + validateSearch, search param validation with Zod/Valibot/ArkType adapters, + fallback(), search middlewares (retainSearchParams, stripSearchParams), + custom serialization (parseSearch, stringifySearch), search param + inheritance, loaderDeps for cache keys, reading and writing search params. +type: sub-skill +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core +sources: + - TanStack/router:docs/router/guide/search-params.md + - TanStack/router:docs/router/how-to/setup-basic-search-params.md + - TanStack/router:docs/router/how-to/validate-search-params.md + - TanStack/router:docs/router/how-to/navigate-with-search-params.md + - TanStack/router:docs/router/how-to/share-search-params-across-routes.md + - TanStack/router:docs/router/guide/custom-search-param-serialization.md +--- + +# Search Params + +TanStack Router treats search params as JSON-first application state. They are automatically parsed from the URL into structured objects (numbers, booleans, arrays, nested objects) and validated via `validateSearch` on each route. + +> **CRITICAL**: Use `fallback()` from `@tanstack/zod-adapter`, NOT zod's `.catch()`. Using `.catch()` makes the output type `unknown`, destroying type safety. + +> **CRITICAL**: Types are fully inferred. Never annotate the return of `useSearch()`. + +## Setup: Zod Adapter (Recommended) + +```bash +npm install zod @tanstack/zod-adapter +``` + +```tsx +// src/routes/products.tsx +import { createFileRoute } from '@tanstack/react-router' +import { zodValidator, fallback } from '@tanstack/zod-adapter' +import { z } from 'zod' + +const productSearchSchema = z.object({ + page: fallback(z.number(), 1).default(1), + filter: fallback(z.string(), '').default(''), + sort: fallback(z.enum(['newest', 'oldest', 'price']), 'newest').default( + 'newest', + ), +}) + +export const Route = createFileRoute('/products')({ + validateSearch: zodValidator(productSearchSchema), + component: ProductsPage, +}) + +function ProductsPage() { + // page: number, filter: string, sort: 'newest' | 'oldest' | 'price' + // ALL INFERRED — do not annotate + const { page, filter, sort } = Route.useSearch() + + return ( +
+

+ Page {page}, filter: {filter}, sort: {sort} +

+
+ ) +} +``` + +## Reading Search Params + +### In Route Components: `Route.useSearch()` + +```tsx +function ProductsPage() { + const { page, sort } = Route.useSearch() + return
Page {page}
+} +``` + +### In Code-Split Components: `getRouteApi()` + +```tsx +import { getRouteApi } from '@tanstack/react-router' + +const routeApi = getRouteApi('/products') + +function ProductFilters() { + const { sort } = routeApi.useSearch() + return +} +``` + +### From Any Component: `useSearch({ from })` + +```tsx +import { useSearch } from '@tanstack/react-router' + +function SortIndicator() { + const { sort } = useSearch({ from: '/products' }) + return Sorted by: {sort} +} +``` + +### Loose Access: `useSearch({ strict: false })` + +```tsx +function GenericPaginator() { + const search = useSearch({ strict: false }) + // search.page is number | undefined (union of all routes) + return Page: {search.page ?? 1} +} +``` + +## Writing Search Params + +### Link with Function Form (Preserves Existing Params) + +```tsx +import { Link } from '@tanstack/react-router' + +function Pagination() { + return ( + ({ ...prev, page: prev.page + 1 })} + > + Next Page + + ) +} +``` + +### Link with Object Form (Replaces All Params) + +```tsx + + Reset + +``` + +### Programmatic: `useNavigate()` + +```tsx +import { useNavigate } from '@tanstack/react-router' + +function SortDropdown() { + const navigate = useNavigate({ from: '/products' }) + + return ( + + ) +} +``` + +## Search Param Inheritance + +Parent route search params are automatically merged into child routes: + +```tsx +// src/routes/shop.tsx — parent defines shared params +import { createFileRoute } from '@tanstack/react-router' +import { zodValidator, fallback } from '@tanstack/zod-adapter' +import { z } from 'zod' + +const shopSearchSchema = z.object({ + currency: fallback(z.enum(['USD', 'EUR']), 'USD').default('USD'), +}) + +export const Route = createFileRoute('/shop')({ + validateSearch: zodValidator(shopSearchSchema), +}) +``` + +```tsx +// src/routes/shop/products.tsx — child inherits currency +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/shop/products')({ + component: ShopProducts, +}) + +function ShopProducts() { + // currency is available here from parent — fully typed + const { currency } = Route.useSearch() + return
Currency: {currency}
+} +``` + +## Search Middlewares + +### `retainSearchParams` — Keep Params Across Navigation + +```tsx +import { createRootRoute, retainSearchParams } from '@tanstack/react-router' +import { zodValidator } from '@tanstack/zod-adapter' +import { z } from 'zod' + +const rootSearchSchema = z.object({ + debug: z.boolean().optional(), +}) + +export const Route = createRootRoute({ + validateSearch: zodValidator(rootSearchSchema), + search: { + middlewares: [retainSearchParams(['debug'])], + }, +}) +``` + +### `stripSearchParams` — Remove Default Values from URL + +```tsx +import { createFileRoute, stripSearchParams } from '@tanstack/react-router' +import { zodValidator } from '@tanstack/zod-adapter' +import { z } from 'zod' + +const defaults = { sort: 'newest', page: 1 } + +const searchSchema = z.object({ + sort: z.string().default(defaults.sort), + page: z.number().default(defaults.page), +}) + +export const Route = createFileRoute('/items')({ + validateSearch: zodValidator(searchSchema), + search: { + middlewares: [stripSearchParams(defaults)], + }, +}) +``` + +### Chaining Middlewares + +```tsx +export const Route = createFileRoute('/search')({ + validateSearch: zodValidator( + z.object({ + retainMe: z.string().optional(), + arrayWithDefaults: z.string().array().default(['foo', 'bar']), + required: z.string(), + }), + ), + search: { + middlewares: [ + retainSearchParams(['retainMe']), + stripSearchParams({ arrayWithDefaults: ['foo', 'bar'] }), + ], + }, +}) +``` + +## Custom Serialization + +Override the default JSON serialization at the router level: + +```tsx +import { + createRouter, + parseSearchWith, + stringifySearchWith, +} from '@tanstack/react-router' + +const router = createRouter({ + routeTree, + // Example: use JSURL2 for compact, human-readable URLs + parseSearch: parseSearchWith(parse), + stringifySearch: stringifySearchWith(stringify), +}) +``` + +## Using Search Params in Loaders via `loaderDeps` + +```tsx +export const Route = createFileRoute('/products')({ + validateSearch: zodValidator(productSearchSchema), + // Pick ONLY the params the loader needs — not the entire search object + loaderDeps: ({ search }) => ({ page: search.page }), + loader: async ({ deps }) => { + return fetchProducts({ page: deps.page }) + }, +}) +``` + +## Common Mistakes + +### 1. HIGH: Using zod `.catch()` instead of adapter `fallback()` + +```tsx +// WRONG — .catch() makes the type unknown +const schema = z.object({ page: z.number().catch(1) }) + +// CORRECT — fallback() preserves the inferred type +import { fallback } from '@tanstack/zod-adapter' +const schema = z.object({ page: fallback(z.number(), 1) }) +``` + +### 2. HIGH: Returning entire search object from `loaderDeps` + +```tsx +// WRONG — loader re-runs on ANY search param change +loaderDeps: ({ search }) => search + +// CORRECT — loader only re-runs when page changes +loaderDeps: ({ search }) => ({ page: search.page }) +``` + +### 3. HIGH: Passing Date objects in search params + +```tsx +// WRONG — Date serializes to "[object Object]" or invalid string + + +// CORRECT — convert to ISO string + +``` + +### 4. MEDIUM: Parent route missing `validateSearch` blocks inheritance + +```tsx +// WRONG — child cannot access shared params +export const Route = createRootRoute({ + component: RootComponent, + // no validateSearch! +}) + +// CORRECT — parent must define validateSearch for children to inherit +export const Route = createRootRoute({ + validateSearch: zodValidator(globalSearchSchema), + component: RootComponent, +}) +``` + +### 5. HIGH (cross-skill): Using search as object instead of function loses params + +```tsx +// WRONG — replaces ALL search params, losing any existing ones +Page 2 + +// CORRECT — preserves existing params, updates only page + ({ ...prev, page: 2 })}>Page 2 +``` + +## References + +- [Validation Patterns Reference](./references/validation-patterns.md) — comprehensive patterns for all validation libraries diff --git a/packages/router-core/skills/router-core/search-params/references/validation-patterns.md b/packages/router-core/skills/router-core/search-params/references/validation-patterns.md new file mode 100644 index 00000000000..e0b8fc0f82f --- /dev/null +++ b/packages/router-core/skills/router-core/search-params/references/validation-patterns.md @@ -0,0 +1,379 @@ +# Search Param Validation Patterns Reference + +Comprehensive validation patterns for TanStack Router search params across all supported validation approaches. + +## Zod with `@tanstack/zod-adapter` + +Always use `fallback()` from the adapter instead of zod's `.catch()`. Always wrap with `zodValidator()`. + +### Basic Types + +```tsx +import { zodValidator, fallback } from '@tanstack/zod-adapter' +import { z } from 'zod' + +const schema = z.object({ + count: fallback(z.number(), 0), + name: fallback(z.string(), ''), + active: fallback(z.boolean(), true), +}) + +export const Route = createFileRoute('/example')({ + validateSearch: zodValidator(schema), +}) +``` + +### Optional Params + +```tsx +const schema = z.object({ + // Truly optional — can be undefined in component + searchTerm: z.string().optional(), + // Optional in URL but always has a value in component + page: fallback(z.number(), 1).default(1), +}) +``` + +### Default Values + +```tsx +const schema = z.object({ + // .default() means the param is optional during navigation + // but always present (with default) when reading + page: fallback(z.number(), 1).default(1), + sort: fallback(z.enum(['name', 'date', 'price']), 'name').default('name'), + ascending: fallback(z.boolean(), true).default(true), +}) +``` + +### Array Params + +```tsx +const schema = z.object({ + tags: fallback(z.string().array(), []).default([]), + selectedIds: fallback(z.number().array(), []).default([]), +}) + +// URL: /items?tags=%5B%22react%22%2C%22typescript%22%5D&selectedIds=%5B1%2C2%2C3%5D +// Parsed: { tags: ['react', 'typescript'], selectedIds: [1, 2, 3] } +``` + +### Nested Object Params + +```tsx +const schema = z.object({ + filters: fallback( + z.object({ + status: z.enum(['active', 'inactive']).optional(), + tags: z.string().array().optional(), + priceRange: z + .object({ + min: z.number().min(0), + max: z.number().min(0), + }) + .optional(), + }), + {}, + ).default({}), +}) +``` + +### Enum with Constraints + +```tsx +const schema = z.object({ + sort: fallback(z.enum(['newest', 'oldest', 'price']), 'newest').default( + 'newest', + ), + page: fallback(z.number().int().min(1).max(1000), 1).default(1), + limit: fallback(z.number().int().min(10).max(100), 20).default(20), +}) +``` + +### Discriminated Union + +```tsx +const schema = z.object({ + searchType: fallback(z.enum(['basic', 'advanced']), 'basic').default('basic'), + query: fallback(z.string(), '').default(''), + // Advanced-only fields are optional + category: z.string().optional(), + minPrice: z.number().optional(), + maxPrice: z.number().optional(), +}) +``` + +For true discriminated union validation: + +```tsx +const basicSearch = z.object({ + searchType: z.literal('basic'), + query: z.string(), +}) + +const advancedSearch = z.object({ + searchType: z.literal('advanced'), + query: z.string(), + category: z.string(), + minPrice: z.number(), + maxPrice: z.number(), +}) + +const schema = z.discriminatedUnion('searchType', [basicSearch, advancedSearch]) + +export const Route = createFileRoute('/search')({ + validateSearch: zodValidator(schema), +}) +``` + +### Input Transforms (String to Number) + +When using the zod adapter with transforms, configure `input` and `output` types: + +```tsx +const schema = z.object({ + page: fallback(z.number(), 1).default(1), + filter: fallback(z.string(), '').default(''), +}) + +export const Route = createFileRoute('/items')({ + // Default: input type used for navigation, output type used for reading + validateSearch: zodValidator(schema), + + // Advanced: swap input/output inference + // validateSearch: zodValidator({ schema, input: 'output', output: 'input' }), +}) +``` + +### Schema Composition + +```tsx +const paginationSchema = z.object({ + page: fallback(z.number().int().positive(), 1).default(1), + limit: fallback(z.number().int().min(1).max(100), 20).default(20), +}) + +const sortSchema = z.object({ + sortBy: z.enum(['name', 'date', 'relevance']).optional(), + sortOrder: z.enum(['asc', 'desc']).optional(), +}) + +// Compose for specific routes +const productSearchSchema = paginationSchema.extend({ + category: z.string().optional(), + inStock: fallback(z.boolean(), true).default(true), +}) + +const userSearchSchema = paginationSchema.merge(sortSchema).extend({ + role: z.enum(['admin', 'user']).optional(), +}) +``` + +--- + +## Valibot (Standard Schema) + +Valibot 1.0+ implements Standard Schema. No adapter wrapper needed — pass the schema directly to `validateSearch`. + +```bash +npm install valibot @tanstack/valibot-adapter +``` + +```tsx +import { createFileRoute } from '@tanstack/react-router' +import * as v from 'valibot' + +const productSearchSchema = v.object({ + page: v.optional(v.fallback(v.number(), 1), 1), + filter: v.optional(v.fallback(v.string(), ''), ''), + sort: v.optional( + v.fallback(v.picklist(['newest', 'oldest', 'price']), 'newest'), + 'newest', + ), +}) + +export const Route = createFileRoute('/products')({ + // Pass schema directly — Standard Schema compliant + validateSearch: productSearchSchema, + component: ProductsPage, +}) + +function ProductsPage() { + const { page, filter, sort } = Route.useSearch() + return
Page {page}
+} +``` + +### Valibot with Constraints + +```tsx +import * as v from 'valibot' + +const schema = v.object({ + page: v.optional( + v.fallback(v.pipe(v.number(), v.integer(), v.minValue(1)), 1), + 1, + ), + query: v.optional(v.pipe(v.string(), v.minLength(1), v.maxLength(100))), + tags: v.optional(v.fallback(v.array(v.string()), []), []), +}) +``` + +### Valibot with Adapter (Alternative) + +If you need explicit input/output type control: + +```tsx +import { valibotValidator } from '@tanstack/valibot-adapter' +import * as v from 'valibot' + +const schema = v.object({ + page: v.optional(v.fallback(v.number(), 1), 1), +}) + +export const Route = createFileRoute('/items')({ + validateSearch: valibotValidator(schema), +}) +``` + +--- + +## ArkType (Standard Schema) + +ArkType 2.0-rc+ implements Standard Schema. No adapter needed — pass the type directly to `validateSearch`. + +```bash +npm install arktype @tanstack/arktype-adapter +``` + +```tsx +import { createFileRoute } from '@tanstack/react-router' +import { type } from 'arktype' + +const productSearchSchema = type({ + page: 'number = 1', + filter: 'string = ""', + sort: '"newest" | "oldest" | "price" = "newest"', +}) + +export const Route = createFileRoute('/products')({ + // Pass directly — Standard Schema compliant + validateSearch: productSearchSchema, + component: ProductsPage, +}) + +function ProductsPage() { + const { page, filter, sort } = Route.useSearch() + return
Page {page}
+} +``` + +### ArkType with Constraints + +```tsx +import { type } from 'arktype' + +const searchSchema = type({ + 'query?': 'string>0&<=100', + page: 'number>0 = 1', + 'sortBy?': "'name'|'date'|'relevance'", + 'filters?': 'string[]', +}) +``` + +--- + +## Manual Validation Function + +For full control without any library. The function receives raw JSON-parsed (but unvalidated) search params. + +```tsx +import { createFileRoute } from '@tanstack/react-router' + +type ProductSearch = { + page: number + filter: string + sort: 'newest' | 'oldest' | 'price' +} + +export const Route = createFileRoute('/products')({ + validateSearch: (search: Record): ProductSearch => ({ + page: Number(search?.page ?? 1), + filter: (search.filter as string) || '', + sort: + search.sort === 'newest' || + search.sort === 'oldest' || + search.sort === 'price' + ? search.sort + : 'newest', + }), + component: ProductsPage, +}) + +function ProductsPage() { + const { page, filter, sort } = Route.useSearch() + return
Page {page}
+} +``` + +### Manual Validation with Error Throwing + +If `validateSearch` throws, the route's `errorComponent` renders instead: + +```tsx +export const Route = createFileRoute('/products')({ + validateSearch: (search: Record) => { + const page = Number(search.page) + if (isNaN(page) || page < 1) { + throw new Error('Invalid page number') + } + return { page } + }, + errorComponent: ({ error }) =>
Bad search params: {error.message}
, +}) +``` + +--- + +## Pattern: Object with `parse` Method + +Any object with a `.parse()` method works as `validateSearch`: + +```tsx +const mySchema = { + parse: (input: Record) => ({ + page: Number(input.page ?? 1), + query: String(input.query ?? ''), + }), +} + +export const Route = createFileRoute('/search')({ + validateSearch: mySchema, +}) +``` + +--- + +## Dates in Search Params + +Never put `Date` objects in search params. Always use ISO strings: + +```tsx +const schema = z.object({ + // Store as string, parse in component if needed + startDate: z.string().optional(), + endDate: z.string().optional(), +}) + +// In component: +function DateFilter() { + const { startDate } = Route.useSearch() + const date = startDate ? new Date(startDate) : null + return
{date?.toLocaleDateString()}
+} + +// When navigating: +; ({ ...prev, startDate: new Date().toISOString() })}> + Set Start Date + +``` diff --git a/packages/router-core/skills/router-core/ssr/SKILL.md b/packages/router-core/skills/router-core/ssr/SKILL.md new file mode 100644 index 00000000000..5acde100a28 --- /dev/null +++ b/packages/router-core/skills/router-core/ssr/SKILL.md @@ -0,0 +1,435 @@ +--- +name: router-core/ssr +description: >- + Non-streaming and streaming SSR, RouterClient/RouterServer, + renderRouterToString/renderRouterToStream, createRequestHandler, + defaultRenderHandler/defaultStreamHandler, HeadContent/Scripts + components, head route option (meta/links/styles/scripts), + ScriptOnce, automatic loader dehydration/hydration, memory + history on server, data serialization, document head management. +type: sub-skill +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core + - router-core/data-loading +sources: + - TanStack/router:docs/router/guide/ssr.md + - TanStack/router:docs/router/guide/document-head-management.md + - TanStack/router:docs/router/how-to/setup-ssr.md +--- + +# SSR (Server-Side Rendering) + +> **WARNING**: SSR APIs are experimental. They share internal implementations with TanStack Start and may change. **TanStack Start is the recommended way to do SSR in production** — use manual SSR setup only when integrating with an existing server. + +> **CRITICAL**: TanStack Router is CLIENT-FIRST. Loaders run on the client by default. With SSR enabled, loaders run on BOTH client AND server. They are NOT server-only like Remix/Next.js loaders. See [router-core/data-loading](../data-loading/SKILL.md). + +> **CRITICAL**: Do not generate Next.js patterns (`getServerSideProps`, App Router, server components) or Remix patterns (server-only loader exports). TanStack Router has its own SSR API. + +## Concepts + +There are two SSR flavors: + +- **Non-streaming**: Full page rendered on server, sent as one HTML response, then hydrated on client. +- **Streaming**: Critical first paint sent immediately; remaining content streamed incrementally as it resolves. + +Key behaviors: + +- Memory history is used automatically on the server (no `window`). +- Loader data is automatically dehydrated on the server and hydrated on the client. +- Data serialization supports `Date`, `Error`, `FormData`, and `undefined` out of the box. + +## Setup: Shared Router Factory + +The router must be created identically on server and client. Export a factory function from a shared file: + +```tsx +// src/router.tsx +import { createRouter as createTanstackRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function createRouter() { + return createTanstackRouter({ routeTree }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} +``` + +## Non-Streaming SSR + +### Server Entry (using `defaultRenderHandler`) + +```tsx +// src/entry-server.tsx +import { + createRequestHandler, + defaultRenderHandler, +} from '@tanstack/react-router/ssr/server' +import { createRouter } from './router' + +export async function render({ request }: { request: Request }) { + const handler = createRequestHandler({ request, createRouter }) + return await handler(defaultRenderHandler) +} +``` + +### Server Entry (using `renderRouterToString` for custom wrappers) + +```tsx +// src/entry-server.tsx +import { + createRequestHandler, + renderRouterToString, + RouterServer, +} from '@tanstack/react-router/ssr/server' +import { createRouter } from './router' + +export function render({ request }: { request: Request }) { + const handler = createRequestHandler({ request, createRouter }) + + return handler(({ responseHeaders, router }) => + renderRouterToString({ + responseHeaders, + router, + children: , + }), + ) +} +``` + +### Client Entry + +```tsx +// src/entry-client.tsx +import { hydrateRoot } from 'react-dom/client' +import { RouterClient } from '@tanstack/react-router/ssr/client' +import { createRouter } from './router' + +const router = createRouter() + +hydrateRoot(document, ) +``` + +## Streaming SSR + +### Server Entry (using `defaultStreamHandler`) + +```tsx +// src/entry-server.tsx +import { + createRequestHandler, + defaultStreamHandler, +} from '@tanstack/react-router/ssr/server' +import { createRouter } from './router' + +export async function render({ request }: { request: Request }) { + const handler = createRequestHandler({ request, createRouter }) + return await handler(defaultStreamHandler) +} +``` + +### Server Entry (using `renderRouterToStream` for custom wrappers) + +```tsx +// src/entry-server.tsx +import { + createRequestHandler, + renderRouterToStream, + RouterServer, +} from '@tanstack/react-router/ssr/server' +import { createRouter } from './router' + +export function render({ request }: { request: Request }) { + const handler = createRequestHandler({ request, createRouter }) + + return handler(({ request, responseHeaders, router }) => + renderRouterToStream({ + request, + responseHeaders, + router, + children: , + }), + ) +} +``` + +Streaming is automatic — deferred data (unawaited promises from loaders) and streamed markup just work when using `defaultStreamHandler` or `renderRouterToStream`. + +## Document Head Management + +Use the `head` route option to manage ``, `<meta>`, `<link>`, and `<style>` tags. Render `<HeadContent />` in `<head>` and `<Scripts />` in `<body>`. + +### Root Route with Head + +```tsx +// src/routes/__root.tsx +import { + createRootRoute, + HeadContent, + Outlet, + Scripts, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'UTF-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1.0' }, + { title: 'My App' }, + ], + links: [{ rel: 'icon', href: '/favicon.ico' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + <html lang="en"> + <head> + <HeadContent /> + </head> + <body> + <Outlet /> + <Scripts /> + </body> + </html> + ) +} +``` + +### Per-Route Head (Nested Deduplication) + +Child route `title` and `meta` tags override parent tags with the same `name`/`property`: + +```tsx +// src/routes/posts/$postId.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params }) => { + const post = await fetchPost(params.postId) + return { post } + }, + head: ({ loaderData }) => ({ + meta: [ + { title: loaderData.post.title }, + { name: 'description', content: loaderData.post.excerpt }, + ], + }), + component: PostPage, +}) + +function PostPage() { + const { post } = Route.useLoaderData() + return <article>{post.content}</article> +} +``` + +### SPA Head (No Full HTML Control) + +For SPAs without server-rendered HTML, render `<HeadContent />` at the top of the component tree: + +```tsx +import { createRootRoute, HeadContent, Outlet } from '@tanstack/react-router' + +const rootRoute = createRootRoute({ + head: () => ({ + meta: [{ title: 'My SPA' }], + }), + component: () => ( + <> + <HeadContent /> + <Outlet /> + </> + ), +}) +``` + +## Body Scripts + +Use `scripts` (separate from `head.scripts`) to inject scripts into `<body>` before the app entry point: + +```tsx +export const Route = createRootRoute({ + scripts: () => [{ children: 'console.log("runs before hydration")' }], +}) +``` + +The `<Scripts />` component renders these. Place it at the end of `<body>`. + +## ScriptOnce for Pre-Hydration Scripts + +`ScriptOnce` renders a `<script>` during SSR that executes immediately and self-removes. On client navigation, it does nothing (no duplicate execution). + +```tsx +import { ScriptOnce } from '@tanstack/react-router' + +const themeScript = `(function() { + try { + const theme = localStorage.getItem('theme') || 'auto'; + const resolved = theme === 'auto' + ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') + : theme; + document.documentElement.classList.add(resolved); + } catch (e) {} +})();` + +function ThemeProvider({ children }: { children: React.ReactNode }) { + return ( + <> + <ScriptOnce children={themeScript} /> + {children} + </> + ) +} +``` + +If the script modifies the DOM (e.g., adds a class to `<html>`), use `suppressHydrationWarning` on the element: + +```tsx +<html lang="en" suppressHydrationWarning> +``` + +## Express Integration Example + +`createRequestHandler` expects a Web API `Request` and returns a Web API `Response`. For Express, convert between formats: + +```tsx +// src/entry-server.tsx +import { pipeline } from 'node:stream/promises' +import { + RouterServer, + createRequestHandler, + renderRouterToString, +} from '@tanstack/react-router/ssr/server' +import { createRouter } from './router' +import type express from 'express' + +export async function render({ + req, + res, +}: { + req: express.Request + res: express.Response +}) { + const url = new URL(req.originalUrl || req.url, 'https://localhost:3000').href + + const request = new Request(url, { + method: req.method, + headers: (() => { + const headers = new Headers() + for (const [key, value] of Object.entries(req.headers)) { + headers.set(key, value as any) + } + return headers + })(), + }) + + const handler = createRequestHandler({ request, createRouter }) + + const response = await handler(({ responseHeaders, router }) => + renderRouterToString({ + responseHeaders, + router, + children: <RouterServer router={router} />, + }), + ) + + res.status(response.status) + response.headers.forEach((value, name) => { + res.setHeader(name, value) + }) + + return pipeline(response.body as any, res) +} +``` + +## Common Mistakes + +### 1. HIGH: Using browser APIs in loaders without environment check + +Loaders run on BOTH client and server with SSR. Browser-only APIs (`window`, `document`, `localStorage`) throw on the server. + +```tsx +// WRONG — crashes on server +loader: async () => { + const token = localStorage.getItem('token') + return fetchData(token) +} + +// CORRECT — guard with environment check +loader: async () => { + const token = + typeof window !== 'undefined' ? localStorage.getItem('token') : null + return fetchData(token) +} +``` + +### 2. MEDIUM: Using hash fragments for server-rendered content + +Hash fragments (`#section`) are never sent to the server. Conditional rendering based on hash causes hydration mismatches. + +```tsx +// WRONG — server has no hash, client does → mismatch +component: () => { + const hash = window.location.hash + return hash === '#admin' ? <AdminPanel /> : <UserPanel /> +} + +// CORRECT — use search params for server-visible state +validateSearch: z.object({ view: fallback(z.enum(['admin', 'user']), 'user') }), +component: () => { + const { view } = Route.useSearch() + return view === 'admin' ? <AdminPanel /> : <UserPanel /> +} +``` + +### 3. CRITICAL: Generating Next.js or Remix SSR patterns + +TanStack Router does NOT use `getServerSideProps`, `getStaticProps`, App Router `page.tsx`, or Remix-style server-only `loader` exports. + +```tsx +// WRONG — Next.js patterns +export async function getServerSideProps() { + return { props: { data: await fetchData() } } +} + +// WRONG — Remix patterns +export async function loader({ request }: LoaderFunctionArgs) { + return json({ data: await fetchData() }) +} + +// CORRECT — TanStack Router pattern +export const Route = createFileRoute('/data')({ + loader: async () => { + const data = await fetchData() + return { data } + }, + component: DataPage, +}) + +function DataPage() { + const { data } = Route.useLoaderData() + return <div>{data}</div> +} +``` + +## Tension: Client-First Loaders vs SSR + +TanStack Router loaders are client-first by design. When SSR is enabled, they run in both environments. This means: + +- Browser APIs work by default (client-only) but break under SSR +- Database access does NOT belong in loaders (unlike Remix/Next) — use API routes +- For server-only data logic with SSR, use TanStack Start's server functions + +See [router-core/data-loading](../data-loading/SKILL.md) for loader fundamentals. + +## Cross-References + +- [router-core/data-loading](../data-loading/SKILL.md) — SSR changes where loaders execute +- [compositions/router-query](../../../../react-router/skills/compositions/router-query/SKILL.md) — SSR dehydration/hydration with TanStack Query diff --git a/packages/router-core/skills/router-core/type-safety/SKILL.md b/packages/router-core/skills/router-core/type-safety/SKILL.md new file mode 100644 index 00000000000..cf25e8a4f4b --- /dev/null +++ b/packages/router-core/skills/router-core/type-safety/SKILL.md @@ -0,0 +1,500 @@ +--- +name: router-core/type-safety +description: >- + Full type inference philosophy (never cast, never annotate inferred + values), Register module declaration, from narrowing on hooks and + Link, strict:false for shared components, getRouteApi for code-split + typed access, addChildren with object syntax for TS perf, LinkProps + and ValidateLinkOptions type utilities, as const satisfies pattern. +type: sub-skill +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core +sources: + - TanStack/router:docs/router/guide/type-safety.md + - TanStack/router:docs/router/guide/type-utilities.md + - TanStack/router:docs/router/guide/render-optimizations.md +--- + +# Type Safety + +TanStack Router is FULLY type-inferred. Params, search params, context, and loader data all flow through the route tree automatically. The **#1 AI agent mistake** is adding type annotations, casts, or generic parameters to values that are already inferred. + +> **CRITICAL**: NEVER use `as Type`, explicit generic params, `satisfies` on hook returns, or type annotations on inferred values. Every cast masks real type errors and breaks the inference chain. + +> **CRITICAL**: Do NOT confuse TanStack Router with Next.js or React Router. There is no `getServerSideProps`, no `useSearchParams()`, no `useLoaderData()` from `react-router-dom`. + +## The ONE Required Type Annotation: Register + +Without this, top-level exports like `Link`, `useNavigate`, `useSearch` have no type safety. + +```tsx +// src/router.tsx +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +const router = createRouter({ routeTree }) + +// THIS IS REQUIRED — the single type registration for the entire app +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} + +export default router +``` + +After registration, every `Link`, `useNavigate`, `useSearch`, `useParams` across the app is fully typed. + +## Types Flow Automatically + +### Route Hooks — No Annotation Needed + +```tsx +// src/routes/posts.$postId.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/$postId')({ + validateSearch: (search: Record<string, unknown>) => ({ + page: Number(search.page ?? 1), + }), + loader: async ({ params }) => { + // params.postId is already typed as string — do not annotate + const post = await fetchPost(params.postId) + return { post } + }, + component: PostComponent, +}) + +function PostComponent() { + // ALL of these are fully inferred — do NOT add type annotations + const { postId } = Route.useParams() + // ^? string + + const { page } = Route.useSearch() + // ^? number + + const { post } = Route.useLoaderData() + // ^? { id: string; title: string; body: string } + + return ( + <div> + <h1>{post.title}</h1> + <p>Page {page}</p> + </div> + ) +} +``` + +### Context Flows Through the Tree + +```tsx +// src/routes/__root.tsx +import { createRootRouteWithContext, Outlet } from '@tanstack/react-router' + +interface RouterContext { + auth: { userId: string; role: 'admin' | 'user' } | null +} + +// Note: createRootRouteWithContext is a FACTORY — call it TWICE: ()() +export const Route = createRootRouteWithContext<RouterContext>()({ + component: () => <Outlet />, +}) +``` + +```tsx +// src/routes/dashboard.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/dashboard')({ + beforeLoad: ({ context }) => { + // context.auth is already typed as { userId: string; role: 'admin' | 'user' } | null + // NO annotation needed + if (!context.auth) throw redirect({ to: '/login' }) + return { user: context.auth } + }, + loader: ({ context }) => { + // context.user is typed as { userId: string; role: 'admin' | 'user' } + // This was added by beforeLoad above — fully inferred + return fetchDashboard(context.user.userId) + }, + component: DashboardComponent, +}) + +function DashboardComponent() { + const data = Route.useLoaderData() + const { user } = Route.useRouteContext() + return <h1>Welcome {user.userId}</h1> +} +``` + +## Narrowing with `from` + +Without `from`, hooks return a union of ALL routes' types — slow for TypeScript and imprecise. + +### On Hooks + +```tsx +import { useSearch, useParams, useNavigate } from '@tanstack/react-router' + +function PostSidebar() { + // WRONG — search is a union of ALL routes' search params + const search = useSearch() + + // CORRECT — search is narrowed to /posts/$postId's search params + const search = useSearch({ from: '/posts/$postId' }) + // ^? { page: number } + + // CORRECT — params narrowed to this route + const { postId } = useParams({ from: '/posts/$postId' }) + + // CORRECT — navigate narrowed for relative paths + const navigate = useNavigate({ from: '/posts/$postId' }) +} +``` + +### On `Link` + +```tsx +import { Link } from '@tanstack/react-router' + +// WRONG — search resolves to union of ALL routes' search params, slow TS check +<Link to=".." search={{ page: 0 }} /> + +// CORRECT — narrowed, fast TS check +<Link from="/posts/$postId" to=".." search={{ page: 0 }} /> + +// Also correct — Route.fullPath in route components +<Link from={Route.fullPath} to=".." search={{ page: 0 }} /> +``` + +## Shared Components: `strict: false` + +When a component is used across multiple routes, use `strict: false` instead of `from`: + +```tsx +import { useSearch } from '@tanstack/react-router' + +function GlobalSearch() { + // Returns union of all routes' search params — no runtime error if route doesn't match + const search = useSearch({ strict: false }) + return <span>Query: {search.q ?? ''}</span> +} +``` + +## Code-Split Files: `getRouteApi` + +Use `getRouteApi` instead of importing `Route` to avoid pulling route config into the lazy chunk: + +```tsx +// src/routes/posts.lazy.tsx +import { createLazyFileRoute, getRouteApi } from '@tanstack/react-router' + +const routeApi = getRouteApi('/posts') + +export const Route = createLazyFileRoute('/posts')({ + component: PostsComponent, +}) + +function PostsComponent() { + const data = routeApi.useLoaderData() + const { page } = routeApi.useSearch() + return <div>Page {page}</div> +} +``` + +## TypeScript Performance + +### Use Object Syntax for `addChildren` in Large Route Trees + +```tsx +// SLOWER — tuple syntax +const routeTree = rootRoute.addChildren([ + postsRoute.addChildren([postRoute, postsIndexRoute]), + indexRoute, +]) + +// FASTER — object syntax (TS checks objects faster than large tuples) +const routeTree = rootRoute.addChildren({ + postsRoute: postsRoute.addChildren({ postRoute, postsIndexRoute }), + indexRoute, +}) +``` + +With file-based routing the route tree is generated, so this is handled for you. + +### Avoid Returning Unused Inferred Types from Loaders + +When using external caches like TanStack Query, don't let the router infer complex return types you never consume: + +```tsx +// SLOWER — TS infers the full ensureQueryData return type into the route tree +export const Route = createFileRoute('/posts/$postId')({ + loader: ({ context: { queryClient }, params: { postId } }) => + queryClient.ensureQueryData(postQueryOptions(postId)), + component: PostComponent, +}) + +// FASTER — void return, inference stays out of the route tree +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ context: { queryClient }, params: { postId } }) => { + await queryClient.ensureQueryData(postQueryOptions(postId)) + }, + component: PostComponent, +}) +``` + +### `as const satisfies` for Link Option Objects + +Never use `LinkProps` as a variable type — it's an enormous union: + +```tsx +import type { LinkProps } from '@tanstack/react-router' + +// WRONG — LinkProps is a massive union, extremely slow TS check +const props: LinkProps = { to: '/posts' } + +// CORRECT — infer a precise type, validate against LinkProps +const props = { to: '/posts' } as const satisfies LinkProps + +// EVEN BETTER — narrow LinkProps with generic params +import type { RegisteredRouter } from '@tanstack/react-router' +const props = { + to: '/posts', +} as const satisfies LinkProps<RegisteredRouter, string, '/posts'> +``` + +### Type-Safe Link Option Arrays + +```tsx +import type { LinkProps } from '@tanstack/react-router' + +export const navLinks = [ + { to: '/posts' }, + { to: '/posts/$postId', params: { postId: '1' } }, +] as const satisfies ReadonlyArray<LinkProps> + +// Use the precise inferred type, not LinkProps directly +export type NavLink = (typeof navLinks)[number] +``` + +## Type Utilities for Generic Components + +### `ValidateLinkOptions` — Type-Safe Link Props in Custom Components + +```tsx +import { + Link, + type RegisteredRouter, + type ValidateLinkOptions, +} from '@tanstack/react-router' + +interface NavItemProps< + TRouter extends RegisteredRouter = RegisteredRouter, + TOptions = unknown, +> { + label: string + linkOptions: ValidateLinkOptions<TRouter, TOptions> +} + +export function NavItem<TRouter extends RegisteredRouter, TOptions>( + props: NavItemProps<TRouter, TOptions>, +): React.ReactNode +export function NavItem(props: NavItemProps): React.ReactNode { + return ( + <li> + <Link {...props.linkOptions}>{props.label}</Link> + </li> + ) +} + +// Usage — fully type-safe +<NavItem label="Posts" linkOptions={{ to: '/posts' }} /> +<NavItem label="Post" linkOptions={{ to: '/posts/$postId', params: { postId: '1' } }} /> +``` + +### `ValidateNavigateOptions` — Type-Safe Navigate in Utilities + +```tsx +import { + useNavigate, + type RegisteredRouter, + type ValidateNavigateOptions, +} from '@tanstack/react-router' +import { useState } from 'react' + +export function useDelayedNavigate< + TRouter extends RegisteredRouter = RegisteredRouter, + TOptions = unknown, +>( + options: ValidateNavigateOptions<TRouter, TOptions>, + delayMs: number, +): () => void +export function useDelayedNavigate( + options: ValidateNavigateOptions, + delayMs: number, +): () => void { + const navigate = useNavigate() + return () => { + setTimeout(() => navigate(options), delayMs) + } +} + +// Usage — type-safe +const go = useDelayedNavigate( + { to: '/posts/$postId', params: { postId: '1' } }, + 500, +) +``` + +### `ValidateRedirectOptions` — Type-Safe Redirect in Utilities + +```tsx +import { + redirect, + type RegisteredRouter, + type ValidateRedirectOptions, +} from '@tanstack/react-router' + +export async function fetchOrRedirect< + TRouter extends RegisteredRouter = RegisteredRouter, + TOptions = unknown, +>( + url: string, + redirectOptions: ValidateRedirectOptions<TRouter, TOptions>, +): Promise<unknown> +export async function fetchOrRedirect( + url: string, + redirectOptions: ValidateRedirectOptions, +): Promise<unknown> { + const response = await fetch(url) + if (!response.ok && response.status === 401) throw redirect(redirectOptions) + return response.json() +} +``` + +### Render Props for Maximum Performance + +Instead of accepting `LinkProps`, invert control so `Link` is narrowed at the call site: + +```tsx +function Card(props: { title: string; renderLink: () => React.ReactNode }) { + return ( + <div> + <h2>{props.title}</h2> + {props.renderLink()} + </div> + ) +} + +// Link narrowed to exactly /posts — no union check +;<Card title="All Posts" renderLink={() => <Link to="/posts">View</Link>} /> +``` + +## Render Optimizations + +### Fine-Grained Selectors with `select` + +```tsx +function PostTitle() { + // Only re-renders when page changes, not when other search params change + const page = Route.useSearch({ select: ({ page }) => page }) + return <span>Page {page}</span> +} +``` + +### Structural Sharing + +Preserve referential identity across re-renders for search params: + +```tsx +const router = createRouter({ + routeTree, + defaultStructuralSharing: true, // Enable globally +}) + +// Or per-hook +const result = Route.useSearch({ + select: (search) => ({ foo: search.foo, label: `Page ${search.foo}` }), + structuralSharing: true, +}) +``` + +Structural sharing only works with JSON-compatible data. TypeScript will error if you return class instances with `structuralSharing: true`. + +## Common Mistakes + +### 1. CRITICAL: Adding type annotations or casts to inferred values + +```tsx +// WRONG — casting masks real type errors +const search = useSearch({ from: '/posts' }) as { page: number } + +// WRONG — unnecessary annotation +const params: { postId: string } = useParams({ from: '/posts/$postId' }) + +// WRONG — generic param on hook +const data = useLoaderData<{ posts: Post[] }>({ from: '/posts' }) + +// CORRECT — let inference work +const search = useSearch({ from: '/posts' }) +const params = useParams({ from: '/posts/$postId' }) +const data = useLoaderData({ from: '/posts' }) +``` + +### 2. HIGH: Using un-narrowed `LinkProps` type + +```tsx +// WRONG — LinkProps is a massive union, causes severe TS slowdown +const myProps: LinkProps = { to: '/posts' } + +// CORRECT — use as const satisfies for precise inference +const myProps = { to: '/posts' } as const satisfies LinkProps +``` + +### 3. HIGH: Not narrowing `Link`/`useNavigate` with `from` + +```tsx +// WRONG — search is a union of ALL routes, TS check grows with route count +<Link to=".." search={{ page: 0 }} /> + +// CORRECT — narrowed, fast check +<Link from={Route.fullPath} to=".." search={{ page: 0 }} /> +``` + +### 4. CRITICAL (cross-skill): Missing router type registration + +```tsx +// WRONG — Link/useNavigate have no autocomplete, all paths are untyped strings +const router = createRouter({ routeTree }) +// (no declare module) + +// CORRECT — always register +const router = createRouter({ routeTree }) +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} +``` + +### 5. CRITICAL (cross-skill): Generating Next.js or Remix patterns + +```tsx +// WRONG — these are NOT TanStack Router APIs +export async function getServerSideProps() { ... } +export async function loader({ request }) { ... } // Remix-style +const [searchParams, setSearchParams] = useSearchParams() // React Router + +// CORRECT — TanStack Router APIs +export const Route = createFileRoute('/posts')({ + loader: async () => { ... }, // TanStack loader + validateSearch: zodValidator(schema), // TanStack search validation + component: PostsComponent, +}) +const search = Route.useSearch() // TanStack hook +``` + +See also: router-core (Register setup), router-core/navigation (from narrowing), router-core/code-splitting (getRouteApi). \ No newline at end of file diff --git a/packages/router-plugin/bin/intent.js b/packages/router-plugin/bin/intent.js new file mode 100755 index 00000000000..2cf2efab472 --- /dev/null +++ b/packages/router-plugin/bin/intent.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// Auto-generated by @tanstack/intent setup +// Exposes the intent end-user CLI for consumers of this library. +// Commit this file, then add to your package.json: +// "bin": { "intent": "./bin/intent.js" } +try { + await import('@tanstack/intent/intent-library') +} catch (e) { + if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + console.error('@tanstack/intent is not installed.') + console.error('') + console.error('Install it as a dev dependency:') + console.error(' npm add -D @tanstack/intent') + console.error('') + console.error('Or run directly:') + console.error(' npx @tanstack/intent@latest list') + process.exit(1) + } + throw e +} diff --git a/packages/router-plugin/package.json b/packages/router-plugin/package.json index 867db80feee..6730c741403 100644 --- a/packages/router-plugin/package.json +++ b/packages/router-plugin/package.json @@ -98,7 +98,10 @@ "sideEffects": false, "files": [ "dist", - "src" + "src", + "skills", + "bin", + "!skills/_artifacts" ], "engines": { "node": ">=20.19" @@ -119,6 +122,7 @@ "zod": "^3.24.2" }, "devDependencies": { + "@tanstack/intent": "^0.0.14", "@types/babel__core": "^7.20.5", "@types/babel__template": "^7.4.4", "@types/babel__traverse": "^7.28.0" @@ -146,5 +150,8 @@ "webpack": { "optional": true } + }, + "bin": { + "intent": "./bin/intent.js" } } diff --git a/packages/router-plugin/skills/_artifacts/domain_map.yaml b/packages/router-plugin/skills/_artifacts/domain_map.yaml new file mode 100644 index 00000000000..8ce9e31f4df --- /dev/null +++ b/packages/router-plugin/skills/_artifacts/domain_map.yaml @@ -0,0 +1,62 @@ +# domain_map.yaml +# Library: @tanstack/router-plugin +# Version: 1.166.2 +# Date: 2026-03-08 +# Status: reviewed + +library: + name: '@tanstack/router-plugin' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Bundler plugin for route generation and automatic code splitting. + Supports Vite, Webpack, Rspack, and esbuild via unplugin. + primary_framework: 'framework-agnostic' + +domains: + - name: 'Bundler Integration' + slug: 'bundler-integration' + description: >- + Route generation and code splitting via bundler plugins. + Vite, Webpack, Rspack, esbuild support through unplugin. + +skills: + - name: 'Router Plugin' + slug: 'router-plugin' + domain: 'bundler-integration' + description: >- + Bundler plugin for route generation and automatic code splitting. + Supports Vite, Webpack, Rspack, and esbuild via unplugin. + type: core + packages: + - '@tanstack/router-plugin' + covers: + - TanStackRouterVite plugin + - TanStackRouterWebpack plugin + - TanStackRouterRspack plugin + - TanStackRouterEsbuild plugin + - Automatic route code generation + - Automatic code splitting + - Route tree configuration options + tasks: + - 'Configure route generation in Vite' + - 'Configure route generation in Webpack/Rspack' + - 'Enable automatic code splitting' + failure_modes: + - mistake: 'Using wrong plugin export for bundler' + mechanism: >- + Each bundler has its own export (TanStackRouterVite, + TanStackRouterWebpack, etc). Using the wrong one causes + build failures. + priority: HIGH + status: active + + - mistake: 'Misconfiguring routesDirectory or generatedRouteTree' + mechanism: >- + routesDirectory must point to the actual routes folder and + generatedRouteTree to the desired output path. Wrong paths + cause missing routes or stale route trees. + priority: MEDIUM + status: active + +gaps: [] diff --git a/packages/router-plugin/skills/_artifacts/skill_spec.md b/packages/router-plugin/skills/_artifacts/skill_spec.md new file mode 100644 index 00000000000..4d4881232fe --- /dev/null +++ b/packages/router-plugin/skills/_artifacts/skill_spec.md @@ -0,0 +1,24 @@ +# @tanstack/router-plugin — Skill Spec + +Bundler plugin for route generation and automatic code splitting. Supports Vite, Webpack, Rspack, and esbuild via unplugin. + +## Domains + +| Domain | Description | Skills | +| ------------------- | ------------------------------------------------------- | ------------- | +| Bundler Integration | Route generation and code splitting via bundler plugins | router-plugin | + +## Skill Inventory + +| Skill | Type | Domain | What it covers | Failure modes | +| ------------- | ---- | ------------------- | -------------------------------------------------------------- | ------------- | +| router-plugin | core | bundler-integration | Vite/Webpack/Rspack/esbuild plugins, route gen, code splitting | 2 | + +## Failure Mode Inventory + +### router-plugin (2 failure modes) + +| # | Mistake | Priority | Source | +| --- | ---------------------------------------------------- | -------- | ----------- | +| 1 | Using wrong plugin export for bundler | HIGH | source/docs | +| 2 | Misconfiguring routesDirectory or generatedRouteTree | MEDIUM | source/docs | diff --git a/packages/router-plugin/skills/_artifacts/skill_tree.yaml b/packages/router-plugin/skills/_artifacts/skill_tree.yaml new file mode 100644 index 00000000000..eccd302d0f1 --- /dev/null +++ b/packages/router-plugin/skills/_artifacts/skill_tree.yaml @@ -0,0 +1,22 @@ +library: + name: '@tanstack/router-plugin' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Bundler plugin for route generation and automatic code splitting. + Supports Vite, Webpack, Rspack, and esbuild via unplugin. +generated_at: '2026-03-08' +skills: + - name: 'Router Plugin' + slug: 'router-plugin' + type: 'core' + domain: 'bundler-integration' + path: 'skills/router-plugin/SKILL.md' + package: 'packages/router-plugin' + description: >- + Bundler plugin for route generation and automatic code splitting. + Supports Vite, Webpack, Rspack, and esbuild via unplugin. + sources: + - 'TanStack/router:packages/router-plugin/src' + - 'TanStack/router:docs/router/routing/file-based-routing.md' + - 'TanStack/router:docs/router/guide/code-splitting.md' diff --git a/packages/router-plugin/skills/router-plugin/SKILL.md b/packages/router-plugin/skills/router-plugin/SKILL.md new file mode 100644 index 00000000000..ab3f9c67e08 --- /dev/null +++ b/packages/router-plugin/skills/router-plugin/SKILL.md @@ -0,0 +1,236 @@ +--- +name: router-plugin +description: >- + TanStack Router bundler plugin for route generation and automatic + code splitting. Supports Vite, Webpack, Rspack, and esbuild. + Configures autoCodeSplitting, routesDirectory, target framework, + and code split groupings. +type: core +library: tanstack-router +library_version: '1.166.2' +sources: + - TanStack/router:packages/router-plugin/src + - TanStack/router:docs/router/routing/file-based-routing.md + - TanStack/router:docs/router/guide/code-splitting.md +--- + +# Router Plugin (`@tanstack/router-plugin`) + +Bundler plugin that powers TanStack Router's file-based routing and automatic code splitting. Works with Vite, Webpack, Rspack, and esbuild via unplugin. + +> **CRITICAL**: The router plugin MUST come before the framework plugin (React, Solid, Vue) in the Vite config. Wrong order causes route generation and code splitting to fail silently. + +## Install + +```bash +npm install -D @tanstack/router-plugin +``` + +## Bundler Setup + +### Vite (most common) + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +export default defineConfig({ + plugins: [ + // MUST come before react() + tanstackRouter({ + target: 'react', + autoCodeSplitting: true, + }), + react(), + ], +}) +``` + +### Webpack + +```ts +// webpack.config.js +const { tanstackRouter } = require('@tanstack/router-plugin/webpack') + +module.exports = { + plugins: [ + tanstackRouter({ + target: 'react', + autoCodeSplitting: true, + }), + ], +} +``` + +### Rspack + +```ts +// rspack.config.js +const { tanstackRouter } = require('@tanstack/router-plugin/rspack') + +module.exports = { + plugins: [ + tanstackRouter({ + target: 'react', + autoCodeSplitting: true, + }), + ], +} +``` + +### esbuild + +```ts +import { tanstackRouter } from '@tanstack/router-plugin/esbuild' +import esbuild from 'esbuild' + +esbuild.build({ + plugins: [ + tanstackRouter({ + target: 'react', + autoCodeSplitting: true, + }), + ], +}) +``` + +## Configuration Options + +### Core Options + +| Option | Type | Default | Description | +| ----------------------- | ----------------------------- | -------------------------- | ------------------------------------------ | +| `target` | `'react' \| 'solid' \| 'vue'` | `'react'` | Target framework | +| `routesDirectory` | `string` | `'./src/routes'` | Directory containing route files | +| `generatedRouteTree` | `string` | `'./src/routeTree.gen.ts'` | Path for generated route tree | +| `autoCodeSplitting` | `boolean` | `undefined` | Enable automatic code splitting | +| `enableRouteGeneration` | `boolean` | `true` | Set to `false` to disable route generation | + +### File Convention Options + +| Option | Type | Default | Description | +| ------------------------ | ------------------ | ----------- | ------------------------------------ | +| `routeFilePrefix` | `string` | `undefined` | Prefix filter for route files | +| `routeFileIgnorePrefix` | `string` | `'-'` | Prefix to exclude files from routing | +| `routeFileIgnorePattern` | `string` | `undefined` | Pattern to exclude from routing | +| `indexToken` | `string \| RegExp` | `'index'` | Token identifying index routes | +| `routeToken` | `string \| RegExp` | `'route'` | Token identifying route config files | + +### Code Splitting Options + +```ts +tanstackRouter({ + target: 'react', + autoCodeSplitting: true, + codeSplittingOptions: { + // Default groupings for all routes + defaultBehavior: [['component'], ['errorComponent'], ['notFoundComponent']], + + // Per-route custom splitting + splitBehavior: ({ routeId }) => { + if (routeId === '/dashboard') { + // Keep loader and component together for dashboard + return [['loader', 'component'], ['errorComponent']] + } + // Return undefined to use defaultBehavior + }, + }, +}) +``` + +### Output Options + +| Option | Type | Default | Description | +| --------------------------- | ---------------------- | ----------- | -------------------------------------------- | +| `quoteStyle` | `'single' \| 'double'` | `'single'` | Quote style in generated code | +| `semicolons` | `boolean` | `false` | Use semicolons in generated code | +| `disableTypes` | `boolean` | `false` | Disable TypeScript types | +| `disableLogging` | `boolean` | `false` | Suppress plugin logs | +| `addExtensions` | `boolean \| string` | `false` | Add file extensions to imports | +| `enableRouteTreeFormatting` | `boolean` | `true` | Format generated route tree | +| `verboseFileRoutes` | `boolean` | `undefined` | When `false`, auto-imports `createFileRoute` | + +### Virtual Route Config + +```ts +import { routes } from './routes' + +tanstackRouter({ + target: 'react', + virtualRouteConfig: routes, // or './routes.ts' +}) +``` + +## How It Works + +The composed plugin assembles up to 4 sub-plugins: + +1. **Route Generator** (always) — Watches route files and generates `routeTree.gen.ts` +2. **Code Splitter** (when `autoCodeSplitting: true`) — Splits route files into lazy-loaded chunks using virtual modules +3. **Auto-Import** (when `verboseFileRoutes: false`) — Auto-injects `createFileRoute` imports +4. **HMR** (dev mode, when code splitter is off) — Hot-reloads route changes without full refresh + +## Individual Plugin Exports + +For advanced use, each sub-plugin is exported separately from the Vite entry: + +```ts +import { + tanstackRouter, // Composed (default) + tanstackRouterGenerator, // Generator only + tanStackRouterCodeSplitter, // Code splitter only + tanstackRouterAutoImport, // Auto-import only +} from '@tanstack/router-plugin/vite' +``` + +## Common Mistakes + +### 1. CRITICAL: Wrong plugin order in Vite config + +The router plugin must come before the framework plugin. Otherwise, route generation and code splitting fail silently. + +```ts +// WRONG — react() before tanstackRouter() +plugins: [react(), tanstackRouter({ target: 'react' })] + +// CORRECT — tanstackRouter() first +plugins: [tanstackRouter({ target: 'react' }), react()] +``` + +### 2. HIGH: Missing target option for non-React frameworks + +The `target` defaults to `'react'`. For Solid or Vue, you must set it explicitly. + +```ts +// WRONG for Solid — generates React imports +tanstackRouter({ autoCodeSplitting: true }) + +// CORRECT for Solid +tanstackRouter({ target: 'solid', autoCodeSplitting: true }) +``` + +### 3. MEDIUM: Confusing autoCodeSplitting with manual lazy routes + +When `autoCodeSplitting` is enabled, the plugin handles splitting automatically. You do NOT need manual `createLazyRoute` or `lazyRouteComponent` calls — the plugin transforms your route files at build time. + +```tsx +// WRONG — manual lazy loading with autoCodeSplitting enabled +const LazyAbout = lazyRouteComponent(() => import('./about')) + +// CORRECT — just write normal route files, plugin handles splitting +// src/routes/about.tsx +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return <h1>About</h1> +} +``` + +## Cross-References + +- [router-core/code-splitting](../../../router-core/skills/router-core/code-splitting/SKILL.md) — manual code splitting concepts +- [virtual-file-routes](../../../virtual-file-routes/skills/virtual-file-routes/SKILL.md) — programmatic route trees diff --git a/packages/solid-router/bin/intent.js b/packages/solid-router/bin/intent.js new file mode 100755 index 00000000000..2cf2efab472 --- /dev/null +++ b/packages/solid-router/bin/intent.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// Auto-generated by @tanstack/intent setup +// Exposes the intent end-user CLI for consumers of this library. +// Commit this file, then add to your package.json: +// "bin": { "intent": "./bin/intent.js" } +try { + await import('@tanstack/intent/intent-library') +} catch (e) { + if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + console.error('@tanstack/intent is not installed.') + console.error('') + console.error('Install it as a dev dependency:') + console.error(' npm add -D @tanstack/intent') + console.error('') + console.error('Or run directly:') + console.error(' npx @tanstack/intent@latest list') + process.exit(1) + } + throw e +} diff --git a/packages/solid-router/package.json b/packages/solid-router/package.json index 2c952faec57..9e5e5f80d26 100644 --- a/packages/solid-router/package.json +++ b/packages/solid-router/package.json @@ -95,7 +95,10 @@ "sideEffects": false, "files": [ "dist", - "src" + "src", + "skills", + "bin", + "!skills/_artifacts" ], "engines": { "node": ">=20.19" @@ -113,6 +116,7 @@ }, "devDependencies": { "@solidjs/testing-library": "^0.8.10", + "@tanstack/intent": "^0.0.14", "@testing-library/jest-dom": "^6.6.3", "combinate": "^1.1.11", "eslint-plugin-solid": "^0.14.5", @@ -123,5 +127,8 @@ }, "peerDependencies": { "solid-js": "^1.9.10" + }, + "bin": { + "intent": "./bin/intent.js" } } diff --git a/packages/solid-router/skills/_artifacts/domain_map.yaml b/packages/solid-router/skills/_artifacts/domain_map.yaml new file mode 100644 index 00000000000..5339b149e49 --- /dev/null +++ b/packages/solid-router/skills/_artifacts/domain_map.yaml @@ -0,0 +1,61 @@ +# domain_map.yaml +# Library: @tanstack/solid-router +# Version: 1.166.2 +# Date: 2026-03-08 +# Status: reviewed + +library: + name: '@tanstack/solid-router' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Solid bindings for TanStack Router. Accessor<T> returns, + Solid primitives, createLink, @solidjs/meta head management. + primary_framework: 'solid' + +domains: + - name: 'Solid Router Bindings' + slug: 'solid-router-bindings' + description: >- + Solid-specific components, hooks, and primitives for + TanStack Router. Accessor-based returns, createLink, + and @solidjs/meta integration. + +skills: + - name: 'Solid Router' + slug: 'solid-router' + domain: 'solid-router-bindings' + description: >- + Solid bindings for TanStack Router. Accessor<T> returns, + Solid primitives, createLink, @solidjs/meta head management. + type: framework + packages: + - '@tanstack/solid-router' + covers: + - Solid-specific router components + - Accessor<T> return types + - createLink component factory + - Solid primitives integration + - '@solidjs/meta head management' + tasks: + - 'Set up TanStack Router in a Solid app' + - 'Create type-safe links with createLink' + - 'Manage document head with @solidjs/meta' + failure_modes: + - mistake: 'Unwrapping accessors incorrectly' + mechanism: >- + Solid Router returns Accessor<T> instead of raw values. + Destructuring or reading outside reactive context loses + reactivity and causes stale data. + priority: HIGH + status: active + + - mistake: 'Using React Router APIs instead of Solid equivalents' + mechanism: >- + Solid Router uses createLink instead of Link component + patterns and Solid primitives instead of React hooks. + Mixing React patterns causes runtime errors. + priority: MEDIUM + status: active + +gaps: [] diff --git a/packages/solid-router/skills/_artifacts/skill_spec.md b/packages/solid-router/skills/_artifacts/skill_spec.md new file mode 100644 index 00000000000..f72f27c7389 --- /dev/null +++ b/packages/solid-router/skills/_artifacts/skill_spec.md @@ -0,0 +1,24 @@ +# @tanstack/solid-router — Skill Spec + +Solid bindings for TanStack Router. Accessor<T> returns, Solid primitives, createLink, @solidjs/meta head management. + +## Domains + +| Domain | Description | Skills | +| --------------------- | ---------------------------------------------------------- | ------------ | +| Solid Router Bindings | Solid components, hooks, Accessor returns, meta management | solid-router | + +## Skill Inventory + +| Skill | Type | Domain | What it covers | Failure modes | +| ------------ | --------- | --------------------- | ---------------------------------------------------------------- | ------------- | +| solid-router | framework | solid-router-bindings | Accessor<T> returns, createLink, Solid primitives, @solidjs/meta | 2 | + +## Failure Mode Inventory + +### solid-router (2 failure modes) + +| # | Mistake | Priority | Source | +| --- | ---------------------------------------------------- | -------- | ----------- | +| 1 | Unwrapping accessors incorrectly | HIGH | source/docs | +| 2 | Using React Router APIs instead of Solid equivalents | MEDIUM | source/docs | diff --git a/packages/solid-router/skills/_artifacts/skill_tree.yaml b/packages/solid-router/skills/_artifacts/skill_tree.yaml new file mode 100644 index 00000000000..560f64b3b95 --- /dev/null +++ b/packages/solid-router/skills/_artifacts/skill_tree.yaml @@ -0,0 +1,20 @@ +library: + name: '@tanstack/solid-router' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Solid bindings for TanStack Router. Accessor<T> returns, + Solid primitives, createLink, @solidjs/meta head management. +generated_at: '2026-03-08' +skills: + - name: 'Solid Router' + slug: 'solid-router' + type: 'framework' + domain: 'solid-router-bindings' + path: 'skills/solid-router/SKILL.md' + package: 'packages/solid-router' + description: >- + Solid bindings for TanStack Router. Accessor<T> returns, + Solid primitives, createLink, @solidjs/meta head management. + sources: + - 'TanStack/router:packages/solid-router/src' diff --git a/packages/solid-router/skills/solid-router/SKILL.md b/packages/solid-router/skills/solid-router/SKILL.md new file mode 100644 index 00000000000..ee27a60af3a --- /dev/null +++ b/packages/solid-router/skills/solid-router/SKILL.md @@ -0,0 +1,471 @@ +--- +name: solid-router +description: >- + Solid bindings for TanStack Router: RouterProvider, useRouter, + useRouterState, useMatch, useMatches, useLocation, useSearch, + useParams, useNavigate, useLoaderData, useLoaderDeps, + useRouteContext, useBlocker, useCanGoBack, Link, Navigate, + Outlet, CatchBoundary, ErrorComponent. Solid-specific patterns + with Accessor<T> returns, createSignal/createMemo/createEffect, + Show/Switch/Match/Dynamic, and @solidjs/meta for head management. +type: framework +library: tanstack-router +library_version: '1.166.2' +framework: solid +requires: + - router-core +sources: + - TanStack/router:packages/solid-router/src +--- + +# Solid Router (`@tanstack/solid-router`) + +This skill builds on router-core. Read [router-core](../../../router-core/skills/router-core/SKILL.md) first for foundational concepts. + +This skill covers the Solid-specific bindings, components, hooks, and setup for TanStack Router. + +> **CRITICAL**: TanStack Router types are FULLY INFERRED. Never cast, never annotate inferred values. + +> **CRITICAL**: TanStack Router is CLIENT-FIRST. Loaders run on the client by default, not on the server. + +> **CRITICAL**: Most hooks return `Accessor<T>` — you MUST call the accessor (`value()`) to read the reactive value. This is the #1 difference from the React version. + +> **CRITICAL**: Do not confuse `@tanstack/solid-router` with `@solidjs/router`. They are completely different libraries with different APIs. + +## Full Setup: File-Based Routing with Vite + +### 1. Install Dependencies + +```bash +npm install @tanstack/solid-router +npm install -D @tanstack/router-plugin @tanstack/solid-router-devtools +``` + +### 2. Configure Vite Plugin + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import solidPlugin from 'vite-plugin-solid' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +export default defineConfig({ + plugins: [ + // MUST come before solid plugin + tanstackRouter({ + target: 'solid', + autoCodeSplitting: true, + }), + solidPlugin(), + ], +}) +``` + +### 3. Create Root Route + +```tsx +// src/routes/__root.tsx +import { createRootRoute, Link, Outlet } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootLayout, +}) + +function RootLayout() { + return ( + <> + <nav> + <Link to="/" activeClass="font-bold"> + Home + </Link> + <Link to="/about" activeClass="font-bold"> + About + </Link> + </nav> + <hr /> + <Outlet /> + </> + ) +} +``` + +### 4. Create Route Files + +```tsx +// src/routes/index.tsx +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return <h1>Welcome Home</h1> +} +``` + +### 5. Create Router Instance and Register Types + +```tsx +// src/main.tsx +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +const router = createRouter({ routeTree }) + +// REQUIRED — without this, Link/useNavigate/useSearch have no type safety +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} + +render( + () => <RouterProvider router={router} />, + document.getElementById('root')!, +) +``` + +## Hooks Reference + +All hooks imported from `@tanstack/solid-router`. Most return `Accessor<T>` — call the result to read the value. + +### `useRouter()` — returns `TRouter` (NOT an Accessor) + +```tsx +import { useRouter } from '@tanstack/solid-router' + +function InvalidateButton() { + const router = useRouter() + return <button onClick={() => router.invalidate()}>Refresh data</button> +} +``` + +### `useRouterState()` — returns `Accessor<T>` + +```tsx +import { useRouterState } from '@tanstack/solid-router' + +function LoadingIndicator() { + const isLoading = useRouterState({ select: (s) => s.isLoading }) + return ( + <Show when={isLoading()}> + <div>Loading...</div> + </Show> + ) +} +``` + +### `useNavigate()` — returns a function (NOT an Accessor) + +```tsx +import { useNavigate } from '@tanstack/solid-router' + +function AfterSubmit() { + const navigate = useNavigate() + + const handleSubmit = async () => { + await saveData() + navigate({ to: '/posts/$postId', params: { postId: '123' } }) + } + + return <button onClick={handleSubmit}>Save</button> +} +``` + +### `useSearch({ from })` — returns `Accessor<T>` + +```tsx +import { useSearch } from '@tanstack/solid-router' + +function Pagination() { + const search = useSearch({ from: '/products' }) + return <span>Page {search().page}</span> +} +``` + +### `useParams({ from })` — returns `Accessor<T>` + +```tsx +import { useParams } from '@tanstack/solid-router' + +function PostHeader() { + const params = useParams({ from: '/posts/$postId' }) + return <h2>Post {params().postId}</h2> +} +``` + +### `useLoaderData({ from })` — returns `Accessor<T>` + +```tsx +import { useLoaderData } from '@tanstack/solid-router' + +function PostContent() { + const data = useLoaderData({ from: '/posts/$postId' }) + return <article>{data().post.content}</article> +} +``` + +### `useMatch({ from })` — returns `Accessor<T>` + +```tsx +import { useMatch } from '@tanstack/solid-router' + +function PostDetails() { + const match = useMatch({ from: '/posts/$postId' }) + return <div>{match().loaderData.post.title}</div> +} +``` + +### Other Hooks + +All imported from `@tanstack/solid-router`: + +- **`useMatches()`** — `Accessor<Array<Match>>`, all active route matches +- **`useRouteContext({ from })`** — `Accessor<T>`, context from `beforeLoad` +- **`useBlocker({ shouldBlockFn })`** — blocks navigation for unsaved changes +- **`useCanGoBack()`** — `Accessor<boolean>` +- **`useLocation()`** — `Accessor<ParsedLocation>` +- **`useLinkProps({ to, params?, search? })`** — returns `ComponentProps<'a'>` (NOT an Accessor) +- **`useMatchRoute()`** — returns a function; calling it returns `Accessor<false | Params>` +- **`useHydrated()`** — `Accessor<boolean>` + +## Components Reference + +### `RouterProvider` + +```tsx +<RouterProvider router={router} /> +``` + +### `Link` + +Type-safe navigation link. Children can be a function for active state: + +```tsx +;<Link to="/posts/$postId" params={{ postId: '42' }}> + View Post +</Link> + +{ + /* Function children for active state */ +} +;<Link to="/about"> + {(state) => <span classList={{ active: state.isActive }}>About</span>} +</Link> +``` + +### `Outlet` + +Renders the matched child route component: + +```tsx +function Layout() { + return ( + <div> + <Sidebar /> + <main> + <Outlet /> + </main> + </div> + ) +} +``` + +### `Navigate` + +Declarative redirect (triggers navigation in `onMount`): + +```tsx +import { Navigate } from '@tanstack/solid-router' + +function OldPage() { + return <Navigate to="/new-page" /> +} +``` + +### `Await` + +Renders deferred data with Solid's `Suspense`: + +```tsx +import { Await } from '@tanstack/solid-router' +import { Suspense } from 'solid-js' + +function PostWithComments() { + const data = Route.useLoaderData() + return ( + <div> + <h1>Post</h1> + <Suspense fallback={<div>Loading comments...</div>}> + <Await promise={data().deferredComments}> + {(comments) => ( + <ul> + <For each={comments}>{(c) => <li>{c.text}</li>}</For> + </ul> + )} + </Await> + </Suspense> + </div> + ) +} +``` + +### `CatchBoundary` + +Error boundary wrapping `Solid.ErrorBoundary`: + +```tsx +import { CatchBoundary } from '@tanstack/solid-router' + +;<CatchBoundary + getResetKey={() => 'widget'} + onCatch={(error) => console.error(error)} + errorComponent={({ error }) => <div>Error: {error.message}</div>} +> + <RiskyWidget /> +</CatchBoundary> +``` + +### `ClientOnly` + +Renders children only after hydration: + +```tsx +import { ClientOnly } from '@tanstack/solid-router' + +;<ClientOnly fallback={<div>Loading...</div>}> + <BrowserOnlyWidget /> +</ClientOnly> +``` + +### Head Management + +Uses `@solidjs/meta` under the hood: + +```tsx +import { HeadContent, Scripts } from '@tanstack/solid-router' + +function RootDocument(props) { + return ( + <html> + <head> + <HeadContent /> + </head> + <body> + {props.children} + <Scripts /> + </body> + </html> + ) +} +``` + +## Solid-Specific Patterns + +### Custom Link Component with `createLink` + +```tsx +import { createLink } from '@tanstack/solid-router' + +const StyledLinkComponent = (props) => ( + <a {...props} class={`styled-link ${props.class ?? ''}`} /> +) + +const StyledLink = createLink(StyledLinkComponent) + +function Nav() { + return ( + <StyledLink to="/posts/$postId" params={{ postId: '42' }}> + Post + </StyledLink> + ) +} +``` + +### Using Solid Primitives with Router State + +```tsx +import { createMemo, Show, For } from 'solid-js' +import { useRouterState } from '@tanstack/solid-router' + +function Breadcrumbs() { + const matches = useRouterState({ select: (s) => s.matches }) + const crumbs = createMemo(() => + matches().filter((m) => m.context?.breadcrumb), + ) + + return ( + <nav> + <For each={crumbs()}> + {(match) => <span>{match.context.breadcrumb}</span>} + </For> + </nav> + ) +} +``` + +### Auth with Router Context + +```tsx +import { createRootRouteWithContext } from '@tanstack/solid-router' + +const rootRoute = createRootRouteWithContext<{ auth: AuthState }>()({ + component: RootComponent, +}) + +// In main.tsx — provide context at router creation +const router = createRouter({ + routeTree, + context: { auth: authState }, +}) + +// In a route — access via beforeLoad (NOT hooks) +beforeLoad: ({ context }) => { + if (!context.auth.isAuthenticated) { + throw redirect({ to: '/login' }) + } +} +``` + +## Common Mistakes + +### 1. CRITICAL: Forgetting to call Accessor + +Hooks return `Accessor<T>` — you must call them to read the value. This is the #1 migration issue from React. + +```tsx +// WRONG — comparing the accessor function, not its value +const params = useParams({ from: '/posts/$postId' }) +if (params.postId === '42') { ... } // params is a function! + +// CORRECT — call the accessor +const params = useParams({ from: '/posts/$postId' }) +if (params().postId === '42') { ... } +``` + +### 2. HIGH: Destructuring reactive values + +Destructuring breaks Solid's reactivity tracking. + +```tsx +// WRONG — loses reactivity +const { page } = useSearch({ from: '/products' })() + +// CORRECT — access through accessor +const search = useSearch({ from: '/products' }) +<span>Page {search().page}</span> +``` + +### 3. HIGH: Using React hooks in beforeLoad or loader + +`beforeLoad` and `loader` are NOT components — they are plain async functions. No hooks (React or Solid) can be used in them. Pass state via router context instead. + +### 4. MEDIUM: Wrong plugin target + +Must set `target: 'solid'` in the router plugin config. Default is `'react'`. + +## Cross-References + +- [router-core/SKILL.md](../../../router-core/skills/router-core/SKILL.md) — all sub-skills for domain-specific patterns (search params, data loading, navigation, auth, SSR, etc.) diff --git a/packages/solid-start/bin/intent.js b/packages/solid-start/bin/intent.js new file mode 100755 index 00000000000..2cf2efab472 --- /dev/null +++ b/packages/solid-start/bin/intent.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// Auto-generated by @tanstack/intent setup +// Exposes the intent end-user CLI for consumers of this library. +// Commit this file, then add to your package.json: +// "bin": { "intent": "./bin/intent.js" } +try { + await import('@tanstack/intent/intent-library') +} catch (e) { + if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + console.error('@tanstack/intent is not installed.') + console.error('') + console.error('Install it as a dev dependency:') + console.error(' npm add -D @tanstack/intent') + console.error('') + console.error('Or run directly:') + console.error(' npx @tanstack/intent@latest list') + process.exit(1) + } + throw e +} diff --git a/packages/solid-start/package.json b/packages/solid-start/package.json index a38565c734f..33b88af1a3c 100644 --- a/packages/solid-start/package.json +++ b/packages/solid-start/package.json @@ -97,7 +97,10 @@ "sideEffects": false, "files": [ "dist", - "src" + "src", + "skills", + "bin", + "!skills/_artifacts" ], "engines": { "node": ">=22.12.0" @@ -112,11 +115,15 @@ "pathe": "^2.0.3" }, "devDependencies": { + "@tanstack/intent": "^0.0.14", "@tanstack/router-utils": "workspace:*", "vite": "*" }, "peerDependencies": { "solid-js": ">=1.0.0", "vite": ">=7.0.0" + }, + "bin": { + "intent": "./bin/intent.js" } } diff --git a/packages/solid-start/skills/_artifacts/domain_map.yaml b/packages/solid-start/skills/_artifacts/domain_map.yaml new file mode 100644 index 00000000000..5fa2f679ffe --- /dev/null +++ b/packages/solid-start/skills/_artifacts/domain_map.yaml @@ -0,0 +1,60 @@ +# domain_map.yaml +# Library: @tanstack/solid-start +# Version: 1.166.2 +# Date: 2026-03-08 +# Status: reviewed + +library: + name: '@tanstack/solid-start' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Solid bindings for TanStack Start. useServerFn hook, + tanstackStart Vite plugin, Solid-specific setup. + primary_framework: 'solid' + +domains: + - name: 'Solid Start Bindings' + slug: 'solid-start-bindings' + description: >- + Solid-specific full-stack framework bindings for TanStack + Start. Server function hooks, Vite plugin, and Solid + application setup. + +skills: + - name: 'Solid Start' + slug: 'solid-start' + domain: 'solid-start-bindings' + description: >- + Solid bindings for TanStack Start. useServerFn hook, + tanstackStart Vite plugin, Solid-specific setup. + type: framework + packages: + - '@tanstack/solid-start' + covers: + - useServerFn hook + - tanstackStart Vite plugin + - Solid-specific app setup + - Server function integration + tasks: + - 'Set up a Solid Start application' + - 'Use server functions with useServerFn' + - 'Configure tanstackStart Vite plugin' + failure_modes: + - mistake: 'Using react-start APIs in Solid context' + mechanism: >- + solid-start has its own useServerFn and tanstackStart + exports. Importing from @tanstack/react-start or + @tanstack/start causes type errors and runtime failures. + priority: HIGH + status: active + + - mistake: 'Missing tanstackStart Vite plugin' + mechanism: >- + The tanstackStart Vite plugin is required for server + function compilation. Without it, server functions are + not properly transformed and fail at runtime. + priority: MEDIUM + status: active + +gaps: [] diff --git a/packages/solid-start/skills/_artifacts/skill_spec.md b/packages/solid-start/skills/_artifacts/skill_spec.md new file mode 100644 index 00000000000..d137832f57a --- /dev/null +++ b/packages/solid-start/skills/_artifacts/skill_spec.md @@ -0,0 +1,24 @@ +# @tanstack/solid-start — Skill Spec + +Solid bindings for TanStack Start. useServerFn hook, tanstackStart Vite plugin, Solid-specific setup. + +## Domains + +| Domain | Description | Skills | +| -------------------- | -------------------------------------------------------- | ----------- | +| Solid Start Bindings | Solid full-stack bindings, server functions, Vite plugin | solid-start | + +## Skill Inventory + +| Skill | Type | Domain | What it covers | Failure modes | +| ----------- | --------- | -------------------- | --------------------------------------------------- | ------------- | +| solid-start | framework | solid-start-bindings | useServerFn, tanstackStart Vite plugin, Solid setup | 2 | + +## Failure Mode Inventory + +### solid-start (2 failure modes) + +| # | Mistake | Priority | Source | +| --- | --------------------------------------- | -------- | ----------- | +| 1 | Using react-start APIs in Solid context | HIGH | source/docs | +| 2 | Missing tanstackStart Vite plugin | MEDIUM | source/docs | diff --git a/packages/solid-start/skills/_artifacts/skill_tree.yaml b/packages/solid-start/skills/_artifacts/skill_tree.yaml new file mode 100644 index 00000000000..bed6d771e9d --- /dev/null +++ b/packages/solid-start/skills/_artifacts/skill_tree.yaml @@ -0,0 +1,21 @@ +library: + name: '@tanstack/solid-start' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Solid bindings for TanStack Start. useServerFn hook, + tanstackStart Vite plugin, Solid-specific setup. +generated_at: '2026-03-08' +skills: + - name: 'Solid Start' + slug: 'solid-start' + type: 'framework' + domain: 'solid-start-bindings' + path: 'skills/solid-start/SKILL.md' + package: 'packages/solid-start' + description: >- + Solid bindings for TanStack Start. useServerFn hook, + tanstackStart Vite plugin, Solid-specific setup. + sources: + - 'TanStack/router:packages/solid-start/src' + - 'TanStack/router:docs/start/framework/solid/build-from-scratch.md' diff --git a/packages/solid-start/skills/solid-start/SKILL.md b/packages/solid-start/skills/solid-start/SKILL.md new file mode 100644 index 00000000000..641bd5a811f --- /dev/null +++ b/packages/solid-start/skills/solid-start/SKILL.md @@ -0,0 +1,276 @@ +--- +name: solid-start +description: >- + Solid bindings for TanStack Start: useServerFn hook, tanstackStart + Vite plugin, StartClient, StartServer, Solid-specific setup, + re-exports from @tanstack/start-client-core. Full project setup + with Solid. +type: framework +library: tanstack-start +library_version: '1.166.2' +framework: solid +requires: + - start-core +sources: + - TanStack/router:packages/solid-start/src + - TanStack/router:docs/start/framework/solid/build-from-scratch.md +--- + +# Solid Start (`@tanstack/solid-start`) + +This skill builds on start-core. Read [start-core](../../../start-client-core/skills/start-core/SKILL.md) first for foundational concepts. + +This skill covers the Solid-specific bindings, setup, and patterns for TanStack Start. + +> **CRITICAL**: All code is ISOMORPHIC by default. Loaders run on BOTH server and client. Use `createServerFn` for server-only logic. + +> **CRITICAL**: Do not confuse `@tanstack/solid-start` with SolidStart (`@solidjs/start`). They are completely different frameworks with different APIs. + +> **CRITICAL**: Types are FULLY INFERRED. Never cast, never annotate inferred values. + +## Package API Surface + +`@tanstack/solid-start` re-exports everything from `@tanstack/start-client-core` plus: + +- `useServerFn` — Solid hook for calling server functions from components + +All core APIs (`createServerFn`, `createMiddleware`, `createStart`, `createIsomorphicFn`, `createServerOnlyFn`, `createClientOnlyFn`) are available from `@tanstack/solid-start`. + +Server utilities (`getRequest`, `getRequestHeader`, `setResponseHeader`, `setCookie`, `getCookie`, `useSession`) are imported from `@tanstack/solid-start/server`. + +## Full Project Setup + +### 1. Install Dependencies + +```bash +npm i @tanstack/solid-start @tanstack/solid-router solid-js +npm i -D vite vite-plugin-solid typescript +``` + +### 2. package.json + +```json +{ + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "start": "node .output/server/index.mjs" + } +} +``` + +### 3. tsconfig.json + +```json +{ + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ES2022", + "skipLibCheck": true, + "strictNullChecks": true + } +} +``` + +### 4. vite.config.ts + +```ts +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solidPlugin from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [ + tanstackStart(), // MUST come before solid plugin + solidPlugin(), + ], +}) +``` + +### 5. Router Factory (src/router.tsx) + +```tsx +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + const router = createRouter({ + routeTree, + scrollRestoration: true, + }) + return router +} +``` + +### 6. Root Route (src/routes/\_\_root.tsx) + +```tsx +import { + Outlet, + createRootRoute, + HeadContent, + Scripts, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: 'My TanStack Start App' }, + ], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + <html> + <head> + <HeadContent /> + </head> + <body> + <Outlet /> + <Scripts /> + </body> + </html> + ) +} +``` + +### 7. Index Route (src/routes/index.tsx) + +```tsx +import { createFileRoute } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' + +const getGreeting = createServerFn({ method: 'GET' }).handler(async () => { + return 'Hello from TanStack Start!' +}) + +export const Route = createFileRoute('/')({ + loader: () => getGreeting(), + component: HomePage, +}) + +function HomePage() { + const greeting = Route.useLoaderData() + return <h1>{greeting()}</h1> +} +``` + +## useServerFn Hook + +Use `useServerFn` to call server functions from Solid components with automatic redirect handling: + +```tsx +import { createServerFn, useServerFn } from '@tanstack/solid-start' +import { createSignal } from 'solid-js' + +const updatePost = createServerFn({ method: 'POST' }) + .inputValidator((data: { id: string; title: string }) => data) + .handler(async ({ data }) => { + await db.posts.update(data.id, { title: data.title }) + return { success: true } + }) + +function EditPostForm(props: { postId: string }) { + const updatePostFn = useServerFn(updatePost) + const [title, setTitle] = createSignal('') + + return ( + <form + onSubmit={async (e) => { + e.preventDefault() + await updatePostFn({ data: { id: props.postId, title: title() } }) + }} + > + <input value={title()} onInput={(e) => setTitle(e.target.value)} /> + <button type="submit">Save</button> + </form> + ) +} +``` + +Unlike the React version, `useServerFn` does NOT wrap the returned function in any memoization (no `useCallback` equivalent needed — Solid's `setup` runs once). + +## Solid-Specific Components + +All routing components from `@tanstack/solid-router` work in Start: + +- `<Outlet>` — renders matched child route +- `<Link>` — type-safe navigation +- `<Navigate>` — declarative redirect +- `<HeadContent>` — renders head tags via `@solidjs/meta` (must be in `<head>`) +- `<Scripts>` — renders body scripts (must be in `<body>`) +- `<Await>` — renders deferred data with `<Suspense>` +- `<ClientOnly>` — renders children only after hydration +- `<CatchBoundary>` — error boundary wrapping `Solid.ErrorBoundary` + +## Hooks Reference + +All hooks from `@tanstack/solid-router` work in Start. Most return `Accessor<T>` — call the accessor to read: + +- `useRouter()` — router instance (NOT an Accessor) +- `useRouterState()` — `Accessor<T>`, subscribe to router state +- `useNavigate()` — navigation function (NOT an Accessor) +- `useSearch({ from })` — `Accessor<T>`, validated search params +- `useParams({ from })` — `Accessor<T>`, path params +- `useLoaderData({ from })` — `Accessor<T>`, loader data +- `useMatch({ from })` — `Accessor<T>`, full route match +- `useRouteContext({ from })` — `Accessor<T>`, route context +- `Route.useLoaderData()` — `Accessor<T>`, typed loader data (preferred in route files) +- `Route.useSearch()` — `Accessor<T>`, typed search params (preferred in route files) + +## Common Mistakes + +### 1. CRITICAL: Importing from wrong package + +```tsx +// WRONG — this is the SPA router, NOT Start +import { createServerFn } from '@tanstack/solid-router' + +// CORRECT — server functions come from solid-start +import { createServerFn } from '@tanstack/solid-start' + +// CORRECT — routing APIs come from solid-router (re-exported by Start too) +import { createFileRoute, Link } from '@tanstack/solid-router' +``` + +### 2. CRITICAL: Forgetting to call Accessor + +Most hooks return `Accessor<T>`. Must call to read the value. + +```tsx +// WRONG +const data = Route.useLoaderData() +return <h1>{data.message}</h1> + +// CORRECT +const data = Route.useLoaderData() +return <h1>{data().message}</h1> +``` + +### 3. HIGH: Missing Scripts component + +Without `<Scripts />` in the root route's `<body>`, client JavaScript doesn't load and the app won't hydrate. + +### 4. HIGH: Solid plugin before Start plugin in Vite config + +```ts +// WRONG +plugins: [solidPlugin(), tanstackStart()] + +// CORRECT +plugins: [tanstackStart(), solidPlugin()] +``` + +## Cross-References + +- [start-core](../../../start-client-core/skills/start-core/SKILL.md) — core Start concepts +- [router-core](../../../router-core/skills/router-core/SKILL.md) — routing fundamentals +- [solid-router](../../../solid-router/skills/solid-router/SKILL.md) — Solid Router hooks and components diff --git a/packages/start-client-core/bin/intent.js b/packages/start-client-core/bin/intent.js new file mode 100755 index 00000000000..2cf2efab472 --- /dev/null +++ b/packages/start-client-core/bin/intent.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// Auto-generated by @tanstack/intent setup +// Exposes the intent end-user CLI for consumers of this library. +// Commit this file, then add to your package.json: +// "bin": { "intent": "./bin/intent.js" } +try { + await import('@tanstack/intent/intent-library') +} catch (e) { + if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + console.error('@tanstack/intent is not installed.') + console.error('') + console.error('Install it as a dev dependency:') + console.error(' npm add -D @tanstack/intent') + console.error('') + console.error('Or run directly:') + console.error(' npx @tanstack/intent@latest list') + process.exit(1) + } + throw e +} diff --git a/packages/start-client-core/package.json b/packages/start-client-core/package.json index 6ecc1f45db0..983c9b23641 100644 --- a/packages/start-client-core/package.json +++ b/packages/start-client-core/package.json @@ -73,7 +73,10 @@ "sideEffects": false, "files": [ "dist", - "src" + "src", + "skills", + "bin", + "!skills/_artifacts" ], "engines": { "node": ">=22.12.0" @@ -87,6 +90,10 @@ "tiny-warning": "^1.0.3" }, "devDependencies": { + "@tanstack/intent": "^0.0.14", "vite": "*" + }, + "bin": { + "intent": "./bin/intent.js" } } diff --git a/packages/start-client-core/skills/_artifacts/domain_map.yaml b/packages/start-client-core/skills/_artifacts/domain_map.yaml new file mode 100644 index 00000000000..b5e84d4dfbc --- /dev/null +++ b/packages/start-client-core/skills/_artifacts/domain_map.yaml @@ -0,0 +1,482 @@ +# domain_map.yaml +# Generated by skill-domain-discovery +# Library: TanStack Start +# Version: 1.166.2 +# Date: 2026-03-07 +# Status: reviewed + +library: + name: '@tanstack/react-start' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Full-stack React framework built on TanStack Router and Vite. Adds + SSR, streaming, server functions (type-safe RPCs), middleware, + server routes, and universal deployment. Isomorphic by default — + all code runs in both environments unless explicitly constrained. + primary_framework: 'React' + +domains: + - name: 'Project Setup' + slug: 'project-setup' + description: >- + Scaffolding a Start project, configuring Vite plugin, router + setup with getRouter(), root route with document shell, client + and server entry points. + + - name: 'Server Functions' + slug: 'server-functions' + description: >- + Creating type-safe RPCs with createServerFn, input validation, + calling from loaders/components/other server functions, error + handling, streaming responses. + + - name: 'Middleware and Context' + slug: 'middleware-and-context' + description: >- + Request middleware, server function middleware, context passing + with sendContext, global middleware via createStart, middleware + factories, fetch override precedence. + + - name: 'Execution Model' + slug: 'execution-model' + description: >- + Isomorphic code execution, environment functions + (createServerFn, createServerOnlyFn, createClientOnlyFn, + createIsomorphicFn), import protection, dead code elimination, + environment variable safety. + + - name: 'Server Routes' + slug: 'server-routes' + description: >- + Server-side API endpoints defined in routes, HTTP method + handlers, handler middleware, request/response patterns. + + - name: 'Deployment and Rendering' + slug: 'deployment-and-rendering' + description: >- + Hosting providers (Cloudflare, Netlify, Vercel, Node/Docker), + selective SSR, SPA mode, static prerendering, ISR with + Cache-Control headers, SEO and head management. + +skills: + # ── Project Setup ──────────────────────────────────────────────── + - name: 'Start Setup' + slug: 'start-setup' + domain: 'project-setup' + description: >- + Scaffold a TanStack Start project, configure Vite plugin with + tanstackStart(), set up router with getRouter(), create root + route with document shell (HeadContent, Scripts, Outlet), + configure client and server entry points. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-plugin-core' + covers: + - tanstackStart Vite plugin + - getRouter() factory pattern + - Root route document shell + - HeadContent / Scripts / Outlet + - Client entry point (optional) + - Server entry point (optional) + - routeTree.gen.ts + - tsconfig configuration + tasks: + - 'Scaffold a new TanStack Start project' + - 'Configure the Vite plugin' + - 'Set up the router factory' + - 'Customize client/server entry points' + failure_modes: + - mistake: 'React plugin before Start plugin in Vite config' + mechanism: >- + Start's Vite plugin must come before React's plugin. + Wrong order causes route generation and server function + compilation to fail. + wrong_pattern: | + plugins: [react(), tanstackStart()] + correct_pattern: | + plugins: [tanstackStart(), react()] + source: 'docs/start/framework/react/build-from-scratch.md' + priority: CRITICAL + status: active + + - mistake: 'Enabling verbatimModuleSyntax in tsconfig' + mechanism: >- + verbatimModuleSyntax causes server bundles to leak into + client bundles. Must be disabled. + source: 'docs/start/framework/react/build-from-scratch.md' + priority: HIGH + status: active + + - mistake: 'Missing Scripts component in root route' + mechanism: >- + The Scripts component must be rendered in the body of + the root route for proper hydration and functionality. + Without it, client-side JavaScript does not load. + source: 'docs/start/framework/react/guide/routing.md' + priority: HIGH + status: active + + # ── Server Functions ───────────────────────────────────────────── + - name: 'Server Functions' + slug: 'server-functions' + domain: 'server-functions' + description: >- + Create type-safe RPCs with createServerFn, validate inputs + with Zod or plain functions, call from loaders/components/ + event handlers, handle errors/redirects/notFound, stream + responses with ReadableStream or async generators. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-client-core' + - '@tanstack/start-server-core' + covers: + - createServerFn (GET/POST) + - inputValidator (Zod or function) + - useServerFn hook + - Server context utilities (getRequest, getRequestHeader, setResponseHeader, setResponseStatus) + - Error handling (throw errors, redirect, notFound) + - Streaming (ReadableStream, async generators) + - File organization (.functions.ts, .server.ts) + - FormData handling + tasks: + - 'Create a server function for data fetching' + - 'Validate server function inputs' + - 'Call server functions from components' + - 'Stream data from server functions' + - 'Handle errors in server functions' + failure_modes: + - mistake: 'Putting server-only code in loaders instead of server functions' + mechanism: >- + Loaders are ISOMORPHIC — they run on both client and server. + Database queries, file system access, and secret API keys + in loaders will either fail on the client or leak to the + client bundle. Use createServerFn for server-only logic. + wrong_pattern: | + export const Route = createFileRoute('/posts')({ + loader: async () => { + const posts = await db.query('SELECT * FROM posts') + return { posts } + }, + }) + correct_pattern: | + const getPosts = createServerFn({ method: 'GET' }) + .handler(async () => { + const posts = await db.query('SELECT * FROM posts') + return { posts } + }) + + export const Route = createFileRoute('/posts')({ + loader: () => getPosts(), + }) + source: 'maintainer interview' + priority: CRITICAL + status: active + skills: ['server-functions', 'execution-model'] + + - mistake: 'Using dynamic imports for server functions' + mechanism: >- + Dynamic imports of server functions can cause bundler + issues. Static imports are safe — the build process + replaces server implementations with RPC stubs. + wrong_pattern: | + const { getUser } = await import('~/utils/users.functions') + correct_pattern: | + import { getUser } from '~/utils/users.functions' + source: 'docs/start/framework/react/guide/server-functions.md' + priority: HIGH + status: active + + - mistake: 'Generating Next.js or Remix server patterns' + mechanism: >- + Agents generate getServerSideProps, "use server" directives, + or Remix-style loader exports. TanStack Start uses + createServerFn for server-only code. + wrong_pattern: | + 'use server' + export async function getUser() { ... } + correct_pattern: | + const getUser = createServerFn({ method: 'GET' }) + .handler(async () => { ... }) + source: 'maintainer interview' + priority: CRITICAL + status: active + + # ── Middleware and Context ─────────────────────────────────────── + - name: 'Middleware' + slug: 'middleware' + domain: 'middleware-and-context' + description: >- + Request middleware and server function middleware with + createMiddleware, context passing via next(), sendContext + for client-server transfer, global middleware via createStart + in src/start.ts, middleware factories, fetch override + precedence, header merging. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-client-core' + - '@tanstack/start-server-core' + covers: + - createMiddleware + - Request middleware (.server only) + - Server function middleware (.client + .server) + - Context passing via next({ context }) + - sendContext for client-server transfer + - Global middleware (createStart in src/start.ts) + - Middleware factories + - Fetch override precedence + - Header merging + - Method order enforcement (middleware → inputValidator → client → server) + tasks: + - 'Add authentication middleware' + - 'Pass context through middleware chain' + - 'Configure global request middleware' + - 'Create reusable middleware factories' + failure_modes: + - mistake: 'Trusting client context without server validation' + mechanism: >- + Client context via sendContext is NOT validated by default. + Dynamic user-generated data must be validated in server-side + middleware before use. + source: 'docs/start/framework/react/guide/middleware.md' + priority: HIGH + status: active + + - mistake: 'Wrong middleware method order' + mechanism: >- + TypeScript enforces method order: middleware → inputValidator + → client → server. Wrong order causes type errors and + runtime failures. + source: 'docs/start/framework/react/guide/middleware.md' + priority: MEDIUM + status: active + + - mistake: 'Confusing request vs server function middleware' + mechanism: >- + Request middleware runs on ALL server requests (SSR, server + routes, server functions). Server function middleware runs + only for server functions and has .client() method. Using + the wrong type causes unexpected scope. + source: 'docs/start/framework/react/guide/middleware.md' + priority: MEDIUM + status: active + + # ── Execution Model ────────────────────────────────────────────── + - name: 'Execution Model' + slug: 'execution-model' + domain: 'execution-model' + description: >- + Isomorphic code execution model, environment boundary functions + (createServerFn, createServerOnlyFn, createClientOnlyFn, + createIsomorphicFn), import protection, dead code elimination, + environment variable safety (VITE_ prefix), useHydrated hook. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-client-core' + - '@tanstack/start-server-core' + covers: + - Isomorphic-by-default principle + - createServerFn (RPC boundary) + - createServerOnlyFn (throws on client) + - createClientOnlyFn (throws on server) + - createIsomorphicFn (different impl per env) + - ClientOnly component + - useHydrated hook + - Import protection (experimental) + - Environment variables (VITE_ prefix, process.env) + - Dead code elimination / tree shaking + tasks: + - 'Protect server-only code from client bundles' + - 'Use environment-specific implementations' + - 'Handle environment variables safely' + - 'Debug import protection violations' + failure_modes: + - mistake: 'Assuming loaders are server-only' + mechanism: >- + ALL code in TanStack Start is isomorphic by default. + Loaders run on BOTH server and client. Server-only + operations must use createServerFn, createServerOnlyFn, + or server routes. + wrong_pattern: | + export const Route = createFileRoute('/dashboard')({ + loader: async () => { + const secret = process.env.API_SECRET + return fetch(`https://api.example.com/data`, { + headers: { Authorization: secret } + }) + }, + }) + correct_pattern: | + const getData = createServerFn({ method: 'GET' }) + .handler(async () => { + const secret = process.env.API_SECRET + return fetch(`https://api.example.com/data`, { + headers: { Authorization: secret } + }) + }) + + export const Route = createFileRoute('/dashboard')({ + loader: () => getData(), + }) + source: 'docs/start/framework/react/guide/execution-model.md' + priority: CRITICAL + status: active + skills: ['execution-model', 'server-functions'] + + - mistake: 'Exposing secrets via module-level process.env' + mechanism: >- + Module-level process.env access runs in both environments. + The variable value leaks into the client bundle. Access + secrets only inside createServerFn or createServerOnlyFn. + wrong_pattern: | + const apiKey = process.env.SECRET_KEY + export function fetchData() { ... } + correct_pattern: | + const fetchData = createServerFn({ method: 'GET' }) + .handler(async () => { + const apiKey = process.env.SECRET_KEY + return fetch(url, { headers: { Authorization: apiKey } }) + }) + source: 'docs/start/framework/react/guide/execution-model.md' + priority: CRITICAL + status: active + + - mistake: 'Using VITE_ prefix for server secrets' + mechanism: >- + VITE_ prefixed variables are exposed to the client bundle. + Server secrets must NOT have the VITE_ prefix. Access + them via process.env inside server functions only. + source: 'docs/start/framework/react/guide/environment-variables.md' + priority: CRITICAL + status: active + + # ── Server Routes ──────────────────────────────────────────────── + - name: 'Server Routes' + slug: 'server-routes' + domain: 'server-routes' + description: >- + Define server-side API endpoints alongside app routes using + the server property with HTTP method handlers, per-handler + middleware via createHandlers, request/response patterns. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-server-core' + covers: + - server property on createFileRoute + - handlers object (GET, POST, PUT, DELETE) + - createHandlers for per-handler middleware + - Handler context (request, params, context) + - Request body parsing (json, text, formData) + - Response helpers (Response.json) + - File naming for API routes + tasks: + - 'Create a REST API endpoint' + - 'Add middleware to server route handlers' + - 'Handle different HTTP methods' + failure_modes: + - mistake: 'Duplicate path resolution for server routes' + mechanism: >- + Each route can only have a single handler file. Having + both users.ts and users/index.ts causes errors. + source: 'docs/start/framework/react/guide/server-routes.md' + priority: MEDIUM + status: active + + # ── Deployment and Rendering ───────────────────────────────────── + - name: 'Deployment' + slug: 'deployment' + domain: 'deployment-and-rendering' + description: >- + Deploy TanStack Start to Cloudflare Workers, Netlify, Vercel, + Node.js/Docker, Bun, Railway. Configure selective SSR, SPA + mode, static prerendering, ISR with Cache-Control headers. + type: core + packages: + - '@tanstack/react-start' + - '@tanstack/start-plugin-core' + covers: + - Cloudflare Workers deployment + - Netlify deployment + - Vercel / Railway deployment + - Node.js / Docker deployment + - Bun deployment + - Selective SSR (ssr option per route) + - SPA mode configuration + - Static prerendering (prerender option) + - ISR with Cache-Control headers + - SEO (head property, structured data) + tasks: + - 'Deploy to Cloudflare Workers' + - 'Deploy to Netlify' + - 'Configure selective SSR per route' + - 'Enable SPA mode' + - 'Set up static prerendering' + - 'Configure ISR with cache headers' + failure_modes: + - mistake: 'Bun deployment with React 18' + mechanism: >- + Bun-specific deployment only works with React 19. + For React 18, use Node.js deployment guidelines. + source: 'docs/start/framework/react/guide/hosting.md' + priority: MEDIUM + status: active + + - mistake: 'Missing nodejs_compat flag for Cloudflare Workers' + mechanism: >- + Cloudflare Workers requires compatibility_flags: + ["nodejs_compat"] in wrangler config. Without it, + Node.js APIs used by Start fail at runtime. + source: 'docs/start/framework/react/guide/hosting.md' + priority: HIGH + status: active + + - mistake: 'Child route loosening parent SSR config' + mechanism: >- + SSR config inherits from parent and can only become MORE + restrictive (true → data-only → false). A child cannot + set ssr: true if parent has ssr: false. + source: 'docs/start/framework/react/guide/selective-ssr.md' + priority: MEDIUM + status: active + +tensions: + - name: 'Isomorphic defaults vs server-only expectations' + skills: ['execution-model', 'server-functions'] + description: >- + All code runs everywhere by default. Agents trained on + Next.js/Remix assume loaders and route code are server-only. + implication: >- + Agents put secrets, DB queries, and file system access in + loaders instead of server functions, causing client-side + failures or security leaks. + + - name: 'Simplicity of isomorphic code vs security boundaries' + skills: ['execution-model', 'middleware'] + description: >- + The isomorphic model makes code easy to write but requires + explicit boundaries for security. Agents don't realize they + need to actively constrain execution environment. + implication: >- + Agents expose secrets via module-level process.env or + forget to validate sendContext data in middleware. + +cross_references: + - from: 'server-functions' + to: 'execution-model' + reason: 'Server functions ARE the isomorphic boundary — understanding the execution model is prerequisite' + - from: 'server-functions' + to: 'middleware' + reason: 'Server function middleware chains compose with server functions' + - from: 'middleware' + to: 'server-routes' + reason: 'Server routes use the same middleware system' + - from: 'deployment' + to: 'execution-model' + reason: 'Deployment target affects which environment code runs in' + +gaps: [] diff --git a/packages/start-client-core/skills/_artifacts/skill_spec.md b/packages/start-client-core/skills/_artifacts/skill_spec.md new file mode 100644 index 00000000000..11b6843bb01 --- /dev/null +++ b/packages/start-client-core/skills/_artifacts/skill_spec.md @@ -0,0 +1,94 @@ +# TanStack Start — Skill Spec + +TanStack Start is a full-stack React framework built on TanStack Router and Vite. It adds SSR, streaming, server functions (type-safe RPCs), middleware, server routes, and universal deployment. All code is isomorphic by default — it runs in both server and client environments unless explicitly constrained. + +## Domains + +| Domain | Description | Skills | +| ------------------------ | ------------------------------------------------------------- | ---------------- | +| Project Setup | Scaffolding, Vite plugin, router factory, root route, entries | start-setup | +| Server Functions | Type-safe RPCs with createServerFn, validation, streaming | server-functions | +| Middleware and Context | Request/function middleware, context, global middleware | middleware | +| Execution Model | Isomorphic defaults, environment boundaries, env vars | execution-model | +| Server Routes | API endpoints, HTTP handlers, handler middleware | server-routes | +| Deployment and Rendering | Hosting, selective SSR, prerendering, SEO | deployment | + +## Skill Inventory + +| Skill | Type | Domain | What it covers | Failure modes | +| ------------------- | --------- | ------------------------ | ------------------------------------------------------- | ------------- | +| start-setup | core | project-setup | tanstackStart(), getRouter(), root route, entries | 3 | +| server-functions | core | server-functions | createServerFn, validation, useServerFn, streaming | 4 | +| middleware | core | middleware-and-context | createMiddleware, context, global middleware, factories | 3 | +| execution-model | core | execution-model | Isomorphic defaults, environment functions, env vars | 4 | +| server-routes | core | server-routes | server property, HTTP handlers, createHandlers | 2 | +| deployment | core | deployment-and-rendering | Hosting, SSR modes, prerendering, SEO | 3 | +| react-start | framework | project-setup | React bindings, useServerFn, full setup | 3 | +| migrate-from-nextjs | lifecycle | project-setup | Next.js App Router migration checklist | 3 | + +## Failure Mode Inventory + +### start-setup (3 failure modes) + +| # | Mistake | Priority | Source | +| --- | ----------------------------------------------- | -------- | ----------------------- | +| 1 | React plugin before Start plugin in Vite config | CRITICAL | docs/build-from-scratch | +| 2 | Enabling verbatimModuleSyntax in tsconfig | HIGH | docs/build-from-scratch | +| 3 | Missing Scripts component in root route | HIGH | docs/guide/routing | + +### server-functions (4 failure modes) + +| # | Mistake | Priority | Source | +| --- | --------------------------------------------------------------------------- | -------- | --------------------------- | +| 1 | Putting server-only code in loaders instead of server functions | CRITICAL | maintainer interview | +| 2 | Generating Next.js/Remix server patterns ("use server", getServerSideProps) | CRITICAL | maintainer interview | +| 3 | Using dynamic imports for server functions | HIGH | docs/guide/server-functions | +| 4 | Not using useServerFn for component calls | MEDIUM | docs/guide/server-functions | + +### middleware (3 failure modes) + +| # | Mistake | Priority | Source | +| --- | ----------------------------------------------------- | -------- | --------------------- | +| 1 | Trusting client sendContext without server validation | HIGH | docs/guide/middleware | +| 2 | Confusing request vs server function middleware | MEDIUM | docs/guide/middleware | +| 3 | Wrong middleware method order | MEDIUM | docs/guide/middleware | + +### execution-model (4 failure modes) + +| # | Mistake | Priority | Source | +| --- | ------------------------------------------------- | -------- | -------------------------------- | +| 1 | Assuming loaders are server-only | CRITICAL | docs/guide/execution-model | +| 2 | Exposing secrets via module-level process.env | CRITICAL | docs/guide/execution-model | +| 3 | Using VITE\_ prefix for server secrets | CRITICAL | docs/guide/environment-variables | +| 4 | Hydration mismatches from env-dependent rendering | HIGH | docs/guide/execution-model | + +### server-routes (2 failure modes) + +| # | Mistake | Priority | Source | +| --- | ---------------------------------------- | -------- | ------------------------ | +| 1 | Duplicate route path resolution | MEDIUM | docs/guide/server-routes | +| 2 | Forgetting to await request body methods | MEDIUM | docs/guide/server-routes | + +### deployment (3 failure modes) + +| # | Mistake | Priority | Source | +| --- | ------------------------------------------------- | -------- | ------------------------ | +| 1 | Missing nodejs_compat flag for Cloudflare Workers | HIGH | docs/guide/hosting | +| 2 | Bun deployment with React 18 | MEDIUM | docs/guide/hosting | +| 3 | Child route loosening parent SSR config | MEDIUM | docs/guide/selective-ssr | + +## Tensions + +| Tension | Skills | Agent implication | +| ---------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------- | +| Isomorphic defaults vs server-only expectations | execution-model ↔ server-functions | Agents put secrets/DB queries in loaders instead of server functions | +| Simplicity of isomorphic code vs security boundaries | execution-model ↔ middleware | Agents expose secrets via module-level process.env or skip context validation | + +## Cross-References + +| From | To | Reason | +| ---------------- | --------------- | ----------------------------------------------- | +| server-functions | execution-model | Server functions ARE the isomorphic boundary | +| server-functions | middleware | Middleware chains compose with server functions | +| middleware | server-routes | Server routes use the same middleware system | +| deployment | execution-model | Deployment target affects where code runs | diff --git a/packages/start-client-core/skills/_artifacts/skill_tree.yaml b/packages/start-client-core/skills/_artifacts/skill_tree.yaml new file mode 100644 index 00000000000..cf743b067ca --- /dev/null +++ b/packages/start-client-core/skills/_artifacts/skill_tree.yaml @@ -0,0 +1,158 @@ +# skills/_artifacts/start_skill_tree.yaml +library: + name: '@tanstack/react-start' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Full-stack React framework built on TanStack Router and Vite. + Adds SSR, streaming, server functions (type-safe RPCs), middleware, + server routes, and universal deployment. Isomorphic by default — + all code runs in both environments unless explicitly constrained. +generated_from: + domain_map: '_artifacts/start_domain_map.yaml' +generated_at: '2026-03-07' + +skills: + # ── Start Core Skills ─────────────────────────────────────────── + - name: 'Start Core' + slug: 'start-core' + type: 'core' + domain: 'project-setup' + path: 'skills/start-core/SKILL.md' + package: 'packages/start-client-core' + description: >- + Core overview for TanStack Start: tanstackStart() Vite plugin, + getRouter() factory, root route document shell (HeadContent, + Scripts, Outlet), client/server entry points, routeTree.gen.ts, + tsconfig configuration. Entry point for all Start skills. + sources: + - 'TanStack/router:docs/start/framework/react/build-from-scratch.md' + - 'TanStack/router:docs/start/framework/react/quick-start.md' + - 'TanStack/router:docs/start/framework/react/guide/routing.md' + + - name: 'Server Functions' + slug: 'start-core/server-functions' + type: 'sub-skill' + domain: 'server-functions' + path: 'skills/start-core/server-functions/SKILL.md' + package: 'packages/start-client-core' + description: >- + createServerFn (GET/POST), inputValidator (Zod or function), + useServerFn hook, server context utilities (getRequest, + getRequestHeader, setResponseHeader, setResponseStatus), error + handling (throw errors, redirect, notFound), streaming + (ReadableStream, async generators), FormData handling, file + organization (.functions.ts, .server.ts). + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/server-functions.md' + + - name: 'Middleware' + slug: 'start-core/middleware' + type: 'sub-skill' + domain: 'middleware-and-context' + path: 'skills/start-core/middleware/SKILL.md' + package: 'packages/start-client-core' + description: >- + createMiddleware, request middleware (.server only), server + function middleware (.client + .server), context passing via + next({ context }), sendContext for client-server transfer, + global middleware via createStart in src/start.ts, middleware + factories, method order enforcement. + requires: + - 'start-core' + - 'start-core/server-functions' + sources: + - 'TanStack/router:docs/start/framework/react/guide/middleware.md' + + - name: 'Execution Model' + slug: 'start-core/execution-model' + type: 'sub-skill' + domain: 'execution-model' + path: 'skills/start-core/execution-model/SKILL.md' + package: 'packages/start-client-core' + description: >- + Isomorphic-by-default principle, environment boundary functions + (createServerFn, createServerOnlyFn, createClientOnlyFn, + createIsomorphicFn), ClientOnly component, useHydrated hook, + import protection, dead code elimination, environment variable + safety (VITE_ prefix, process.env). + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/execution-model.md' + - 'TanStack/router:docs/start/framework/react/guide/environment-variables.md' + + - name: 'Server Routes' + slug: 'start-core/server-routes' + type: 'sub-skill' + domain: 'server-routes' + path: 'skills/start-core/server-routes/SKILL.md' + package: 'packages/start-client-core' + description: >- + Server-side API endpoints using the server property on + createFileRoute, HTTP method handlers (GET, POST, PUT, DELETE), + createHandlers for per-handler middleware, handler context + (request, params, context), request body parsing, response + helpers, file naming for API routes. + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/server-routes.md' + + - name: 'Deployment' + slug: 'start-core/deployment' + type: 'sub-skill' + domain: 'deployment-and-rendering' + path: 'skills/start-core/deployment/SKILL.md' + package: 'packages/start-client-core' + description: >- + Deploy to Cloudflare Workers, Netlify, Vercel, Node.js/Docker, + Bun, Railway. Selective SSR (ssr option per route), SPA mode, + static prerendering, ISR with Cache-Control headers, SEO and + head management. + requires: + - 'start-core' + sources: + - 'TanStack/router:docs/start/framework/react/guide/hosting.md' + - 'TanStack/router:docs/start/framework/react/guide/selective-ssr.md' + - 'TanStack/router:docs/start/framework/react/guide/static-prerendering.md' + - 'TanStack/router:docs/start/framework/react/guide/full-stack-seo.md' + + # ── React Start Skills ────────────────────────────────────────── + - name: 'React Start' + slug: 'react-start' + type: 'framework' + domain: 'project-setup' + path: 'skills/react-start/SKILL.md' + package: 'packages/react-start' + description: >- + React bindings for TanStack Start: createStart, StartClient, + StartServer, React-specific imports, re-exports from + @tanstack/react-router, full project setup with React. + requires: + - 'start-core' + sources: + - 'TanStack/router:packages/react-start/src' + - 'TanStack/router:docs/start/framework/react/build-from-scratch.md' + + # ── Lifecycle Skills ──────────────────────────────────────────── + - name: 'Migrate from Next.js' + slug: 'lifecycle/migrate-from-nextjs' + type: 'lifecycle' + domain: 'project-setup' + path: 'skills/lifecycle/migrate-from-nextjs/SKILL.md' + package: 'packages/react-start' + description: >- + Step-by-step migration from Next.js App Router to TanStack + Start: route definition conversion, API mapping, server + function conversion from Server Actions, middleware conversion, + data fetching pattern changes. + requires: + - 'start-core' + - 'react-start' + sources: + - 'TanStack/router:docs/start/framework/react/guide/server-functions.md' + - 'TanStack/router:docs/start/framework/react/guide/middleware.md' + - 'TanStack/router:docs/start/framework/react/guide/execution-model.md' diff --git a/packages/start-client-core/skills/start-core/SKILL.md b/packages/start-client-core/skills/start-core/SKILL.md new file mode 100644 index 00000000000..bf8f80c2218 --- /dev/null +++ b/packages/start-client-core/skills/start-core/SKILL.md @@ -0,0 +1,212 @@ +--- +name: start-core +description: >- + Core overview for TanStack Start: tanstackStart() Vite plugin, + getRouter() factory, root route document shell (HeadContent, + Scripts, Outlet), client/server entry points, routeTree.gen.ts, + tsconfig configuration. Entry point for all Start skills. +type: core +library: tanstack-start +library_version: '1.166.2' +sources: + - TanStack/router:docs/start/framework/react/build-from-scratch.md + - TanStack/router:docs/start/framework/react/quick-start.md + - TanStack/router:docs/start/framework/react/guide/routing.md +--- + +# TanStack Start Core + +TanStack Start is a full-stack React framework built on TanStack Router and Vite. It adds SSR, streaming, server functions (type-safe RPCs), middleware, server routes, and universal deployment. + +> **CRITICAL**: All code in TanStack Start is ISOMORPHIC by default — it runs in BOTH server and client environments. Loaders run on both server AND client. To run code exclusively on the server, use `createServerFn`. This is the #1 AI agent mistake. + +> **CRITICAL**: TanStack Start is NOT Next.js. Do not generate `getServerSideProps`, `"use server"` directives, `app/layout.tsx`, or any Next.js/Remix patterns. Use `createServerFn` for server-only code. + +> **CRITICAL**: Types are FULLY INFERRED. Never cast, never annotate inferred values. + +## Sub-Skills + +| Task | Sub-Skill | +| -------------------------------------------- | ------------------------------------------------------------------- | +| Type-safe RPCs, data fetching, mutations | [start-core/server-functions/SKILL.md](./server-functions/SKILL.md) | +| Request/function middleware, context, auth | [start-core/middleware/SKILL.md](./middleware/SKILL.md) | +| Isomorphic execution, environment boundaries | [start-core/execution-model/SKILL.md](./execution-model/SKILL.md) | +| REST API endpoints alongside app routes | [start-core/server-routes/SKILL.md](./server-routes/SKILL.md) | +| Hosting, SSR modes, prerendering, SEO | [start-core/deployment/SKILL.md](./deployment/SKILL.md) | + +## Quick Decision Tree + +``` +Need to run code exclusively on the server (DB, secrets)? + → start-core/server-functions + +Need auth checks, logging, or shared logic across server functions? + → start-core/middleware + +Need to understand where code runs (server vs client)? + → start-core/execution-model + +Need a REST API endpoint (GET/POST/PUT/DELETE)? + → start-core/server-routes + +Need to deploy, configure SSR, or prerender? + → start-core/deployment +``` + +## Project Setup + +### 1. Install Dependencies + +```bash +npm i @tanstack/react-start @tanstack/react-router react react-dom +npm i -D vite @vitejs/plugin-react typescript +``` + +### 2. Configure Vite + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import viteReact from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [ + // MUST come before react() + tanstackStart(), + viteReact(), + ], +}) +``` + +### 3. Create Router Factory + +```tsx +// src/router.tsx +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + const router = createRouter({ + routeTree, + scrollRestoration: true, + }) + + return router +} +``` + +### 4. Create Root Route with Document Shell + +```tsx +// src/routes/__root.tsx +import type { ReactNode } from 'react' +import { + Outlet, + createRootRoute, + HeadContent, + Scripts, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: 'My App' }, + ], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + <html> + <head> + <HeadContent /> + </head> + <body> + <Outlet /> + <Scripts /> + </body> + </html> + ) +} +``` + +### 5. Create Index Route with Server Function + +```tsx +// src/routes/index.tsx +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-start' + +const getGreeting = createServerFn({ method: 'GET' }).handler(async () => { + return { message: 'Hello from the server!' } +}) + +export const Route = createFileRoute('/')({ + loader: () => getGreeting(), + component: HomePage, +}) + +function HomePage() { + const data = Route.useLoaderData() + return <h1>{data.message}</h1> +} +``` + +## Common Mistakes + +### 1. CRITICAL: React plugin before Start plugin in Vite config + +```ts +// WRONG — route generation and server function compilation fail +plugins: [react(), tanstackStart()] + +// CORRECT — Start plugin must come first +plugins: [tanstackStart(), react()] +``` + +### 2. HIGH: Enabling verbatimModuleSyntax in tsconfig + +`verbatimModuleSyntax` causes server bundles to leak into client bundles. Keep it disabled. + +### 3. HIGH: Missing Scripts component in root route + +The `<Scripts />` component must be rendered in the `<body>` of the root route. Without it, client-side JavaScript does not load and hydration fails. + +```tsx +// WRONG — no Scripts +function RootComponent() { + return ( + <html> + <head> + <HeadContent /> + </head> + <body> + <Outlet /> + </body> + </html> + ) +} + +// CORRECT — Scripts in body +function RootComponent() { + return ( + <html> + <head> + <HeadContent /> + </head> + <body> + <Outlet /> + <Scripts /> + </body> + </html> + ) +} +``` + +## Version Note + +This skill targets `@tanstack/react-start` v1.166.2 and `@tanstack/start-client-core` v1.166.2. diff --git a/packages/start-client-core/skills/start-core/deployment/SKILL.md b/packages/start-client-core/skills/start-core/deployment/SKILL.md new file mode 100644 index 00000000000..e37607fb161 --- /dev/null +++ b/packages/start-client-core/skills/start-core/deployment/SKILL.md @@ -0,0 +1,306 @@ +--- +name: start-core/deployment +description: >- + Deploy to Cloudflare Workers, Netlify, Vercel, Node.js/Docker, + Bun, Railway. Selective SSR (ssr option per route), SPA mode, + static prerendering, ISR with Cache-Control headers, SEO and + head management. +type: sub-skill +library: tanstack-start +library_version: '1.166.2' +requires: + - start-core +sources: + - TanStack/router:docs/start/framework/react/guide/hosting.md + - TanStack/router:docs/start/framework/react/guide/selective-ssr.md + - TanStack/router:docs/start/framework/react/guide/static-prerendering.md + - TanStack/router:docs/start/framework/react/guide/seo.md +--- + +# Deployment and Rendering + +TanStack Start deploys to any hosting provider via Vite and Nitro. This skill covers hosting setup, SSR configuration, prerendering, and SEO. + +## Hosting Providers + +### Cloudflare Workers + +```bash +pnpm add -D @cloudflare/vite-plugin wrangler +``` + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import { cloudflare } from '@cloudflare/vite-plugin' +import viteReact from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [ + cloudflare({ viteEnvironment: { name: 'ssr' } }), + tanstackStart(), + viteReact(), + ], +}) +``` + +```jsonc +// wrangler.jsonc +{ + "name": "my-app", + "compatibility_date": "2025-09-02", + "compatibility_flags": ["nodejs_compat"], + "main": "@tanstack/react-start/server-entry", +} +``` + +Deploy: `npx wrangler login && pnpm run deploy` + +### Netlify + +```bash +pnpm add -D @netlify/vite-plugin-tanstack-start +``` + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import netlify from '@netlify/vite-plugin-tanstack-start' +import viteReact from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [tanstackStart(), netlify(), viteReact()], +}) +``` + +Deploy: `npx netlify deploy` + +### Nitro (Vercel, Railway, Node.js, Docker) + +```bash +npm install nitro@npm:nitro-nightly@latest +``` + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import { nitro } from 'nitro/vite' +import viteReact from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [tanstackStart(), nitro(), viteReact()], +}) +``` + +Build and start: `npm run build && node .output/server/index.mjs` + +### Bun + +Bun deployment requires React 19. For React 18, use Node.js deployment. + +```ts +// vite.config.ts — add bun preset to nitro +plugins: [tanstackStart(), nitro({ preset: 'bun' }), viteReact()] +``` + +## Selective SSR + +Control SSR per route with the `ssr` property. + +### `ssr: true` (default) + +Runs `beforeLoad` and `loader` on server, renders component on server: + +```tsx +export const Route = createFileRoute('/posts/$postId')({ + ssr: true, // default + loader: () => fetchPost(), // runs on server during SSR + component: PostPage, // rendered on server +}) +``` + +### `ssr: false` + +Disables server execution of `beforeLoad`/`loader` and server rendering: + +```tsx +export const Route = createFileRoute('/dashboard')({ + ssr: false, + loader: () => fetchDashboard(), // runs on client only + component: DashboardPage, // rendered on client only +}) +``` + +### `ssr: 'data-only'` + +Runs `beforeLoad`/`loader` on server but renders component on client only: + +```tsx +export const Route = createFileRoute('/canvas')({ + ssr: 'data-only', + loader: () => fetchCanvasData(), // runs on server + component: CanvasPage, // rendered on client only +}) +``` + +### Functional Form + +Decide SSR at runtime based on params/search: + +```tsx +export const Route = createFileRoute('/docs/$docType/$docId')({ + ssr: ({ params }) => { + if (params.status === 'success' && params.value.docType === 'sheet') { + return false + } + }, +}) +``` + +### SSR Inheritance + +Children inherit parent SSR config and can only be MORE restrictive: + +- `true` → `data-only` or `false` (allowed) +- `false` → `true` (NOT allowed — parent `false` wins) + +### Default SSR + +Change the default for all routes in `src/start.ts`: + +```tsx +import { createStart } from '@tanstack/react-start' + +export const startInstance = createStart(() => ({ + defaultSsr: false, +})) +``` + +## Static Prerendering + +Generate static HTML at build time: + +```ts +// vite.config.ts +tanstackStart({ + prerender: { + enabled: true, + crawlLinks: true, + concurrency: 14, + failOnError: true, + }, +}) +``` + +Static routes are auto-discovered. Dynamic routes (e.g. `/users/$userId`) require `crawlLinks` or explicit `pages` config. + +## SEO and Head Management + +### Basic Meta Tags + +```tsx +export const Route = createFileRoute('/')({ + head: () => ({ + meta: [ + { title: 'My App - Home' }, + { name: 'description', content: 'Welcome to My App' }, + ], + }), +}) +``` + +### Dynamic Meta from Loader Data + +```tsx +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params }) => fetchPost(params.postId), + head: ({ loaderData }) => ({ + meta: [ + { title: loaderData.title }, + { name: 'description', content: loaderData.excerpt }, + { property: 'og:title', content: loaderData.title }, + { property: 'og:image', content: loaderData.coverImage }, + ], + }), +}) +``` + +### Structured Data (JSON-LD) + +```tsx +head: ({ loaderData }) => ({ + scripts: [ + { + type: 'application/ld+json', + children: JSON.stringify({ + '@context': 'https://schema.org', + '@type': 'Article', + headline: loaderData.title, + }), + }, + ], +}) +``` + +### Dynamic Sitemap via Server Route + +```ts +// src/routes/sitemap[.]xml.ts +export const Route = createFileRoute('/sitemap.xml')({ + server: { + handlers: { + GET: async () => { + const posts = await fetchAllPosts() + const sitemap = `<?xml version="1.0" encoding="UTF-8"?> +<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> + ${posts.map((p) => `<url><loc>https://myapp.com/posts/${p.id}</loc></url>`).join('')} +</urlset>` + return new Response(sitemap, { + headers: { 'Content-Type': 'application/xml' }, + }) + }, + }, + }, +}) +``` + +## Common Mistakes + +### 1. HIGH: Missing nodejs_compat flag for Cloudflare Workers + +```jsonc +// WRONG — Node.js APIs fail at runtime +{ "compatibility_flags": [] } + +// CORRECT +{ "compatibility_flags": ["nodejs_compat"] } +``` + +### 2. MEDIUM: Bun deployment with React 18 + +Bun-specific deployment only works with React 19. Use Node.js deployment for React 18. + +### 3. MEDIUM: Child route loosening parent SSR config + +```tsx +// Parent sets ssr: false +// WRONG — child cannot upgrade to ssr: true +const parentRoute = createFileRoute('/dashboard')({ ssr: false }) +const childRoute = createFileRoute('/dashboard/stats')({ + ssr: true, // IGNORED — parent false wins +}) + +// CORRECT — children can only be MORE restrictive +const parentRoute = createFileRoute('/dashboard')({ ssr: 'data-only' }) +const childRoute = createFileRoute('/dashboard/stats')({ + ssr: false, // OK — more restrictive than parent +}) +``` + +## Cross-References + +- [start-core/server-routes](../server-routes/SKILL.md) — API endpoints for sitemaps, robots.txt +- [start-core/execution-model](../execution-model/SKILL.md) — SSR affects where code runs diff --git a/packages/start-client-core/skills/start-core/execution-model/SKILL.md b/packages/start-client-core/skills/start-core/execution-model/SKILL.md new file mode 100644 index 00000000000..0813eb1a2ae --- /dev/null +++ b/packages/start-client-core/skills/start-core/execution-model/SKILL.md @@ -0,0 +1,297 @@ +--- +name: start-core/execution-model +description: >- + Isomorphic-by-default principle, environment boundary functions + (createServerFn, createServerOnlyFn, createClientOnlyFn, + createIsomorphicFn), ClientOnly component, useHydrated hook, + import protection, dead code elimination, environment variable + safety (VITE_ prefix, process.env). +type: sub-skill +library: tanstack-start +library_version: '1.166.2' +requires: + - start-core +sources: + - TanStack/router:docs/start/framework/react/guide/execution-model.md + - TanStack/router:docs/start/framework/react/guide/environment-variables.md +--- + +# Execution Model + +Understanding where code runs is fundamental to TanStack Start. This skill covers the isomorphic execution model and how to control environment boundaries. + +> **CRITICAL**: ALL code in TanStack Start is isomorphic by default — it runs in BOTH server and client bundles. Route loaders run on BOTH server (during SSR) AND client (during navigation). Server-only operations MUST use `createServerFn`. + +> **CRITICAL**: Module-level `process.env` access runs in both environments. Secret values leak into the client bundle. Access secrets ONLY inside `createServerFn` or `createServerOnlyFn`. + +> **CRITICAL**: `VITE_` prefixed environment variables are exposed to the client bundle. Server secrets must NOT have the `VITE_` prefix. + +## Execution Control APIs + +| API | Use Case | Client Behavior | Server Behavior | +| ------------------------ | ------------------------- | ------------------------- | --------------------- | +| `createServerFn()` | RPC calls, data mutations | Network request to server | Direct execution | +| `createServerOnlyFn(fn)` | Utility functions | Throws error | Direct execution | +| `createClientOnlyFn(fn)` | Browser utilities | Direct execution | Throws error | +| `createIsomorphicFn()` | Different impl per env | Uses `.client()` impl | Uses `.server()` impl | +| `<ClientOnly>` | Browser-only components | Renders children | Renders fallback | +| `useHydrated()` | Hydration-dependent logic | `true` after hydration | `false` | + +## Server-Only Execution + +### createServerFn (RPC pattern) + +The primary way to run server-only code. On the client, calls become fetch requests: + +```tsx +import { createServerFn } from '@tanstack/react-start' + +const fetchUser = createServerFn().handler(async () => { + const secret = process.env.API_SECRET // safe — server only + return await db.users.find() +}) + +// Client calls this via network request +const user = await fetchUser() +``` + +### createServerOnlyFn (throws on client) + +For utility functions that must never run on client: + +```tsx +import { createServerOnlyFn } from '@tanstack/react-start' + +const getSecret = createServerOnlyFn(() => process.env.DATABASE_URL) + +// Server: returns the value +// Client: THROWS an error +``` + +## Client-Only Execution + +### createClientOnlyFn + +```tsx +import { createClientOnlyFn } from '@tanstack/react-start' + +const saveToStorage = createClientOnlyFn((key: string, value: string) => { + localStorage.setItem(key, value) +}) +``` + +### ClientOnly Component + +```tsx +import { ClientOnly } from '@tanstack/react-router' + +function Analytics() { + return ( + <ClientOnly fallback={null}> + <GoogleAnalyticsScript /> + </ClientOnly> + ) +} +``` + +### useHydrated Hook + +```tsx +import { useHydrated } from '@tanstack/react-router' + +function TimeZoneDisplay() { + const hydrated = useHydrated() + const timeZone = hydrated + ? Intl.DateTimeFormat().resolvedOptions().timeZone + : 'UTC' + + return <div>Your timezone: {timeZone}</div> +} +``` + +Behavior: SSR → `false`, first client render → `false`, after hydration → `true` (stays `true`). + +## Environment-Specific Implementations + +```tsx +import { createIsomorphicFn } from '@tanstack/react-start' + +const getDeviceInfo = createIsomorphicFn() + .server(() => ({ type: 'server', platform: process.platform })) + .client(() => ({ type: 'client', userAgent: navigator.userAgent })) +``` + +## Environment Variables + +### Server-Side (inside createServerFn) + +Access any variable via `process.env`: + +```tsx +const connectDb = createServerFn().handler(async () => { + const url = process.env.DATABASE_URL // no prefix needed + return createConnection(url) +}) +``` + +### Client-Side (components) + +Only `VITE_` prefixed variables are available: + +```tsx +function ApiProvider({ children }: { children: React.ReactNode }) { + const apiUrl = import.meta.env.VITE_API_URL // available + // import.meta.env.DATABASE_URL → undefined (security) + return ( + <ApiContext.Provider value={{ apiUrl }}>{children}</ApiContext.Provider> + ) +} +``` + +### Runtime Client Variables + +If you need server-side variables on the client without `VITE_` prefix, pass them through a server function: + +```tsx +const getRuntimeVar = createServerFn({ method: 'GET' }).handler(() => { + return process.env.MY_RUNTIME_VAR +}) + +export const Route = createFileRoute('/')({ + loader: async () => { + const foo = await getRuntimeVar() + return { foo } + }, + component: () => { + const { foo } = Route.useLoaderData() + return <div>{foo}</div> + }, +}) +``` + +### Type Safety for Environment Variables + +```tsx +// src/env.d.ts +/// <reference types="vite/client" /> + +interface ImportMetaEnv { + readonly VITE_APP_NAME: string + readonly VITE_API_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + +declare global { + namespace NodeJS { + interface ProcessEnv { + readonly DATABASE_URL: string + readonly JWT_SECRET: string + } + } +} + +export {} +``` + +## Common Mistakes + +### 1. CRITICAL: Assuming loaders are server-only + +```tsx +// WRONG — loader runs on BOTH server and client +export const Route = createFileRoute('/dashboard')({ + loader: async () => { + const secret = process.env.API_SECRET // LEAKED to client + return fetch(`https://api.example.com/data`, { + headers: { Authorization: secret }, + }) + }, +}) + +// CORRECT — use createServerFn +const getData = createServerFn({ method: 'GET' }).handler(async () => { + const secret = process.env.API_SECRET + return fetch(`https://api.example.com/data`, { + headers: { Authorization: secret }, + }) +}) + +export const Route = createFileRoute('/dashboard')({ + loader: () => getData(), +}) +``` + +### 2. CRITICAL: Exposing secrets via module-level process.env + +```tsx +// WRONG — runs in both environments, value in client bundle +const apiKey = process.env.SECRET_KEY +export function fetchData() { + /* uses apiKey */ +} + +// CORRECT — access inside server function only +const fetchData = createServerFn({ method: 'GET' }).handler(async () => { + const apiKey = process.env.SECRET_KEY + return fetch(url, { headers: { Authorization: apiKey } }) +}) +``` + +### 3. CRITICAL: Using VITE\_ prefix for server secrets + +```bash +# WRONG — exposed to client bundle +VITE_SECRET_API_KEY=sk_live_xxx + +# CORRECT — no prefix for server secrets +SECRET_API_KEY=sk_live_xxx + +# CORRECT — VITE_ only for public client values +VITE_APP_NAME=My App +``` + +### 4. HIGH: Hydration mismatches + +```tsx +// WRONG — different content server vs client +function CurrentTime() { + return <div>{new Date().toLocaleString()}</div> +} + +// CORRECT — consistent rendering +function CurrentTime() { + const [time, setTime] = useState<string>() + useEffect(() => { + setTime(new Date().toLocaleString()) + }, []) + return <div>{time || 'Loading...'}</div> +} +``` + +## Architecture Decision Framework + +**Server-Only** (`createServerFn` / `createServerOnlyFn`): + +- Sensitive data (env vars, secrets) +- Database connections, file system +- External API keys + +**Client-Only** (`createClientOnlyFn` / `<ClientOnly>`): + +- DOM manipulation, browser APIs +- localStorage, geolocation +- Analytics/tracking + +**Isomorphic** (default / `createIsomorphicFn`): + +- Data formatting, business logic +- Shared utilities +- Route loaders (they're isomorphic by nature) + +## Cross-References + +- [start-core/server-functions](../server-functions/SKILL.md) — the primary server boundary +- [start-core/deployment](../deployment/SKILL.md) — deployment target affects execution diff --git a/packages/start-client-core/skills/start-core/middleware/SKILL.md b/packages/start-client-core/skills/start-core/middleware/SKILL.md new file mode 100644 index 00000000000..f63cea0e1c0 --- /dev/null +++ b/packages/start-client-core/skills/start-core/middleware/SKILL.md @@ -0,0 +1,361 @@ +--- +name: start-core/middleware +description: >- + createMiddleware, request middleware (.server only), server function + middleware (.client + .server), context passing via next({ context }), + sendContext for client-server transfer, global middleware via + createStart in src/start.ts, middleware factories, method order + enforcement, fetch override precedence. +type: sub-skill +library: tanstack-start +library_version: '1.166.2' +requires: + - start-core + - start-core/server-functions +sources: + - TanStack/router:docs/start/framework/react/guide/middleware.md +--- + +# Middleware + +Middleware customizes the behavior of server functions and server routes. It is composable — middleware can depend on other middleware to form a chain. + +> **CRITICAL**: TypeScript enforces method order: `middleware()` → `inputValidator()` → `client()` → `server()`. Wrong order causes type errors. + +> **CRITICAL**: Client context sent via `sendContext` is NOT validated by default. If you send dynamic user-generated data, validate it in server-side middleware before use. + +## Two Types of Middleware + +| Feature | Request Middleware | Server Function Middleware | +| ----------------- | -------------------------------------------- | ---------------------------------------- | +| Scope | All server requests (SSR, routes, functions) | Server functions only | +| Methods | `.server()` | `.client()`, `.server()` | +| Input validation | No | Yes (`.inputValidator()`) | +| Client-side logic | No | Yes | +| Created with | `createMiddleware()` | `createMiddleware({ type: 'function' })` | + +Request middleware cannot depend on server function middleware. Server function middleware can depend on both types. + +## Request Middleware + +Runs on ALL server requests (SSR, server routes, server functions): + +```tsx +import { createMiddleware } from '@tanstack/react-start' + +const loggingMiddleware = createMiddleware().server( + async ({ next, context, request }) => { + console.log('Request:', request.url) + const result = await next() + return result + }, +) +``` + +## Server Function Middleware + +Has both client and server phases: + +```tsx +import { createMiddleware } from '@tanstack/react-start' + +const authMiddleware = createMiddleware({ type: 'function' }) + .client(async ({ next }) => { + // Runs on client BEFORE the RPC call + const result = await next() + // Runs on client AFTER the RPC response + return result + }) + .server(async ({ next, context }) => { + // Runs on server BEFORE the handler + const result = await next() + // Runs on server AFTER the handler + return result + }) +``` + +## Attaching Middleware to Server Functions + +```tsx +import { createServerFn } from '@tanstack/react-start' + +const fn = createServerFn() + .middleware([authMiddleware]) + .handler(async ({ context }) => { + // context contains data from middleware + return { user: context.user } + }) +``` + +## Context Passing via next() + +Pass context down the middleware chain: + +```tsx +const authMiddleware = createMiddleware().server(async ({ next, request }) => { + const session = await getSession(request.headers) + if (!session) throw new Error('Unauthorized') + + return next({ + context: { session }, + }) +}) + +const roleMiddleware = createMiddleware() + .middleware([authMiddleware]) + .server(async ({ next, context }) => { + console.log('Session:', context.session) // typed! + return next() + }) +``` + +## Sending Context Between Client and Server + +### Client → Server (sendContext) + +```tsx +const workspaceMiddleware = createMiddleware({ type: 'function' }) + .client(async ({ next, context }) => { + return next({ + sendContext: { + workspaceId: context.workspaceId, + }, + }) + }) + .server(async ({ next, context }) => { + // workspaceId available here, but VALIDATE IT + console.log('Workspace:', context.workspaceId) + return next() + }) +``` + +### Server → Client (sendContext in server) + +```tsx +const serverTimer = createMiddleware({ type: 'function' }).server( + async ({ next }) => { + return next({ + sendContext: { + timeFromServer: new Date(), + }, + }) + }, +) + +const clientLogger = createMiddleware({ type: 'function' }) + .middleware([serverTimer]) + .client(async ({ next }) => { + const result = await next() + console.log('Server time:', result.context.timeFromServer) + return result + }) +``` + +## Input Validation in Middleware + +```tsx +import { z } from 'zod' +import { zodValidator } from '@tanstack/zod-adapter' + +const workspaceMiddleware = createMiddleware({ type: 'function' }) + .inputValidator(zodValidator(z.object({ workspaceId: z.string() }))) + .server(async ({ next, data }) => { + console.log('Workspace:', data.workspaceId) + return next() + }) +``` + +## Global Middleware + +Create `src/start.ts` to configure global middleware: + +```tsx +// src/start.ts +import { createStart, createMiddleware } from '@tanstack/react-start' + +const requestLogger = createMiddleware().server(async ({ next, request }) => { + console.log(`${request.method} ${request.url}`) + return next() +}) + +const functionAuth = createMiddleware({ type: 'function' }).server( + async ({ next }) => { + // runs for every server function + return next() + }, +) + +export const startInstance = createStart(() => ({ + requestMiddleware: [requestLogger], + functionMiddleware: [functionAuth], +})) +``` + +## Using Middleware with Server Routes + +### All handlers in a route + +```tsx +export const Route = createFileRoute('/api/users')({ + server: { + middleware: [authMiddleware], + handlers: { + GET: async ({ context }) => Response.json(context.user), + POST: async ({ request }) => { + /* ... */ + }, + }, + }, +}) +``` + +### Specific handlers only + +```tsx +export const Route = createFileRoute('/api/users')({ + server: { + handlers: ({ createHandlers }) => + createHandlers({ + GET: async () => Response.json({ public: true }), + POST: { + middleware: [authMiddleware], + handler: async ({ context }) => { + return Response.json({ user: context.session.user }) + }, + }, + }), + }, +}) +``` + +## Middleware Factories + +Create parameterized middleware for reusable patterns like authorization: + +```tsx +const authMiddleware = createMiddleware().server(async ({ next, request }) => { + const session = await auth.getSession({ headers: request.headers }) + if (!session) throw new Error('Unauthorized') + return next({ context: { session } }) +}) + +type Permissions = Record<string, string[]> + +function authorizationMiddleware(permissions: Permissions) { + return createMiddleware({ type: 'function' }) + .middleware([authMiddleware]) + .server(async ({ next, context }) => { + const granted = await auth.hasPermission(context.session, permissions) + if (!granted) throw new Error('Forbidden') + return next() + }) +} + +// Usage +const getClients = createServerFn() + .middleware([authorizationMiddleware({ client: ['read'] })]) + .handler(async () => { + return { message: 'The user can read clients.' } + }) +``` + +## Custom Headers and Fetch + +### Setting headers from client middleware + +```tsx +const authMiddleware = createMiddleware({ type: 'function' }).client( + async ({ next }) => { + return next({ + headers: { Authorization: `Bearer ${getToken()}` }, + }) + }, +) +``` + +Headers merge across middleware. Later middleware overrides earlier. Call-site headers override all middleware headers. + +### Custom fetch + +```tsx +import type { CustomFetch } from '@tanstack/react-start' + +const loggingMiddleware = createMiddleware({ type: 'function' }).client( + async ({ next }) => { + const customFetch: CustomFetch = async (url, init) => { + console.log('Request:', url) + return fetch(url, init) + } + return next({ fetch: customFetch }) + }, +) +``` + +Fetch precedence (highest to lowest): call site → later middleware → earlier middleware → createStart global → default fetch. + +## Common Mistakes + +### 1. HIGH: Trusting client sendContext without validation + +```tsx +// WRONG — client can send arbitrary data +.server(async ({ next, context }) => { + await db.query(`SELECT * FROM workspace_${context.workspaceId}`) + return next() +}) + +// CORRECT — validate before use +.server(async ({ next, context }) => { + const workspaceId = z.string().uuid().parse(context.workspaceId) + await db.query('SELECT * FROM workspaces WHERE id = $1', [workspaceId]) + return next() +}) +``` + +### 2. MEDIUM: Confusing request vs server function middleware + +Request middleware runs on ALL requests (SSR, routes, functions). Server function middleware runs only for `createServerFn` calls and has `.client()` method. + +### 3. HIGH: Browser APIs in .client() crash during SSR + +During SSR, `.client()` callbacks run on the server. Browser-only APIs like `localStorage` or `window` will throw `ReferenceError`: + +```tsx +// WRONG — localStorage doesn't exist on the server during SSR +const middleware = createMiddleware({ type: 'function' }).client( + async ({ next }) => { + const token = localStorage.getItem('token') + return next({ sendContext: { token } }) + }, +) + +// CORRECT — use cookies/headers or guard with typeof window check +const middleware = createMiddleware({ type: 'function' }).client( + async ({ next }) => { + const token = + typeof window !== 'undefined' ? localStorage.getItem('token') : null + return next({ sendContext: { token } }) + }, +) +``` + +### 4. MEDIUM: Wrong method order + +```tsx +// WRONG — type error +createMiddleware({ type: 'function' }) + .server(() => { ... }) + .client(() => { ... }) + +// CORRECT — middleware → inputValidator → client → server +createMiddleware({ type: 'function' }) + .middleware([dep]) + .inputValidator(schema) + .client(({ next }) => next()) + .server(({ next }) => next()) +``` + +## Cross-References + +- [start-core/server-functions](../server-functions/SKILL.md) — what middleware wraps +- [start-core/server-routes](../server-routes/SKILL.md) — middleware on API endpoints diff --git a/packages/start-client-core/skills/start-core/server-functions/SKILL.md b/packages/start-client-core/skills/start-core/server-functions/SKILL.md new file mode 100644 index 00000000000..91aed114f54 --- /dev/null +++ b/packages/start-client-core/skills/start-core/server-functions/SKILL.md @@ -0,0 +1,338 @@ +--- +name: start-core/server-functions +description: >- + createServerFn (GET/POST), inputValidator (Zod or function), + useServerFn hook, server context utilities (getRequest, + getRequestHeader, setResponseHeader, setResponseStatus), error + handling (throw errors, redirect, notFound), streaming, FormData + handling, file organization (.functions.ts, .server.ts). +type: sub-skill +library: tanstack-start +library_version: '1.166.2' +requires: + - start-core +sources: + - TanStack/router:docs/start/framework/react/guide/server-functions.md +--- + +# Server Functions + +Server functions are type-safe RPCs created with `createServerFn`. They run exclusively on the server but can be called from anywhere — loaders, components, hooks, event handlers, or other server functions. + +> **CRITICAL**: Loaders are ISOMORPHIC — they run on BOTH client and server. Database queries, file system access, and secret API keys MUST go inside `createServerFn`, NOT in loaders directly. + +> **CRITICAL**: Do not use `"use server"` directives, `getServerSideProps`, or any Next.js/Remix server patterns. TanStack Start uses `createServerFn` exclusively. + +## Basic Usage + +```tsx +import { createServerFn } from '@tanstack/react-start' + +// GET (default) +const getData = createServerFn().handler(async () => { + return { message: 'Hello from server!' } +}) + +// POST +const saveData = createServerFn({ method: 'POST' }).handler(async () => { + return { success: true } +}) +``` + +## Calling from Loaders + +```tsx +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-start' + +const getPosts = createServerFn({ method: 'GET' }).handler(async () => { + const posts = await db.query('SELECT * FROM posts') + return { posts } +}) + +export const Route = createFileRoute('/posts')({ + loader: () => getPosts(), + component: PostList, +}) + +function PostList() { + const { posts } = Route.useLoaderData() + return ( + <ul> + {posts.map((p) => ( + <li key={p.id}>{p.title}</li> + ))} + </ul> + ) +} +``` + +## Calling from Components + +Use the `useServerFn` hook to call server functions from event handlers: + +```tsx +import { useServerFn } from '@tanstack/react-start' + +const deletePost = createServerFn({ method: 'POST' }) + .inputValidator((data: { id: string }) => data) + .handler(async ({ data }) => { + await db.delete('posts').where({ id: data.id }) + return { success: true } + }) + +function DeleteButton({ postId }: { postId: string }) { + const deletePostFn = useServerFn(deletePost) + + return ( + <button onClick={() => deletePostFn({ data: { id: postId } })}> + Delete + </button> + ) +} +``` + +## Input Validation + +### Basic Validator + +```tsx +const greetUser = createServerFn({ method: 'GET' }) + .inputValidator((data: { name: string }) => data) + .handler(async ({ data }) => { + return `Hello, ${data.name}!` + }) + +await greetUser({ data: { name: 'John' } }) +``` + +### Zod Validator + +```tsx +import { z } from 'zod' + +const createUser = createServerFn({ method: 'POST' }) + .inputValidator( + z.object({ + name: z.string().min(1), + age: z.number().min(0), + }), + ) + .handler(async ({ data }) => { + return `Created user: ${data.name}, age ${data.age}` + }) +``` + +### FormData + +```tsx +const submitForm = createServerFn({ method: 'POST' }) + .inputValidator((data) => { + if (!(data instanceof FormData)) { + throw new Error('Expected FormData') + } + return { + name: data.get('name')?.toString() || '', + email: data.get('email')?.toString() || '', + } + }) + .handler(async ({ data }) => { + return { success: true } + }) +``` + +## Error Handling + +### Errors + +```tsx +const riskyFunction = createServerFn().handler(async () => { + throw new Error('Something went wrong!') +}) + +try { + await riskyFunction() +} catch (error) { + console.log(error.message) // "Something went wrong!" +} +``` + +### Redirects + +```tsx +import { redirect } from '@tanstack/react-router' + +const requireAuth = createServerFn().handler(async () => { + const user = await getCurrentUser() + if (!user) { + throw redirect({ to: '/login' }) + } + return user +}) +``` + +### Not Found + +```tsx +import { notFound } from '@tanstack/react-router' + +const getPost = createServerFn() + .inputValidator((data: { id: string }) => data) + .handler(async ({ data }) => { + const post = await db.findPost(data.id) + if (!post) { + throw notFound() + } + return post + }) +``` + +## Server Context Utilities + +Access request/response details inside server function handlers: + +```tsx +import { createServerFn } from '@tanstack/react-start' +import { + getRequest, + getRequestHeader, + setResponseHeaders, + setResponseStatus, +} from '@tanstack/react-start/server' + +const getCachedData = createServerFn({ method: 'GET' }).handler(async () => { + const request = getRequest() + const authHeader = getRequestHeader('Authorization') + + setResponseHeaders( + new Headers({ + 'Cache-Control': 'public, max-age=300', + }), + ) + setResponseStatus(200) + + return fetchData() +}) +``` + +Available utilities: + +- `getRequest()` — full Request object +- `getRequestHeader(name)` — single request header +- `setResponseHeader(name, value)` — single response header +- `setResponseHeaders(headers)` — multiple response headers +- `setResponseStatus(code)` — HTTP status code + +## File Organization + +``` +src/utils/ +├── users.functions.ts # createServerFn wrappers (safe to import anywhere) +├── users.server.ts # Server-only helpers (DB queries, internal logic) +└── schemas.ts # Shared validation schemas (client-safe) +``` + +```tsx +// users.server.ts — server-only helpers +import { db } from '~/db' + +export async function findUserById(id: string) { + return db.query.users.findFirst({ where: eq(users.id, id) }) +} +``` + +```tsx +// users.functions.ts — server functions +import { createServerFn } from '@tanstack/react-start' +import { findUserById } from './users.server' + +export const getUser = createServerFn({ method: 'GET' }) + .inputValidator((data: { id: string }) => data) + .handler(async ({ data }) => { + return findUserById(data.id) + }) +``` + +Static imports of server functions are safe — the build replaces implementations with RPC stubs in client bundles. + +## Common Mistakes + +### 1. CRITICAL: Putting server-only code in loaders + +```tsx +// WRONG — loader is ISOMORPHIC, runs on BOTH client and server +export const Route = createFileRoute('/posts')({ + loader: async () => { + const posts = await db.query('SELECT * FROM posts') + return { posts } + }, +}) + +// CORRECT — use createServerFn for server-only logic +const getPosts = createServerFn({ method: 'GET' }).handler(async () => { + const posts = await db.query('SELECT * FROM posts') + return { posts } +}) + +export const Route = createFileRoute('/posts')({ + loader: () => getPosts(), +}) +``` + +### 2. CRITICAL: Using Next.js/Remix server patterns + +```tsx +// WRONG — "use server" is a React/Next.js directive +'use server' +export async function getUser() { ... } + +// WRONG — getServerSideProps is Next.js +export async function getServerSideProps() { ... } + +// CORRECT — TanStack Start uses createServerFn +const getUser = createServerFn({ method: 'GET' }) + .handler(async () => { ... }) +``` + +### 3. HIGH: Dynamic imports for server functions + +```tsx +// WRONG — can cause bundler issues +const { getUser } = await import('~/utils/users.functions') + +// CORRECT — static imports are safe, build handles environment shaking +import { getUser } from '~/utils/users.functions' +``` + +### 4. HIGH: Awaiting server function without calling it + +`createServerFn` returns a function — it must be invoked with `()`: + +```tsx +// WRONG — getItems is a function, not a Promise +const data = await getItems + +// CORRECT — call the function +const data = await getItems() + +// With validated input +const data = await getItems({ data: { id: '1' } }) +``` + +### 5. MEDIUM: Not using useServerFn for component calls + +When calling server functions from event handlers in components, use `useServerFn` to get proper React integration: + +```tsx +// WRONG — direct call doesn't integrate with React lifecycle +<button onClick={() => deletePost({ data: { id } })}>Delete</button> + +// CORRECT — useServerFn integrates with React +const deletePostFn = useServerFn(deletePost) +<button onClick={() => deletePostFn({ data: { id } })}>Delete</button> +``` + +## Cross-References + +- [start-core/execution-model](../execution-model/SKILL.md) — understanding where code runs +- [start-core/middleware](../middleware/SKILL.md) — composing server functions with middleware diff --git a/packages/start-client-core/skills/start-core/server-routes/SKILL.md b/packages/start-client-core/skills/start-core/server-routes/SKILL.md new file mode 100644 index 00000000000..a3e9b5d7d8a --- /dev/null +++ b/packages/start-client-core/skills/start-core/server-routes/SKILL.md @@ -0,0 +1,278 @@ +--- +name: start-core/server-routes +description: >- + Server-side API endpoints using the server property on + createFileRoute, HTTP method handlers (GET, POST, PUT, DELETE), + createHandlers for per-handler middleware, handler context + (request, params, context), request body parsing, response + helpers, file naming for API routes. +type: sub-skill +library: tanstack-start +library_version: '1.166.2' +requires: + - start-core +sources: + - TanStack/router:docs/start/framework/react/guide/server-routes.md +--- + +# Server Routes + +Server routes are API endpoints defined alongside app routes in the `src/routes` directory. They use the `server` property on `createFileRoute` and handle raw HTTP requests. + +## Basic Server Route + +```ts +// src/routes/api/hello.ts +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/api/hello')({ + server: { + handlers: { + GET: async ({ request }) => { + return new Response('Hello, World!') + }, + }, + }, +}) +``` + +## Combining Server Route and App Route + +The same file can define both a server route and a UI route: + +```tsx +// src/routes/hello.tsx +import { createFileRoute } from '@tanstack/react-router' +import { useState } from 'react' + +export const Route = createFileRoute('/hello')({ + server: { + handlers: { + POST: async ({ request }) => { + const body = await request.json() + return Response.json({ message: `Hello, ${body.name}!` }) + }, + }, + }, + component: HelloComponent, +}) + +function HelloComponent() { + const [reply, setReply] = useState('') + return ( + <button + onClick={() => { + fetch('/hello', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: 'Tanner' }), + }) + .then((res) => res.json()) + .then((data) => setReply(data.message)) + }} + > + Say Hello {reply && `- ${reply}`} + </button> + ) +} +``` + +## File Route Conventions + +Server routes follow TanStack Router file-based routing conventions: + +| File | Route | +| --------------------------- | ----------------------------- | +| `routes/users.ts` | `/users` | +| `routes/users/$id.ts` | `/users/$id` | +| `routes/users/$id/posts.ts` | `/users/$id/posts` | +| `routes/api/file/$.ts` | `/api/file/$` (splat) | +| `routes/my-script[.]js.ts` | `/my-script.js` (escaped dot) | + +## Unique Route Paths + +Each route can only have a single handler file. These would conflict: + +- `routes/users.ts` +- `routes/users.index.ts` +- `routes/users/index.ts` + +## Handler Context + +Each handler receives: + +- `request` — the incoming [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object +- `params` — dynamic path parameters +- `context` — context from middleware + +## Dynamic Path Params + +```ts +// routes/users/$id.ts +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/users/$id')({ + server: { + handlers: { + GET: async ({ params }) => { + return new Response(`User ID: ${params.id}`) + }, + }, + }, +}) +``` + +## Splat/Wildcard Params + +```ts +// routes/file/$.ts +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/file/$')({ + server: { + handlers: { + GET: async ({ params }) => { + return new Response(`File: ${params._splat}`) + }, + }, + }, +}) +``` + +## Request Body Handling + +```ts +export const Route = createFileRoute('/api/users')({ + server: { + handlers: { + POST: async ({ request }) => { + const body = await request.json() + return Response.json({ created: body.name }) + }, + }, + }, +}) +``` + +Other body methods: `request.text()`, `request.formData()`. + +## JSON Responses + +```ts +// Using Response.json helper +handlers: { + GET: async () => { + return Response.json({ message: 'Hello!' }) + }, +} +``` + +## Status Codes and Headers + +```ts +handlers: { + GET: async ({ params }) => { + const user = await findUser(params.id) + if (!user) { + return new Response('Not found', { status: 404 }) + } + return Response.json(user) + }, +} +``` + +```ts +handlers: { + GET: async () => { + return new Response('Hello', { + headers: { 'Content-Type': 'text/plain' }, + }) + }, +} +``` + +## Middleware on Server Routes + +### All handlers + +```tsx +export const Route = createFileRoute('/api/admin')({ + server: { + middleware: [authMiddleware, loggerMiddleware], + handlers: { + GET: async ({ context }) => Response.json(context.user), + POST: async ({ request, context }) => { + /* ... */ + }, + }, + }, +}) +``` + +### Specific handlers with createHandlers + +```tsx +export const Route = createFileRoute('/api/data')({ + server: { + handlers: ({ createHandlers }) => + createHandlers({ + GET: async () => Response.json({ public: true }), + POST: { + middleware: [authMiddleware], + handler: async ({ context }) => { + return Response.json({ user: context.session.user }) + }, + }, + }), + }, +}) +``` + +### Combined route-level and handler-specific + +```tsx +export const Route = createFileRoute('/api/posts')({ + server: { + middleware: [authMiddleware], // runs first for all + handlers: ({ createHandlers }) => + createHandlers({ + GET: async () => Response.json([]), + POST: { + middleware: [validationMiddleware], // runs after auth, POST only + handler: async ({ request }) => { + const body = await request.json() + return Response.json({ created: true }) + }, + }, + }), + }, +}) +``` + +## Common Mistakes + +### 1. MEDIUM: Duplicate route paths + +``` +# WRONG — both resolve to /users, causes error +routes/users.ts +routes/users/index.ts + +# CORRECT — pick one +routes/users.ts +``` + +### 2. MEDIUM: Forgetting to await request body methods + +```ts +// WRONG — body is a Promise, not the actual data +const body = request.json() + +// CORRECT — await the promise +const body = await request.json() +``` + +## Cross-References + +- [start-core/middleware](../middleware/SKILL.md) — middleware for server routes +- [start-core/server-functions](../server-functions/SKILL.md) — alternative for RPC-style calls diff --git a/packages/start-server-core/bin/intent.js b/packages/start-server-core/bin/intent.js new file mode 100755 index 00000000000..2cf2efab472 --- /dev/null +++ b/packages/start-server-core/bin/intent.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// Auto-generated by @tanstack/intent setup +// Exposes the intent end-user CLI for consumers of this library. +// Commit this file, then add to your package.json: +// "bin": { "intent": "./bin/intent.js" } +try { + await import('@tanstack/intent/intent-library') +} catch (e) { + if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + console.error('@tanstack/intent is not installed.') + console.error('') + console.error('Install it as a dev dependency:') + console.error(' npm add -D @tanstack/intent') + console.error('') + console.error('Or run directly:') + console.error(' npx @tanstack/intent@latest list') + process.exit(1) + } + throw e +} diff --git a/packages/start-server-core/package.json b/packages/start-server-core/package.json index 94ab2f4f437..9832051c5d8 100644 --- a/packages/start-server-core/package.json +++ b/packages/start-server-core/package.json @@ -69,7 +69,10 @@ "sideEffects": false, "files": [ "dist", - "src" + "src", + "skills", + "bin", + "!skills/_artifacts" ], "engines": { "node": ">=22.12.0" @@ -85,8 +88,12 @@ }, "devDependencies": { "@standard-schema/spec": "^1.0.0", + "@tanstack/intent": "^0.0.14", "cookie-es": "^2.0.0", "fetchdts": "^0.1.6", "vite": "*" + }, + "bin": { + "intent": "./bin/intent.js" } } diff --git a/packages/start-server-core/skills/_artifacts/domain_map.yaml b/packages/start-server-core/skills/_artifacts/domain_map.yaml new file mode 100644 index 00000000000..1a6351bdea7 --- /dev/null +++ b/packages/start-server-core/skills/_artifacts/domain_map.yaml @@ -0,0 +1,65 @@ +# domain_map.yaml +# Library: @tanstack/start-server-core +# Version: 1.166.2 +# Date: 2026-03-08 +# Status: reviewed + +library: + name: '@tanstack/start-server-core' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Server-side runtime for TanStack Start: createStartHandler, + request/response utilities, cookie/session management, + three-phase request handling, AsyncLocalStorage context. + primary_framework: 'framework-agnostic' + +domains: + - name: 'Server Runtime' + slug: 'server-runtime' + description: >- + Server-side request handling, start handler creation, + cookie/session management, and AsyncLocalStorage context + for TanStack Start applications. + +skills: + - name: 'Start Server Core' + slug: 'start-server-core' + domain: 'server-runtime' + description: >- + Server-side runtime for TanStack Start: createStartHandler, + request/response utilities, cookie/session management, + three-phase request handling, AsyncLocalStorage context. + type: core + packages: + - '@tanstack/start-server-core' + covers: + - createStartHandler factory + - Request/response utilities + - Cookie management + - Session management + - Three-phase request handling + - AsyncLocalStorage context + - Server entry point setup + tasks: + - 'Create server entry point with createStartHandler' + - 'Manage cookies and sessions' + - 'Handle server-side request lifecycle' + failure_modes: + - mistake: 'Missing AsyncLocalStorage setup' + mechanism: >- + Server functions rely on AsyncLocalStorage for request + context. Not initializing the context store causes + undefined access errors at runtime. + priority: HIGH + status: active + + - mistake: 'Incorrect handler export for deployment target' + mechanism: >- + createStartHandler returns a handler shaped for the + deployment target. Exporting the wrong format (e.g. + Node handler for edge runtime) causes request failures. + priority: MEDIUM + status: active + +gaps: [] diff --git a/packages/start-server-core/skills/_artifacts/skill_spec.md b/packages/start-server-core/skills/_artifacts/skill_spec.md new file mode 100644 index 00000000000..19270dc59a6 --- /dev/null +++ b/packages/start-server-core/skills/_artifacts/skill_spec.md @@ -0,0 +1,24 @@ +# @tanstack/start-server-core — Skill Spec + +Server-side runtime for TanStack Start: createStartHandler, request/response utilities, cookie/session management, three-phase request handling, AsyncLocalStorage context. + +## Domains + +| Domain | Description | Skills | +| -------------- | ------------------------------------------------------------ | ----------------- | +| Server Runtime | Server request handling, cookies/sessions, AsyncLocalStorage | start-server-core | + +## Skill Inventory + +| Skill | Type | Domain | What it covers | Failure modes | +| ----------------- | ---- | -------------- | ---------------------------------------------------------------- | ------------- | +| start-server-core | core | server-runtime | createStartHandler, cookies, sessions, three-phase handling, ALS | 2 | + +## Failure Mode Inventory + +### start-server-core (2 failure modes) + +| # | Mistake | Priority | Source | +| --- | ---------------------------------------------- | -------- | ----------- | +| 1 | Missing AsyncLocalStorage setup | HIGH | source/docs | +| 2 | Incorrect handler export for deployment target | MEDIUM | source/docs | diff --git a/packages/start-server-core/skills/_artifacts/skill_tree.yaml b/packages/start-server-core/skills/_artifacts/skill_tree.yaml new file mode 100644 index 00000000000..6c792cf1bd8 --- /dev/null +++ b/packages/start-server-core/skills/_artifacts/skill_tree.yaml @@ -0,0 +1,23 @@ +library: + name: '@tanstack/start-server-core' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Server-side runtime for TanStack Start: createStartHandler, + request/response utilities, cookie/session management, + three-phase request handling, AsyncLocalStorage context. +generated_at: '2026-03-08' +skills: + - name: 'Start Server Core' + slug: 'start-server-core' + type: 'core' + domain: 'server-runtime' + path: 'skills/start-server-core/SKILL.md' + package: 'packages/start-server-core' + description: >- + Server-side runtime for TanStack Start: createStartHandler, + request/response utilities, cookie/session management, + three-phase request handling, AsyncLocalStorage context. + sources: + - 'TanStack/router:packages/start-server-core/src' + - 'TanStack/router:docs/start/framework/react/guide/server-entry-point.md' diff --git a/packages/start-server-core/skills/start-server-core/SKILL.md b/packages/start-server-core/skills/start-server-core/SKILL.md new file mode 100644 index 00000000000..7199a9ad33d --- /dev/null +++ b/packages/start-server-core/skills/start-server-core/SKILL.md @@ -0,0 +1,244 @@ +--- +name: start-server-core +description: >- + Server-side runtime for TanStack Start: createStartHandler, + request/response utilities (getRequest, setResponseHeader, + setCookie, getCookie, useSession), three-phase request handling, + AsyncLocalStorage context. +type: core +library: tanstack-start +library_version: '1.166.2' +sources: + - TanStack/router:packages/start-server-core/src + - TanStack/router:docs/start/framework/react/guide/server-entry-point.md +--- + +# Start Server Core (`@tanstack/start-server-core`) + +Server-side runtime for TanStack Start. Provides the request handler, request/response utilities, cookie management, and session management. All utilities are available anywhere in the call stack during a request via AsyncLocalStorage. + +> **CRITICAL**: These utilities are SERVER-ONLY. Import them from `@tanstack/<framework>-start/server`, not from the main entry point. They throw if called outside a server request context. + +> **CRITICAL**: Types are FULLY INFERRED. Never cast, never annotate inferred values. + +## `createStartHandler` + +Creates the main request handler that processes all incoming requests through three phases: server functions, server routes, then app SSR. + +```ts +// src/entry.server.ts (or use framework defaults) +import { createStartHandler } from '@tanstack/react-start/server' +import { defaultStreamHandler } from '@tanstack/react-start/server' + +export default createStartHandler({ + handler: defaultStreamHandler, +}) +``` + +With asset URL transforms (CDN): + +```ts +export default createStartHandler({ + handler: defaultStreamHandler, + transformAssetUrls: 'https://cdn.example.com', +}) +``` + +## Request Utilities + +All imported from `@tanstack/<framework>-start/server`. Available anywhere during request handling — no parameter passing needed. + +### Reading Request Data + +```ts +import { + getRequest, + getRequestHeaders, + getRequestHeader, + getRequestIP, + getRequestHost, + getRequestUrl, + getRequestProtocol, +} from '@tanstack/react-start/server' + +const serverFn = createServerFn({ method: 'GET' }).handler(async () => { + const request = getRequest() + const headers = getRequestHeaders() + const auth = getRequestHeader('authorization') + const ip = getRequestIP({ xForwardedFor: true }) + const host = getRequestHost() + const url = getRequestUrl() + const protocol = getRequestProtocol() + + return { ip, host } +}) +``` + +### Setting Response Data + +```ts +import { + setResponseHeader, + setResponseHeaders, + setResponseStatus, + getResponseHeaders, + getResponseHeader, + getResponseStatus, + removeResponseHeader, + clearResponseHeaders, +} from '@tanstack/react-start/server' + +const serverFn = createServerFn({ method: 'POST' }).handler(async () => { + setResponseStatus(201) + setResponseHeader('x-custom', 'value') + setResponseHeaders({ 'cache-control': 'no-store' }) + + return { created: true } +}) +``` + +## Cookie Management + +```ts +import { + getCookies, + getCookie, + setCookie, + deleteCookie, +} from '@tanstack/react-start/server' + +const serverFn = createServerFn({ method: 'POST' }).handler(async () => { + const allCookies = getCookies() + const token = getCookie('session-token') + + setCookie('preference', 'dark', { + httpOnly: true, + secure: true, + maxAge: 60 * 60 * 24 * 30, // 30 days + path: '/', + }) + + deleteCookie('old-cookie') +}) +``` + +## Session Management + +Encrypted sessions stored in cookies. Requires a password for encryption. + +```ts +import { + useSession, + getSession, + updateSession, + clearSession, +} from '@tanstack/react-start/server' + +const sessionConfig = { + password: process.env.SESSION_SECRET!, + name: 'my-app-session', + maxAge: 60 * 60 * 24 * 7, // 7 days +} + +// Full session manager +const getUser = createServerFn({ method: 'GET' }).handler(async () => { + const session = await useSession<{ userId: string }>(sessionConfig) + return session.data +}) + +// Update session +const login = createServerFn({ method: 'POST' }) + .inputValidator((data: { userId: string }) => data) + .handler(async ({ data }) => { + await updateSession(sessionConfig, { userId: data.userId }) + return { success: true } + }) + +// Clear session +const logout = createServerFn({ method: 'POST' }).handler(async () => { + await clearSession(sessionConfig) + return { success: true } +}) +``` + +### Session Config + +| Option | Type | Default | Description | +| ---------- | ------------------------ | ----------- | ----------------- | +| `password` | `string` | required | Encryption key | +| `name` | `string` | `'start'` | Cookie name | +| `maxAge` | `number` | `undefined` | Expiry in seconds | +| `cookie` | `false \| CookieOptions` | `undefined` | Cookie settings | + +### Session Manager Methods + +```ts +const session = await useSession<{ userId: string }>(config) + +session.id // Session ID (string | undefined) +session.data // Session data (typed) +session.update() // Update and return new session manager +session.clear() // Clear session data +``` + +## Query Validation + +```ts +import { getValidatedQuery } from '@tanstack/react-start/server' + +const serverFn = createServerFn({ method: 'GET' }).handler(async () => { + const query = getValidatedQuery((q) => ({ + page: Number(q.page) || 1, + limit: Math.min(Number(q.limit) || 20, 100), + })) + + return { page: query.page } +}) +``` + +## How Request Handling Works + +`createStartHandler` processes requests in three phases: + +1. **Server Function Dispatch** — If URL matches the server function prefix (`/_server`), deserializes the payload, runs global request middleware, executes the server function, and returns the serialized result. + +2. **Server Route Handler** — For non-server-function requests, matches the URL against routes with `server.handlers`. Runs route middleware, then the matched HTTP method handler. Handlers can return a `Response` or call `next()` to fall through to SSR. + +3. **App Router SSR** — Loads all route loaders, dehydrates state for client hydration, and calls the handler callback (e.g., `defaultStreamHandler`) to render HTML. + +## Common Mistakes + +### 1. CRITICAL: Importing server utilities in client code + +Server utilities use AsyncLocalStorage and only work during server request handling. Importing them in client code causes build errors or runtime crashes. + +```ts +// WRONG — importing in a component file that runs on client +import { getCookie } from '@tanstack/react-start/server' + +function MyComponent() { + const token = getCookie('auth') // crashes on client +} + +// CORRECT — use inside server functions only +import { createServerFn } from '@tanstack/react-start' +import { getCookie } from '@tanstack/react-start/server' + +const getAuth = createServerFn({ method: 'GET' }).handler(async () => { + return getCookie('auth') +}) +``` + +### 2. HIGH: Forgetting session password is required + +`useSession`, `getSession`, and `updateSession` all require a `password` field for encryption. Missing it throws at runtime. + +### 3. MEDIUM: Using session without HTTPS in production + +Session cookies should use `secure: true` in production. The default cookie options may not enforce this. + +## Cross-References + +- [start-core/server-functions](../../../start-client-core/skills/start-core/server-functions/SKILL.md) — creating server functions that use these utilities +- [start-core/middleware](../../../start-client-core/skills/start-core/middleware/SKILL.md) — request middleware +- [start-core/server-routes](../../../start-client-core/skills/start-core/server-routes/SKILL.md) — server route handlers diff --git a/packages/virtual-file-routes/bin/intent.js b/packages/virtual-file-routes/bin/intent.js new file mode 100755 index 00000000000..2cf2efab472 --- /dev/null +++ b/packages/virtual-file-routes/bin/intent.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// Auto-generated by @tanstack/intent setup +// Exposes the intent end-user CLI for consumers of this library. +// Commit this file, then add to your package.json: +// "bin": { "intent": "./bin/intent.js" } +try { + await import('@tanstack/intent/intent-library') +} catch (e) { + if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + console.error('@tanstack/intent is not installed.') + console.error('') + console.error('Install it as a dev dependency:') + console.error(' npm add -D @tanstack/intent') + console.error('') + console.error('Or run directly:') + console.error(' npx @tanstack/intent@latest list') + process.exit(1) + } + throw e +} diff --git a/packages/virtual-file-routes/package.json b/packages/virtual-file-routes/package.json index dbaac9ec41f..7d210b9747a 100644 --- a/packages/virtual-file-routes/package.json +++ b/packages/virtual-file-routes/package.json @@ -49,12 +49,19 @@ "sideEffects": false, "files": [ "dist", - "src" + "src", + "skills", + "bin", + "!skills/_artifacts" ], "engines": { "node": ">=20.19" }, "devDependencies": { + "@tanstack/intent": "^0.0.14", "vite": "*" + }, + "bin": { + "intent": "./bin/intent.js" } } diff --git a/packages/virtual-file-routes/skills/_artifacts/domain_map.yaml b/packages/virtual-file-routes/skills/_artifacts/domain_map.yaml new file mode 100644 index 00000000000..fc2b84be488 --- /dev/null +++ b/packages/virtual-file-routes/skills/_artifacts/domain_map.yaml @@ -0,0 +1,64 @@ +# domain_map.yaml +# Library: @tanstack/virtual-file-routes +# Version: 1.161.4 +# Date: 2026-03-08 +# Status: reviewed + +library: + name: '@tanstack/virtual-file-routes' + version: '1.161.4' + repository: 'https://github.com/TanStack/router' + description: >- + Programmatic route tree building as an alternative to filesystem + conventions. Provides rootRoute, index, route, layout, physical, + and defineVirtualSubtreeConfig helpers for explicit route control. + primary_framework: 'framework-agnostic' + +domains: + - name: 'Virtual Route Configuration' + slug: 'virtual-route-config' + description: >- + Building route trees programmatically with helper functions + instead of filesystem conventions. Mixing virtual and physical + routes, subtree configs, and route plugin integration. + +skills: + - name: 'Virtual File Routes' + slug: 'virtual-file-routes' + domain: 'virtual-route-config' + description: >- + Programmatic route tree building with rootRoute, index, route, + layout, physical, defineVirtualSubtreeConfig. Integrates with + TanStack Router plugin's virtualRouteConfig option. + type: core + packages: + - '@tanstack/virtual-file-routes' + covers: + - rootRoute helper + - index helper + - route helper with children + - layout helper + - physical helper for filesystem subtrees + - defineVirtualSubtreeConfig for per-directory config + - Vite plugin virtualRouteConfig option + tasks: + - 'Define routes programmatically' + - 'Mix virtual and physical routes' + - 'Create virtual subtree configs' + failure_modes: + - mistake: 'Forgetting rootRoute wrapper' + mechanism: >- + All virtual route configs must be wrapped in rootRoute(). + Without it, routes are not properly nested under the root. + priority: HIGH + status: active + + - mistake: 'Using physical() path outside routesDirectory' + mechanism: >- + physical() paths are relative to routesDirectory. Using + absolute or wrong-relative paths causes routes to not + be found. + priority: MEDIUM + status: active + +gaps: [] diff --git a/packages/virtual-file-routes/skills/_artifacts/skill_spec.md b/packages/virtual-file-routes/skills/_artifacts/skill_spec.md new file mode 100644 index 00000000000..834294f8f66 --- /dev/null +++ b/packages/virtual-file-routes/skills/_artifacts/skill_spec.md @@ -0,0 +1,24 @@ +# @tanstack/virtual-file-routes — Skill Spec + +Programmatic route tree building as an alternative to filesystem conventions. Provides helper functions for explicit route control. + +## Domains + +| Domain | Description | Skills | +| --------------------------- | ----------------------------------------------------- | ------------------- | +| Virtual Route Configuration | Programmatic route trees, mixing virtual and physical | virtual-file-routes | + +## Skill Inventory + +| Skill | Type | Domain | What it covers | Failure modes | +| ------------------- | ---- | -------------------- | ---------------------------------------------------------- | ------------- | +| virtual-file-routes | core | virtual-route-config | rootRoute, index, route, layout, physical, subtree configs | 2 | + +## Failure Mode Inventory + +### virtual-file-routes (2 failure modes) + +| # | Mistake | Priority | Source | +| --- | --------------------------------------- | -------- | ----------- | +| 1 | Forgetting rootRoute wrapper | HIGH | source/docs | +| 2 | Using physical() path outside routesDir | MEDIUM | source/docs | diff --git a/packages/virtual-file-routes/skills/_artifacts/skill_tree.yaml b/packages/virtual-file-routes/skills/_artifacts/skill_tree.yaml new file mode 100644 index 00000000000..f615e46c8f1 --- /dev/null +++ b/packages/virtual-file-routes/skills/_artifacts/skill_tree.yaml @@ -0,0 +1,23 @@ +library: + name: '@tanstack/virtual-file-routes' + version: '1.161.4' + repository: 'https://github.com/TanStack/router' + description: >- + Programmatic route tree building as an alternative to filesystem + conventions. Provides rootRoute, index, route, layout, physical, + and defineVirtualSubtreeConfig helpers. +generated_at: '2026-03-08' +skills: + - name: 'Virtual File Routes' + slug: 'virtual-file-routes' + type: 'core' + domain: 'virtual-route-config' + path: 'skills/virtual-file-routes/SKILL.md' + package: 'packages/virtual-file-routes' + description: >- + Programmatic route tree building with rootRoute, index, route, + layout, physical, defineVirtualSubtreeConfig. Integrates with + TanStack Router plugin's virtualRouteConfig option. + sources: + - 'TanStack/router:packages/virtual-file-routes/src' + - 'TanStack/router:docs/router/routing/virtual-file-routes.md' diff --git a/packages/virtual-file-routes/skills/virtual-file-routes/SKILL.md b/packages/virtual-file-routes/skills/virtual-file-routes/SKILL.md new file mode 100644 index 00000000000..3e3d821645b --- /dev/null +++ b/packages/virtual-file-routes/skills/virtual-file-routes/SKILL.md @@ -0,0 +1,219 @@ +--- +name: virtual-file-routes +description: >- + Programmatic route tree building as an alternative to filesystem + conventions: rootRoute, index, route, layout, physical, + defineVirtualSubtreeConfig. Use with TanStack Router plugin's + virtualRouteConfig option. +type: core +library: tanstack-router +library_version: '1.161.4' +sources: + - TanStack/router:packages/virtual-file-routes/src + - TanStack/router:docs/router/routing/virtual-file-routes.md +--- + +# Virtual File Routes (`@tanstack/virtual-file-routes`) + +Build route trees programmatically instead of relying on filesystem conventions. Useful when you want explicit control over route structure, need to mix virtual and physical routes, or want to define route subtrees within file-based routing directories. + +> **CRITICAL**: Types are FULLY INFERRED. Never cast, never annotate inferred values. + +## Install + +```bash +npm install @tanstack/virtual-file-routes +``` + +## API Reference + +### `rootRoute(file, children?)` + +Creates the root of a virtual route tree. + +```ts +import { rootRoute, index, route } from '@tanstack/virtual-file-routes' + +const routes = rootRoute('root.tsx', [ + index('index.tsx'), + route('/about', 'about.tsx'), +]) +``` + +### `index(file)` + +Creates an index route — the default rendered when the parent path matches exactly. + +```ts +import { index } from '@tanstack/virtual-file-routes' + +index('home.tsx') +``` + +### `route(path, ...)` + +Creates a route node. Three call signatures: + +```ts +import { route, index } from '@tanstack/virtual-file-routes' + +// Leaf route: path + file +route('/about', 'about.tsx') + +// Branch route: path + file + children +route('/dashboard', 'dashboard.tsx', [ + index('dashboard-index.tsx'), + route('/settings', 'settings.tsx'), +]) + +// Path prefix only (no file): groups children under a URL segment +route('/api', [route('/users', 'users.tsx'), route('/posts', 'posts.tsx')]) +``` + +### `layout(file, children)` or `layout(id, file, children)` + +Creates a pathless layout route — wraps children without adding a URL segment. + +```ts +import { layout, route, index } from '@tanstack/virtual-file-routes' + +// ID derived from filename +layout('authLayout.tsx', [ + route('/dashboard', 'dashboard.tsx'), + route('/settings', 'settings.tsx'), +]) + +// Explicit ID +layout('admin-layout', 'adminLayout.tsx', [route('/admin', 'admin.tsx')]) +``` + +### `physical(pathPrefix, directory)` or `physical(directory)` + +Mounts a directory of file-based routes at a URL prefix. Uses TanStack Router's standard file-based routing conventions within that directory. + +```ts +import { physical } from '@tanstack/virtual-file-routes' + +// Mount posts/ directory under /posts +physical('/posts', 'posts') + +// Merge features/ directory at the current level +physical('features') +``` + +### `defineVirtualSubtreeConfig(config)` + +Type helper for `__virtual.ts` files inside file-based routing directories. Identity function that provides type inference. + +```ts +// src/routes/admin/__virtual.ts +import { + defineVirtualSubtreeConfig, + index, + route, +} from '@tanstack/virtual-file-routes' + +export default defineVirtualSubtreeConfig([ + index('home.tsx'), + route('$id', 'details.tsx'), +]) +``` + +## Integration with Router Plugin + +Pass the virtual route config to the TanStack Router plugin: + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { tanstackRouter } from '@tanstack/router-plugin/vite' +import { routes } from './routes' + +export default defineConfig({ + plugins: [ + tanstackRouter({ + target: 'react', + virtualRouteConfig: routes, + }), + react(), + ], +}) +``` + +Or reference a file path: + +```ts +tanstackRouter({ + target: 'react', + virtualRouteConfig: './routes.ts', +}) +``` + +## Full Example + +```ts +// routes.ts +import { + rootRoute, + route, + index, + layout, + physical, +} from '@tanstack/virtual-file-routes' + +export const routes = rootRoute('root.tsx', [ + index('index.tsx'), + + layout('authLayout.tsx', [ + route('/dashboard', 'app/dashboard.tsx', [ + index('app/dashboard-index.tsx'), + route('/invoices', 'app/dashboard-invoices.tsx', [ + index('app/invoices-index.tsx'), + route('$id', 'app/invoice-detail.tsx'), + ]), + ]), + ]), + + // Mount file-based routing from posts/ directory + physical('/posts', 'posts'), +]) +``` + +## Common Mistakes + +### 1. HIGH: Forgetting that file paths are relative to routesDirectory + +File paths in `rootRoute`, `index`, `route`, and `layout` are relative to the `routesDirectory` configured in the router plugin (default: `./src/routes`). Do not use absolute paths or paths relative to the project root. + +```ts +// WRONG — absolute path +route('/about', '/src/routes/about.tsx') + +// CORRECT — relative to routesDirectory +route('/about', 'about.tsx') +``` + +### 2. MEDIUM: Using physical() without matching directory structure + +The directory passed to `physical()` must exist inside `routesDirectory` and follow TanStack Router's file-based routing conventions. + +```ts +// WRONG — directory doesn't exist or wrong location +physical('/blog', 'src/blog') + +// CORRECT — relative to routesDirectory +physical('/blog', 'blog') +// Expects: src/routes/blog/ (with route files inside) +``` + +### 3. MEDIUM: Confusing layout() with route() + +`layout()` creates a pathless wrapper — it does NOT add a URL segment. Use `route()` for URL segments. + +```ts +// This does NOT create a /dashboard URL +layout('dashboardLayout.tsx', [route('/dashboard', 'dashboard.tsx')]) + +// The URL is /dashboard, and dashboardLayout.tsx wraps it +``` diff --git a/packages/vue-router/bin/intent.js b/packages/vue-router/bin/intent.js new file mode 100755 index 00000000000..2cf2efab472 --- /dev/null +++ b/packages/vue-router/bin/intent.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// Auto-generated by @tanstack/intent setup +// Exposes the intent end-user CLI for consumers of this library. +// Commit this file, then add to your package.json: +// "bin": { "intent": "./bin/intent.js" } +try { + await import('@tanstack/intent/intent-library') +} catch (e) { + if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + console.error('@tanstack/intent is not installed.') + console.error('') + console.error('Install it as a dev dependency:') + console.error(' npm add -D @tanstack/intent') + console.error('') + console.error('Or run directly:') + console.error(' npx @tanstack/intent@latest list') + process.exit(1) + } + throw e +} diff --git a/packages/vue-router/package.json b/packages/vue-router/package.json index 1eedffd27b9..1edf52e892f 100644 --- a/packages/vue-router/package.json +++ b/packages/vue-router/package.json @@ -64,7 +64,10 @@ "sideEffects": false, "files": [ "dist", - "src" + "src", + "skills", + "bin", + "!skills/_artifacts" ], "engines": { "node": ">=20.19" @@ -80,6 +83,7 @@ "tiny-warning": "^1.0.3" }, "devDependencies": { + "@tanstack/intent": "^0.0.14", "@testing-library/jest-dom": "^6.6.3", "@testing-library/vue": "^8.1.0", "@types/jsesc": "^3.0.3", @@ -92,5 +96,8 @@ }, "peerDependencies": { "vue": "^3.3.0" + }, + "bin": { + "intent": "./bin/intent.js" } } diff --git a/packages/vue-router/skills/_artifacts/domain_map.yaml b/packages/vue-router/skills/_artifacts/domain_map.yaml new file mode 100644 index 00000000000..d38fba35012 --- /dev/null +++ b/packages/vue-router/skills/_artifacts/domain_map.yaml @@ -0,0 +1,64 @@ +# domain_map.yaml +# Library: @tanstack/vue-router +# Version: 1.166.2 +# Date: 2026-03-08 +# Status: reviewed + +library: + name: '@tanstack/vue-router' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Vue bindings for TanStack Router. Ref<T> returns, + defineComponent, h() render functions, provide/inject, + Html/Body components. + primary_framework: 'vue' + +domains: + - name: 'Vue Router Bindings' + slug: 'vue-router-bindings' + description: >- + Vue-specific components, composables, and render functions + for TanStack Router. Ref-based returns, provide/inject, + and Html/Body SSR components. + +skills: + - name: 'Vue Router' + slug: 'vue-router' + domain: 'vue-router-bindings' + description: >- + Vue bindings for TanStack Router. Ref<T> returns, + defineComponent, h() render functions, provide/inject, + Html/Body components. + type: framework + packages: + - '@tanstack/vue-router' + covers: + - Vue-specific router components + - Ref<T> return types + - defineComponent usage + - h() render functions + - provide/inject integration + - Html/Body SSR components + tasks: + - 'Set up TanStack Router in a Vue app' + - 'Create components with defineComponent and h()' + - 'Use provide/inject for router context' + failure_modes: + - mistake: 'Not unwrapping Ref values in templates' + mechanism: >- + Vue Router returns Ref<T> from composables. Forgetting + .value in script or using incorrectly in templates + causes rendering of Ref objects instead of values. + priority: HIGH + status: active + + - mistake: 'Missing provide/inject setup for router context' + mechanism: >- + Vue Router uses provide/inject for context propagation. + Not providing the router instance at the app root causes + injection errors in child components. + priority: MEDIUM + status: active + +gaps: [] diff --git a/packages/vue-router/skills/_artifacts/skill_spec.md b/packages/vue-router/skills/_artifacts/skill_spec.md new file mode 100644 index 00000000000..2dc319246d7 --- /dev/null +++ b/packages/vue-router/skills/_artifacts/skill_spec.md @@ -0,0 +1,24 @@ +# @tanstack/vue-router — Skill Spec + +Vue bindings for TanStack Router. Ref<T> returns, defineComponent, h() render functions, provide/inject, Html/Body components. + +## Domains + +| Domain | Description | Skills | +| ------------------- | ------------------------------------------------------------- | ---------- | +| Vue Router Bindings | Vue components, composables, Ref returns, provide/inject, SSR | vue-router | + +## Skill Inventory + +| Skill | Type | Domain | What it covers | Failure modes | +| ---------- | --------- | ------------------- | --------------------------------------------------------------- | ------------- | +| vue-router | framework | vue-router-bindings | Ref<T> returns, defineComponent, h(), provide/inject, Html/Body | 2 | + +## Failure Mode Inventory + +### vue-router (2 failure modes) + +| # | Mistake | Priority | Source | +| --- | ----------------------------------------------- | -------- | ----------- | +| 1 | Not unwrapping Ref values in templates | HIGH | source/docs | +| 2 | Missing provide/inject setup for router context | MEDIUM | source/docs | diff --git a/packages/vue-router/skills/_artifacts/skill_tree.yaml b/packages/vue-router/skills/_artifacts/skill_tree.yaml new file mode 100644 index 00000000000..5ec5621d9c5 --- /dev/null +++ b/packages/vue-router/skills/_artifacts/skill_tree.yaml @@ -0,0 +1,22 @@ +library: + name: '@tanstack/vue-router' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Vue bindings for TanStack Router. Ref<T> returns, + defineComponent, h() render functions, provide/inject, + Html/Body components. +generated_at: '2026-03-08' +skills: + - name: 'Vue Router' + slug: 'vue-router' + type: 'framework' + domain: 'vue-router-bindings' + path: 'skills/vue-router/SKILL.md' + package: 'packages/vue-router' + description: >- + Vue bindings for TanStack Router. Ref<T> returns, + defineComponent, h() render functions, provide/inject, + Html/Body components. + sources: + - 'TanStack/router:packages/vue-router/src' diff --git a/packages/vue-router/skills/vue-router/SKILL.md b/packages/vue-router/skills/vue-router/SKILL.md new file mode 100644 index 00000000000..2744c716013 --- /dev/null +++ b/packages/vue-router/skills/vue-router/SKILL.md @@ -0,0 +1,387 @@ +--- +name: vue-router +description: >- + Vue bindings for TanStack Router: RouterProvider, useRouter, + useRouterState, useMatch, useMatches, useLocation, useSearch, + useParams, useNavigate, useLoaderData, useLoaderDeps, + useRouteContext, useBlocker, useCanGoBack, Link, Navigate, + Outlet, CatchBoundary, ErrorComponent, Html, Body. + Vue-specific patterns with Ref<T> returns, defineComponent, + h() render functions, provide/inject, and computed refs. +type: framework +library: tanstack-router +library_version: '1.166.2' +framework: vue +requires: + - router-core +sources: + - TanStack/router:packages/vue-router/src +--- + +# Vue Router (`@tanstack/vue-router`) + +This skill builds on router-core. Read [router-core](../../../router-core/skills/router-core/SKILL.md) first for foundational concepts. + +This skill covers the Vue-specific bindings, components, composables, and setup for TanStack Router. + +> **CRITICAL**: TanStack Router types are FULLY INFERRED. Never cast, never annotate inferred values. + +> **CRITICAL**: TanStack Router is CLIENT-FIRST. Loaders run on the client by default, not on the server. + +> **CRITICAL**: Most composables return `Ref<T>` — access via `.value` in script, auto-unwrapped in templates. This is the #1 difference from the React version. + +> **CRITICAL**: Do not confuse `@tanstack/vue-router` with `vue-router` (the official Vue router). They are completely different libraries with different APIs. + +## Full Setup: File-Based Routing with Vite + +### 1. Install Dependencies + +```bash +npm install @tanstack/vue-router +npm install -D @tanstack/router-plugin +``` + +### 2. Configure Vite Plugin + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +export default defineConfig({ + plugins: [ + // MUST come before vue() + tanstackRouter({ + target: 'vue', + autoCodeSplitting: true, + }), + vue(), + ], +}) +``` + +### 3. Create Root Route + +```tsx +// src/routes/__root.tsx +import { createRootRoute, Link, Outlet } from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootLayout, +}) + +function RootLayout() { + return ( + <> + <nav> + <Link to="/" activeProps={{ class: 'font-bold' }}> + Home + </Link> + <Link to="/about" activeProps={{ class: 'font-bold' }}> + About + </Link> + </nav> + <hr /> + <Outlet /> + </> + ) +} +``` + +### 4. Create Route Files + +```tsx +// src/routes/index.tsx +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return <h1>Welcome Home</h1> +} +``` + +### 5. Create Router Instance and Register Types + +```tsx +// src/main.tsx +import { createApp } from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +const router = createRouter({ routeTree }) + +// REQUIRED — without this, Link/useNavigate/useSearch have no type safety +declare module '@tanstack/vue-router' { + interface Register { + router: typeof router + } +} + +const app = createApp(RouterProvider, { router }) +app.mount('#root') +``` + +## Composables Reference + +All composables imported from `@tanstack/vue-router`. Most return `Ref<T>` — access via `.value` in script or auto-unwrap in templates. + +### `useRouter()` — returns `TRouter` (NOT a Ref) + +```tsx +import { useRouter } from '@tanstack/vue-router' + +const router = useRouter() +router.invalidate() +``` + +### `useRouterState()` — returns `Ref<T>` + +```tsx +import { useRouterState } from '@tanstack/vue-router' + +const isLoading = useRouterState({ select: (s) => s.isLoading }) +// Access: isLoading.value +``` + +### `useNavigate()` — returns a function (NOT a Ref) + +```tsx +import { useNavigate } from '@tanstack/vue-router' + +const navigate = useNavigate() + +async function handleSubmit() { + await saveData() + navigate({ to: '/posts/$postId', params: { postId: '123' } }) +} +``` + +### `useSearch({ from })` — returns `Ref<T>` + +```tsx +import { useSearch } from '@tanstack/vue-router' + +const search = useSearch({ from: '/products' }) +// Access: search.value.page +``` + +### `useParams({ from })` — returns `Ref<T>` + +```tsx +import { useParams } from '@tanstack/vue-router' + +const params = useParams({ from: '/posts/$postId' }) +// Access: params.value.postId +``` + +### `useLoaderData({ from })` — returns `Ref<T>` + +```tsx +import { useLoaderData } from '@tanstack/vue-router' + +const data = useLoaderData({ from: '/posts/$postId' }) +// Access: data.value.post.content +``` + +### `useMatch({ from })` — returns `Ref<T>` + +```tsx +import { useMatch } from '@tanstack/vue-router' + +const match = useMatch({ from: '/posts/$postId' }) +// Access: match.value.loaderData.post.title +``` + +### Other Composables + +- **`useMatches()`** — `Ref<Array<Match>>`, all active route matches +- **`useRouteContext({ from })`** — `Ref<T>`, context from `beforeLoad` +- **`useBlocker({ shouldBlockFn })`** — blocks navigation for unsaved changes +- **`useCanGoBack()`** — `Ref<boolean>` +- **`useLocation()`** — `Ref<ParsedLocation>` +- **`useLoaderDeps({ from })`** — returns raw value (NOT a Ref) +- **`useLinkProps()`** — returns `ComputedRef<LinkHTMLAttributes>` +- **`useMatchRoute()`** — returns a function; calling it returns `Ref<false | Params>` + +## Components Reference + +### `RouterProvider` + +```tsx +import { RouterProvider } from '@tanstack/vue-router' +// In createApp or template +<RouterProvider :router="router" /> +``` + +### `Link` + +Type-safe navigation link with scoped slot for active state: + +```tsx +<Link to="/posts/$postId" :params="{ postId: '42' }"> + View Post +</Link> + +{/* Scoped slot for active state */} +<Link to="/about"> + <template #default="{ isActive }"> + <span :class="{ active: isActive }">About</span> + </template> +</Link> +``` + +### `Outlet` + +Renders the matched child route component. + +### `Navigate` + +Declarative redirect (triggers navigation in `onMounted`). + +### `Await` + +Async setup component for deferred data — use with Vue's `<Suspense>`. + +### `CatchBoundary` + +Error boundary using Vue's `onErrorCaptured`. + +### `Html` and `Body` + +Vue-specific SSR shell components: + +```tsx +function RootComponent() { + return ( + <Html> + <head> + <HeadContent /> + </head> + <Body> + <Outlet /> + <Scripts /> + </Body> + </Html> + ) +} +``` + +### `ClientOnly` + +Renders children only after `onMounted` (hydration complete): + +```tsx +<ClientOnly fallback={<div>Loading...</div>}> + <BrowserOnlyWidget /> +</ClientOnly> +``` + +## Vue-Specific Patterns + +### Custom Link Component with `createLink` + +```tsx +import { createLink } from '@tanstack/vue-router' +import { defineComponent, h } from 'vue' + +const StyledLinkComponent = defineComponent({ + setup(props, { slots, attrs }) { + return () => h('a', { ...attrs, class: 'styled-link' }, slots.default?.()) + }, +}) + +const StyledLink = createLink(StyledLinkComponent) +``` + +### Render Functions (h()) + +All components in `@tanstack/vue-router` use `h()` render functions internally. Route components can use either SFC templates or render functions: + +```tsx +// SFC template (most common for user code) +// MyRoute.component.vue +<template> + <div>{{ data.title }}</div> +</template> + +<script setup> +import { useLoaderData } from '@tanstack/vue-router' +const data = useLoaderData({ from: '/posts/$postId' }) +</script> +``` + +### Auth with Router Context + +```tsx +import { createRootRouteWithContext } from '@tanstack/vue-router' + +const rootRoute = createRootRouteWithContext<{ auth: AuthState }>()({ + component: RootComponent, +}) + +const router = createRouter({ + routeTree, + context: { auth: authState }, +}) + +// In a route — access via beforeLoad +beforeLoad: ({ context }) => { + if (!context.auth.isAuthenticated) { + throw redirect({ to: '/login' }) + } +} +``` + +### Vue File Conventions for Code Splitting + +With `autoCodeSplitting`, Vue routes support split-file conventions: + +- `myRoute.route.ts` — route configuration (search params, loader, beforeLoad) +- `myRoute.component.vue` — route component (lazy-loaded) +- `myRoute.errorComponent.vue` — error component (lazy-loaded) +- `myRoute.notFoundComponent.vue` — not-found component (lazy-loaded) +- `myRoute.lazy.ts` — lazy-loaded route options + +## Common Mistakes + +### 1. CRITICAL: Forgetting .value in script blocks + +Composables return `Ref<T>` — access via `.value` in `<script>`. Templates auto-unwrap. + +```tsx +// WRONG — accessing Ref without .value in script +const params = useParams({ from: '/posts/$postId' }) +console.log(params.postId) // undefined! + +// CORRECT — use .value +const params = useParams({ from: '/posts/$postId' }) +console.log(params.value.postId) +``` + +### 2. HIGH: Confusing with vue-router (official) + +`@tanstack/vue-router` is NOT `vue-router`. Do not use `<router-view>`, `<router-link>`, `useRoute()`, `useRouter()` from `vue-router`. + +```ts +// WRONG — official vue-router imports +import { useRoute, useRouter } from 'vue-router' + +// CORRECT — TanStack Vue Router imports +import { useMatch, useRouter } from '@tanstack/vue-router' +``` + +### 3. HIGH: Using Vue hooks in beforeLoad or loader + +`beforeLoad` and `loader` are NOT component setup functions — they are plain async functions. Vue composables cannot be used in them. Pass state via router context instead. + +### 4. MEDIUM: Wrong plugin target + +Must set `target: 'vue'` in the router plugin config. Default is `'react'`. + +## Cross-References + +- [router-core/SKILL.md](../../../router-core/skills/router-core/SKILL.md) — all sub-skills for domain-specific patterns (search params, data loading, navigation, auth, SSR, etc.) diff --git a/packages/vue-start/bin/intent.js b/packages/vue-start/bin/intent.js new file mode 100755 index 00000000000..2cf2efab472 --- /dev/null +++ b/packages/vue-start/bin/intent.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// Auto-generated by @tanstack/intent setup +// Exposes the intent end-user CLI for consumers of this library. +// Commit this file, then add to your package.json: +// "bin": { "intent": "./bin/intent.js" } +try { + await import('@tanstack/intent/intent-library') +} catch (e) { + if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + console.error('@tanstack/intent is not installed.') + console.error('') + console.error('Install it as a dev dependency:') + console.error(' npm add -D @tanstack/intent') + console.error('') + console.error('Or run directly:') + console.error(' npx @tanstack/intent@latest list') + process.exit(1) + } + throw e +} diff --git a/packages/vue-start/package.json b/packages/vue-start/package.json index 79de835fd8b..aa0b9e200ee 100644 --- a/packages/vue-start/package.json +++ b/packages/vue-start/package.json @@ -97,7 +97,10 @@ "sideEffects": false, "files": [ "dist", - "src" + "src", + "skills", + "bin", + "!skills/_artifacts" ], "engines": { "node": ">=22.12.0" @@ -112,6 +115,7 @@ "pathe": "^2.0.3" }, "devDependencies": { + "@tanstack/intent": "^0.0.14", "@tanstack/router-utils": "workspace:*", "@vitejs/plugin-vue-jsx": "^4.1.2", "vite": "*", @@ -120,5 +124,8 @@ "peerDependencies": { "vue": "^3.3.0", "vite": ">=7.0.0" + }, + "bin": { + "intent": "./bin/intent.js" } } diff --git a/packages/vue-start/skills/_artifacts/domain_map.yaml b/packages/vue-start/skills/_artifacts/domain_map.yaml new file mode 100644 index 00000000000..95c1be0f4ec --- /dev/null +++ b/packages/vue-start/skills/_artifacts/domain_map.yaml @@ -0,0 +1,63 @@ +# domain_map.yaml +# Library: @tanstack/vue-start +# Version: 1.166.2 +# Date: 2026-03-08 +# Status: reviewed + +library: + name: '@tanstack/vue-start' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Vue bindings for TanStack Start. useServerFn hook, + tanstackStart Vite plugin, Html/Body SSR shell, + Vue-specific setup. + primary_framework: 'vue' + +domains: + - name: 'Vue Start Bindings' + slug: 'vue-start-bindings' + description: >- + Vue-specific full-stack framework bindings for TanStack + Start. Server function hooks, Vite plugin, Html/Body + SSR shell, and Vue application setup. + +skills: + - name: 'Vue Start' + slug: 'vue-start' + domain: 'vue-start-bindings' + description: >- + Vue bindings for TanStack Start. useServerFn hook, + tanstackStart Vite plugin, Html/Body SSR shell, + Vue-specific setup. + type: framework + packages: + - '@tanstack/vue-start' + covers: + - useServerFn hook + - tanstackStart Vite plugin + - Html/Body SSR shell components + - Vue-specific app setup + - Server function integration + tasks: + - 'Set up a Vue Start application' + - 'Use server functions with useServerFn' + - 'Configure Html/Body SSR shell' + failure_modes: + - mistake: 'Using react-start APIs in Vue context' + mechanism: >- + vue-start has its own useServerFn and tanstackStart + exports. Importing from @tanstack/react-start or + @tanstack/start causes type errors and runtime failures. + priority: HIGH + status: active + + - mistake: 'Missing Html/Body SSR shell setup' + mechanism: >- + Vue Start requires Html and Body components for the + SSR document shell. Without them, server-rendered + pages lack proper document structure. + priority: MEDIUM + status: active + +gaps: [] diff --git a/packages/vue-start/skills/_artifacts/skill_spec.md b/packages/vue-start/skills/_artifacts/skill_spec.md new file mode 100644 index 00000000000..901f94383b8 --- /dev/null +++ b/packages/vue-start/skills/_artifacts/skill_spec.md @@ -0,0 +1,24 @@ +# @tanstack/vue-start — Skill Spec + +Vue bindings for TanStack Start. useServerFn hook, tanstackStart Vite plugin, Html/Body SSR shell, Vue-specific setup. + +## Domains + +| Domain | Description | Skills | +| ------------------ | ----------------------------------------------------------- | --------- | +| Vue Start Bindings | Vue full-stack bindings, server functions, Vite plugin, SSR | vue-start | + +## Skill Inventory + +| Skill | Type | Domain | What it covers | Failure modes | +| --------- | --------- | ------------------ | ------------------------------------------------------------ | ------------- | +| vue-start | framework | vue-start-bindings | useServerFn, tanstackStart Vite plugin, Html/Body SSR, setup | 2 | + +## Failure Mode Inventory + +### vue-start (2 failure modes) + +| # | Mistake | Priority | Source | +| --- | ------------------------------------- | -------- | ----------- | +| 1 | Using react-start APIs in Vue context | HIGH | source/docs | +| 2 | Missing Html/Body SSR shell setup | MEDIUM | source/docs | diff --git a/packages/vue-start/skills/_artifacts/skill_tree.yaml b/packages/vue-start/skills/_artifacts/skill_tree.yaml new file mode 100644 index 00000000000..905f28b0421 --- /dev/null +++ b/packages/vue-start/skills/_artifacts/skill_tree.yaml @@ -0,0 +1,22 @@ +library: + name: '@tanstack/vue-start' + version: '1.166.2' + repository: 'https://github.com/TanStack/router' + description: >- + Vue bindings for TanStack Start. useServerFn hook, + tanstackStart Vite plugin, Html/Body SSR shell, + Vue-specific setup. +generated_at: '2026-03-08' +skills: + - name: 'Vue Start' + slug: 'vue-start' + type: 'framework' + domain: 'vue-start-bindings' + path: 'skills/vue-start/SKILL.md' + package: 'packages/vue-start' + description: >- + Vue bindings for TanStack Start. useServerFn hook, + tanstackStart Vite plugin, Html/Body SSR shell, + Vue-specific setup. + sources: + - 'TanStack/router:packages/vue-start/src' diff --git a/packages/vue-start/skills/vue-start/SKILL.md b/packages/vue-start/skills/vue-start/SKILL.md new file mode 100644 index 00000000000..628624d0423 --- /dev/null +++ b/packages/vue-start/skills/vue-start/SKILL.md @@ -0,0 +1,302 @@ +--- +name: vue-start +description: >- + Vue bindings for TanStack Start: useServerFn hook, tanstackStart + Vite plugin, StartClient, StartServer, Vue-specific setup, + re-exports from @tanstack/start-client-core. Full project setup + with Vue. +type: framework +library: tanstack-start +library_version: '1.166.2' +framework: vue +requires: + - start-core +sources: + - TanStack/router:packages/vue-start/src +--- + +# Vue Start (`@tanstack/vue-start`) + +This skill builds on start-core. Read [start-core](../../../start-client-core/skills/start-core/SKILL.md) first for foundational concepts. + +This skill covers the Vue-specific bindings, setup, and patterns for TanStack Start. + +> **CRITICAL**: All code is ISOMORPHIC by default. Loaders run on BOTH server and client. Use `createServerFn` for server-only logic. + +> **CRITICAL**: Do not confuse `@tanstack/vue-start` with Nuxt. They are completely different frameworks with different APIs. + +> **CRITICAL**: Types are FULLY INFERRED. Never cast, never annotate inferred values. + +## Package API Surface + +`@tanstack/vue-start` re-exports everything from `@tanstack/start-client-core` plus: + +- `useServerFn` — Vue composable for calling server functions from components + +All core APIs (`createServerFn`, `createMiddleware`, `createStart`, `createIsomorphicFn`, `createServerOnlyFn`, `createClientOnlyFn`) are available from `@tanstack/vue-start`. + +Server utilities (`getRequest`, `getRequestHeader`, `setResponseHeader`, `setCookie`, `getCookie`, `useSession`) are imported from `@tanstack/vue-start/server`. + +## Full Project Setup + +### 1. Install Dependencies + +```bash +npm i @tanstack/vue-start @tanstack/vue-router vue +npm i -D vite @vitejs/plugin-vue typescript +``` + +### 2. package.json + +```json +{ + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "start": "node .output/server/index.mjs" + } +} +``` + +### 3. tsconfig.json + +```json +{ + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ES2022", + "skipLibCheck": true, + "strictNullChecks": true + } +} +``` + +### 4. vite.config.ts + +```ts +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vuePlugin from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [ + tanstackStart(), // MUST come before vue plugin + vuePlugin(), + ], +}) +``` + +### 5. Router Factory (src/router.tsx) + +```tsx +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + const router = createRouter({ + routeTree, + scrollRestoration: true, + }) + return router +} +``` + +### 6. Root Route (src/routes/\_\_root.tsx) + +```tsx +import { + Outlet, + createRootRoute, + HeadContent, + Scripts, + Html, + Body, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: 'My TanStack Start App' }, + ], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + <Html> + <head> + <HeadContent /> + </head> + <Body> + <Outlet /> + <Scripts /> + </Body> + </Html> + ) +} +``` + +### 7. Index Route (src/routes/index.tsx) + +```tsx +import { createFileRoute } from '@tanstack/vue-router' +import { createServerFn } from '@tanstack/vue-start' + +const getGreeting = createServerFn({ method: 'GET' }).handler(async () => { + return 'Hello from TanStack Start!' +}) + +export const Route = createFileRoute('/')({ + loader: () => getGreeting(), + component: HomePage, +}) + +function HomePage() { + const greeting = Route.useLoaderData() + return <h1>{greeting.value}</h1> +} +``` + +## useServerFn Composable + +Use `useServerFn` to call server functions from Vue components with automatic redirect handling: + +```tsx +import { createServerFn, useServerFn } from '@tanstack/vue-start' +import { ref } from 'vue' + +const updatePost = createServerFn({ method: 'POST' }) + .inputValidator((data: { id: string; title: string }) => data) + .handler(async ({ data }) => { + await db.posts.update(data.id, { title: data.title }) + return { success: true } + }) + +// In a component setup: +const updatePostFn = useServerFn(updatePost) +const title = ref('') + +async function handleSubmit(postId: string) { + await updatePostFn({ data: { id: postId, title: title.value } }) +} +``` + +Unlike the React version, `useServerFn` does NOT wrap the returned function in `useCallback` — Vue's `setup()` runs once per component instance, so no memoization is needed. + +## Vue-Specific Components + +All routing components from `@tanstack/vue-router` work in Start: + +- `<Outlet>` — renders matched child route +- `<Link>` — type-safe navigation with scoped slots +- `<Navigate>` — declarative redirect +- `<HeadContent>` — renders head tags (must be in `<head>`) +- `<Scripts>` — renders body scripts (must be in `<body>`) +- `<Await>` — renders deferred data with Vue `<Suspense>` +- `<ClientOnly>` — renders children only after `onMounted` +- `<CatchBoundary>` — error boundary via `onErrorCaptured` +- `<Html>` — SSR shell `<html>` wrapper +- `<Body>` — SSR shell `<body>` wrapper + +## Composables Reference + +All composables from `@tanstack/vue-router` work in Start. Most return `Ref<T>` — access via `.value`: + +- `useRouter()` — router instance (NOT a Ref) +- `useRouterState()` — `Ref<T>`, subscribe to router state +- `useNavigate()` — navigation function (NOT a Ref) +- `useSearch({ from })` — `Ref<T>`, validated search params +- `useParams({ from })` — `Ref<T>`, path params +- `useLoaderData({ from })` — `Ref<T>`, loader data +- `useMatch({ from })` — `Ref<T>`, full route match +- `useRouteContext({ from })` — `Ref<T>`, route context +- `Route.useLoaderData()` — `Ref<T>`, typed loader data (preferred in route files) +- `Route.useSearch()` — `Ref<T>`, typed search params (preferred in route files) + +## Common Mistakes + +### 1. CRITICAL: Importing from wrong package + +```tsx +// WRONG — this is the SPA router, NOT Start +import { createServerFn } from '@tanstack/vue-router' + +// CORRECT — server functions come from vue-start +import { createServerFn } from '@tanstack/vue-start' + +// CORRECT — routing APIs come from vue-router +import { createFileRoute, Link } from '@tanstack/vue-router' +``` + +### 2. CRITICAL: Forgetting .value in script blocks + +Most composables return `Ref<T>`. In `<script>`, access via `.value`. + +```tsx +// WRONG +const data = Route.useLoaderData() +console.log(data.message) // undefined! + +// CORRECT +const data = Route.useLoaderData() +console.log(data.value.message) +``` + +### 3. HIGH: Missing Scripts component + +Without `<Scripts />` in the root route's `<body>`, client JavaScript doesn't load and the app won't hydrate. + +### 4. HIGH: Vue plugin before Start plugin in Vite config + +```ts +// WRONG +plugins: [vuePlugin(), tanstackStart()] + +// CORRECT +plugins: [tanstackStart(), vuePlugin()] +``` + +### 5. HIGH: Using Html/Body incorrectly + +Vue Start uses `<Html>` and `<Body>` components for the SSR document shell. On the server they render `<html>` and `<body>` tags; on the client they handle hydration properly. + +```tsx +// WRONG — plain HTML tags can cause hydration mismatches +function RootComponent() { + return ( + <html> + <body> + <Outlet /> + </body> + </html> + ) +} + +// CORRECT — use Html and Body components +function RootComponent() { + return ( + <Html> + <head> + <HeadContent /> + </head> + <Body> + <Outlet /> + <Scripts /> + </Body> + </Html> + ) +} +``` + +## Cross-References + +- [start-core](../../../start-client-core/skills/start-core/SKILL.md) — core Start concepts +- [router-core](../../../router-core/skills/router-core/SKILL.md) — routing fundamentals +- [vue-router](../../../vue-router/skills/vue-router/SKILL.md) — Vue Router composables and components diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59d051d753d..e3f5aac6dc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11950,6 +11950,9 @@ importers: specifier: ^1.0.3 version: 1.0.3 devDependencies: + '@tanstack/intent': + specifier: ^0.0.14 + version: 0.0.14 '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -12066,6 +12069,10 @@ importers: vite: specifier: ^7.3.1 version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + devDependencies: + '@tanstack/intent': + specifier: ^0.0.14 + version: 0.0.14 packages/react-start-client: dependencies: @@ -12178,6 +12185,9 @@ importers: specifier: ^1.0.3 version: 1.0.3 devDependencies: + '@tanstack/intent': + specifier: ^0.0.14 + version: 0.0.14 esbuild: specifier: ^0.25.0 version: 0.25.4 @@ -12335,6 +12345,9 @@ importers: specifier: ^3.24.2 version: 3.25.57 devDependencies: + '@tanstack/intent': + specifier: ^0.0.14 + version: 0.0.14 '@types/babel__core': specifier: ^7.20.5 version: 7.20.5 @@ -12443,6 +12456,9 @@ importers: '@solidjs/testing-library': specifier: ^0.8.10 version: 0.8.10(solid-js@1.9.10) + '@tanstack/intent': + specifier: ^0.0.14 + version: 0.0.14 '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -12542,6 +12558,9 @@ importers: specifier: 1.9.10 version: 1.9.10 devDependencies: + '@tanstack/intent': + specifier: ^0.0.14 + version: 0.0.14 '@tanstack/router-utils': specifier: workspace:* version: link:../router-utils @@ -12638,6 +12657,9 @@ importers: specifier: ^1.0.3 version: 1.0.3 devDependencies: + '@tanstack/intent': + specifier: ^0.0.14 + version: 0.0.14 vite: specifier: ^7.3.1 version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) @@ -12754,6 +12776,9 @@ importers: '@standard-schema/spec': specifier: ^1.0.0 version: 1.0.0 + '@tanstack/intent': + specifier: ^0.0.14 + version: 0.0.14 cookie-es: specifier: ^2.0.0 version: 2.0.0 @@ -12819,6 +12844,9 @@ importers: packages/virtual-file-routes: devDependencies: + '@tanstack/intent': + specifier: ^0.0.14 + version: 0.0.14 vite: specifier: ^7.3.1 version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) @@ -12850,6 +12878,9 @@ importers: specifier: ^1.0.3 version: 1.0.3 devDependencies: + '@tanstack/intent': + specifier: ^0.0.14 + version: 0.0.14 '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -12949,6 +12980,9 @@ importers: specifier: ^2.0.3 version: 2.0.3 devDependencies: + '@tanstack/intent': + specifier: ^0.0.14 + version: 0.0.14 '@tanstack/router-utils': specifier: workspace:* version: link:../router-utils @@ -18039,6 +18073,10 @@ packages: peerDependencies: eslint: ^9.22.0 + '@tanstack/intent@0.0.14': + resolution: {integrity: sha512-83cjen+tYll6z0MIN0JC9GuMljUkfYfHFDOLhmROz4XMmbD/0ZjvKNOsFgpO7eq957XHB/qyK+hCO+3OS/D6HQ==} + hasBin: true + '@tanstack/match-sorter-utils@8.19.4': resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} engines: {node: '>=12'} @@ -30980,6 +31018,10 @@ snapshots: - supports-color - typescript + '@tanstack/intent@0.0.14': + dependencies: + yaml: 2.8.1 + '@tanstack/match-sorter-utils@8.19.4': dependencies: remove-accents: 0.5.0 From baf9f5d829f1b1163d7054848e87c72a2275b659 Mon Sep 17 00:00:00 2001 From: Tanner Linsley <tannerlinsley@gmail.com> Date: Mon, 9 Mar 2026 15:22:58 -0600 Subject: [PATCH 2/3] fix: address PR review comments on skill files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix incorrect createTanstackRouter → createRouter in router-query examples - Fix missing React type imports (ComponentPropsWithoutRef, FormEvent) - Fix hardcoded localhost:3000 in SSR example, use req headers instead - Fix session.update/clear to use await with data payload - Fix start-setup → start-core slug mismatch in domain_map + skill_spec - Make start-client-core skill_tree self-contained (remove react-start refs) - Add framework-agnostic comments to execution-model and middleware skills - Make virtual-file-routes vite example framework-agnostic - Fix solid-router domain_map: Link is primary, createLink for wrappers - Fix vue-router domain_map: scope Ref unwrapping to script, not templates - Fix vue-router SKILL.md: tsx → vue fence language for template snippets - Fix virtual-file-routes domain_map failure mode wording - Fix MD028 blockquote blank lines across 11 files - Fix MD040 missing fence languages across 4 files - Update pnpm-lock.yaml --- .../skills/compositions/router-query/SKILL.md | 4 +- .../migrate-from-react-router/SKILL.md | 3 +- .../react-router/skills/react-router/SKILL.md | 6 +-- .../router-core/auth-and-guards/SKILL.md | 4 +- .../router-core/not-found-and-errors/SKILL.md | 1 - .../skills/router-core/search-params/SKILL.md | 1 - .../skills/router-core/ssr/SKILL.md | 4 +- .../skills/router-core/type-safety/SKILL.md | 3 +- .../skills/_artifacts/domain_map.yaml | 7 +-- .../solid-router/skills/solid-router/SKILL.md | 5 -- .../skills/_artifacts/domain_map.yaml | 4 +- .../skills/_artifacts/skill_spec.md | 6 +-- .../skills/_artifacts/skill_tree.yaml | 52 +++---------------- .../skills/start-core/SKILL.md | 4 +- .../start-core/execution-model/SKILL.md | 9 +++- .../skills/start-core/middleware/SKILL.md | 6 ++- .../start-core/server-functions/SKILL.md | 3 +- .../skills/start-core/server-routes/SKILL.md | 2 +- .../skills/start-server-core/SKILL.md | 4 +- .../skills/_artifacts/domain_map.yaml | 16 +++--- .../skills/virtual-file-routes/SKILL.md | 5 +- .../skills/_artifacts/domain_map.yaml | 9 ++-- .../vue-router/skills/vue-router/SKILL.md | 9 ++-- pnpm-lock.yaml | 24 ++++----- 24 files changed, 76 insertions(+), 115 deletions(-) diff --git a/packages/react-router/skills/compositions/router-query/SKILL.md b/packages/react-router/skills/compositions/router-query/SKILL.md index 5be24645b9c..ac0f5d4b13b 100644 --- a/packages/react-router/skills/compositions/router-query/SKILL.md +++ b/packages/react-router/skills/compositions/router-query/SKILL.md @@ -341,7 +341,7 @@ A module-level singleton `QueryClient` is shared across all server requests, lea // WRONG — shared across SSR requests const queryClient = new QueryClient() export function createRouter() { - return createTanstackRouter({ + return createRouter({ routeTree, context: { queryClient }, }) @@ -350,7 +350,7 @@ export function createRouter() { // CORRECT — new QueryClient per createRouter call export function createRouter() { const queryClient = new QueryClient() - return createTanstackRouter({ + return createRouter({ routeTree, context: { queryClient }, }) diff --git a/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md b/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md index 6ef7d11be1b..f513a0bdaf2 100644 --- a/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md +++ b/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md @@ -21,7 +21,6 @@ sources: This is a step-by-step migration checklist. Each check covers one conversion task. Complete them in order. > **CRITICAL**: If your UI is blank after migration, open the console. Errors like "cannot use useNavigate outside of context" mean React Router imports remain alongside TanStack Router imports. Uninstall `react-router` to surface them as TypeScript errors. - > **CRITICAL**: TanStack Router uses `to` + `params` for navigation, NOT template literal paths. Never interpolate params into the `to` string. ## Pre-Migration @@ -459,7 +458,7 @@ const { page } = Route.useSearch() ### 4. MEDIUM: Using `:param` syntax instead of `$param` -``` +```text React Router: /posts/:postId TanStack Router: /posts/$postId ``` diff --git a/packages/react-router/skills/react-router/SKILL.md b/packages/react-router/skills/react-router/SKILL.md index 1ad81095dba..ffd60c322e0 100644 --- a/packages/react-router/skills/react-router/SKILL.md +++ b/packages/react-router/skills/react-router/SKILL.md @@ -27,9 +27,7 @@ This skill builds on router-core. Read [router-core](../../../router-core/skills This skill covers the React-specific bindings, components, hooks, and setup for TanStack Router. > **CRITICAL**: TanStack Router types are FULLY INFERRED. Never cast, never annotate inferred values. - > **CRITICAL**: TanStack Router is CLIENT-FIRST. Loaders run on the client by default, not on the server. - > **CRITICAL**: Do not confuse `@tanstack/react-router` with `react-router-dom`/`react-router`. They are completely different libraries with different APIs. ## Full Setup: File-Based Routing with Vite @@ -362,11 +360,11 @@ Wrap `Link` in a custom component while preserving type safety: ```tsx import { createLink } from '@tanstack/react-router' -import { forwardRef } from 'react' +import { forwardRef, type ComponentPropsWithoutRef } from 'react' const StyledLinkComponent = forwardRef< HTMLAnchorElement, - React.ComponentPropsWithoutRef<'a'> + ComponentPropsWithoutRef<'a'> >((props, ref) => ( <a ref={ref} {...props} className={`styled-link ${props.className ?? ''}`} /> )) diff --git a/packages/router-core/skills/router-core/auth-and-guards/SKILL.md b/packages/router-core/skills/router-core/auth-and-guards/SKILL.md index 74d26bb199c..1823f752fe1 100644 --- a/packages/router-core/skills/router-core/auth-and-guards/SKILL.md +++ b/packages/router-core/skills/router-core/auth-and-guards/SKILL.md @@ -151,7 +151,7 @@ export const Route = createFileRoute('/_authenticated')({ ```tsx // src/routes/login.tsx import { createFileRoute, redirect } from '@tanstack/react-router' -import { useState } from 'react' +import { useState, type FormEvent } from 'react' export const Route = createFileRoute('/login')({ validateSearch: (search) => ({ @@ -173,7 +173,7 @@ function LoginComponent() { const [password, setPassword] = useState('') const [error, setError] = useState('') - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: FormEvent) => { e.preventDefault() try { await auth.login(username, password) diff --git a/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md b/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md index f234702308a..72bd46c2ce0 100644 --- a/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md +++ b/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md @@ -20,7 +20,6 @@ sources: TanStack Router handles two categories of "not found": unmatched URL paths (automatic) and missing resources like a post that doesn't exist (manual via `notFound()`). Error boundaries are configured per-route via `errorComponent`. > **CRITICAL**: Do NOT use the deprecated `NotFoundRoute`. When present, `notFound()` and `notFoundComponent` will NOT work. Remove it and use `notFoundComponent` instead. - > **CRITICAL**: `useLoaderData` may be undefined inside `notFoundComponent`. Use `useParams`, `useSearch`, or `useRouteContext` instead. ## Not Found Handling diff --git a/packages/router-core/skills/router-core/search-params/SKILL.md b/packages/router-core/skills/router-core/search-params/SKILL.md index 95b5fecc66d..273373d9f78 100644 --- a/packages/router-core/skills/router-core/search-params/SKILL.md +++ b/packages/router-core/skills/router-core/search-params/SKILL.md @@ -24,7 +24,6 @@ sources: TanStack Router treats search params as JSON-first application state. They are automatically parsed from the URL into structured objects (numbers, booleans, arrays, nested objects) and validated via `validateSearch` on each route. > **CRITICAL**: Use `fallback()` from `@tanstack/zod-adapter`, NOT zod's `.catch()`. Using `.catch()` makes the output type `unknown`, destroying type safety. - > **CRITICAL**: Types are fully inferred. Never annotate the return of `useSearch()`. ## Setup: Zod Adapter (Recommended) diff --git a/packages/router-core/skills/router-core/ssr/SKILL.md b/packages/router-core/skills/router-core/ssr/SKILL.md index 5acde100a28..1871c0f9500 100644 --- a/packages/router-core/skills/router-core/ssr/SKILL.md +++ b/packages/router-core/skills/router-core/ssr/SKILL.md @@ -317,7 +317,9 @@ export async function render({ req: express.Request res: express.Response }) { - const url = new URL(req.originalUrl || req.url, 'https://localhost:3000').href + const protocol = req.get('x-forwarded-proto') ?? req.protocol + const host = req.get('x-forwarded-host') ?? req.get('host') + const url = new URL(req.originalUrl || req.url, `${protocol}://${host}`).href const request = new Request(url, { method: req.method, diff --git a/packages/router-core/skills/router-core/type-safety/SKILL.md b/packages/router-core/skills/router-core/type-safety/SKILL.md index cf25e8a4f4b..8bfcaa38005 100644 --- a/packages/router-core/skills/router-core/type-safety/SKILL.md +++ b/packages/router-core/skills/router-core/type-safety/SKILL.md @@ -22,7 +22,6 @@ sources: TanStack Router is FULLY type-inferred. Params, search params, context, and loader data all flow through the route tree automatically. The **#1 AI agent mistake** is adding type annotations, casts, or generic parameters to values that are already inferred. > **CRITICAL**: NEVER use `as Type`, explicit generic params, `satisfies` on hook returns, or type annotations on inferred values. Every cast masks real type errors and breaks the inference chain. - > **CRITICAL**: Do NOT confuse TanStack Router with Next.js or React Router. There is no `getServerSideProps`, no `useSearchParams()`, no `useLoaderData()` from `react-router-dom`. ## The ONE Required Type Annotation: Register @@ -497,4 +496,4 @@ export const Route = createFileRoute('/posts')({ const search = Route.useSearch() // TanStack hook ``` -See also: router-core (Register setup), router-core/navigation (from narrowing), router-core/code-splitting (getRouteApi). \ No newline at end of file +See also: router-core (Register setup), router-core/navigation (from narrowing), router-core/code-splitting (getRouteApi). diff --git a/packages/solid-router/skills/_artifacts/domain_map.yaml b/packages/solid-router/skills/_artifacts/domain_map.yaml index 5339b149e49..28424762a9c 100644 --- a/packages/solid-router/skills/_artifacts/domain_map.yaml +++ b/packages/solid-router/skills/_artifacts/domain_map.yaml @@ -52,9 +52,10 @@ skills: - mistake: 'Using React Router APIs instead of Solid equivalents' mechanism: >- - Solid Router uses createLink instead of Link component - patterns and Solid primitives instead of React hooks. - Mixing React patterns causes runtime errors. + Solid Router provides a Link component plus createLink + for custom wrappers, and uses Solid-specific primitives + instead of React hooks. Mixing React or React Router + patterns into Solid Router causes type/runtime errors. priority: MEDIUM status: active diff --git a/packages/solid-router/skills/solid-router/SKILL.md b/packages/solid-router/skills/solid-router/SKILL.md index ee27a60af3a..d6f5a673afe 100644 --- a/packages/solid-router/skills/solid-router/SKILL.md +++ b/packages/solid-router/skills/solid-router/SKILL.md @@ -25,11 +25,8 @@ This skill builds on router-core. Read [router-core](../../../router-core/skills This skill covers the Solid-specific bindings, components, hooks, and setup for TanStack Router. > **CRITICAL**: TanStack Router types are FULLY INFERRED. Never cast, never annotate inferred values. - > **CRITICAL**: TanStack Router is CLIENT-FIRST. Loaders run on the client by default, not on the server. - > **CRITICAL**: Most hooks return `Accessor<T>` — you MUST call the accessor (`value()`) to read the reactive value. This is the #1 difference from the React version. - > **CRITICAL**: Do not confuse `@tanstack/solid-router` with `@solidjs/router`. They are completely different libraries with different APIs. ## Full Setup: File-Based Routing with Vite @@ -318,7 +315,6 @@ Error boundary wrapping `Solid.ErrorBoundary`: ```tsx import { CatchBoundary } from '@tanstack/solid-router' - ;<CatchBoundary getResetKey={() => 'widget'} onCatch={(error) => console.error(error)} @@ -334,7 +330,6 @@ Renders children only after hydration: ```tsx import { ClientOnly } from '@tanstack/solid-router' - ;<ClientOnly fallback={<div>Loading...</div>}> <BrowserOnlyWidget /> </ClientOnly> diff --git a/packages/start-client-core/skills/_artifacts/domain_map.yaml b/packages/start-client-core/skills/_artifacts/domain_map.yaml index b5e84d4dfbc..6f9c0b2de34 100644 --- a/packages/start-client-core/skills/_artifacts/domain_map.yaml +++ b/packages/start-client-core/skills/_artifacts/domain_map.yaml @@ -61,8 +61,8 @@ domains: skills: # ── Project Setup ──────────────────────────────────────────────── - - name: 'Start Setup' - slug: 'start-setup' + - name: 'Start Core' + slug: 'start-core' domain: 'project-setup' description: >- Scaffold a TanStack Start project, configure Vite plugin with diff --git a/packages/start-client-core/skills/_artifacts/skill_spec.md b/packages/start-client-core/skills/_artifacts/skill_spec.md index 11b6843bb01..a29621e4784 100644 --- a/packages/start-client-core/skills/_artifacts/skill_spec.md +++ b/packages/start-client-core/skills/_artifacts/skill_spec.md @@ -6,7 +6,7 @@ TanStack Start is a full-stack React framework built on TanStack Router and Vite | Domain | Description | Skills | | ------------------------ | ------------------------------------------------------------- | ---------------- | -| Project Setup | Scaffolding, Vite plugin, router factory, root route, entries | start-setup | +| Project Setup | Scaffolding, Vite plugin, router factory, root route, entries | start-core | | Server Functions | Type-safe RPCs with createServerFn, validation, streaming | server-functions | | Middleware and Context | Request/function middleware, context, global middleware | middleware | | Execution Model | Isomorphic defaults, environment boundaries, env vars | execution-model | @@ -17,7 +17,7 @@ TanStack Start is a full-stack React framework built on TanStack Router and Vite | Skill | Type | Domain | What it covers | Failure modes | | ------------------- | --------- | ------------------------ | ------------------------------------------------------- | ------------- | -| start-setup | core | project-setup | tanstackStart(), getRouter(), root route, entries | 3 | +| start-core | core | project-setup | tanstackStart(), getRouter(), root route, entries | 3 | | server-functions | core | server-functions | createServerFn, validation, useServerFn, streaming | 4 | | middleware | core | middleware-and-context | createMiddleware, context, global middleware, factories | 3 | | execution-model | core | execution-model | Isomorphic defaults, environment functions, env vars | 4 | @@ -28,7 +28,7 @@ TanStack Start is a full-stack React framework built on TanStack Router and Vite ## Failure Mode Inventory -### start-setup (3 failure modes) +### start-core (3 failure modes) | # | Mistake | Priority | Source | | --- | ----------------------------------------------- | -------- | ----------------------- | diff --git a/packages/start-client-core/skills/_artifacts/skill_tree.yaml b/packages/start-client-core/skills/_artifacts/skill_tree.yaml index cf743b067ca..6ee1e02b162 100644 --- a/packages/start-client-core/skills/_artifacts/skill_tree.yaml +++ b/packages/start-client-core/skills/_artifacts/skill_tree.yaml @@ -1,16 +1,15 @@ -# skills/_artifacts/start_skill_tree.yaml +# packages/start-client-core/skills/_artifacts/skill_tree.yaml library: - name: '@tanstack/react-start' + name: '@tanstack/start-client-core' version: '1.166.2' repository: 'https://github.com/TanStack/router' description: >- - Full-stack React framework built on TanStack Router and Vite. - Adds SSR, streaming, server functions (type-safe RPCs), middleware, - server routes, and universal deployment. Isomorphic by default — - all code runs in both environments unless explicitly constrained. + Framework-agnostic client-side core for TanStack Start. + Provides server functions, middleware, execution model, + server routes, and deployment configuration. generated_from: - domain_map: '_artifacts/start_domain_map.yaml' -generated_at: '2026-03-07' + domain_map: 'skills/_artifacts/domain_map.yaml' +generated_at: '2026-03-08' skills: # ── Start Core Skills ─────────────────────────────────────────── @@ -119,40 +118,3 @@ skills: - 'TanStack/router:docs/start/framework/react/guide/selective-ssr.md' - 'TanStack/router:docs/start/framework/react/guide/static-prerendering.md' - 'TanStack/router:docs/start/framework/react/guide/full-stack-seo.md' - - # ── React Start Skills ────────────────────────────────────────── - - name: 'React Start' - slug: 'react-start' - type: 'framework' - domain: 'project-setup' - path: 'skills/react-start/SKILL.md' - package: 'packages/react-start' - description: >- - React bindings for TanStack Start: createStart, StartClient, - StartServer, React-specific imports, re-exports from - @tanstack/react-router, full project setup with React. - requires: - - 'start-core' - sources: - - 'TanStack/router:packages/react-start/src' - - 'TanStack/router:docs/start/framework/react/build-from-scratch.md' - - # ── Lifecycle Skills ──────────────────────────────────────────── - - name: 'Migrate from Next.js' - slug: 'lifecycle/migrate-from-nextjs' - type: 'lifecycle' - domain: 'project-setup' - path: 'skills/lifecycle/migrate-from-nextjs/SKILL.md' - package: 'packages/react-start' - description: >- - Step-by-step migration from Next.js App Router to TanStack - Start: route definition conversion, API mapping, server - function conversion from Server Actions, middleware conversion, - data fetching pattern changes. - requires: - - 'start-core' - - 'react-start' - sources: - - 'TanStack/router:docs/start/framework/react/guide/server-functions.md' - - 'TanStack/router:docs/start/framework/react/guide/middleware.md' - - 'TanStack/router:docs/start/framework/react/guide/execution-model.md' diff --git a/packages/start-client-core/skills/start-core/SKILL.md b/packages/start-client-core/skills/start-core/SKILL.md index bf8f80c2218..1dd98450e7f 100644 --- a/packages/start-client-core/skills/start-core/SKILL.md +++ b/packages/start-client-core/skills/start-core/SKILL.md @@ -19,9 +19,7 @@ sources: TanStack Start is a full-stack React framework built on TanStack Router and Vite. It adds SSR, streaming, server functions (type-safe RPCs), middleware, server routes, and universal deployment. > **CRITICAL**: All code in TanStack Start is ISOMORPHIC by default — it runs in BOTH server and client environments. Loaders run on both server AND client. To run code exclusively on the server, use `createServerFn`. This is the #1 AI agent mistake. - > **CRITICAL**: TanStack Start is NOT Next.js. Do not generate `getServerSideProps`, `"use server"` directives, `app/layout.tsx`, or any Next.js/Remix patterns. Use `createServerFn` for server-only code. - > **CRITICAL**: Types are FULLY INFERRED. Never cast, never annotate inferred values. ## Sub-Skills @@ -36,7 +34,7 @@ TanStack Start is a full-stack React framework built on TanStack Router and Vite ## Quick Decision Tree -``` +```text Need to run code exclusively on the server (DB, secrets)? → start-core/server-functions diff --git a/packages/start-client-core/skills/start-core/execution-model/SKILL.md b/packages/start-client-core/skills/start-core/execution-model/SKILL.md index 0813eb1a2ae..9567d93d07f 100644 --- a/packages/start-client-core/skills/start-core/execution-model/SKILL.md +++ b/packages/start-client-core/skills/start-core/execution-model/SKILL.md @@ -21,9 +21,7 @@ sources: Understanding where code runs is fundamental to TanStack Start. This skill covers the isomorphic execution model and how to control environment boundaries. > **CRITICAL**: ALL code in TanStack Start is isomorphic by default — it runs in BOTH server and client bundles. Route loaders run on BOTH server (during SSR) AND client (during navigation). Server-only operations MUST use `createServerFn`. - > **CRITICAL**: Module-level `process.env` access runs in both environments. Secret values leak into the client bundle. Access secrets ONLY inside `createServerFn` or `createServerOnlyFn`. - > **CRITICAL**: `VITE_` prefixed environment variables are exposed to the client bundle. Server secrets must NOT have the `VITE_` prefix. ## Execution Control APIs @@ -44,6 +42,7 @@ Understanding where code runs is fundamental to TanStack Start. This skill cover The primary way to run server-only code. On the client, calls become fetch requests: ```tsx +// Use @tanstack/<framework>-start for your framework (react, solid, vue) import { createServerFn } from '@tanstack/react-start' const fetchUser = createServerFn().handler(async () => { @@ -60,6 +59,7 @@ const user = await fetchUser() For utility functions that must never run on client: ```tsx +// Use @tanstack/<framework>-start for your framework (react, solid, vue) import { createServerOnlyFn } from '@tanstack/react-start' const getSecret = createServerOnlyFn(() => process.env.DATABASE_URL) @@ -73,6 +73,7 @@ const getSecret = createServerOnlyFn(() => process.env.DATABASE_URL) ### createClientOnlyFn ```tsx +// Use @tanstack/<framework>-start for your framework (react, solid, vue) import { createClientOnlyFn } from '@tanstack/react-start' const saveToStorage = createClientOnlyFn((key: string, value: string) => { @@ -83,6 +84,7 @@ const saveToStorage = createClientOnlyFn((key: string, value: string) => { ### ClientOnly Component ```tsx +// Use @tanstack/<framework>-router for your framework (react, solid, vue) import { ClientOnly } from '@tanstack/react-router' function Analytics() { @@ -97,6 +99,7 @@ function Analytics() { ### useHydrated Hook ```tsx +// Use @tanstack/<framework>-router for your framework (react, solid, vue) import { useHydrated } from '@tanstack/react-router' function TimeZoneDisplay() { @@ -114,6 +117,7 @@ Behavior: SSR → `false`, first client render → `false`, after hydration → ## Environment-Specific Implementations ```tsx +// Use @tanstack/<framework>-start for your framework (react, solid, vue) import { createIsomorphicFn } from '@tanstack/react-start' const getDeviceInfo = createIsomorphicFn() @@ -139,6 +143,7 @@ const connectDb = createServerFn().handler(async () => { Only `VITE_` prefixed variables are available: ```tsx +// Framework-specific component type (React.ReactNode, JSX.Element, etc.) function ApiProvider({ children }: { children: React.ReactNode }) { const apiUrl = import.meta.env.VITE_API_URL // available // import.meta.env.DATABASE_URL → undefined (security) diff --git a/packages/start-client-core/skills/start-core/middleware/SKILL.md b/packages/start-client-core/skills/start-core/middleware/SKILL.md index f63cea0e1c0..a1c5b81b7f2 100644 --- a/packages/start-client-core/skills/start-core/middleware/SKILL.md +++ b/packages/start-client-core/skills/start-core/middleware/SKILL.md @@ -21,7 +21,6 @@ sources: Middleware customizes the behavior of server functions and server routes. It is composable — middleware can depend on other middleware to form a chain. > **CRITICAL**: TypeScript enforces method order: `middleware()` → `inputValidator()` → `client()` → `server()`. Wrong order causes type errors. - > **CRITICAL**: Client context sent via `sendContext` is NOT validated by default. If you send dynamic user-generated data, validate it in server-side middleware before use. ## Two Types of Middleware @@ -41,6 +40,7 @@ Request middleware cannot depend on server function middleware. Server function Runs on ALL server requests (SSR, server routes, server functions): ```tsx +// Use @tanstack/<framework>-start for your framework (react, solid, vue) import { createMiddleware } from '@tanstack/react-start' const loggingMiddleware = createMiddleware().server( @@ -57,6 +57,7 @@ const loggingMiddleware = createMiddleware().server( Has both client and server phases: ```tsx +// Use @tanstack/<framework>-start for your framework (react, solid, vue) import { createMiddleware } from '@tanstack/react-start' const authMiddleware = createMiddleware({ type: 'function' }) @@ -77,6 +78,7 @@ const authMiddleware = createMiddleware({ type: 'function' }) ## Attaching Middleware to Server Functions ```tsx +// Use @tanstack/<framework>-start for your framework (react, solid, vue) import { createServerFn } from '@tanstack/react-start' const fn = createServerFn() @@ -171,6 +173,7 @@ Create `src/start.ts` to configure global middleware: ```tsx // src/start.ts +// Use @tanstack/<framework>-start for your framework (react, solid, vue) import { createStart, createMiddleware } from '@tanstack/react-start' const requestLogger = createMiddleware().server(async ({ next, request }) => { @@ -278,6 +281,7 @@ Headers merge across middleware. Later middleware overrides earlier. Call-site h ### Custom fetch ```tsx +// Use @tanstack/<framework>-start for your framework (react, solid, vue) import type { CustomFetch } from '@tanstack/react-start' const loggingMiddleware = createMiddleware({ type: 'function' }).client( diff --git a/packages/start-client-core/skills/start-core/server-functions/SKILL.md b/packages/start-client-core/skills/start-core/server-functions/SKILL.md index 91aed114f54..cc4f6998102 100644 --- a/packages/start-client-core/skills/start-core/server-functions/SKILL.md +++ b/packages/start-client-core/skills/start-core/server-functions/SKILL.md @@ -20,7 +20,6 @@ sources: Server functions are type-safe RPCs created with `createServerFn`. They run exclusively on the server but can be called from anywhere — loaders, components, hooks, event handlers, or other server functions. > **CRITICAL**: Loaders are ISOMORPHIC — they run on BOTH client and server. Database queries, file system access, and secret API keys MUST go inside `createServerFn`, NOT in loaders directly. - > **CRITICAL**: Do not use `"use server"` directives, `getServerSideProps`, or any Next.js/Remix server patterns. TanStack Start uses `createServerFn` exclusively. ## Basic Usage @@ -225,7 +224,7 @@ Available utilities: ## File Organization -``` +```text src/utils/ ├── users.functions.ts # createServerFn wrappers (safe to import anywhere) ├── users.server.ts # Server-only helpers (DB queries, internal logic) diff --git a/packages/start-client-core/skills/start-core/server-routes/SKILL.md b/packages/start-client-core/skills/start-core/server-routes/SKILL.md index a3e9b5d7d8a..be21baf63df 100644 --- a/packages/start-client-core/skills/start-core/server-routes/SKILL.md +++ b/packages/start-client-core/skills/start-core/server-routes/SKILL.md @@ -253,7 +253,7 @@ export const Route = createFileRoute('/api/posts')({ ### 1. MEDIUM: Duplicate route paths -``` +```text # WRONG — both resolve to /users, causes error routes/users.ts routes/users/index.ts diff --git a/packages/start-server-core/skills/start-server-core/SKILL.md b/packages/start-server-core/skills/start-server-core/SKILL.md index 7199a9ad33d..567c5da6aa2 100644 --- a/packages/start-server-core/skills/start-server-core/SKILL.md +++ b/packages/start-server-core/skills/start-server-core/SKILL.md @@ -177,8 +177,8 @@ const session = await useSession<{ userId: string }>(config) session.id // Session ID (string | undefined) session.data // Session data (typed) -session.update() // Update and return new session manager -session.clear() // Clear session data +await session.update({ userId: '123' }) // Persist session data +await session.clear() // Clear session data ``` ## Query Validation diff --git a/packages/virtual-file-routes/skills/_artifacts/domain_map.yaml b/packages/virtual-file-routes/skills/_artifacts/domain_map.yaml index fc2b84be488..db82f87ec03 100644 --- a/packages/virtual-file-routes/skills/_artifacts/domain_map.yaml +++ b/packages/virtual-file-routes/skills/_artifacts/domain_map.yaml @@ -46,18 +46,20 @@ skills: - 'Mix virtual and physical routes' - 'Create virtual subtree configs' failure_modes: - - mistake: 'Forgetting rootRoute wrapper' + - mistake: 'Omitting rootRoute() in a top-level virtual route config' mechanism: >- - All virtual route configs must be wrapped in rootRoute(). - Without it, routes are not properly nested under the root. + Top-level virtual route trees should be wrapped in rootRoute(). + defineVirtualSubtreeConfig() in __virtual.ts files is the + exception and should export an array of child nodes instead. priority: HIGH status: active - - mistake: 'Using physical() path outside routesDirectory' + - mistake: 'Passing a physical() directory outside routesDirectory' mechanism: >- - physical() paths are relative to routesDirectory. Using - absolute or wrong-relative paths causes routes to not - be found. + Only the directory argument of physical() is relative to + routesDirectory; the first argument is a URL prefix. Using + an absolute or wrong-relative directory causes routes to + not be found. priority: MEDIUM status: active diff --git a/packages/virtual-file-routes/skills/virtual-file-routes/SKILL.md b/packages/virtual-file-routes/skills/virtual-file-routes/SKILL.md index 3e3d821645b..093d73c0ba4 100644 --- a/packages/virtual-file-routes/skills/virtual-file-routes/SKILL.md +++ b/packages/virtual-file-routes/skills/virtual-file-routes/SKILL.md @@ -126,17 +126,16 @@ Pass the virtual route config to the TanStack Router plugin: ```ts // vite.config.ts import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' import { tanstackRouter } from '@tanstack/router-plugin/vite' import { routes } from './routes' export default defineConfig({ plugins: [ tanstackRouter({ - target: 'react', + target: 'react', // or 'solid', 'vue' virtualRouteConfig: routes, }), - react(), + // Add your framework's Vite plugin here ], }) ``` diff --git a/packages/vue-router/skills/_artifacts/domain_map.yaml b/packages/vue-router/skills/_artifacts/domain_map.yaml index d38fba35012..453dffd76c2 100644 --- a/packages/vue-router/skills/_artifacts/domain_map.yaml +++ b/packages/vue-router/skills/_artifacts/domain_map.yaml @@ -45,11 +45,12 @@ skills: - 'Create components with defineComponent and h()' - 'Use provide/inject for router context' failure_modes: - - mistake: 'Not unwrapping Ref values in templates' + - mistake: 'Forgetting .value when reading Ref values in script' mechanism: >- - Vue Router returns Ref<T> from composables. Forgetting - .value in script or using incorrectly in templates - causes rendering of Ref objects instead of values. + Vue Router returns Ref<T> from composables. The common + mistake is reading them like plain values in script/setup + code. Templates auto-unwrap refs, but script logic still + needs .value. priority: HIGH status: active diff --git a/packages/vue-router/skills/vue-router/SKILL.md b/packages/vue-router/skills/vue-router/SKILL.md index 2744c716013..3cd2bd08819 100644 --- a/packages/vue-router/skills/vue-router/SKILL.md +++ b/packages/vue-router/skills/vue-router/SKILL.md @@ -221,12 +221,12 @@ import { RouterProvider } from '@tanstack/vue-router' Type-safe navigation link with scoped slot for active state: -```tsx +```vue <Link to="/posts/$postId" :params="{ postId: '42' }"> View Post </Link> -{/* Scoped slot for active state */} +<!-- Scoped slot for active state --> <Link to="/about"> <template #default="{ isActive }"> <span :class="{ active: isActive }">About</span> @@ -301,9 +301,8 @@ const StyledLink = createLink(StyledLinkComponent) All components in `@tanstack/vue-router` use `h()` render functions internally. Route components can use either SFC templates or render functions: -```tsx -// SFC template (most common for user code) -// MyRoute.component.vue +```vue +// SFC template (most common for user code) // MyRoute.component.vue <template> <div>{{ data.title }}</div> </template> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3f5aac6dc2..d7d2bf96af4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11048,7 +11048,7 @@ importers: devDependencies: '@netlify/vite-plugin-tanstack-start': specifier: ^1.1.4 - version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) '@tailwindcss/vite': specifier: ^4.1.18 version: 4.1.18(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) @@ -28402,13 +28402,13 @@ snapshots: uuid: 11.1.0 write-file-atomic: 5.0.1 - '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)': + '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)': dependencies: '@netlify/blobs': 10.1.0 '@netlify/config': 23.2.0 '@netlify/dev-utils': 4.3.0 '@netlify/edge-functions-dev': 1.0.0 - '@netlify/functions-dev': 1.0.0(encoding@0.1.13)(rollup@4.55.3) + '@netlify/functions-dev': 1.0.0(rollup@4.55.3) '@netlify/headers': 2.1.0 '@netlify/images': 1.3.0(@netlify/blobs@10.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2) '@netlify/redirects': 3.1.0 @@ -28476,12 +28476,12 @@ snapshots: dependencies: '@netlify/types': 2.1.0 - '@netlify/functions-dev@1.0.0(encoding@0.1.13)(rollup@4.55.3)': + '@netlify/functions-dev@1.0.0(rollup@4.55.3)': dependencies: '@netlify/blobs': 10.1.0 '@netlify/dev-utils': 4.3.0 '@netlify/functions': 5.0.0 - '@netlify/zip-it-and-ship-it': 14.1.11(encoding@0.1.13)(rollup@4.55.3) + '@netlify/zip-it-and-ship-it': 14.1.11(rollup@4.55.3) cron-parser: 4.9.0 decache: 4.6.2 extract-zip: 2.0.1 @@ -28571,9 +28571,9 @@ snapshots: '@netlify/types@2.1.0': {} - '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: - '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) optionalDependencies: '@tanstack/solid-start': link:packages/solid-start @@ -28601,9 +28601,9 @@ snapshots: - supports-color - uploadthing - '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: - '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3) + '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3) '@netlify/dev-utils': 4.3.0 dedent: 1.7.0(babel-plugin-macros@3.1.0) vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) @@ -28631,13 +28631,13 @@ snapshots: - supports-color - uploadthing - '@netlify/zip-it-and-ship-it@14.1.11(encoding@0.1.13)(rollup@4.55.3)': + '@netlify/zip-it-and-ship-it@14.1.11(rollup@4.55.3)': dependencies: '@babel/parser': 7.28.5 '@babel/types': 7.28.4 '@netlify/binary-info': 1.0.0 '@netlify/serverless-functions-api': 2.7.1 - '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.55.3) + '@vercel/nft': 0.29.4(rollup@4.55.3) archiver: 7.0.1 common-path-prefix: 3.0.0 copy-file: 11.1.0 @@ -32022,7 +32022,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vercel/nft@0.29.4(encoding@0.1.13)(rollup@4.55.3)': + '@vercel/nft@0.29.4(rollup@4.55.3)': dependencies: '@mapbox/node-pre-gyp': 2.0.0(encoding@0.1.13) '@rollup/pluginutils': 5.1.4(rollup@4.55.3) From 5a08dfd4df03cf89540b1a047b5bd5ab1f7e9191 Mon Sep 17 00:00:00 2001 From: Tanner Linsley <tannerlinsley@gmail.com> Date: Thu, 12 Mar 2026 23:44:55 -0600 Subject: [PATCH 3/3] fix: address review feedback and audit all 28 skills against source - Address all 9 Sheraff review comments (ensureQueryData, auth patterns, open redirect, useLocation migration, reusable components, zodValidator) - Address all 11 actionable CodeRabbit comments (naming collisions, framework-agnostic comments, missing imports, bin/intent.js error handling) - Full source-verified audit of all 28 skills across 7 re-audited packages - Fix solid-start: add HydrationScript, move HeadContent to body, use shellComponent pattern - Fix solid-router: useLoaderDeps returns Accessor<T>, ScrollRestoration deprecated, add missing hooks/components - Fix vue-router: useLinkProps returns LinkHTMLAttributes, useLoaderDeps returns Ref<T>, fix split-file convention - Fix start-server-core: correct file path, server fn prefix, getValidatedQuery API, add createServerFn imports to all examples - Fix router-core skills: preloadStaleTime wording, loader route param, error retry pattern, search-params adapter scoping - All 28 skills pass @tanstack/intent validate --- packages/react-router/bin/intent.js | 7 +- packages/react-router/eslint.config.ts | 3 + packages/react-router/package.json | 9 +- .../skills/compositions/router-query/SKILL.md | 29 ++++-- .../migrate-from-react-router/SKILL.md | 95 ++++++++++--------- .../react-router/skills/react-router/SKILL.md | 57 ++++++----- packages/react-start/bin/intent.js | 7 +- .../lifecycle/migrate-from-nextjs/SKILL.md | 3 +- packages/router-core/bin/intent.js | 7 +- .../router-core/auth-and-guards/SKILL.md | 32 ++++--- .../skills/router-core/data-loading/SKILL.md | 9 +- .../skills/router-core/navigation/SKILL.md | 2 +- .../router-core/not-found-and-errors/SKILL.md | 23 ++--- .../skills/router-core/search-params/SKILL.md | 9 +- .../references/validation-patterns.md | 10 +- .../skills/router-core/type-safety/SKILL.md | 12 +-- packages/router-plugin/bin/intent.js | 7 +- .../skills/router-plugin/SKILL.md | 14 +-- packages/solid-router/bin/intent.js | 7 +- packages/solid-router/eslint.config.ts | 3 + .../solid-router/skills/solid-router/SKILL.md | 57 ++++++++--- packages/solid-start/bin/intent.js | 7 +- .../solid-start/skills/solid-start/SKILL.md | 16 ++-- packages/start-client-core/bin/intent.js | 7 +- .../start-core/server-functions/SKILL.md | 10 +- .../skills/start-core/server-routes/SKILL.md | 2 + packages/start-server-core/bin/intent.js | 7 +- .../skills/start-server-core/SKILL.md | 36 +++++-- packages/virtual-file-routes/bin/intent.js | 7 +- packages/vue-router/bin/intent.js | 7 +- packages/vue-router/eslint.config.ts | 3 + .../vue-router/skills/vue-router/SKILL.md | 15 +-- packages/vue-start/bin/intent.js | 7 +- packages/vue-start/eslint.config.ts | 3 + packages/vue-start/skills/vue-start/SKILL.md | 4 +- pnpm-lock.yaml | 58 +++++------ 36 files changed, 364 insertions(+), 227 deletions(-) diff --git a/packages/react-router/bin/intent.js b/packages/react-router/bin/intent.js index 2cf2efab472..5b085328854 100755 --- a/packages/react-router/bin/intent.js +++ b/packages/react-router/bin/intent.js @@ -6,7 +6,12 @@ try { await import('@tanstack/intent/intent-library') } catch (e) { - if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + const isModuleNotFound = + e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND' + const missingIntentLibrary = + typeof e?.message === 'string' && e.message.includes('@tanstack/intent') + + if (isModuleNotFound && missingIntentLibrary) { console.error('@tanstack/intent is not installed.') console.error('') console.error('Install it as a dev dependency:') diff --git a/packages/react-router/eslint.config.ts b/packages/react-router/eslint.config.ts index 977bd6da552..e5f4ab41206 100644 --- a/packages/react-router/eslint.config.ts +++ b/packages/react-router/eslint.config.ts @@ -6,6 +6,9 @@ import type { Linter } from 'eslint' export default [ ...rootConfig, + { + ignores: ['bin/**'], + }, { files: ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'], }, diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 2af9103d163..ca3fc012e33 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -90,10 +90,7 @@ "sideEffects": false, "files": [ "dist", - "src", - "skills", - "bin", - "!skills/_artifacts" + "src" ], "engines": { "node": ">=20.19" @@ -107,7 +104,6 @@ "tiny-warning": "^1.0.3" }, "devDependencies": { - "@tanstack/intent": "^0.0.14", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@vitejs/plugin-react": "^4.3.4", @@ -121,8 +117,5 @@ "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" - }, - "bin": { - "intent": "./bin/intent.js" } } diff --git a/packages/react-router/skills/compositions/router-query/SKILL.md b/packages/react-router/skills/compositions/router-query/SKILL.md index ac0f5d4b13b..89fba76b540 100644 --- a/packages/react-router/skills/compositions/router-query/SKILL.md +++ b/packages/react-router/skills/compositions/router-query/SKILL.md @@ -86,7 +86,7 @@ export const Route = createRootRouteWithContext<{ ```tsx // src/router.tsx -import { QueryClient } from '@tanstack/react-query' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' @@ -190,14 +190,21 @@ This is the recommended pattern. The loader ensures data is in the cache before import { queryOptions, useSuspenseQuery } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' +interface Post { + id: string + title: string +} + const postsQueryOptions = queryOptions({ queryKey: ['posts'], - queryFn: () => fetch('/api/posts').then((r) => r.json()), + queryFn: (): Promise<Array<Post>> => + fetch('/api/posts').then((r) => r.json()), }) export const Route = createFileRoute('/posts')({ loader: ({ context }) => { - // ensureQueryData returns cached data if fresh, fetches if stale + // ensureQueryData returns cached data if available, fetches if not in cache + // To also refetch stale data, pass revalidateIfStale: true return context.queryClient.ensureQueryData(postsQueryOptions) }, component: PostsPage, @@ -209,7 +216,7 @@ function PostsPage() { return ( <ul> - {posts.map((post: any) => ( + {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> @@ -224,6 +231,12 @@ function PostsPage() { import { queryOptions, useSuspenseQuery } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' +interface Post { + id: string + title: string + content: string +} + const postQueryOptions = (postId: string) => queryOptions({ queryKey: ['posts', postId], @@ -250,6 +263,8 @@ function PostPage() { For non-critical data, start the fetch without blocking navigation: ```tsx +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' + export const Route = createFileRoute('/dashboard')({ loader: ({ context }) => { // Await critical data @@ -340,15 +355,15 @@ A module-level singleton `QueryClient` is shared across all server requests, lea ```tsx // WRONG — shared across SSR requests const queryClient = new QueryClient() -export function createRouter() { +export function createAppRouter() { return createRouter({ routeTree, context: { queryClient }, }) } -// CORRECT — new QueryClient per createRouter call -export function createRouter() { +// CORRECT — new QueryClient per createAppRouter call +export function createAppRouter() { const queryClient = new QueryClient() return createRouter({ routeTree, diff --git a/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md b/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md index f513a0bdaf2..602c49a7a65 100644 --- a/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md +++ b/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md @@ -20,8 +20,11 @@ sources: This is a step-by-step migration checklist. Each check covers one conversion task. Complete them in order. -> **CRITICAL**: If your UI is blank after migration, open the console. Errors like "cannot use useNavigate outside of context" mean React Router imports remain alongside TanStack Router imports. Uninstall `react-router` to surface them as TypeScript errors. +> **CRITICAL**: If your UI is blank after migration, open the console. Errors like "cannot use useNavigate outside of context" mean React Router imports remain alongside TanStack Router imports. Uninstall `react-router` (and `react-router-dom` if present) to surface them as TypeScript errors. +> > **CRITICAL**: TanStack Router uses `to` + `params` for navigation, NOT template literal paths. Never interpolate params into the `to` string. +> +> **NOTE**: React Router v7 recommends importing from `react-router` (not `react-router-dom`). The `react-router-dom` package still exists but just re-exports from `react-router`. Check for imports from both. ## Pre-Migration @@ -34,8 +37,8 @@ git checkout -b migrate-to-tanstack-router - [ ] **Install TanStack Router alongside React Router temporarily** ```bash -npm install @tanstack/react-router -npm install -D @tanstack/router-plugin @tanstack/react-router-devtools +npm install @tanstack/react-router @tanstack/react-router-devtools +npm install -D @tanstack/router-plugin ``` - [ ] **Configure bundler plugin (Vite example)** @@ -197,7 +200,7 @@ Key differences: - `to` is a route path pattern, NOT an interpolated string - `params` is a separate prop with typed values -- Active class: `className="[&.active]:font-bold"` (automatic `active` data attribute) +- Active styling: use `activeProps={{ className: 'font-bold' }}` or `data-status="active"` attribute for CSS - [ ] **Convert all `useNavigate` calls** @@ -294,6 +297,43 @@ Or from within the route component: const { postId } = Route.useParams() ``` +## `useLocation` — Common Pitfall + +- [ ] **Replace `useLocation` with specific hooks** + +React Router's `useLocation` is heavily used, and TanStack Router has a hook with the same name — but they are NOT equivalent. TanStack Router's `useLocation()` returns the router's current location, which during pending navigations may differ from what's currently rendered. Most React Router `useLocation` usage should be replaced with more specific hooks. See [#3110](https://github.com/TanStack/router/issues/3110). + +Replace based on what you actually need: + +```tsx +// React Router +import { useLocation } from 'react-router' +const location = useLocation() + +// ❌ DON'T just swap to TanStack Router's useLocation — it's the "live" URL +import { useLocation } from '@tanstack/react-router' + +// ✅ DO use the specific hook for what you need: +import { + useMatch, + useMatches, + useParams, + useSearch, +} from '@tanstack/react-router' + +// Current route match (replaces most useLocation().pathname usage) +const match = useMatch({ from: '/posts/$postId' }) + +// All active matches (replaces useLocation for breadcrumbs/analytics) +const matches = useMatches() + +// Path params (replaces useLocation + manual parsing) +const { postId } = useParams({ from: '/posts/$postId' }) + +// Search params (replaces useLocation().search parsing) +const { page } = useSearch({ from: '/posts' }) +``` + ## Outlet - [ ] **Replace React Router `Outlet` with TanStack Router `Outlet`** @@ -352,62 +392,28 @@ Key differences: ## Code Splitting -- [ ] **Convert lazy route imports** - -React Router: - -```tsx -const LazyPage = lazy(() => import('./pages/LazyPage')) -{ path: '/lazy', element: <Suspense><LazyPage /></Suspense> } -``` - -TanStack Router (with `autoCodeSplitting: true` in plugin config, this is automatic). For manual splitting: +- [ ] **Convert lazy route imports** — with `autoCodeSplitting: true` in the plugin config, this is automatic. For manual splitting, use `.lazy.tsx` files: ```tsx // src/routes/lazy-page.lazy.tsx import { createLazyFileRoute } from '@tanstack/react-router' export const Route = createLazyFileRoute('/lazy-page')({ - component: LazyPage, + component: () => <div>Lazy loaded</div>, }) - -function LazyPage() { - return <div>Lazy loaded</div> -} ``` ## Cleanup -- [ ] **Remove React Router** +- [ ] **Remove React Router and verify** ```bash npm uninstall react-router react-router-dom +grep -r "from 'react-router" src/ # find stale imports +npx tsc --noEmit # verify clean build ``` -- [ ] **Search for remaining React Router imports** - -```bash -grep -r "from 'react-router" src/ -grep -r 'from "react-router' src/ -``` - -Any remaining imports will now produce TypeScript errors after uninstalling. - -- [ ] **Verify TypeScript compiles cleanly** - -```bash -npx tsc --noEmit -``` - -- [ ] **Test all routes manually** - -Verify: - -- All routes render -- Navigation works (including browser back/forward) -- Search params persist and validate -- Dynamic route params resolve -- Loaders execute and data displays +- [ ] **Test all routes** — verify rendering, navigation (incl. back/forward), search params, dynamic params, and loaders ## Common Mistakes @@ -477,6 +483,7 @@ File naming also uses `$`: `src/routes/posts/$postId.tsx` | `useParams()` | `useParams({ from: '/route/$param' })` | | `useSearchParams()` | `validateSearch` + `useSearch({ from })` | | `useLoaderData()` | `Route.useLoaderData()` | +| `useLocation()` | `useMatch`, `useMatches`, `useParams`, `useSearch` | | `<Outlet />` | `<Outlet />` | | `loader({ params })` | `loader: ({ params }) => ...` (route option) | | `action({ request })` | Use mutations / form libraries | diff --git a/packages/react-router/skills/react-router/SKILL.md b/packages/react-router/skills/react-router/SKILL.md index ffd60c322e0..c1502bfe7bb 100644 --- a/packages/react-router/skills/react-router/SKILL.md +++ b/packages/react-router/skills/react-router/SKILL.md @@ -381,6 +381,23 @@ function Nav() { } ``` +### Reusable Components with Router Hooks + +To create a component that uses router hooks across multiple routes, pass a union of route paths as the `from` prop: + +```tsx +function PostIdDisplay({ from }: { from: '/posts/$id' | '/drafts/$id' }) { + const { id } = useParams({ from }) + return <span>ID: {id}</span> +} + +// Usage in different route components +<PostIdDisplay from="/posts/$id" /> +<PostIdDisplay from="/drafts/$id" /> +``` + +This pattern avoids `strict: false` (which returns an imprecise union) while keeping the component reusable across specific known routes. + ### Auth Provider Must Wrap RouterProvider If routes use auth context (via `createRootRouteWithContext`), the auth provider must be an ancestor of `RouterProvider`: @@ -420,7 +437,7 @@ const router = createRouter({ ### 1. HIGH: Using React hooks in `beforeLoad` or `loader` -`beforeLoad` and `loader` are NOT React components — they are plain async functions called by the router. React hooks cannot be used in them. +`beforeLoad` and `loader` are NOT React components — they are plain async functions. React hooks cannot be called in them. Pass auth state via router context instead. ```tsx // WRONG — useAuth is a React hook, cannot be called here @@ -429,18 +446,7 @@ beforeLoad: () => { if (!auth.user) throw redirect({ to: '/login' }) } -// CORRECT — pass auth state via router context -const rootRoute = createRootRouteWithContext<{ auth: AuthState }>()({ - component: RootComponent, -}) - -// In the component that creates the router: -const router = createRouter({ - routeTree, - context: { auth: getAuthState() }, -}) - -// Then in a route: +// CORRECT — read auth from router context beforeLoad: ({ context }) => { if (!context.auth.isAuthenticated) { throw redirect({ to: '/login' }) @@ -450,25 +456,26 @@ beforeLoad: ({ context }) => { ### 2. HIGH: Wrapping RouterProvider inside an auth provider incorrectly -If you use `createRootRouteWithContext<{ auth: AuthState }>()`, the auth state must be available when the router is created — not injected after. +Create the router once with an `undefined!` placeholder, then inject live auth via `RouterProvider`'s `context` prop. Do NOT recreate the router on auth changes — this resets caches and rebuilds the tree. ```tsx -// WRONG — router created before auth is available -const router = createRouter({ routeTree, context: {} }) +// CORRECT — create router once, inject live auth via context prop +const router = createRouter({ + routeTree, + context: { auth: undefined! }, // placeholder, filled by RouterProvider +}) -function App() { - const auth = useAuth() // too late - return <RouterProvider router={router} /> +function InnerApp() { + const auth = useAuth() + return <RouterProvider router={router} context={{ auth }} /> } -// CORRECT — provide context at creation time function App() { - const auth = useAuth() - const router = useMemo( - () => createRouter({ routeTree, context: { auth } }), - [auth], + return ( + <AuthProvider> + <InnerApp /> + </AuthProvider> ) - return <RouterProvider router={router} /> } ``` diff --git a/packages/react-start/bin/intent.js b/packages/react-start/bin/intent.js index 2cf2efab472..5b085328854 100755 --- a/packages/react-start/bin/intent.js +++ b/packages/react-start/bin/intent.js @@ -6,7 +6,12 @@ try { await import('@tanstack/intent/intent-library') } catch (e) { - if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + const isModuleNotFound = + e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND' + const missingIntentLibrary = + typeof e?.message === 'string' && e.message.includes('@tanstack/intent') + + if (isModuleNotFound && missingIntentLibrary) { console.error('@tanstack/intent is not installed.') console.error('') console.error('Install it as a dev dependency:') diff --git a/packages/react-start/skills/lifecycle/migrate-from-nextjs/SKILL.md b/packages/react-start/skills/lifecycle/migrate-from-nextjs/SKILL.md index 2206a2a71bc..00f86ad3766 100644 --- a/packages/react-start/skills/lifecycle/migrate-from-nextjs/SKILL.md +++ b/packages/react-start/skills/lifecycle/migrate-from-nextjs/SKILL.md @@ -330,8 +330,9 @@ export const config = { matcher: ['/dashboard/:path*'] } TanStack Start: ```tsx -// src/start.ts +// src/start.ts — must be manually created import { createStart, createMiddleware } from '@tanstack/react-start' +import { redirect } from '@tanstack/react-router' const authMiddleware = createMiddleware().server(async ({ next, request }) => { const cookie = request.headers.get('cookie') diff --git a/packages/router-core/bin/intent.js b/packages/router-core/bin/intent.js index 2cf2efab472..5b085328854 100755 --- a/packages/router-core/bin/intent.js +++ b/packages/router-core/bin/intent.js @@ -6,7 +6,12 @@ try { await import('@tanstack/intent/intent-library') } catch (e) { - if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + const isModuleNotFound = + e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND' + const missingIntentLibrary = + typeof e?.message === 'string' && e.message.includes('@tanstack/intent') + + if (isModuleNotFound && missingIntentLibrary) { console.error('@tanstack/intent is not installed.') console.error('') console.error('Install it as a dev dependency:') diff --git a/packages/router-core/skills/router-core/auth-and-guards/SKILL.md b/packages/router-core/skills/router-core/auth-and-guards/SKILL.md index 1823f752fe1..b11ab95fb38 100644 --- a/packages/router-core/skills/router-core/auth-and-guards/SKILL.md +++ b/packages/router-core/skills/router-core/auth-and-guards/SKILL.md @@ -27,7 +27,7 @@ Protect routes with `beforeLoad` + `redirect()` in a pathless layout route (`_au ```tsx // src/routes/_authenticated.tsx -import { createFileRoute, redirect, Outlet } from '@tanstack/react-router' +import { createFileRoute, redirect } from '@tanstack/react-router' export const Route = createFileRoute('/_authenticated')({ beforeLoad: ({ context, location }) => { @@ -40,7 +40,7 @@ export const Route = createFileRoute('/_authenticated')({ }) } }, - component: () => <Outlet />, + // component defaults to Outlet — no need to declare it }) ``` @@ -94,7 +94,7 @@ import { routeTree } from './routeTree.gen' export const router = createRouter({ routeTree, context: { - auth: undefined!, // will be set by RouterProvider context prop + auth: undefined!, // placeholder — filled by RouterProvider context prop }, }) @@ -113,6 +113,7 @@ import { router } from './router' function InnerApp() { const auth = useAuth() + // context prop injects live auth state WITHOUT recreating the router return <RouterProvider router={router} context={{ auth }} /> } @@ -125,7 +126,7 @@ function App() { } ``` -The auth hook is called inside a React component (`InnerApp`), satisfying the Rules of Hooks. The returned value is injected into the router context via `RouterProvider`'s `context` prop. +The router is created once with a placeholder. `RouterProvider`'s `context` prop injects the live auth state on each render — this avoids recreating the router on auth changes (which would reset caches and rebuild the route tree). ### Redirect-Based Auth with Redirect-Back @@ -133,7 +134,7 @@ Save the current location in search params so you can redirect back after login: ```tsx // src/routes/_authenticated.tsx -import { createFileRoute, redirect, Outlet } from '@tanstack/react-router' +import { createFileRoute, redirect } from '@tanstack/react-router' export const Route = createFileRoute('/_authenticated')({ beforeLoad: ({ context, location }) => { @@ -144,7 +145,6 @@ export const Route = createFileRoute('/_authenticated')({ }) } }, - component: () => <Outlet />, }) ``` @@ -153,9 +153,17 @@ export const Route = createFileRoute('/_authenticated')({ import { createFileRoute, redirect } from '@tanstack/react-router' import { useState, type FormEvent } from 'react' +// Validate redirect target to prevent open redirect attacks +function sanitizeRedirect(url: unknown): string { + if (typeof url !== 'string' || !url.startsWith('/') || url.startsWith('//')) { + return '/' + } + return url +} + export const Route = createFileRoute('/login')({ validateSearch: (search) => ({ - redirect: (search.redirect as string) || '/', + redirect: sanitizeRedirect(search.redirect), }), beforeLoad: ({ context, search }) => { if (context.auth.isAuthenticated) { @@ -253,7 +261,7 @@ Admin-only layout route: ```tsx // src/routes/_authenticated/_admin.tsx -import { createFileRoute, redirect, Outlet } from '@tanstack/react-router' +import { createFileRoute, redirect } from '@tanstack/react-router' export const Route = createFileRoute('/_authenticated/_admin')({ beforeLoad: ({ context, location }) => { @@ -264,7 +272,6 @@ export const Route = createFileRoute('/_authenticated/_admin')({ }) } }, - component: () => <Outlet />, }) ``` @@ -272,7 +279,7 @@ Multi-role access: ```tsx // src/routes/_authenticated/_moderator.tsx -import { createFileRoute, redirect, Outlet } from '@tanstack/react-router' +import { createFileRoute, redirect } from '@tanstack/react-router' export const Route = createFileRoute('/_authenticated/_moderator')({ beforeLoad: ({ context, location }) => { @@ -283,7 +290,6 @@ export const Route = createFileRoute('/_authenticated/_moderator')({ }) } }, - component: () => <Outlet />, }) ``` @@ -291,7 +297,7 @@ Permission-based: ```tsx // src/routes/_authenticated/_users.tsx -import { createFileRoute, redirect, Outlet } from '@tanstack/react-router' +import { createFileRoute, redirect } from '@tanstack/react-router' export const Route = createFileRoute('/_authenticated/_users')({ beforeLoad: ({ context, location }) => { @@ -302,7 +308,6 @@ export const Route = createFileRoute('/_authenticated/_users')({ }) } }, - component: () => <Outlet />, }) ``` @@ -441,7 +446,6 @@ export const Route = createFileRoute('/_authenticated')({ throw redirect({ to: '/login' }) } }, - component: () => <Outlet />, }) ``` diff --git a/packages/router-core/skills/router-core/data-loading/SKILL.md b/packages/router-core/skills/router-core/data-loading/SKILL.md index 9bd266fd577..59a71a0cca2 100644 --- a/packages/router-core/skills/router-core/data-loading/SKILL.md +++ b/packages/router-core/skills/router-core/data-loading/SKILL.md @@ -106,7 +106,7 @@ TanStack Router has built-in Stale-While-Revalidate caching keyed on the route's Defaults: - **`staleTime`: 0** — data is always considered stale, reloads in background on re-match -- **`preloadStaleTime`: 30 seconds** — preloaded data stays fresh for 30s +- **`preloadStaleTime`: 30 seconds** — preloaded data won't be refetched for 30s - **`gcTime`: 30 minutes** — unused cache entries garbage collected after 30min ```tsx @@ -337,6 +337,7 @@ The `loader` function receives: - `preload` — `true` during preloading - `location` — current location object - `parentMatchPromise` — promise of parent route match +- `route` — the route object itself ```tsx export const Route = createFileRoute('/posts/$postId')({ @@ -384,7 +385,7 @@ export const Route = createFileRoute('/posts')({ ### HIGH: Using reset() instead of router.invalidate() in error components -`reset()` only resets the error boundary UI. It does NOT re-run the loader. For loader errors, use `router.invalidate()`: +`reset()` only resets the error boundary UI. It does NOT re-run the loader. For loader errors, use `router.invalidate()` which re-runs loaders and resets the boundary: ```tsx // WRONG — resets boundary but loader still has stale error @@ -392,8 +393,8 @@ function PostErrorComponent({ error, reset }) { return <button onClick={reset}>Retry</button> } -// CORRECT — re-runs loader and resets boundary -function PostErrorComponent({ error, reset }) { +// CORRECT — re-runs loader and resets the error boundary +function PostErrorComponent({ error }) { const router = useRouter() return <button onClick={() => router.invalidate()}>Retry</button> } diff --git a/packages/router-core/skills/router-core/navigation/SKILL.md b/packages/router-core/skills/router-core/navigation/SKILL.md index 3f21161e467..0e09ddbcec3 100644 --- a/packages/router-core/skills/router-core/navigation/SKILL.md +++ b/packages/router-core/skills/router-core/navigation/SKILL.md @@ -168,7 +168,7 @@ Or per-link: </Link> ``` -Preloaded data is cached for 30 seconds by default (`defaultPreloadStaleTime`). When using an external cache like TanStack Query, set `defaultPreloadStaleTime: 0` to let the external library control freshness. +Preloaded data stays fresh for 30 seconds by default (`defaultPreloadStaleTime: 30_000`). During that window it won't be refetched. When using an external cache like TanStack Query, set `defaultPreloadStaleTime: 0` to let the external library control freshness. Manual preloading via the router instance: diff --git a/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md b/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md index 72bd46c2ce0..b4fde048f86 100644 --- a/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md +++ b/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md @@ -2,9 +2,9 @@ name: router-core/not-found-and-errors description: >- notFound() function, notFoundComponent, defaultNotFoundComponent, - notFoundMode (fuzzy/root), errorComponent, onError/onCatch, - CatchBoundary, NotFoundRoute (deprecated), route masking (mask - option, createRouteMask, unmaskOnReload). + notFoundMode (fuzzy/root), errorComponent, CatchBoundary, + CatchNotFound, isNotFound, NotFoundRoute (deprecated), route + masking (mask option, createRouteMask, unmaskOnReload). type: sub-skill library: tanstack-router library_version: '1.166.2' @@ -149,7 +149,7 @@ const router = createRouter({ ### `errorComponent` Per Route -`errorComponent` receives `error` and `reset` props. For loader errors, use `router.invalidate()` to re-run the loader, then call `reset()` to clear the error boundary. +`errorComponent` receives `error`, `info`, and `reset` props. For loader errors, use `router.invalidate()` to re-run the loader — it automatically resets the error boundary. ```tsx // src/routes/posts.$postId.tsx @@ -167,9 +167,9 @@ export const Route = createFileRoute('/posts/$postId')({ function PostErrorComponent({ error, - reset, }: { error: Error + info: { componentStack: string } reset: () => void }) { const router = useRouter() @@ -179,9 +179,8 @@ function PostErrorComponent({ <p>Error: {error.message}</p> <button onClick={() => { - // Invalidate re-runs the loader, reset clears the boundary + // Invalidate re-runs the loader and resets the error boundary router.invalidate() - reset() }} > Retry @@ -201,7 +200,7 @@ function PostComponent() { ```tsx const router = createRouter({ routeTree, - defaultErrorComponent: ({ error, reset }) => { + defaultErrorComponent: ({ error }) => { const router = useRouter() return ( <div> @@ -209,7 +208,6 @@ const router = createRouter({ <button onClick={() => { router.invalidate() - reset() }} > Retry @@ -408,7 +406,7 @@ export const Route = createFileRoute('/posts/$postId')({ Masking data lives in `location.state` (browser history). When a masked URL is copied, shared, or opened in a new tab, the masking data is lost. The browser navigates to the visible (masked) URL directly. -### 5. HIGH (cross-skill): Using `reset()` alone instead of `router.invalidate()` + `reset()` +### 5. HIGH (cross-skill): Using `reset()` alone instead of `router.invalidate()` ```tsx // WRONG — reset() clears the error boundary but does NOT re-run the loader @@ -416,14 +414,13 @@ function ErrorFallback({ error, reset }: { error: Error; reset: () => void }) { return <button onClick={reset}>Retry</button> } -// CORRECT — invalidate re-runs loaders, reset clears the boundary -function ErrorFallback({ error, reset }: { error: Error; reset: () => void }) { +// CORRECT — invalidate re-runs loaders and resets the error boundary +function ErrorFallback({ error }: { error: Error; reset: () => void }) { const router = useRouter() return ( <button onClick={() => { router.invalidate() - reset() }} > Retry diff --git a/packages/router-core/skills/router-core/search-params/SKILL.md b/packages/router-core/skills/router-core/search-params/SKILL.md index 273373d9f78..d714969416d 100644 --- a/packages/router-core/skills/router-core/search-params/SKILL.md +++ b/packages/router-core/skills/router-core/search-params/SKILL.md @@ -23,7 +23,7 @@ sources: TanStack Router treats search params as JSON-first application state. They are automatically parsed from the URL into structured objects (numbers, booleans, arrays, nested objects) and validated via `validateSearch` on each route. -> **CRITICAL**: Use `fallback()` from `@tanstack/zod-adapter`, NOT zod's `.catch()`. Using `.catch()` makes the output type `unknown`, destroying type safety. +> **CRITICAL**: When using `zodValidator()`, use `fallback()` from `@tanstack/zod-adapter`, NOT zod's `.catch()`. Using `.catch()` with the zod adapter makes the output type `unknown`, destroying type safety. This does not apply to Valibot or ArkType (which use their own fallback mechanisms). > **CRITICAL**: Types are fully inferred. Never annotate the return of `useSearch()`. ## Setup: Zod Adapter (Recommended) @@ -292,11 +292,12 @@ export const Route = createFileRoute('/products')({ ## Common Mistakes -### 1. HIGH: Using zod `.catch()` instead of adapter `fallback()` +### 1. HIGH: Using zod `.catch()` with `zodValidator()` instead of adapter `fallback()` ```tsx -// WRONG — .catch() makes the type unknown +// WRONG — .catch() with zodValidator makes the type unknown const schema = z.object({ page: z.number().catch(1) }) +validateSearch: zodValidator(schema) // page is typed as unknown! // CORRECT — fallback() preserves the inferred type import { fallback } from '@tanstack/zod-adapter' @@ -316,7 +317,7 @@ loaderDeps: ({ search }) => ({ page: search.page }) ### 3. HIGH: Passing Date objects in search params ```tsx -// WRONG — Date serializes to "[object Object]" or invalid string +// WRONG — Date does not serialize correctly to JSON in URLs <Link search={{ startDate: new Date() }}> // CORRECT — convert to ISO string diff --git a/packages/router-core/skills/router-core/search-params/references/validation-patterns.md b/packages/router-core/skills/router-core/search-params/references/validation-patterns.md index e0b8fc0f82f..999bcb2ebf9 100644 --- a/packages/router-core/skills/router-core/search-params/references/validation-patterns.md +++ b/packages/router-core/skills/router-core/search-params/references/validation-patterns.md @@ -4,7 +4,7 @@ Comprehensive validation patterns for TanStack Router search params across all s ## Zod with `@tanstack/zod-adapter` -Always use `fallback()` from the adapter instead of zod's `.catch()`. Always wrap with `zodValidator()`. +Zod v3 does not implement Standard Schema, so the `@tanstack/zod-adapter` wrapper is required. Always use `fallback()` from the adapter instead of zod's `.catch()`. Always wrap with `zodValidator()`. ### Basic Types @@ -173,10 +173,10 @@ const userSearchSchema = paginationSchema.merge(sortSchema).extend({ ## Valibot (Standard Schema) -Valibot 1.0+ implements Standard Schema. No adapter wrapper needed — pass the schema directly to `validateSearch`. +Valibot 1.0+ implements Standard Schema. No adapter wrapper needed — pass the schema directly to `validateSearch`. The `@tanstack/valibot-adapter` is optional and only needed for explicit input/output type control. ```bash -npm install valibot @tanstack/valibot-adapter +npm install valibot ``` ```tsx @@ -240,10 +240,10 @@ export const Route = createFileRoute('/items')({ ## ArkType (Standard Schema) -ArkType 2.0-rc+ implements Standard Schema. No adapter needed — pass the type directly to `validateSearch`. +ArkType 2.0-rc+ implements Standard Schema. No adapter needed — pass the type directly to `validateSearch`. The `@tanstack/arktype-adapter` is optional and only needed for explicit input/output type control. ```bash -npm install arktype @tanstack/arktype-adapter +npm install arktype ``` ```tsx diff --git a/packages/router-core/skills/router-core/type-safety/SKILL.md b/packages/router-core/skills/router-core/type-safety/SKILL.md index 8bfcaa38005..eb258a994f7 100644 --- a/packages/router-core/skills/router-core/type-safety/SKILL.md +++ b/packages/router-core/skills/router-core/type-safety/SKILL.md @@ -105,7 +105,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({ ```tsx // src/routes/dashboard.tsx -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute, redirect } from '@tanstack/react-router' export const Route = createFileRoute('/dashboard')({ beforeLoad: ({ context }) => { @@ -250,17 +250,16 @@ export const Route = createFileRoute('/posts/$postId')({ Never use `LinkProps` as a variable type — it's an enormous union: ```tsx -import type { LinkProps } from '@tanstack/react-router' +import type { LinkProps, RegisteredRouter } from '@tanstack/react-router' // WRONG — LinkProps is a massive union, extremely slow TS check -const props: LinkProps = { to: '/posts' } +const wrongProps: LinkProps = { to: '/posts' } // CORRECT — infer a precise type, validate against LinkProps -const props = { to: '/posts' } as const satisfies LinkProps +const goodProps = { to: '/posts' } as const satisfies LinkProps // EVEN BETTER — narrow LinkProps with generic params -import type { RegisteredRouter } from '@tanstack/react-router' -const props = { +const narrowedProps = { to: '/posts', } as const satisfies LinkProps<RegisteredRouter, string, '/posts'> ``` @@ -322,7 +321,6 @@ import { type RegisteredRouter, type ValidateNavigateOptions, } from '@tanstack/react-router' -import { useState } from 'react' export function useDelayedNavigate< TRouter extends RegisteredRouter = RegisteredRouter, diff --git a/packages/router-plugin/bin/intent.js b/packages/router-plugin/bin/intent.js index 2cf2efab472..5b085328854 100755 --- a/packages/router-plugin/bin/intent.js +++ b/packages/router-plugin/bin/intent.js @@ -6,7 +6,12 @@ try { await import('@tanstack/intent/intent-library') } catch (e) { - if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + const isModuleNotFound = + e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND' + const missingIntentLibrary = + typeof e?.message === 'string' && e.message.includes('@tanstack/intent') + + if (isModuleNotFound && missingIntentLibrary) { console.error('@tanstack/intent is not installed.') console.error('') console.error('Install it as a dev dependency:') diff --git a/packages/router-plugin/skills/router-plugin/SKILL.md b/packages/router-plugin/skills/router-plugin/SKILL.md index ab3f9c67e08..3196bbc4a6e 100644 --- a/packages/router-plugin/skills/router-plugin/SKILL.md +++ b/packages/router-plugin/skills/router-plugin/SKILL.md @@ -110,13 +110,13 @@ esbuild.build({ ### File Convention Options -| Option | Type | Default | Description | -| ------------------------ | ------------------ | ----------- | ------------------------------------ | -| `routeFilePrefix` | `string` | `undefined` | Prefix filter for route files | -| `routeFileIgnorePrefix` | `string` | `'-'` | Prefix to exclude files from routing | -| `routeFileIgnorePattern` | `string` | `undefined` | Pattern to exclude from routing | -| `indexToken` | `string \| RegExp` | `'index'` | Token identifying index routes | -| `routeToken` | `string \| RegExp` | `'route'` | Token identifying route config files | +| Option | Type | Default | Description | +| ------------------------ | ------------------------------------------------------- | ----------- | ------------------------------------ | +| `routeFilePrefix` | `string` | `undefined` | Prefix filter for route files | +| `routeFileIgnorePrefix` | `string` | `'-'` | Prefix to exclude files from routing | +| `routeFileIgnorePattern` | `string` | `undefined` | Pattern to exclude from routing | +| `indexToken` | `string \| RegExp \| { regex: string; flags?: string }` | `'index'` | Token identifying index routes | +| `routeToken` | `string \| RegExp \| { regex: string; flags?: string }` | `'route'` | Token identifying route config files | ### Code Splitting Options diff --git a/packages/solid-router/bin/intent.js b/packages/solid-router/bin/intent.js index 2cf2efab472..5b085328854 100755 --- a/packages/solid-router/bin/intent.js +++ b/packages/solid-router/bin/intent.js @@ -6,7 +6,12 @@ try { await import('@tanstack/intent/intent-library') } catch (e) { - if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + const isModuleNotFound = + e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND' + const missingIntentLibrary = + typeof e?.message === 'string' && e.message.includes('@tanstack/intent') + + if (isModuleNotFound && missingIntentLibrary) { console.error('@tanstack/intent is not installed.') console.error('') console.error('Install it as a dev dependency:') diff --git a/packages/solid-router/eslint.config.ts b/packages/solid-router/eslint.config.ts index ec79e9ff637..672ce4e4da9 100644 --- a/packages/solid-router/eslint.config.ts +++ b/packages/solid-router/eslint.config.ts @@ -3,6 +3,9 @@ import rootConfig from '../../eslint.config.js' export default [ ...rootConfig, + { + ignores: ['bin/**'], + }, { files: ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'], plugins: { diff --git a/packages/solid-router/skills/solid-router/SKILL.md b/packages/solid-router/skills/solid-router/SKILL.md index d6f5a673afe..a146f02ffb2 100644 --- a/packages/solid-router/skills/solid-router/SKILL.md +++ b/packages/solid-router/skills/solid-router/SKILL.md @@ -220,7 +220,10 @@ function PostDetails() { All imported from `@tanstack/solid-router`: - **`useMatches()`** — `Accessor<Array<Match>>`, all active route matches +- **`useParentMatches()`** — `Accessor<Array<Match>>`, parent route matches +- **`useChildMatches()`** — `Accessor<Array<Match>>`, child route matches - **`useRouteContext({ from })`** — `Accessor<T>`, context from `beforeLoad` +- **`useLoaderDeps({ from })`** — `Accessor<T>`, loader dependency values - **`useBlocker({ shouldBlockFn })`** — blocks navigation for unsaved changes - **`useCanGoBack()`** — `Accessor<boolean>` - **`useLocation()`** — `Accessor<ParsedLocation>` @@ -293,18 +296,11 @@ import { Suspense } from 'solid-js' function PostWithComments() { const data = Route.useLoaderData() return ( - <div> - <h1>Post</h1> - <Suspense fallback={<div>Loading comments...</div>}> - <Await promise={data().deferredComments}> - {(comments) => ( - <ul> - <For each={comments}>{(c) => <li>{c.text}</li>}</For> - </ul> - )} - </Await> - </Suspense> - </div> + <Suspense fallback={<div>Loading...</div>}> + <Await promise={data().deferredComments}> + {(comments) => <For each={comments}>{(c) => <li>{c.text}</li>}</For>} + </Await> + </Suspense> ) } ``` @@ -317,13 +313,48 @@ Error boundary wrapping `Solid.ErrorBoundary`: import { CatchBoundary } from '@tanstack/solid-router' ;<CatchBoundary getResetKey={() => 'widget'} - onCatch={(error) => console.error(error)} errorComponent={({ error }) => <div>Error: {error.message}</div>} > <RiskyWidget /> </CatchBoundary> ``` +### Other Components + +- **`CatchNotFound`** — catches `notFound()` errors in children; `fallback` receives the error data +- **`Block`** — declarative navigation blocker; use `shouldBlockFn` and `withResolver` for custom UI +- **`ScrollRestoration`** — **deprecated**; use `createRouter`'s `scrollRestoration: true` option instead +- **`ClientOnly`** — renders children only after hydration; accepts `fallback` prop + +### `Block` + +Declarative navigation blocker component: + +```tsx +import { Block } from '@tanstack/solid-router' +;<Block shouldBlockFn={() => formIsDirty()} withResolver> + {({ status, proceed, reset }) => ( + <Show when={status === 'blocked'}> + <div> + <p>Are you sure?</p> + <button onClick={proceed}>Yes</button> + <button onClick={reset}>No</button> + </div> + </Show> + )} +</Block> +``` + +### `ScrollRestoration` + +Restores scroll position on navigation: + +```tsx +import { ScrollRestoration } from '@tanstack/solid-router' +// In root route component +;<ScrollRestoration /> +``` + ### `ClientOnly` Renders children only after hydration: diff --git a/packages/solid-start/bin/intent.js b/packages/solid-start/bin/intent.js index 2cf2efab472..5b085328854 100755 --- a/packages/solid-start/bin/intent.js +++ b/packages/solid-start/bin/intent.js @@ -6,7 +6,12 @@ try { await import('@tanstack/intent/intent-library') } catch (e) { - if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + const isModuleNotFound = + e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND' + const missingIntentLibrary = + typeof e?.message === 'string' && e.message.includes('@tanstack/intent') + + if (isModuleNotFound && missingIntentLibrary) { console.error('@tanstack/intent is not installed.') console.error('') console.error('Install it as a dev dependency:') diff --git a/packages/solid-start/skills/solid-start/SKILL.md b/packages/solid-start/skills/solid-start/SKILL.md index 641bd5a811f..da285df1636 100644 --- a/packages/solid-start/skills/solid-start/SKILL.md +++ b/packages/solid-start/skills/solid-start/SKILL.md @@ -86,7 +86,7 @@ import solidPlugin from 'vite-plugin-solid' export default defineConfig({ plugins: [ tanstackStart(), // MUST come before solid plugin - solidPlugin(), + solidPlugin({ ssr: true }), ], }) ``` @@ -115,6 +115,8 @@ import { HeadContent, Scripts, } from '@tanstack/solid-router' +import { HydrationScript } from 'solid-js/web' +import { Suspense } from 'solid-js' export const Route = createRootRoute({ head: () => ({ @@ -124,17 +126,19 @@ export const Route = createRootRoute({ { title: 'My TanStack Start App' }, ], }), - component: RootComponent, + // shellComponent renders the HTML document shell (always SSR'd) + shellComponent: RootDocument, }) -function RootComponent() { +function RootDocument(props: { children: any }) { return ( <html> <head> - <HeadContent /> + <HydrationScript /> </head> <body> - <Outlet /> + <HeadContent /> + <Suspense>{props.children}</Suspense> <Scripts /> </body> </html> @@ -205,7 +209,7 @@ All routing components from `@tanstack/solid-router` work in Start: - `<Outlet>` — renders matched child route - `<Link>` — type-safe navigation - `<Navigate>` — declarative redirect -- `<HeadContent>` — renders head tags via `@solidjs/meta` (must be in `<head>`) +- `<HeadContent>` — renders head tags via `@solidjs/meta` (must be in `<body>` — uses portals to inject into `<head>`) - `<Scripts>` — renders body scripts (must be in `<body>`) - `<Await>` — renders deferred data with `<Suspense>` - `<ClientOnly>` — renders children only after hydration diff --git a/packages/start-client-core/bin/intent.js b/packages/start-client-core/bin/intent.js index 2cf2efab472..5b085328854 100755 --- a/packages/start-client-core/bin/intent.js +++ b/packages/start-client-core/bin/intent.js @@ -6,7 +6,12 @@ try { await import('@tanstack/intent/intent-library') } catch (e) { - if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + const isModuleNotFound = + e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND' + const missingIntentLibrary = + typeof e?.message === 'string' && e.message.includes('@tanstack/intent') + + if (isModuleNotFound && missingIntentLibrary) { console.error('@tanstack/intent is not installed.') console.error('') console.error('Install it as a dev dependency:') diff --git a/packages/start-client-core/skills/start-core/server-functions/SKILL.md b/packages/start-client-core/skills/start-core/server-functions/SKILL.md index cc4f6998102..513065da235 100644 --- a/packages/start-client-core/skills/start-core/server-functions/SKILL.md +++ b/packages/start-client-core/skills/start-core/server-functions/SKILL.md @@ -203,11 +203,9 @@ const getCachedData = createServerFn({ method: 'GET' }).handler(async () => { const request = getRequest() const authHeader = getRequestHeader('Authorization') - setResponseHeaders( - new Headers({ - 'Cache-Control': 'public, max-age=300', - }), - ) + setResponseHeaders({ + 'Cache-Control': 'public, max-age=300', + }) setResponseStatus(200) return fetchData() @@ -281,7 +279,7 @@ export const Route = createFileRoute('/posts')({ ### 2. CRITICAL: Using Next.js/Remix server patterns ```tsx -// WRONG — "use server" is a React/Next.js directive +// WRONG — "use server" is a React directive, not used in TanStack Start 'use server' export async function getUser() { ... } diff --git a/packages/start-client-core/skills/start-core/server-routes/SKILL.md b/packages/start-client-core/skills/start-core/server-routes/SKILL.md index be21baf63df..101541b2aaa 100644 --- a/packages/start-client-core/skills/start-core/server-routes/SKILL.md +++ b/packages/start-client-core/skills/start-core/server-routes/SKILL.md @@ -104,6 +104,8 @@ Each handler receives: - `request` — the incoming [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object - `params` — dynamic path parameters - `context` — context from middleware +- `pathname` — the matched pathname +- `next` — call to fall through to SSR (returns a `Response`) ## Dynamic Path Params diff --git a/packages/start-server-core/bin/intent.js b/packages/start-server-core/bin/intent.js index 2cf2efab472..5b085328854 100755 --- a/packages/start-server-core/bin/intent.js +++ b/packages/start-server-core/bin/intent.js @@ -6,7 +6,12 @@ try { await import('@tanstack/intent/intent-library') } catch (e) { - if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + const isModuleNotFound = + e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND' + const missingIntentLibrary = + typeof e?.message === 'string' && e.message.includes('@tanstack/intent') + + if (isModuleNotFound && missingIntentLibrary) { console.error('@tanstack/intent is not installed.') console.error('') console.error('Install it as a dev dependency:') diff --git a/packages/start-server-core/skills/start-server-core/SKILL.md b/packages/start-server-core/skills/start-server-core/SKILL.md index 567c5da6aa2..4767576b481 100644 --- a/packages/start-server-core/skills/start-server-core/SKILL.md +++ b/packages/start-server-core/skills/start-server-core/SKILL.md @@ -18,7 +18,7 @@ sources: Server-side runtime for TanStack Start. Provides the request handler, request/response utilities, cookie management, and session management. All utilities are available anywhere in the call stack during a request via AsyncLocalStorage. > **CRITICAL**: These utilities are SERVER-ONLY. Import them from `@tanstack/<framework>-start/server`, not from the main entry point. They throw if called outside a server request context. - +> > **CRITICAL**: Types are FULLY INFERRED. Never cast, never annotate inferred values. ## `createStartHandler` @@ -26,7 +26,8 @@ Server-side runtime for TanStack Start. Provides the request handler, request/re Creates the main request handler that processes all incoming requests through three phases: server functions, server routes, then app SSR. ```ts -// src/entry.server.ts (or use framework defaults) +// src/server.ts +// Use @tanstack/<framework>-start for your framework (react, solid, vue) import { createStartHandler } from '@tanstack/react-start/server' import { defaultStreamHandler } from '@tanstack/react-start/server' @@ -51,6 +52,8 @@ All imported from `@tanstack/<framework>-start/server`. Available anywhere durin ### Reading Request Data ```ts +// Use @tanstack/<framework>-start for your framework (react, solid, vue) +import { createServerFn } from '@tanstack/react-start' import { getRequest, getRequestHeaders, @@ -77,6 +80,8 @@ const serverFn = createServerFn({ method: 'GET' }).handler(async () => { ### Setting Response Data ```ts +// Use @tanstack/<framework>-start for your framework (react, solid, vue) +import { createServerFn } from '@tanstack/react-start' import { setResponseHeader, setResponseHeaders, @@ -100,6 +105,8 @@ const serverFn = createServerFn({ method: 'POST' }).handler(async () => { ## Cookie Management ```ts +// Use @tanstack/<framework>-start for your framework (react, solid, vue) +import { createServerFn } from '@tanstack/react-start' import { getCookies, getCookie, @@ -127,6 +134,8 @@ const serverFn = createServerFn({ method: 'POST' }).handler(async () => { Encrypted sessions stored in cookies. Requires a password for encryption. ```ts +// Use @tanstack/<framework>-start for your framework (react, solid, vue) +import { createServerFn } from '@tanstack/react-start' import { useSession, getSession, @@ -183,24 +192,32 @@ await session.clear() // Clear session data ## Query Validation +Validate query string parameters using a Standard Schema: + ```ts +// Use @tanstack/<framework>-start for your framework (react, solid, vue) import { getValidatedQuery } from '@tanstack/react-start/server' +import { z } from 'zod' const serverFn = createServerFn({ method: 'GET' }).handler(async () => { - const query = getValidatedQuery((q) => ({ - page: Number(q.page) || 1, - limit: Math.min(Number(q.limit) || 20, 100), - })) + const query = await getValidatedQuery( + z.object({ + page: z.coerce.number().default(1), + limit: z.coerce.number().default(20), + }), + ) return { page: query.page } }) ``` +> Note: `getValidatedQuery` accepts a Standard Schema validator, not a callback function. + ## How Request Handling Works `createStartHandler` processes requests in three phases: -1. **Server Function Dispatch** — If URL matches the server function prefix (`/_server`), deserializes the payload, runs global request middleware, executes the server function, and returns the serialized result. +1. **Server Function Dispatch** — If URL matches the server function prefix (`/_serverFn`), deserializes the payload, runs global request middleware, executes the server function, and returns the serialized result. 2. **Server Route Handler** — For non-server-function requests, matches the URL against routes with `server.handlers`. Runs route middleware, then the matched HTTP method handler. Handlers can return a `Response` or call `next()` to fall through to SSR. @@ -221,6 +238,7 @@ function MyComponent() { } // CORRECT — use inside server functions only +// Use @tanstack/<framework>-start for your framework (react, solid, vue) import { createServerFn } from '@tanstack/react-start' import { getCookie } from '@tanstack/react-start/server' @@ -229,9 +247,9 @@ const getAuth = createServerFn({ method: 'GET' }).handler(async () => { }) ``` -### 2. HIGH: Forgetting session password is required +### 2. HIGH: Forgetting session password for most session operations -`useSession`, `getSession`, and `updateSession` all require a `password` field for encryption. Missing it throws at runtime. +`useSession`, `getSession`, `updateSession`, and `sealSession` all require a `password` field for encryption. Missing it throws at runtime. `clearSession` accepts `Partial<SessionConfig>`, so password is optional for clearing. ### 3. MEDIUM: Using session without HTTPS in production diff --git a/packages/virtual-file-routes/bin/intent.js b/packages/virtual-file-routes/bin/intent.js index 2cf2efab472..5b085328854 100755 --- a/packages/virtual-file-routes/bin/intent.js +++ b/packages/virtual-file-routes/bin/intent.js @@ -6,7 +6,12 @@ try { await import('@tanstack/intent/intent-library') } catch (e) { - if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + const isModuleNotFound = + e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND' + const missingIntentLibrary = + typeof e?.message === 'string' && e.message.includes('@tanstack/intent') + + if (isModuleNotFound && missingIntentLibrary) { console.error('@tanstack/intent is not installed.') console.error('') console.error('Install it as a dev dependency:') diff --git a/packages/vue-router/bin/intent.js b/packages/vue-router/bin/intent.js index 2cf2efab472..5b085328854 100755 --- a/packages/vue-router/bin/intent.js +++ b/packages/vue-router/bin/intent.js @@ -6,7 +6,12 @@ try { await import('@tanstack/intent/intent-library') } catch (e) { - if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + const isModuleNotFound = + e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND' + const missingIntentLibrary = + typeof e?.message === 'string' && e.message.includes('@tanstack/intent') + + if (isModuleNotFound && missingIntentLibrary) { console.error('@tanstack/intent is not installed.') console.error('') console.error('Install it as a dev dependency:') diff --git a/packages/vue-router/eslint.config.ts b/packages/vue-router/eslint.config.ts index a8ba3a730cb..e192e65890b 100644 --- a/packages/vue-router/eslint.config.ts +++ b/packages/vue-router/eslint.config.ts @@ -2,6 +2,9 @@ import rootConfig from '../../eslint.config.js' export default [ ...rootConfig, + { + ignores: ['bin/**'], + }, { files: ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'], plugins: {}, diff --git a/packages/vue-router/skills/vue-router/SKILL.md b/packages/vue-router/skills/vue-router/SKILL.md index 3cd2bd08819..769b538c14a 100644 --- a/packages/vue-router/skills/vue-router/SKILL.md +++ b/packages/vue-router/skills/vue-router/SKILL.md @@ -38,7 +38,7 @@ This skill covers the Vue-specific bindings, components, composables, and setup ```bash npm install @tanstack/vue-router -npm install -D @tanstack/router-plugin +npm install -D @tanstack/router-plugin @vitejs/plugin-vue-jsx ``` ### 2. Configure Vite Plugin @@ -47,6 +47,7 @@ npm install -D @tanstack/router-plugin // vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' import { tanstackRouter } from '@tanstack/router-plugin/vite' export default defineConfig({ @@ -57,6 +58,7 @@ export default defineConfig({ autoCodeSplitting: true, }), vue(), + vueJsx(), // Required for JSX/TSX route files ], }) ``` @@ -203,8 +205,8 @@ const match = useMatch({ from: '/posts/$postId' }) - **`useBlocker({ shouldBlockFn })`** — blocks navigation for unsaved changes - **`useCanGoBack()`** — `Ref<boolean>` - **`useLocation()`** — `Ref<ParsedLocation>` -- **`useLoaderDeps({ from })`** — returns raw value (NOT a Ref) -- **`useLinkProps()`** — returns `ComputedRef<LinkHTMLAttributes>` +- **`useLoaderDeps({ from })`** — `Ref<T>`, loader dependency values +- **`useLinkProps()`** — returns `LinkHTMLAttributes` - **`useMatchRoute()`** — returns a function; calling it returns `Ref<false | Params>` ## Components Reference @@ -301,8 +303,9 @@ const StyledLink = createLink(StyledLinkComponent) All components in `@tanstack/vue-router` use `h()` render functions internally. Route components can use either SFC templates or render functions: +SFC template (most common for user code) in `MyRoute.component.vue`: + ```vue -// SFC template (most common for user code) // MyRoute.component.vue <template> <div>{{ data.title }}</div> </template> @@ -337,9 +340,9 @@ beforeLoad: ({ context }) => { ### Vue File Conventions for Code Splitting -With `autoCodeSplitting`, Vue routes support split-file conventions: +With `autoCodeSplitting`, Vue routes can optionally use split-file conventions. These are NOT required — single-file `.tsx` routes work fine. Split files are useful for separating route config from components: -- `myRoute.route.ts` — route configuration (search params, loader, beforeLoad) +- `myRoute.ts` — route configuration (search params, loader, beforeLoad) - `myRoute.component.vue` — route component (lazy-loaded) - `myRoute.errorComponent.vue` — error component (lazy-loaded) - `myRoute.notFoundComponent.vue` — not-found component (lazy-loaded) diff --git a/packages/vue-start/bin/intent.js b/packages/vue-start/bin/intent.js index 2cf2efab472..5b085328854 100755 --- a/packages/vue-start/bin/intent.js +++ b/packages/vue-start/bin/intent.js @@ -6,7 +6,12 @@ try { await import('@tanstack/intent/intent-library') } catch (e) { - if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + const isModuleNotFound = + e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND' + const missingIntentLibrary = + typeof e?.message === 'string' && e.message.includes('@tanstack/intent') + + if (isModuleNotFound && missingIntentLibrary) { console.error('@tanstack/intent is not installed.') console.error('') console.error('Install it as a dev dependency:') diff --git a/packages/vue-start/eslint.config.ts b/packages/vue-start/eslint.config.ts index a8ba3a730cb..e192e65890b 100644 --- a/packages/vue-start/eslint.config.ts +++ b/packages/vue-start/eslint.config.ts @@ -2,6 +2,9 @@ import rootConfig from '../../eslint.config.js' export default [ ...rootConfig, + { + ignores: ['bin/**'], + }, { files: ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'], plugins: {}, diff --git a/packages/vue-start/skills/vue-start/SKILL.md b/packages/vue-start/skills/vue-start/SKILL.md index 628624d0423..7b8eb77c967 100644 --- a/packages/vue-start/skills/vue-start/SKILL.md +++ b/packages/vue-start/skills/vue-start/SKILL.md @@ -43,7 +43,7 @@ Server utilities (`getRequest`, `getRequestHeader`, `setResponseHeader`, `setCoo ```bash npm i @tanstack/vue-start @tanstack/vue-router vue -npm i -D vite @vitejs/plugin-vue typescript +npm i -D vite @vitejs/plugin-vue @vitejs/plugin-vue-jsx typescript ``` ### 2. package.json @@ -81,11 +81,13 @@ npm i -D vite @vitejs/plugin-vue typescript import { defineConfig } from 'vite' import { tanstackStart } from '@tanstack/vue-start/plugin/vite' import vuePlugin from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' export default defineConfig({ plugins: [ tanstackStart(), // MUST come before vue plugin vuePlugin(), + vueJsx(), // Required for JSX/TSX route files ], }) ``` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7d2bf96af4..26cec5fd20a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11048,7 +11048,7 @@ importers: devDependencies: '@netlify/vite-plugin-tanstack-start': specifier: ^1.1.4 - version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) '@tailwindcss/vite': specifier: ^4.1.18 version: 4.1.18(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) @@ -11950,9 +11950,6 @@ importers: specifier: ^1.0.3 version: 1.0.3 devDependencies: - '@tanstack/intent': - specifier: ^0.0.14 - version: 0.0.14 '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -21179,8 +21176,8 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-extra@11.3.0: - resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} engines: {node: '>=14.14'} fs-extra@7.0.1: @@ -21615,8 +21612,8 @@ packages: engines: {node: '>=8'} hasBin: true - import-meta-resolve@4.1.0: - resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} @@ -22041,9 +22038,6 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} @@ -28402,13 +28396,13 @@ snapshots: uuid: 11.1.0 write-file-atomic: 5.0.1 - '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)': + '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)': dependencies: '@netlify/blobs': 10.1.0 '@netlify/config': 23.2.0 '@netlify/dev-utils': 4.3.0 '@netlify/edge-functions-dev': 1.0.0 - '@netlify/functions-dev': 1.0.0(rollup@4.55.3) + '@netlify/functions-dev': 1.0.0(encoding@0.1.13)(rollup@4.55.3) '@netlify/headers': 2.1.0 '@netlify/images': 1.3.0(@netlify/blobs@10.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2) '@netlify/redirects': 3.1.0 @@ -28476,12 +28470,12 @@ snapshots: dependencies: '@netlify/types': 2.1.0 - '@netlify/functions-dev@1.0.0(rollup@4.55.3)': + '@netlify/functions-dev@1.0.0(encoding@0.1.13)(rollup@4.55.3)': dependencies: '@netlify/blobs': 10.1.0 '@netlify/dev-utils': 4.3.0 '@netlify/functions': 5.0.0 - '@netlify/zip-it-and-ship-it': 14.1.11(rollup@4.55.3) + '@netlify/zip-it-and-ship-it': 14.1.11(encoding@0.1.13)(rollup@4.55.3) cron-parser: 4.9.0 decache: 4.6.2 extract-zip: 2.0.1 @@ -28571,9 +28565,9 @@ snapshots: '@netlify/types@2.1.0': {} - '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: - '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) optionalDependencies: '@tanstack/solid-start': link:packages/solid-start @@ -28601,9 +28595,9 @@ snapshots: - supports-color - uploadthing - '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: - '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.55.3) + '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.9.2)(rollup@4.55.3) '@netlify/dev-utils': 4.3.0 dedent: 1.7.0(babel-plugin-macros@3.1.0) vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) @@ -28631,13 +28625,13 @@ snapshots: - supports-color - uploadthing - '@netlify/zip-it-and-ship-it@14.1.11(rollup@4.55.3)': + '@netlify/zip-it-and-ship-it@14.1.11(encoding@0.1.13)(rollup@4.55.3)': dependencies: '@babel/parser': 7.28.5 '@babel/types': 7.28.4 '@netlify/binary-info': 1.0.0 '@netlify/serverless-functions-api': 2.7.1 - '@vercel/nft': 0.29.4(rollup@4.55.3) + '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.55.3) archiver: 7.0.1 common-path-prefix: 3.0.0 copy-file: 11.1.0 @@ -32022,7 +32016,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vercel/nft@0.29.4(rollup@4.55.3)': + '@vercel/nft@0.29.4(encoding@0.1.13)(rollup@4.55.3)': dependencies: '@mapbox/node-pre-gyp': 2.0.0(encoding@0.1.13) '@rollup/pluginutils': 5.1.4(rollup@4.55.3) @@ -35141,10 +35135,10 @@ snapshots: fs-constants@1.0.0: {} - fs-extra@11.3.0: + fs-extra@11.3.4: dependencies: graceful-fs: 4.2.11 - jsonfile: 6.1.0 + jsonfile: 6.2.0 universalify: 2.0.1 fs-extra@7.0.1: @@ -35335,7 +35329,7 @@ snapshots: optionalDependencies: crossws: 0.4.3(srvx@0.10.1) - h3@2.0.1-rc.14(crossws@0.4.3(srvx@0.11.9)): + h3@2.0.1-rc.14(crossws@0.4.3(srvx@0.10.1)): dependencies: rou3: 0.7.12 srvx: 0.11.9 @@ -35590,7 +35584,7 @@ snapshots: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 - import-meta-resolve@4.1.0: {} + import-meta-resolve@4.2.0: {} imurmurhash@0.1.4: {} @@ -36022,12 +36016,6 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jsonfile@6.1.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - jsonfile@6.2.0: dependencies: universalify: 2.0.1 @@ -36760,7 +36748,7 @@ snapshots: consola: 3.4.2 crossws: 0.4.3(srvx@0.10.1) db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) - h3: 2.0.1-rc.14(crossws@0.4.3(srvx@0.11.9)) + h3: 2.0.1-rc.14(crossws@0.4.3(srvx@0.10.1)) jiti: 2.6.1 nf3: 0.3.6 ofetch: 2.0.0-alpha.3 @@ -39490,8 +39478,8 @@ snapshots: dependencies: chalk: 4.1.2 commander: 11.1.0 - fs-extra: 11.3.0 - import-meta-resolve: 4.1.0 + fs-extra: 11.3.4 + import-meta-resolve: 4.2.0 zod: 3.25.57 vite-node@3.2.4(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1):