diff --git a/.agents/analytics.md b/.agents/analytics.md deleted file mode 100644 index bcd12c824..000000000 --- a/.agents/analytics.md +++ /dev/null @@ -1,375 +0,0 @@ -# Analytics - -Reference for the TanStack.com event taxonomy in Google Analytics 4. - -**GA4 property:** `G-JMT1Z50SPS` -**First-party proxy:** all hits route through `/_a/g/collect` on tanstack.com (see `netlify.toml`) -**Implementation:** [src/utils/analytics.ts](../src/utils/analytics.ts), [src/utils/analytics/events.ts](../src/utils/analytics/events.ts) - -## Design principles - -1. **Event names describe what happened.** Properties carry context. Adding a new partner placement is an enum value, not a new event. -2. **One event per outcome, not per intent.** No `_clicked`/`_attempted` events when an outcome event is going to fire anyway. -3. **No per-row events.** Counts and joined-string arrays as properties, not N rows per generation. -4. **Session context propagates.** Slow-changing props like `mode_used` and `idea_used` are stamped on every builder event so any breakdown works without joins. -5. **Typed registry.** Wrong props for an event = TypeScript error, not silent bad data. - ---- - -## The funnel - -Application Builder, four steps. No `OR` conditions, no overlapping event names. - -| # | Step | Event | Filter | -| --- | --------------------- | ------------------- | --------------------------------- | -| 1 | Landed on builder | `page_view` | `page_type = application_builder` | -| 2 | Got an analysis | `builder_analyzed` | — | -| 3 | Got a generation | `builder_generated` | — | -| 4 | Took action on result | `builder_activated` | — | - -Drop-off between any two steps is unambiguous. - -**Failure rates** are separate explorations, not branches off the funnel: - -- Analysis failure rate = `builder_failed[stage=analysis]` ÷ (`builder_analyzed` + `builder_failed[stage=analysis]`) -- Generation failure rate = `builder_failed[stage=generation]` ÷ (`builder_generated` + `builder_failed[stage=generation]`) -- Login wall rate = `builder_failed[stage=login_blocked]` ÷ `page_view[page_type=application_builder]` - ---- - -## Event reference (9 events) - -Every event automatically receives `page_location`, `page_path`, `page_title`, `page_type` from the analytics utility. - -### `page_view` - -Fires on initial load (auto from gtag config) and on every SPA navigation. - -| Prop | Type | Notes | -| --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | -| `page_location` | string | Full URL | -| `page_path` | string | Pathname | -| `page_title` | string | `document.title` | -| `page_type` | enum | `home`, `partners_index`, `partner_detail`, `blog_index`, `blog_post`, `docs`, `partners_embed`, `application_builder`, `page` | - ---- - -### `partner_viewed` - -A partner UI element scrolled ≥50% into the viewport. Fires once per element-mount per session (the underlying `IntersectionObserver` disconnects after first fire). - -| Prop | Type | Notes | -| ------------ | ------- | --------------------------------------------------- | -| `partner_id` | string | Stable partner identifier | -| `placement` | enum | See `PartnerPlacement` below | -| `slot_index` | number? | Position in the surface (0-indexed) when applicable | - -**Note on inflation:** when filters change on the partners directory, cards unmount/remount and `partner_viewed` re-fires. Treat session-unique impressions as the dedup'd metric (compute in BigQuery with `FIRST_VALUE(... PARTITION BY session_id, partner_id)`). - ---- - -### `partner_clicked` - -User clicked a partner UI element to navigate somewhere. - -| Prop | Type | Notes | -| ------------------ | ------- | -------------------------------------------------------------------------- | -| `partner_id` | string | | -| `placement` | enum | See `PartnerPlacement` below | -| `destination` | enum | `external` (partner's site) or `internal_detail` (our partner detail page) | -| `destination_host` | string? | Host of the destination URL when `external` | -| `slot_index` | number? | Position in the surface when applicable | - -CTR per placement = `partner_clicked` ÷ `partner_viewed` filtered to same `placement`. - ---- - -### `partner_filter_applied` - -User changed the filter state on the partners directory. - -| Prop | Type | Notes | -| ----------------- | -------------- | --------------------------------------------------------------------------------------------- | -| `change` | enum | `libraries_changed`, `status_changed`, `cleared_all` | -| `library_filters` | string | Comma-joined library ids in the filter, or empty string | -| `status_filter` | string \| null | `null` means "no status filter applied" — distinguishes from "user explicitly chose 'active'" | -| `result_count` | number | Number of partners visible after the filter | - ---- - -### `partner_inquiry_started` - -User clicked a "get in touch", "let's chat", or "become a partner" CTA. - -| Prop | Type | Notes | -| ----------- | ---- | ---------------------------------------------------------- | -| `placement` | enum | `partners_index_cta`, `library_callout`, `docs_right_rail` | - ---- - -### `builder_analyzed` - -Analysis API call succeeded. Outcome event — always preceded by user intent, no separate `_requested` event. - -| Prop | Type | Notes | -| ------------------------ | ------- | --------------------------------------------- | -| `mode_used` | enum | Session context: `lucky`, `confident`, `none` | -| `idea_used` | string | Session context: idea label, or `none` | -| `analysis_deployment` | string? | Inferred deploy target | -| `inferred_library_count` | number | | -| `inferred_partner_count` | number | | -| `feature_count` | number | | - ---- - -### `builder_generated` - -Generation API call succeeded. - -| Prop | Type | Notes | -| ----------------------- | ------- | ---------------------------------------------------------------------- | -| `mode_used` | enum | Session context | -| `idea_used` | string | Session context | -| `final_deployment` | string? | The chosen deploy target on the final result | -| `final_package_manager` | string | `pnpm`, `npm`, `yarn`, `bun` | -| `final_library_count` | number | | -| `final_partner_count` | number | | -| `final_addon_count` | number | | -| `library_ids` | string | Comma-joined LibraryIds — use `SPLIT()` in BigQuery for top-N analysis | -| `partner_ids` | string | Comma-joined | -| `addon_ids` | string | Comma-joined | - ---- - -### `builder_failed` - -Single umbrella event for analysis failures, generation failures, and login-wall blocks. Use the `stage` prop to distinguish. - -| Prop | Type | Notes | -| --------------------------------- | ------- | -------------------------------------------------------------------------------------------- | -| `mode_used` | enum | Session context | -| `idea_used` | string | Session context | -| `stage` | enum | `analysis`, `generation`, `login_blocked` | -| `error_message` | string? | Free-form error message — high cardinality, don't register as a dimension; query in BigQuery | -| `retry_after` | number? | Seconds until retry permitted (login_blocked only) | -| `anonymous_generations_remaining` | number? | When the failure was rate-limit related | - ---- - -### `builder_activated` - -User took an action on the generated result. Single event with `action` prop covers all post-generation actions. - -| Prop | Type | Notes | -| ----------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `mode_used` | enum | Session context | -| `idea_used` | string | Session context | -| `action` | enum | See `BuilderAction` below | -| `surface` | enum | `result_panel` (main builder UI) or `deploy_dialog` | -| `provider` | string? | Deploy provider when applicable: `vercel`, `netlify`, `cloudflare` | -| `automatic` | boolean | `true` for system-driven actions (e.g., deploy_dialog auto-redirect countdown). Filter to `false` for true user click rates. | - -**Important:** automatic prompt-copies that fire as a side-effect of generation do NOT emit `builder_activated`. Only user-driven actions count as activation. - ---- - -## Enums - -### `PartnerPlacement` - -| Value | Where it appears | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -| `directory` | Partner cards in `/partners` index | -| `detail` | Partner detail page CTA | -| `docs_rail` | Right rail on docs pages — partner cards AND the "Become a Partner" link both fire with this placement (event name distinguishes) | -| `blog_rail` | Right rail on blog pages | -| `grid` | Generic partners grid (fallback) | -| `home_grid` | Home page social-proof grid | -| `library_grid` | Library page partners section — filter `page_path` to know which library | -| `embed_grid` | Partners embed view | -| `docs_strip` | Mobile partner strip in docs | -| `ecosystem_game` | 3D ecosystem game islands | -| `partners_index_cta` | "Get in touch" mailto on `/partners` | -| `library_callout` | "Let's chat" callout per library | - -### `BuilderAction` - -| Value | Means | -| -------------------------- | ---------------------------------------------------------------------- | -| `copy_prompt` | User clicked a copy button on the prompt | -| `deploy` | Started a deploy through the deploy dialog | -| `clone_repo` | Cloned the GitHub repo | -| `open_codex` | Opened the result in Codex | -| `open_claude` | Opened the result in Claude | -| `open_cursor` | Opened the result in Cursor | -| `download` | Downloaded the project as a zip | -| `open_advanced` | Opened the advanced builder editor | -| `netlify_start` | Started a Netlify deploy from the result | -| `provider_redirect_manual` | User clicked through to deploy provider | -| `provider_redirect_auto` | Countdown auto-redirected user to deploy provider (`automatic = true`) | -| `open_repo` | Opened the project repo from the deploy dialog | - ---- - -## Session context - -`mode_used` and `idea_used` are tracked in the builder hook and stamped on **every** builder event. This means any builder event can be sliced by mode or idea without session joins. - -**`mode_used`** transitions: - -- `none` → `lucky` when user clicks "I'm feeling lucky" -- `none` → `confident` when user clicks "I'm feeling confident" - -**`idea_used`** transitions: - -- `none` → idea label string when user picks a suggested idea -- Reset back to `none` if user clears or types fresh input (TBD — currently sticks for the session) - ---- - -## Custom dimensions to register in GA4 - -Admin → Custom definitions → Create custom dimension. **Event scope** for all of these. Without registration, they're stored in BigQuery export but invisible in the GA4 UI. - -| Dimension name | API name | Used on | -| --------------------- | ----------------------- | -------------------------------------------------------- | -| Placement | `placement` | partner_viewed, partner_clicked, partner_inquiry_started | -| Partner ID | `partner_id` | partner_viewed, partner_clicked | -| Mode used | `mode_used` | all builder events | -| Idea used | `idea_used` | all builder events | -| Action | `action` | builder_activated | -| Surface | `surface` | builder_activated | -| Stage | `stage` | builder_failed | -| Final deployment | `final_deployment` | builder_generated | -| Final package manager | `final_package_manager` | builder_generated | -| Final library count | `final_library_count` | builder_generated | -| Final partner count | `final_partner_count` | builder_generated | -| Page type | `page_type` | all events | - -12 dimensions. Well under the 50-dimension event-scoped limit. - -**Don't register:** `error_message`, `library_ids`, `partner_ids`, `addon_ids`, `destination_host`. High cardinality. Query in BigQuery. - ---- - -## Common GA4 queries - -### "What's our funnel completion rate?" - -**Explore → Funnel exploration**, four steps as defined above. Open funnel. Show elapsed time. - -### "Lucky vs Confident: which mode converts better?" - -Same funnel, breakdown dropdown = `mode_used`. Compare side by side. - -### "Which deploy targets do successful generations land on?" - -**Explore → Free-form**, dimension = `final_deployment`, metric = event count of `builder_generated`. - -### "What % of users hit the login wall?" - -Free-form, filter `event_name = builder_failed`, breakdown by `stage`. - -### "Which placement converts best for partner discovery?" - -Free-form, filter `event_name = partner_clicked OR partner_viewed`, breakdown by `placement`. Compute CTR yourself: clicks ÷ views per placement. - -### "Top 10 libraries in completed generations" - -Requires BigQuery — `library_ids` is high-cardinality. Sample query: - -```sql -SELECT - library_id, - COUNT(*) AS generations -FROM `tanstack.analytics_*.events_*`, -UNNEST(SPLIT((SELECT value.string_value - FROM UNNEST(event_params) - WHERE key = 'library_ids'), ',')) AS library_id -WHERE event_name = 'builder_generated' - AND _TABLE_SUFFIX BETWEEN '20260101' AND '20260131' -GROUP BY library_id -ORDER BY generations DESC -LIMIT 10 -``` - ---- - -## BigQuery export - -**Strongly recommended.** Free at our event volume. Without it, anything beyond stock reports is painful. - -Setup: - -1. GA4 Admin → BigQuery Links → Link -2. Pick a GCP project, daily export, US multi-region -3. Tables appear at `tanstack.analytics_.events_YYYYMMDD` after ~24h - -Once enabled, all event properties are queryable — including the ones not registered as custom dimensions. Use SQL for everything dimensional or aggregate-heavy. Use the GA4 UI for funnel exploration and headline numbers. - ---- - -## Adding new events - -Anything additive should not require schema migration of the existing taxonomy. - -**Add a new partner placement:** - -1. Add the value to `PartnerPlacement` in [src/utils/analytics/events.ts](../src/utils/analytics/events.ts) -2. Use it at the call site -3. (Optional) update the placement table in this doc - -**Add a new builder action:** - -1. Add the value to `BuilderAction` -2. Use it at the `builder_activated` call site - -**Add a new event:** - -1. Add a new union member to `AnalyticsEvent` with its prop interface -2. Call `trackEvent({ name: '...', props: { ... } })` -3. Register relevant breakdown props as custom dimensions in GA4 admin -4. Document the event in this file - -**Don't:** add new properties to existing events without updating both the type definition and this doc. Schema drift in analytics events is the slowest bug to detect. - ---- - -## Code locations - -| File | Purpose | -| ------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | -| [src/utils/analytics.ts](../src/utils/analytics.ts) | `trackEvent`, `useTrackedImpression`, `trackPageView`, `getPageType` | -| [src/utils/analytics/events.ts](../src/utils/analytics/events.ts) | Typed event registry — discriminated union of all events | -| [src/utils/analytics/providers/google.ts](../src/utils/analytics/providers/google.ts) | gtag wrapper | -| [src/utils/analytics/types.ts](../src/utils/analytics/types.ts) | Provider interface | -| [src/routes/\_\_root.tsx](../src/routes/__root.tsx) | gtag bootstrap, `PageViewTracker` | -| [netlify.toml](../netlify.toml) | First-party proxy redirects | - ---- - -## Migration history - -### 2026-05 — schema v2 - -Collapsed 28 events into 9. Removed `application_starter_*` event family. Replaced with `builder_*` events. Mode and idea selection moved from standalone events into session-context props on every builder event. - -**Events removed entirely** (no replacement): - -- `application_starter_library_toggled`, `_integration_toggled`, `_package_manager_toggled`, `_toolchain_toggled` — config exploration depth no longer tracked. Final config is on `builder_generated`. -- `application_starter_continue_clicked`, `_generate_clicked` — intent implied by outcome events. -- `application_starter_login_clicked`, `_value_copied` (auto), `_builder_result_applied`, `_final_partner_in_prompt`, `_final_addon_in_prompt`. - -**Events folded into others:** - -- `application_starter_action_clicked` (mode_selected) → `mode_used` prop on subsequent events -- `application_starter_idea_selected` → `idea_used` prop on subsequent events -- `application_starter_login_required` → `builder_failed[stage=login_blocked]` -- `application_starter_value_copied` (user trigger) → `builder_activated[action=copy_prompt]` -- All other `application_starter_action_clicked` calls → `builder_activated` -- `partner_card_clicked`, `partner_click` → `partner_clicked` -- `partner_impression`, `partner_detail_viewed` → `partner_viewed` -- `partners_filter_changed` → `partner_filter_applied` -- `partner_inquiry_clicked`, `become_partner_clicked` → `partner_inquiry_started` - -Historical data with old event names is still queryable in GA4 and BigQuery. Cutover date: see git log for the migration commit. diff --git a/.agents/index.md b/.agents/index.md deleted file mode 100644 index 01316aeb9..000000000 --- a/.agents/index.md +++ /dev/null @@ -1,19 +0,0 @@ -# Agent Guidelines - -TanStack.com marketing site built with TanStack Start. - -## Essentials - -- Package manager: `pnpm` -- Run `pnpm test` before commits not after every tiny edit -- Don't run builds or tests after every change. This is a visual site; assume changes work unless reported otherwise. -- Rely on `tsc` or your built-in LSP integration (hopefully you have this) -- **Typesafety is paramount.** Never cast types; fix at source instead. See [typescript.md](./typescript.md). - -## Topic Guides - -- [TypeScript Conventions](./typescript.md): Type inference, casting rules, generic naming -- [TanStack Patterns](./tanstack-patterns.md): Loaders, server functions, environment shaking -- [UI Style Guide](./ui-style.md): Visual design principles for 2026 -- [Workflow](./workflow.md): Build commands, debugging, Playwright -- [Analytics](./analytics.md): GA4 event taxonomy, funnel definition, custom dimensions diff --git a/.agents/tanstack-patterns.md b/.agents/tanstack-patterns.md deleted file mode 100644 index d0ff4e278..000000000 --- a/.agents/tanstack-patterns.md +++ /dev/null @@ -1,76 +0,0 @@ -# TanStack Patterns - -## loaderDeps Must Be Specific - -Only include properties actually used in the loader. This ensures proper cache invalidation. - -```typescript -// Bad: includes everything -loaderDeps: ({ search }) => search, -loader: async ({ deps }) => { - await fetchData({ page: deps.page, pageSize: deps.pageSize }) -} - -// Good: only what's used -loaderDeps: ({ search }) => ({ - page: search.page, - pageSize: search.pageSize, -}), -loader: async ({ deps }) => { - await fetchData({ page: deps.page, pageSize: deps.pageSize }) -} -``` - -## Loaders Are Isomorphic - -Loaders run on both server and client. They cannot directly access server-only APIs. - -```typescript -// Bad: direct server API access -loader: async () => { - const data = await fs.readFile('data.json') - return { data } -} - -// Good: call a server function -loader: async () => { - const data = await serverFn({ data: { id: '123' } }) - return { data } -} -``` - -## Environment Shaking - -TanStack Start strips any code not referenced by a `createServerFn` handler from the client build. - -- Server-only code (database, fs) is automatically excluded from client bundles -- Only code inside `createServerFn` handlers goes to server bundles -- Code outside handlers is included in both bundles - -## Importing Server Functions - -Server functions wrapped in `createServerFn` can be imported statically. Never use dynamic imports for server-only code in components. - -```typescript -// Bad: dynamic import causes bundler issues -const rolesQuery = useQuery({ - queryFn: async () => { - const { listRoles } = await import('~/utils/roles.server') - return listRoles({ data: {} }) - }, -}) - -// Good: static import -import { listRoles } from '~/utils/roles.server' - -const rolesQuery = useQuery({ - queryFn: async () => listRoles({ data: {} }), -}) -``` - -## Server-Only Import Rules - -1. `createServerFn` wrappers can be imported statically anywhere -2. Direct server-only code (database clients, fs) must only be imported: - - Inside `createServerFn` handlers - - In `*.server.ts` files diff --git a/.agents/typescript.md b/.agents/typescript.md deleted file mode 100644 index e63301ec4..000000000 --- a/.agents/typescript.md +++ /dev/null @@ -1,45 +0,0 @@ -# TypeScript Conventions - -## Avoid Type Casting - -Never cast types unless absolutely necessary. This includes: - -- Manual generic type parameters (e.g., ``) -- Type assertions using `as` -- Type assertions using `satisfies` - -## Prefer Type Inference - -Infer types by going up the logical chain: - -1. **Schema validation** as source of truth (Convex, Zod, etc.) -2. **Type inference** from function return types, API responses -3. **Fix at source** (schema, API definition, function signature) rather than casting at point of use - -```typescript -// Bad -const result = api.getData() as MyType -const value = getValue() - -// Good -const result = api.getData() // Type inferred from return type -const value = getValue() // Type inferred from implementation -``` - -## Generic Type Parameter Naming - -All generic type parameters must be prefixed with `T`. - -```typescript -// Bad -function withCapability( - handler: (user: AuthUser, ...args: Args) => R, -) { ... } - -// Good -function withCapability( - handler: (user: AuthUser, ...args: TArgs) => TReturn, -) { ... } -``` - -Common names: `T`, `TArgs`, `TReturn`, `TData`, `TError`, `TKey`, `TValue` diff --git a/.agents/ui-style.md b/.agents/ui-style.md deleted file mode 100644 index d696b3346..000000000 --- a/.agents/ui-style.md +++ /dev/null @@ -1,52 +0,0 @@ -# UI Style Guide 2026 - -## Layout - -- Fewer, well-defined containers over many small sections -- Generous spacing creates separation before adding visual effects -- Cards are acceptable when they express grouping or hierarchy - -## Corners - -- Rounded corners are standard -- Subtle radius values that feel intentional, not playful -- Avoid sharp 90-degree corners unless intentionally industrial - -## Shadows and Depth - -- Soft, low-contrast, diffused shadows -- Shadows imply separation, not elevation theatrics -- No heavy drop shadows or strong directional lighting -- One to two shadow layers max - -## Cards - -- Cards should feel grounded, not floating -- Light elevation, border plus shadow, or surface contrast -- Don't overuse cards as a default layout primitive - -## Color and Surfaces - -- Soft neutrals, off-whites, warm grays -- Surface contrast or translucency instead of strong outlines -- Glass/frosted effects acceptable when subtle and accessible - -## Interaction - -- Micro transitions reinforce spatial relationships -- Hover/focus states feel responsive, not animated -- No excessive motion or springy effects - -## Typography - -- Strong headings, calm body text -- No visual noise around content - -## What to Avoid - -- Chunky shadows -- Overly flat, sterile layouts -- Neumorphism as primary style -- Over-designed card grids - -**Summary: If depth does not improve comprehension, remove it.** diff --git a/.agents/workflow.md b/.agents/workflow.md deleted file mode 100644 index 07838ff50..000000000 --- a/.agents/workflow.md +++ /dev/null @@ -1,46 +0,0 @@ -# Workflow - -## Build Commands - -- `pnpm test`: Run at end of task batches -- `pnpm build`: Only for build/bundler issues or verifying production output -- `pnpm lint`: Check for code issues -- `dev` runs indefinitely in watch mode - -Don't build after every change. This is a visual site; assume changes work. - -## Dev Authentication - -The dev server uses the real production database and OAuth. There are no auth bypasses. - -To authenticate a dev session, the human must run: - -```sh -pnpm auth:login -``` - -This opens `tanstack.com`, completes OAuth, and saves `DEV_SESSION_TOKEN` to `.env.local`. The dev server then auto-injects that token as a session cookie on startup. - -If the user asks for help with anything requiring authentication (account pages, admin, mutations) and `DEV_SESSION_TOKEN` is not set in `.env.local`, tell them to run `pnpm auth:login` first and restart the dev server. - -## Debugging Visual Issues - -When something doesn't work or look right: - -1. Use Playwright MCP to view the page and debug visually -2. Use `pnpm build` only for build/bundler issues -3. Use `pnpm lint` for code issues - -## Playwright - -Playwright is available via `npx playwright` and should be used freely to interact with the running dev server like a human would. This includes testing, debugging, visual verification, design iteration, and general development workflows. - -Since the dev server runs with a real authenticated session (via `DEV_SESSION_TOKEN`), Playwright has full access to gated pages, account features, admin areas, and authenticated mutations — exactly as the signed-in user would. - -Common uses: - -- `npx playwright screenshot ` — capture current state of any page -- Navigate to a page, interact with elements, re-screenshot to verify changes -- Debug layout/visual issues without needing a human to look -- Verify authenticated flows end-to-end (account, showcase submissions, admin, etc.) -- Iterate on UI by screenshotting, editing, screenshotting again diff --git a/.claude/launch.json b/.claude/launch.json deleted file mode 100644 index 7e34cf155..000000000 --- a/.claude/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "0.0.1", - "configurations": [ - { - "name": "web", - "runtimeExecutable": "pnpm", - "runtimeArgs": ["dev"], - "port": 3000 - }, - { - "name": "db-studio", - "runtimeExecutable": "pnpm", - "runtimeArgs": ["db:studio"], - "port": 4983 - } - ] -} diff --git a/.claude/tanstack-patterns.md b/.claude/tanstack-patterns.md deleted file mode 120000 index 7335c6c22..000000000 --- a/.claude/tanstack-patterns.md +++ /dev/null @@ -1 +0,0 @@ -../.agents/tanstack-patterns.md \ No newline at end of file diff --git a/.claude/typescript.md b/.claude/typescript.md deleted file mode 120000 index bf4e25139..000000000 --- a/.claude/typescript.md +++ /dev/null @@ -1 +0,0 @@ -../.agents/typescript.md \ No newline at end of file diff --git a/.claude/ui-style.md b/.claude/ui-style.md deleted file mode 120000 index f59b7412f..000000000 --- a/.claude/ui-style.md +++ /dev/null @@ -1 +0,0 @@ -../.agents/ui-style.md \ No newline at end of file diff --git a/.claude/workflow.md b/.claude/workflow.md deleted file mode 120000 index c19e63b75..000000000 --- a/.claude/workflow.md +++ /dev/null @@ -1 +0,0 @@ -../.agents/workflow.md \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..715697546 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node"] +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index fd99b3b17..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: tannerlinsley diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml deleted file mode 100644 index b336e672a..000000000 --- a/.github/workflows/autofix.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: autofix.ci # needed to securely identify the workflow - -on: - pull_request: - push: - branches: [main] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.number || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - autofix: - name: autofix - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - name: Setup Tools - uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - - name: Fix formatting - run: pnpm format - - name: Apply fixes - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 - with: - commit-message: 'ci: apply automated fixes' diff --git a/.github/workflows/docs-links.yml b/.github/workflows/docs-links.yml deleted file mode 100644 index 4771fdd25..000000000 --- a/.github/workflows/docs-links.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Docs Links - -on: - workflow_dispatch: - schedule: - - cron: '0 9 * * *' - -permissions: - contents: read - -jobs: - docs-links: - name: Docs Links - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Setup Tools - uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - - - name: Check docs menu links - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - set +e - pnpm docs:links:check -- --json docs-menu-link-report.json - status=$? - - if [ -f docs-menu-link-report.json ]; then - { - echo "### Docs menu link report" - echo - echo '```json' - cat docs-menu-link-report.json - echo '```' - } >> "$GITHUB_STEP_SUMMARY" - fi - - exit $status diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml deleted file mode 100644 index d4497f807..000000000 --- a/.github/workflows/pr.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: PR - -on: - pull_request: - -permissions: - contents: read - -jobs: - pr: - name: PR - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - name: Setup Tools - uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - - name: Run Build - run: pnpm build - - name: Run Tests - run: pnpm test diff --git a/.github/workflows/update-tanstack-deps.yml b/.github/workflows/update-tanstack-deps.yml deleted file mode 100644 index 7d4ad61d9..000000000 --- a/.github/workflows/update-tanstack-deps.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Update TanStack Dependencies - -on: - workflow_dispatch: - schedule: - # Run weekly on Sundays at 10:00 AM UTC - - cron: '0 10 * * 0' - -jobs: - update-deps: - name: Update TanStack Dependencies - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Git Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - # This scheduled job commits dependency updates back to the branch. - persist-credentials: true - - - name: Setup pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 - - - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: .nvmrc - cache: pnpm - - - name: Install Packages - run: pnpm install --frozen-lockfile - - - name: Update TanStack Dependencies - run: pnpm up "@tanstack/*" --latest - - - name: Check for Changes - id: git-check - run: | - git status --porcelain . - if [[ -z $(git status --porcelain .) ]]; then - echo "No changes detected" - echo "changes=false" >> $GITHUB_OUTPUT - else - echo "Changes detected" - echo "changes=true" >> $GITHUB_OUTPUT - fi - - - name: Run Lint - if: steps.git-check.outputs.changes == 'true' - run: pnpm lint - - - name: Run Build - if: steps.git-check.outputs.changes == 'true' - run: pnpm build - - - name: Commit and Push Changes - if: steps.git-check.outputs.changes == 'true' - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git add . - git commit -m "chore: update @tanstack/* dependencies" - git push diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml deleted file mode 100644 index 1d4088db8..000000000 --- a/.github/workflows/zizmor.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: GitHub Actions Security Analysis - -on: - push: - branches: [main] - pull_request: - branches: ['**'] - -permissions: {} - -jobs: - zizmor: - name: Run zizmor - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Run zizmor - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 - with: - advanced-security: false - annotations: true diff --git a/.gitignore b/.gitignore index 7021fe7e7..c14ffa0ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +1,15 @@ node_modules -package-lock.json -yarn.lock -drizzle/migrations -.tanstack .DS_Store .cache .env -.env.* .vercel .output -.vinxi -.tanstack-start/build -.nitro/* -.netlify/* -.wrangler/* /build/ -/api/ +/api/_assets/ /server/build /public/build - -# Sentry Config File -.env.sentry-build-plugin -dist -.vscode/ -.env.local - -# Content Collections generated files -.content-collections - -test-results -.claude/CLAUDE.md -.claude/worktrees -.eslintcache -.tsbuildinfo -src/routeTree.gen.ts -.og-preview/ +/api/index.js +/api/index.js.map +/app/styles/app.generated.css diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index 63860b5bd..000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -pnpm husky diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index d9b042470..000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v25.9.0 diff --git a/.oxfmtrc.json b/.oxfmtrc.json deleted file mode 100644 index ac154e249..000000000 --- a/.oxfmtrc.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "./node_modules/oxfmt/configuration_schema.json", - "semi": false, - "singleQuote": true, - "trailingComma": "all", - "printWidth": 80, - "sortPackageJson": false, - "ignorePatterns": [ - "**/api", - "**/build", - "**/public", - "pnpm-lock.yaml", - "routeTree.gen.ts", - "src/blog/tanstack-db-0.1-the-embedded-client-database-for-tanstack-query.md", - ".content-collections", - ".claude", - "dist/**", - ".output/**" - ] -} diff --git a/.oxlintrc.json b/.oxlintrc.json deleted file mode 100644 index f6467d88c..000000000 --- a/.oxlintrc.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": ["typescript"], - "categories": { - "correctness": "off" - }, - "options": { - "typeAware": true, - "typeCheck": true - }, - "env": { - "builtin": true - }, - "ignorePatterns": [ - "node_modules", - "dist", - "build", - ".content-collections", - ".tanstack-start", - ".netlify", - "public", - "convex/.temp", - ".claude" - ], - "rules": { - "no-array-constructor": "error", - "no-unused-expressions": "error", - "no-unused-vars": "error", - "typescript/ban-ts-comment": "error", - "typescript/no-duplicate-enum-values": "error", - "typescript/no-empty-object-type": "error", - "typescript/no-explicit-any": "error", - "typescript/no-extra-non-null-assertion": "error", - "typescript/no-misused-new": "error", - "typescript/no-namespace": "error", - "typescript/no-non-null-asserted-optional-chain": "error", - "typescript/no-require-imports": "error", - "typescript/no-this-alias": "error", - "typescript/no-unnecessary-type-constraint": "error", - "typescript/no-unsafe-declaration-merging": "error", - "typescript/no-unsafe-function-type": "error", - "typescript/no-wrapper-object-types": "error", - "typescript/prefer-as-const": "error", - "typescript/prefer-namespace-keyword": "error", - "typescript/triple-slash-reference": "error" - }, - "overrides": [ - { - "files": ["**/*.{js,jsx}"], - "rules": { - "constructor-super": "error", - "for-direction": "error", - "no-async-promise-executor": "error", - "no-case-declarations": "error", - "no-class-assign": "error", - "no-compare-neg-zero": "error", - "no-cond-assign": "error", - "no-const-assign": "error", - "no-constant-binary-expression": "error", - "no-constant-condition": "error", - "no-control-regex": "error", - "no-debugger": "error", - "no-delete-var": "error", - "no-dupe-class-members": "error", - "no-dupe-else-if": "error", - "no-dupe-keys": "error", - "no-duplicate-case": "error", - "no-empty": "error", - "no-empty-character-class": "error", - "no-empty-pattern": "error", - "no-empty-static-block": "error", - "no-ex-assign": "error", - "no-extra-boolean-cast": "error", - "no-fallthrough": "error", - "no-func-assign": "error", - "no-global-assign": "error", - "no-import-assign": "error", - "no-invalid-regexp": "error", - "no-irregular-whitespace": "error", - "no-loss-of-precision": "error", - "no-misleading-character-class": "error", - "no-new-native-nonconstructor": "error", - "no-nonoctal-decimal-escape": "error", - "no-obj-calls": "error", - "no-prototype-builtins": "error", - "no-redeclare": "error", - "no-regex-spaces": "error", - "no-self-assign": "error", - "no-setter-return": "error", - "no-shadow-restricted-names": "error", - "no-sparse-arrays": "error", - "no-this-before-super": "error", - "no-unexpected-multiline": "error", - "no-unsafe-finally": "error", - "no-unsafe-negation": "error", - "no-unsafe-optional-chaining": "error", - "no-unused-labels": "error", - "no-unused-private-class-members": "error", - "no-useless-backreference": "error", - "no-useless-catch": "error", - "no-useless-escape": "error", - "no-with": "error", - "require-yield": "error", - "use-isnan": "error", - "valid-typeof": "error" - } - }, - { - "files": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"], - "rules": { - "constructor-super": "off", - "no-class-assign": "off", - "no-const-assign": "off", - "no-dupe-class-members": "off", - "no-dupe-keys": "off", - "no-func-assign": "off", - "no-import-assign": "off", - "no-new-native-nonconstructor": "off", - "no-obj-calls": "off", - "no-redeclare": "off", - "no-setter-return": "off", - "no-this-before-super": "off", - "no-unsafe-negation": "off", - "no-var": "error", - "no-with": "off", - "prefer-const": "error", - "prefer-rest-params": "error", - "prefer-spread": "error" - } - }, - { - "files": ["**/*.{ts,tsx}"], - "rules": { - "no-redeclare": "error", - "no-shadow": "off", - "no-unused-vars": [ - "warn", - { - "argsIgnorePattern": "(^_)|(^__+$)|(^e$)|(^error$)", - "varsIgnorePattern": "(^_)|(^__+$)|(^e$)|(^error$)", - "caughtErrorsIgnorePattern": "(^_)|(^__+$)|(^e$)|(^error$)" - } - ], - "typescript/no-explicit-any": "off" - } - }, - { - "files": ["**/*.{ts,tsx,js,jsx}"], - "rules": { - "jsx-a11y/alt-text": "error", - "jsx-a11y/anchor-ambiguous-text": "off", - "jsx-a11y/anchor-has-content": "error", - "jsx-a11y/anchor-is-valid": "error", - "jsx-a11y/aria-activedescendant-has-tabindex": "error", - "jsx-a11y/aria-props": "error", - "jsx-a11y/aria-proptypes": "error", - "jsx-a11y/aria-role": "error", - "jsx-a11y/aria-unsupported-elements": "error", - "jsx-a11y/autocomplete-valid": "error", - "jsx-a11y/click-events-have-key-events": "error", - "jsx-a11y/heading-has-content": "error", - "jsx-a11y/html-has-lang": "error", - "jsx-a11y/iframe-has-title": "error", - "jsx-a11y/img-redundant-alt": "error", - "jsx-a11y/label-has-associated-control": "error", - "jsx-a11y/media-has-caption": "error", - "jsx-a11y/mouse-events-have-key-events": "error", - "jsx-a11y/no-access-key": "error", - "jsx-a11y/no-autofocus": "error", - "jsx-a11y/no-distracting-elements": "error", - "jsx-a11y/no-noninteractive-tabindex": [ - "error", - { - "tags": [], - "roles": ["tabpanel"], - "allowExpressionValues": true - } - ], - "jsx-a11y/no-redundant-roles": "error", - "jsx-a11y/no-static-element-interactions": [ - "error", - { - "allowExpressionValues": true, - "handlers": [ - "onClick", - "onMouseDown", - "onMouseUp", - "onKeyPress", - "onKeyDown", - "onKeyUp" - ] - } - ], - "jsx-a11y/role-has-required-aria-props": "error", - "jsx-a11y/role-supports-aria-props": "error", - "jsx-a11y/scope": "error", - "jsx-a11y/tabindex-no-positive": "error", - "react/rules-of-hooks": "error", - "react/exhaustive-deps": "warn" - }, - "plugins": ["react", "jsx-a11y"] - } - ] -} diff --git a/AGENTS.md b/AGENTS.md deleted file mode 120000 index 49055565e..000000000 --- a/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -.agents/index.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index 49055565e..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -.agents/index.md \ No newline at end of file diff --git a/README.md b/README.md index f168f491e..5f2f3e965 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,20 @@ -
+# Welcome to TanStack.com! -# TanStack.com +This site is built with Remix! -The home of the TanStack ecosystem. Built with [TanStack Router](https://tanstack.com/router) and deployed on [Cloudflare Workers](https://workers.cloudflare.com/). +- [Remix Docs](https://remix.run/docs) -Follow @TanStack +It's deployed automagically with Vercel! -### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/) - -
+- [Vercel](https://vercel.com/) ## Development -### Quick Start - From your terminal: ```sh -pnpm install -pnpm dev +yarn install +yarn dev ``` This starts your app in development mode, rebuilding assets on file changes. - -### Local Setup - -The documentation for all TanStack projects (except `React Charts`) is hosted on [tanstack.com](https://tanstack.com). In production, doc pages are fetched from GitHub. In development, they're read from your local file system. - -Create a `tanstack` parent directory and clone this repo alongside the projects: - -```sh -mkdir tanstack && cd tanstack -git clone git@github.com:TanStack/tanstack.com.git -git clone git@github.com:TanStack/query.git -git clone git@github.com:TanStack/router.git -git clone git@github.com:TanStack/table.git -``` - -Your directory structure should look like this: - -``` -tanstack/ - ├── tanstack.com/ - ├── query/ - ├── router/ - └── table/ -``` - -> [!WARNING] -> Directory names must match repo names exactly (e.g., `query` not `tanstack-query`). The app finds docs by looking for sibling directories by name. - -### Editing Docs - -To edit docs for a project, make changes in its `docs/` folder (e.g., `../form/docs/`) and visit http://localhost:3000/form/latest/docs/overview to preview. - -> [!NOTE] -> Updated pages need to be manually reloaded in the browser. - -> [!WARNING] -> Update the project's `docs/config.json` if you add a new doc page! - -## Get Involved - -- We welcome issues and pull requests! -- Participate in [GitHub Discussions](https://github.com/TanStack/tanstack.com/discussions) -- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ) - -## Explore the TanStack Ecosystem - -- TanStack Config – Tooling for JS/TS packages -- TanStack DB – Reactive sync client store -- TanStack DevTools – Unified devtools panel -- TanStack Form – Type‑safe form state -- TanStack Pacer – Debouncing, throttling, batching -- TanStack Query – Async state & caching -- TanStack Ranger – Range & slider primitives -- TanStack Router – Type‑safe routing, caching & URL state -- TanStack Start – Full‑stack SSR & streaming -- TanStack Store – Reactive data store -- TanStack Table – Headless datagrids -- TanStack Virtual – Virtualized rendering - -… and more at TanStack.com » - - diff --git a/src/blog/ag-grid-partnership.md b/app/blog/ag-grid-partnership.md similarity index 79% rename from src/blog/ag-grid-partnership.md rename to app/blog/ag-grid-partnership.md index 8dcf3e1d6..87477c72b 100644 --- a/src/blog/ag-grid-partnership.md +++ b/app/blog/ag-grid-partnership.md @@ -1,11 +1,6 @@ --- title: TanStack Table + Ag-Grid Partnership -published: 2022-06-17 -excerpt: AG Grid is now the official TanStack Table open-source partner. Together we'll educate the ecosystem about the differences between the two libraries and when to choose which. -library: table -authors: - - Tanner Linsley - - Niall Crosby +published: 6/17/2022 --- We're excited to announce that [AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable) is now the official **TanStack Table** open-source partner! Together we will strive to achieve the following goals: @@ -16,7 +11,7 @@ We're excited to announce that [AG Grid](https://ag-grid.com/react-data-grid/?ut TanStack Table and [AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable) share the same general problem space, but are implemented via drastically different architectures and paradigms, each offering unique trade-offs, opinions and optimizations depending on use-case. These differences and trade-offs are complimentary to one another and together form what we believe are the best two datagrid/table options available in the JavaScript and TypeScript ecosystem. -To learn about the differences and trade-offs between the two libraries, start by reading the [introduction to TanStack Table](/table/v8/docs/introduction) and the [introduction to AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable)! +To learn about the differences and trade-offs between the two libraries, start by reading the [introduction to TanStack Table](/table/v8/docs/guide/introduction) and the [introduction to AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable)! We are excited about the future of datagrids and tables and we at TanStack are honored that AG Grid is invested in the success of it's open source ecosystem! diff --git a/src/blog/announcing-tanstack-query-v4.md b/app/blog/announcing-tanstack-query-v4.md similarity index 91% rename from src/blog/announcing-tanstack-query-v4.md rename to app/blog/announcing-tanstack-query-v4.md index 2094f1ad3..e35c537a8 100644 --- a/src/blog/announcing-tanstack-query-v4.md +++ b/app/blog/announcing-tanstack-query-v4.md @@ -1,10 +1,6 @@ --- title: Announcing TanStack Query v4 -published: 2022-07-14 -excerpt: The next version of TanStack Query is here. The rebranding and monorepo restructuring finally allows us to bring the joy of react-query to other frameworks like Vue, Svelte, and Solid. -library: query -authors: - - Dominik Dorfmeister +published: 7/14/2022 --- We're excited to announce the next version of [TanStack Query](/query/v4), previously known as `react-query` 🎉. @@ -30,7 +26,6 @@ To achieve this, we've introduced a brand new [Network Mode](/query/v4/docs/guid ### Stable Persisters Since v3, persisters existed as an experimental feature. They allow you to sync the Query Cache to an external location (e.g. localStorage) for later use. We have revamped and improved the APIs after getting lots of feedback, and we are now providing two main peristers out of the box: - - SyncStoragePersister - AsyncStoragePersister @@ -47,12 +42,11 @@ Tracked Queries are a performance optimization that were added in v3.6.0 as an o ### Streamlined APIs Over time, some of our APIs have become quite complex, to the extent that they were contradicting each other. Some examples include: - - QueryKeys sometimes being converted to an Array when exposed, sometimes not. - Query Filters being unintuitive and mutually exclusive. - Default values for parameters defaulting to opposite values on different methods -We've cleaned up a lot of these inconsistencies to make the developer experience even better. v4 also comes with a codemod to help you with the migration path. +We've cleaned up a lot of these inconsistencies to make the developer experience even better. v4 also comes with a codemod to help you with the migration path. ## What's next? diff --git a/app/components/BytesForm.tsx b/app/components/BytesForm.tsx new file mode 100644 index 000000000..080b75291 --- /dev/null +++ b/app/components/BytesForm.tsx @@ -0,0 +1,48 @@ +import useBytesSubmit from './useBytesSubmit' + +export default function BytesForm() { + const { state, handleSubmit, error } = useBytesSubmit() + if (state === 'submitted') { + return ( +

Success! Please, check your email to confirm your subscription.

+ ) + } + return ( +
+
+
+ Bytes +
+ + +
+

+ No spam. Unsubscribe at any time. +

+ {error &&

{error}

} +
+ ) +} \ No newline at end of file diff --git a/app/components/Carbon.tsx b/app/components/Carbon.tsx new file mode 100644 index 000000000..236b97a3b --- /dev/null +++ b/app/components/Carbon.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' + +export function Carbon() { + const ref = React.useRef(null!) + + React.useEffect(() => { + ref.current.innerHTML = '' + const s = document.createElement('script') + s.id = '_carbonads_js' + s.src = `//cdn.carbonads.com/carbon.js?serve=CE7DEKQI&placement=react-tannerlinsleycom` + ref.current.appendChild(s) + }, []) + + return
+} diff --git a/app/components/CodeBlock.tsx b/app/components/CodeBlock.tsx new file mode 100644 index 000000000..c9ac73b8c --- /dev/null +++ b/app/components/CodeBlock.tsx @@ -0,0 +1,62 @@ +import type { FC, HTMLAttributes, ReactElement } from 'react' +import { Children } from 'react' +import invariant from 'tiny-invariant' +import type { Language } from 'prism-react-renderer' +import Highlight, { defaultProps, Prism } from 'prism-react-renderer' + +// @ts-ignore Alias markup as vue highlight +Prism.languages.vue = Prism.languages.markup; + +function getLanguageFromClassName(className: string) { + const match = className.match(/language-(\w+)/) + return match ? match[1] : '' +} + +function isLanguageSupported(lang: string): lang is Language { + return lang in Prism.languages +} + +export const CodeBlock: FC> = ({ children }) => { + invariant(!!children, 'children is required') + const child = children as ReactElement + const className = child.props?.className || '' + const userLang = getLanguageFromClassName(className) + const lang = isLanguageSupported(userLang) ? userLang : 'bash' + const code = child.props.children || '' + return ( +
+ + {({ className, tokens, getLineProps, getTokenProps }) => ( +
+
+ {lang} +
+
+
+                
+                  {tokens.map((line, i) => (
+                    
+ {line.map((token, key) => ( + + ))} +
+ ))} +
+
+
+
+ )} +
+
+ ) +} diff --git a/app/components/DefaultCatchBoundary.tsx b/app/components/DefaultCatchBoundary.tsx new file mode 100644 index 000000000..ecbe16dff --- /dev/null +++ b/app/components/DefaultCatchBoundary.tsx @@ -0,0 +1,45 @@ +import { Link, useCatch } from '@remix-run/react' + +export function DefaultCatchBoundary({ isRoot }: { isRoot?: boolean }) { + let caught = useCatch() + + let message + switch (caught.status) { + case 401: + message = ( +

+ Oops! Looks like you tried to visit a page that you do not have access + to. +

+ ) + break + case 404: + message = ( +

Oops! Looks like you tried to visit a page that does not exist.

+ ) + break + + default: + throw new Error(caught.data || caught.statusText) + } + + return ( +
+

+
{caught.status}
+ {caught.statusText ? ( +
{caught.statusText}
+ ) : null} +

+ {message ?
{message}
: null} + {isRoot ? ( + + TanStack Home + + ) : null} +
+ ) +} diff --git a/app/components/DefaultErrorBoundary.tsx b/app/components/DefaultErrorBoundary.tsx new file mode 100644 index 000000000..27ed303b1 --- /dev/null +++ b/app/components/DefaultErrorBoundary.tsx @@ -0,0 +1,19 @@ +import { ErrorBoundaryComponent } from '@remix-run/node' +import { Link } from '@remix-run/react' + +export const DefaultErrorBoundary: ErrorBoundaryComponent = ({ error }) => { + console.error(error) + + return ( +
+

Something went wrong!

+
We'll get this fixed asap.
+ + TanStack Home + +
+ ) +} diff --git a/src/components/DocTitle.tsx b/app/components/DocTitle.tsx similarity index 56% rename from src/components/DocTitle.tsx rename to app/components/DocTitle.tsx index 0953700ff..a71402606 100644 --- a/src/components/DocTitle.tsx +++ b/app/components/DocTitle.tsx @@ -1,9 +1,7 @@ export function DocTitle(props: { children: React.ReactNode }) { return ( <> -

+

{props.children}

diff --git a/app/components/Docs.tsx b/app/components/Docs.tsx new file mode 100644 index 000000000..3d141723d --- /dev/null +++ b/app/components/Docs.tsx @@ -0,0 +1,270 @@ +import { DocSearch } from "@docsearch/react"; +import * as React from "react"; +import { CgClose, CgMenuLeft } from "react-icons/cg"; +import { FaArrowLeft, FaArrowRight } from "react-icons/fa"; +import { NavLink, Outlet, useMatches } from "@remix-run/react"; +import { last } from "~/utils/utils"; +import { Carbon } from "./Carbon"; +import { LinkOrA } from "./LinkOrA"; +import { Search } from "./Search"; +import { gradientText } from "~/routes/query/$version/index"; +import BytesForm from "./BytesForm"; +import type { SelectProps } from "./Select"; +import { Select } from "./Select"; + +export type DocsConfig = { + docSearch: { + appId: string; + indexName: string; + apiKey: string; + }; + menu: { + label: string | React.ReactNode; + children: { + label: string | React.ReactNode; + to: string; + }[]; + }[]; +}; + +export function Docs({ + colorFrom, + colorTo, + textColor, + logo, + config, + framework, + version, +}: { + colorFrom: string; + colorTo: string; + textColor: string; + logo: React.ReactNode; + config: DocsConfig; + framework?: SelectProps; + version?: SelectProps; +}) { + const matches = useMatches(); + const lastMatch = last(matches); + + const detailsRef = React.useRef(null!); + + const flatMenu = React.useMemo( + () => config.menu.flatMap((d) => d.children), + [config.menu] + ); + + const docsMatch = matches.find((d) => d.pathname.includes("/docs")); + + const relativePathname = lastMatch.pathname.replace( + docsMatch?.pathname! + "/", + "" + ); + + const index = flatMenu.findIndex((d) => d.to === relativePathname); + const prevItem = flatMenu[index - 1]; + const nextItem = flatMenu[index + 1]; + + const menuItems = config.menu.map((group, i) => { + return ( +
+
{group.label}
+
+
+ {group.children?.map((child, i) => { + return ( +
+ {child.to.startsWith("http") ? ( + {child.label} + ) : ( + + props.isActive + ? `font-bold text-transparent bg-clip-text bg-gradient-to-r ${colorFrom} ${colorTo}` + : "" + } + onClick={() => { + detailsRef.current.removeAttribute("open"); + }} + end + > + {child.label} + + )} +
+ ); + })} +
+
+ ); + }); + + const smallMenu = ( +
+
+ +
+ + + {logo} +
+ +
+
+
+ {framework?.selected ? ( + + ) : null} +
+ {menuItems} +
+
+
+ ); + + const largeMenu = ( +
+
{logo}
+
+ +
+
+ {framework?.selected ? ( + + ) : null} +
+
+ {menuItems} +
+
+ +
+
+ ); + + const aside = ( + + ); + + return ( +
+ {smallMenu} + {largeMenu} +
+ +
+
+ {prevItem ? ( + + {prevItem.label} + + ) : null} + {nextItem ? ( + +
+ + {nextItem.label} + {" "} + +
+
+ ) : null} +
+
+
+ {aside} +
+ ); +} diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx new file mode 100644 index 000000000..f54b80dee --- /dev/null +++ b/app/components/Footer.tsx @@ -0,0 +1,50 @@ +import { Link } from '@remix-run/react' + +const footerLinks = [ + { label: 'Blog', to: '/blog' }, + { label: '@Tan_Stack Twitter', to: 'https://twitter.com/tan_stack' }, + { + label: '@TannerLinsley Twitter', + to: 'https://twitter.com/tannerlinsley', + }, + { label: 'GitHub', to: 'https://github.com/tanstack' }, + { + label: 'Youtube', + to: 'https://www.youtube.com/user/tannerlinsley', + }, + { + label: 'Nozzle.io - Keyword Rank Tracker', + to: 'https://nozzle.io', + }, +] + +export function Footer() { + return ( +
+
+ {footerLinks.map((item) => ( +
+ {item.to.startsWith('http') ? ( + + {item.label} + + ) : ( + {item.label} + )} +
+ ))} +
+
+ © {new Date().getFullYear()} Tanner Linsley +
+
+ ) +} diff --git a/app/components/LinkOrA.tsx b/app/components/LinkOrA.tsx new file mode 100644 index 000000000..500d5c3b0 --- /dev/null +++ b/app/components/LinkOrA.tsx @@ -0,0 +1,20 @@ +import type { HTMLProps } from 'react' +import type { LinkProps} from '@remix-run/react'; +import { Link } from '@remix-run/react' + +export function LinkOrA( + props: HTMLProps & LinkProps & { to?: string } +) { + const Comp = props.to?.startsWith('http') ? 'a' : Link + + return ( + // @ts-ignore + + ) +} diff --git a/app/components/MarkdownLink.tsx b/app/components/MarkdownLink.tsx new file mode 100644 index 000000000..b27ba7d55 --- /dev/null +++ b/app/components/MarkdownLink.tsx @@ -0,0 +1,10 @@ +import { HTMLProps } from 'react' +import { Link } from '@remix-run/react' + +export function MarkdownLink(props: HTMLProps) { + if ((props as { href: string }).href?.startsWith('http')) { + return + } + + return +} diff --git a/app/components/Mdx.tsx b/app/components/Mdx.tsx new file mode 100644 index 000000000..f0d40a4ec --- /dev/null +++ b/app/components/Mdx.tsx @@ -0,0 +1,64 @@ +import { MDXComponents } from 'mdx/types' +import { getMDXComponent } from 'mdx-bundler/client' +import * as React from 'react' +import { CodeBlock } from './CodeBlock' +import { MarkdownLink } from './MarkdownLink' + +const CustomHeading = ({ + Comp, + id, + ...props +}: React.HTMLProps & { + Comp: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' +}) => { + if (id) { + return ( + + + + ) + } + return +} + +const makeHeading = + (type: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6') => + (props: React.HTMLProps) => + ( + + ) + +const markdownComponents = { + a: MarkdownLink, + pre: CodeBlock, + h1: makeHeading('h1'), + h2: makeHeading('h2'), + h3: makeHeading('h3'), + h4: makeHeading('h4'), + h5: makeHeading('h5'), + h6: makeHeading('h6'), + code: ({ className = '', ...props }: React.HTMLProps) => { + return ( + + ) + }, +} + +export function Mdx({ + code, + components, +}: { + code: string + components?: MDXComponents +}) { + const Doc = React.useMemo(() => getMDXComponent(code), [code]) + + return +} diff --git a/app/components/PPPBanner.tsx b/app/components/PPPBanner.tsx new file mode 100644 index 000000000..cd57b6c36 --- /dev/null +++ b/app/components/PPPBanner.tsx @@ -0,0 +1,59 @@ +import * as React from 'react' +import { flag } from 'country-emoji' +import { IoIosClose } from 'react-icons/io' +import { useLocalStorage } from '~/utils/useLocalStorage' +import { useClientOnlyRender } from '~/utils/useClientOnlyRender' + +export function PPPBanner() { + const [hidden, setHidden] = useLocalStorage('pppbanner-hidden', false) + const [data, setData] = useLocalStorage('pppbanner-data', null) + + React.useEffect(() => { + // This function has CORS configured to allow + // react-query.tanstack.com and tanstack.com + if (!data) { + fetch('https://ui.dev/api/ppp-discount') + .then(res => res.json()) + .then(res => { + if (res?.code) { + setData(res) + } + }) + } + }, [data, setData]) + + if (!useClientOnlyRender()) { + return null + } + + return ( + <> + {data && !hidden && ( +
+

+ {flag(data.code)} We noticed you're in{' '} + {data.country}. Get{' '} + {data.discount * 100}% off the Official React Query + Course with code {' '} + + {data.coupon} + + . +

+ +
+ )} + + ) +} \ No newline at end of file diff --git a/app/components/Search.tsx b/app/components/Search.tsx new file mode 100644 index 000000000..b7a032b03 --- /dev/null +++ b/app/components/Search.tsx @@ -0,0 +1,9 @@ +import { + DocSearch, + DocSearchButtonProps, + DocSearchProps, +} from '@docsearch/react' + +export function Search(props: DocSearchProps) { + return +} diff --git a/app/components/Select.tsx b/app/components/Select.tsx new file mode 100644 index 000000000..12a262d59 --- /dev/null +++ b/app/components/Select.tsx @@ -0,0 +1,108 @@ +import { Fragment } from "react"; +import { Listbox, Transition } from "@headlessui/react"; + +import { HiCheck, HiChevronDown } from "react-icons/hi"; +import { Form } from "@remix-run/react"; + +export type AvailableOptions = Record< + string, + { label: string; value: string; logo?: string } +>; + +export type SelectProps = { + className?: string; + label: string; + selected: string; + available: AvailableOptions; + onSelect: (selected: { label: string; value: string }) => void; +}; + +export function Select({ + className = "", + label, + selected, + available, + onSelect, +}: SelectProps) { + const selectedOption = available[selected]; + + return ( +
+
{label}
+
+ +
+ + {selectedOption.logo ? ( +
+ {`${selectedOption.label} +
+ ) : null} + {selectedOption.label} + + +
+ + + {Object.values(available).map((option) => ( + + `relative cursor-default select-none py-2 pr-10 ${ + active + ? "bg-gray-100 dark:bg-gray-700" + : "text-gray-900 dark:text-gray-300" + } ${option.logo ? "pl-10" : "pl-2"}` + } + value={option} + > + {({ selected }) => ( + <> + {option.logo ? ( +
+ {`${option.label} +
+ ) : null} + + {option.label} + + {selected ? ( + + + ) : null} + + )} +
+ ))} +
+
+
+
+
+
+ ); +} diff --git a/src/components/SponsorPack.tsx b/app/components/SponsorPack.tsx similarity index 70% rename from src/components/SponsorPack.tsx rename to app/components/SponsorPack.tsx index 38cdca9dc..89a1800eb 100644 --- a/src/components/SponsorPack.tsx +++ b/app/components/SponsorPack.tsx @@ -1,27 +1,9 @@ import React from 'react' import { Pack, hierarchy } from '@visx/hierarchy' -import { ParentSize } from './ParentSize' +import { ParentSize } from '@visx/responsive' import { twMerge } from 'tailwind-merge' -import { getSponsorsForSponsorPack } from '~/utils/sponsors.functions' -type DisplaySponsor = Awaited< - ReturnType ->[number] - -type PackNode = - | DisplaySponsor - | { - children: DisplaySponsor[] - name: string - radius: number - distance: number - } - -export default function SponsorPack({ - sponsors, -}: { - sponsors: DisplaySponsor[] -}) { +export default function SponsorPack({ sponsors }: { sponsors: any }) { const pack = React.useMemo( () => ({ children: sponsors, @@ -29,30 +11,29 @@ export default function SponsorPack({ radius: 0, distance: 0, }), - [sponsors], + [sponsors] ) const root = React.useMemo( () => - hierarchy(pack) - .sum((d) => 0.0007 + ('size' in d ? d.size : 0)) + hierarchy(pack) + .sum((d) => 1 + d?.tier?.monthlyPriceInDollars) .sort( (a, b) => - ('size' in b.data ? b.data.size : 0) - - ('size' in a.data ? a.data.size : 0), + (b.data.tier?.monthlyPriceInDollars ?? 0) - + (a.data.tier?.monthlyPriceInDollars ?? 0) ), - [pack], + [pack] ) return ( - {({ height, width }) => { - return width < 10 || height < 10 ? null : ( + {({ width = 800 }) => { + return width < 10 ? null : (
@@ -63,7 +44,6 @@ export default function SponsorPack({ .spon-link { transition: all .2s ease; transform: translate(-50%, -50%); - will-change: transform; } .spon-link:hover { @@ -77,11 +57,7 @@ export default function SponsorPack({ `, }} /> - + {(packData) => { const circles = packData.descendants().slice(1) // skip first layer return ( @@ -89,18 +65,17 @@ export default function SponsorPack({ {[...circles].reverse().map((circle, i) => { const tooltipX = circle.x > width / 2 ? 'left' : 'right' const tooltipY = circle.y > width / 2 ? 'top' : 'bottom' - const sponsor = circle.data as DisplaySponsor return (

- {sponsor.name || sponsor.login} + {circle.data.name || circle.data.login}

diff --git a/src/components/Style.tsx b/app/components/Style.tsx similarity index 100% rename from src/components/Style.tsx rename to app/components/Style.tsx diff --git a/app/components/useBytesSubmit.tsx b/app/components/useBytesSubmit.tsx new file mode 100644 index 000000000..954dff746 --- /dev/null +++ b/app/components/useBytesSubmit.tsx @@ -0,0 +1,32 @@ +import { useState } from 'react'; + +function sendBytesOptIn({ email, influencer, source, referral }) { + return fetch(`https://bytes.dev/api/bytes-optin-cors`, { + method: 'POST', + body: JSON.stringify({ email, influencer, source, referral }), + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }).then((res) => res.json()) +} + +export default function useBytesSubmit() { + const [state, setState] = useState("initial"); + const [error, setError] = useState(null); + + const handleSubmit = (e) => { + e.preventDefault(); + const email = e.target.email_address.value; + setState("loading"); + sendBytesOptIn({ email, influencer: "tanstack" }) + .then(() => { + setState("submitted"); + }) + .catch((err) => { + setError(err); + }); + }; + + return { handleSubmit, state, error }; +} \ No newline at end of file diff --git a/app/entry.client.tsx b/app/entry.client.tsx new file mode 100644 index 000000000..c91de88fb --- /dev/null +++ b/app/entry.client.tsx @@ -0,0 +1,4 @@ +import { RemixBrowser } from '@remix-run/react' +import { hydrate } from 'react-dom' + +hydrate(, document) diff --git a/app/entry.server.tsx b/app/entry.server.tsx new file mode 100644 index 000000000..d8ce203e8 --- /dev/null +++ b/app/entry.server.tsx @@ -0,0 +1,21 @@ +import type { EntryContext } from '@remix-run/node' +import { RemixServer } from '@remix-run/react' +import { renderToString } from 'react-dom/server' + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + let markup = renderToString( + + ) + + responseHeaders.set('Content-Type', 'text/html') + + return new Response('' + markup, { + status: responseStatusCode, + headers: responseHeaders, + }) +} diff --git a/app/hooks/useSessionStorage.ts b/app/hooks/useSessionStorage.ts new file mode 100644 index 000000000..0ba407cb3 --- /dev/null +++ b/app/hooks/useSessionStorage.ts @@ -0,0 +1,24 @@ +import React from 'react' + +export default function useSessionStorage(key, defaultValue = '') { + const [state, setState] = React.useState(() => { + if (typeof document !== 'undefined') { + const data = sessionStorage.getItem(key) + if (data) { + try { + return JSON.parse(data) + } catch { + // + } + } + return defaultValue + } + return defaultValue + }) + + React.useEffect(() => { + sessionStorage.setItem(key, JSON.stringify(state)) + }, [state]) + + return [state, setState] +} diff --git a/app/hooks/useWindowSize.ts b/app/hooks/useWindowSize.ts new file mode 100644 index 000000000..05da702f3 --- /dev/null +++ b/app/hooks/useWindowSize.ts @@ -0,0 +1,30 @@ +import { useState, useEffect } from 'react' + +export function useWindowSize() { + // Initialize state with undefined width/height so server and client renders match + // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ + const [windowSize, setWindowSize] = useState<{ + width?: number + height?: number + }>({ + width: undefined, + height: undefined, + }) + useEffect(() => { + // Handler to call on window resize + function handleResize() { + // Set window width/height to state + setWindowSize({ + width: window.innerWidth, + height: window.innerHeight, + }) + } + // Add event listener + window.addEventListener('resize', handleResize) + // Call handler right away so state gets updated with initial window size + handleResize() + // Remove event listener on cleanup + return () => window.removeEventListener('resize', handleResize) + }, []) // Empty array ensures that effect is only run on mount + return windowSize +} diff --git a/app/images/ag-grid.png b/app/images/ag-grid.png new file mode 100644 index 000000000..93914f0ac Binary files /dev/null and b/app/images/ag-grid.png differ diff --git a/app/images/angular-logo.svg b/app/images/angular-logo.svg new file mode 100644 index 000000000..6431ef536 --- /dev/null +++ b/app/images/angular-logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/images/bytes.svg b/app/images/bytes.svg new file mode 100644 index 000000000..f0d358694 --- /dev/null +++ b/app/images/bytes.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/images/discord-logo-white.svg b/app/images/discord-logo-white.svg new file mode 100644 index 000000000..13dd38057 --- /dev/null +++ b/app/images/discord-logo-white.svg @@ -0,0 +1,9 @@ + + + + Combined Shape + Created with Sketch. + + + + \ No newline at end of file diff --git a/app/images/header-left-overlay.svg b/app/images/header-left-overlay.svg new file mode 100644 index 000000000..6a89649f6 --- /dev/null +++ b/app/images/header-left-overlay.svg @@ -0,0 +1,32 @@ + + + + header-left-overlay + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/javascript-logo-white.svg b/app/images/javascript-logo-white.svg new file mode 100644 index 000000000..196c77bbc --- /dev/null +++ b/app/images/javascript-logo-white.svg @@ -0,0 +1,11 @@ + + + + Combined Shape + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/app/images/logo-white.svg b/app/images/logo-white.svg new file mode 100644 index 000000000..009b679a9 --- /dev/null +++ b/app/images/logo-white.svg @@ -0,0 +1,11 @@ + + + + TanStack + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/app/images/og.png b/app/images/og.png new file mode 100644 index 000000000..058bde1ae Binary files /dev/null and b/app/images/og.png differ diff --git a/app/images/react-logo-white.svg b/app/images/react-logo-white.svg new file mode 100644 index 000000000..0168678de --- /dev/null +++ b/app/images/react-logo-white.svg @@ -0,0 +1,11 @@ + + + + Combined Shape + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/app/images/react-logo.svg b/app/images/react-logo.svg new file mode 100644 index 000000000..a0d5b3de4 --- /dev/null +++ b/app/images/react-logo.svg @@ -0,0 +1,11 @@ + + + React Logo + + + + + + + + diff --git a/app/images/solid-logo.svg b/app/images/solid-logo.svg new file mode 100644 index 000000000..1c988d43e --- /dev/null +++ b/app/images/solid-logo.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/svelte-logo.svg b/app/images/svelte-logo.svg new file mode 100644 index 000000000..1779853a4 --- /dev/null +++ b/app/images/svelte-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/images/vue-logo.svg b/app/images/vue-logo.svg similarity index 100% rename from src/images/vue-logo.svg rename to app/images/vue-logo.svg diff --git a/app/root.tsx b/app/root.tsx new file mode 100644 index 000000000..6cd5cd6ba --- /dev/null +++ b/app/root.tsx @@ -0,0 +1,179 @@ +import * as React from 'react' +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useCatch, + useMatches, + useTransition, +} from '@remix-run/react' +import type { LinksFunction, MetaFunction } from '@remix-run/node' + +import styles from './styles/app.generated.css' +import prismThemeLight from './styles/prismThemeLight.css' +import prismThemeDark from './styles/prismThemeDark.css' +import docSearchStyles from '@docsearch/css/dist/style.css' +import { CgSpinner } from 'react-icons/cg' + +import { seo } from './utils/seo' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' + +export const meta: MetaFunction = () => ({ + charset: 'utf-8', + viewport: 'width=device-width,initial-scale=1', + ...seo({ + title: 'TanStack | High Quality Open-Source Software for Web Developers', + description: `Headless, type-safe, powerful utilities for complex workflows like Data Management, Data Visualization, Charts, Tables, and UI Components.`, + image: require('./images/og.png'), + keywords: + 'tanstack,react,reactjs,react query,react table,open source,open source software,oss,software', + }), +}) + +export let links: LinksFunction = () => { + return [ + { rel: 'stylesheet', href: styles }, + { + rel: 'stylesheet', + href: prismThemeLight, + media: '(prefers-color-scheme: light)', + }, + { + rel: 'stylesheet', + href: prismThemeDark, + media: '(prefers-color-scheme: dark)', + }, + { + rel: 'stylesheet', + href: docSearchStyles + }, + { + rel: 'stylesheet', + href: require('./styles/carbon.css'), + }, + { + rel: 'apple-touch-icon', + sizes: '180x180', + href: '/favicons/apple-touch-icon.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '32x32', + href: '/favicons/favicon-32x32.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '16x16', + href: '/favicons/favicon-16x16.png', + }, + { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' }, + { rel: 'icon', href: '/favicon.ico' }, + ] +} + +export default function App() { + return ( + + + + ) +} + +function Document({ + children, + title, +}: { + children: React.ReactNode + title?: string +}) { + const transition = useTransition() + const matches = useMatches() + // const styles = useStylesLink() + + return ( + // + + + {/* {styles} */} + {matches.find((d) => d.handle?.baseParent) ? ( + + ) : null} + {title ? {title} : null} + + + + + + + + {children} + + + +
+ +
+ + + ) +} + +export function CatchBoundary() { + let caught = useCatch() + + return ( + +
+ +
+
+ ) +} + +export function ErrorBoundary({ error }: { error: Error }) { + console.error(error) + return ( + +
+

There was an error!

+

{error.message}

+
+
+ ) +} diff --git a/app/routes/api/github-sponsors-webhook.ts b/app/routes/api/github-sponsors-webhook.ts new file mode 100644 index 000000000..df00feea4 --- /dev/null +++ b/app/routes/api/github-sponsors-webhook.ts @@ -0,0 +1,81 @@ +import crypto from 'crypto' +import { ActionFunction, LoaderFunction } from '@remix-run/react' + +import { + sponsorCreated, + sponsorCancelled, + sponsorEdited, +} from '../../server/sponsors' + +async function verifySecret(req: Request) { + const sig = req.headers.get('x-hub-signature') || '' + const hmac = crypto.createHmac( + 'sha1', + process.env.GITHUB_SPONSORS_WEBHOOK_SECRET as any + ) + const digest = Buffer.from( + 'sha1=' + hmac.update(JSON.stringify(req.body)).digest('hex'), + 'utf8' + ) + const checksum = Buffer.from(sig, 'utf8') + if ( + checksum.length !== digest.length || + !crypto.timingSafeEqual(digest, checksum) + ) { + throw new Error( + `Request body digest (${digest}) did not match x-hub-signature (${checksum})` + ) + } +} + +export const action: ActionFunction = async (ctx) => { + await verifySecret(ctx.request) + + const githubEvent = ctx.request.headers.get('x-github-event') + const id = ctx.request.headers.get('x-github-delivery') + + if (!githubEvent) { + throw new Response('No X-Github-Event found on request', { + status: 400, + }) + } + + if (!id) { + throw new Response('No X-Github-Delivery found on request', { + status: 400, + }) + } + + const event = await ctx.request.json() + + if (!event?.action) { + throw new Error('No event body action found on request') + } + + if (event.action == 'created') { + sponsorCreated({ + login: event.sponsorship.sponsor.login, + newTier: event.sponsorship.tier, + }) + return new Response('Created', { status: 200 }) + } + + if (event.action == 'cancelled') { + sponsorCancelled({ + login: event.sponsorship.sponsor.login, + oldTier: event.sponsorship.tier, + }) + return new Response('Cancelled', { status: 200 }) + } + + if (event.action == 'tier_changed') { + sponsorEdited({ + login: event.sponsorship.sponsor.login, + oldTier: event.changes.tier, + newTier: event.sponsorship.tier, + }) + return new Response('Updated', { status: 200 }) + } + + return new Response('OK', { status: 200 }) +} diff --git a/app/routes/api/link-discord-github.ts b/app/routes/api/link-discord-github.ts new file mode 100644 index 000000000..243a5fe4d --- /dev/null +++ b/app/routes/api/link-discord-github.ts @@ -0,0 +1,32 @@ +import { linkGithubAndDiscordUser } from '../../server/discord-github' +import { exchangeDiscordCodeForToken } from '../../server/discord' +import { exchangeGithubCodeForToken } from '../../server/github' +import { ActionFunction } from '@remix-run/react' + +export const action: ActionFunction = async (ctx) => { + const { discordCode, githubCode, githubState, redirectUrl } = + await ctx.request.json() + + try { + let [githubToken, discordToken] = await Promise.all([ + exchangeGithubCodeForToken({ + code: githubCode, + state: githubState, + redirectUrl, + }), + exchangeDiscordCodeForToken({ + code: discordCode, + redirectUrl, + }), + ]) + + const message = await linkGithubAndDiscordUser({ + githubToken, + discordToken, + }) + + return { message } + } catch (err: any) { + return new Response(JSON.stringify({ error: err.message }), { status: 400 }) + } +} diff --git a/app/routes/blog.tsx b/app/routes/blog.tsx new file mode 100644 index 000000000..a4cc31b8b --- /dev/null +++ b/app/routes/blog.tsx @@ -0,0 +1,136 @@ +import * as React from 'react' +import { CgClose, CgMenuLeft } from 'react-icons/cg' +import { Link, NavLink, Outlet } from '@remix-run/react' +import { MetaFunction } from '@remix-run/node' +import { DefaultErrorBoundary } from '~/components/DefaultErrorBoundary' +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' +import { Carbon } from '~/components/Carbon' +import { seo } from '~/utils/seo' + +export const ErrorBoundary = DefaultErrorBoundary +export const CatchBoundary = DefaultCatchBoundary + +const logo = ( + <> + + TanStack + + + + Blog + + + +) + +const localMenu = [ + { + label: 'Blog', + children: [ + { + label: 'Latest', + to: '', + }, + ], + }, +] as const + +export let meta: MetaFunction = () => { + return seo({ + title: 'Blog | TanStack', + description: 'The latest news and blog posts from TanStack!', + }) +} + +export default function RouteBlog() { + const detailsRef = React.useRef(null!) + + const menuItems = localMenu.map((group) => { + return ( +
+
{group.label}
+
+
+ {group.children?.map((child, i) => { + return ( +
+ {child.to.startsWith('http') ? ( + {child.label} + ) : ( + + props.isActive + ? `font-bold text-transparent bg-clip-text bg-gradient-to-r from-rose-500 to-violet-600` + : '' + } + onClick={() => { + detailsRef.current.removeAttribute('open') + }} + end + > + {child.label} + + )} +
+ ) + })} +
+
+ ) + }) + + const smallMenu = ( +
+
+ +
+ + + {logo} +
+
+
+ {menuItems} +
+
+
+ ) + + const largeMenu = ( +
+
{logo}
+
+
+ {menuItems} +
+
+ +
+
+ ) + + return ( +
+ {smallMenu} + {largeMenu} +
+ +
+
+ ) +} diff --git a/app/routes/blog/$.tsx b/app/routes/blog/$.tsx new file mode 100644 index 000000000..2f623bf41 --- /dev/null +++ b/app/routes/blog/$.tsx @@ -0,0 +1,91 @@ +import * as React from 'react' +import { useLoaderData } from '@remix-run/react' +import type { LoaderArgs, MetaFunction } from '@remix-run/node' +import { json } from '@remix-run/node' +import { + extractFrontMatter, + fetchRepoFile, + markdownToMdx, +} from '~/utils/documents.server' +import { FaEdit } from 'react-icons/fa' +import { DocTitle } from '~/components/DocTitle' +import { Mdx } from '~/components/Mdx' +import { format } from 'date-fns' +import { DefaultErrorBoundary } from '~/components/DefaultErrorBoundary' +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' +import removeMarkdown from 'remove-markdown' +import { seo } from '~/utils/seo' + +export const loader = async (context: LoaderArgs) => { + const { '*': docsPath } = context.params + + if (!docsPath) { + throw new Error('Invalid docs path') + } + + const filePath = `app/blog/${docsPath}.md` + + const file = await fetchRepoFile('tanstack/tanstack.com', 'main', filePath) + + if (!file) { + throw new Response('Not Found', { + status: 404, + }) + } + + const frontMatter = extractFrontMatter(file) + const description = removeMarkdown(frontMatter.excerpt ?? '') + + const mdx = await markdownToMdx(frontMatter.content) + + return json({ + title: frontMatter.data.title, + description, + published: frontMatter.data.published, + code: mdx.code, + filePath, + }) +} + +export let meta: MetaFunction = ({ data }) => { + return seo({ + title: `${data?.title ?? 'Docs'} | TanStack Blog`, + description: data?.description, + }) +} + +export const ErrorBoundary = DefaultErrorBoundary +export const CatchBoundary = DefaultCatchBoundary + +export default function RouteReactTableDocs() { + const { title, published, code, filePath } = useLoaderData() + + return ( +
+
+ {title ?? ''} +
+ {published ? ( +
{format(new Date(published), 'MMM d, yyyy')}
+ ) : null} +
+
+
+
+
+ +
+
+
+ +
+
+ ) +} diff --git a/app/routes/blog/index.tsx b/app/routes/blog/index.tsx new file mode 100644 index 000000000..3336605ad --- /dev/null +++ b/app/routes/blog/index.tsx @@ -0,0 +1,114 @@ +import * as React from 'react' +import { useLoaderData, Link } from '@remix-run/react' +import type { LoaderArgs } from '@remix-run/node' +import { json } from '@remix-run/node' +import { + extractFrontMatter, + fetchRepoFile, + markdownToMdx, +} from '~/utils/documents.server' +import { getPostList } from '~/utils/blog' +import { DocTitle } from '~/components/DocTitle' +import { Mdx } from '~/components/Mdx' +import { format } from 'date-fns' +import { DefaultErrorBoundary } from '~/components/DefaultErrorBoundary' +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' +import { Footer } from '~/components/Footer' + +export const loader = async (context: LoaderArgs) => { + const postInfos = getPostList() + const frontMatters = await Promise.all( + postInfos.map(async (info) => { + const filePath = `app/blog/${info.id}.md` + + const file = await fetchRepoFile( + 'tanstack/tanstack.com', + 'main', + filePath + ) + + if (!file) { + throw new Response('Not Found', { + status: 404, + }) + } + + const frontMatter = extractFrontMatter(file) + + const mdx = await markdownToMdx(frontMatter.excerpt ?? '') + + return [ + info.id, + { + title: frontMatter.data.title, + published: frontMatter.data.published, + exerptCode: mdx.code, + }, + ] + }) + ) + + return json(frontMatters) +} + +export const ErrorBoundary = DefaultErrorBoundary +export const CatchBoundary = DefaultCatchBoundary + +export default function RouteReactTableDocs() { + const frontMatters = useLoaderData() as [ + string, + { title: string; published: string; exerptCode: string } + ][] + + return ( +
+
+
+ Latest Posts +
+
+
+
+
+ {frontMatters.map(([id, { title, published, exerptCode }]) => { + return ( + +
+
{title}
+ {published ? ( +
+ {format(new Date(published), 'MMM dd, yyyy')} +
+ ) : null} +
+ , + }} + /> +
+
+
+
+ Read More +
+
+ + ) + })} +
+
+
+
+
+ ) +} diff --git a/app/routes/index.tsx b/app/routes/index.tsx new file mode 100644 index 000000000..e4c27e295 --- /dev/null +++ b/app/routes/index.tsx @@ -0,0 +1,554 @@ +import { + Form, + Link, + useActionData, + useLoaderData, + useTransition, +} from '@remix-run/react' +import { json, ActionFunction, LoaderArgs } from '@remix-run/node' +import { Carbon } from '~/components/Carbon' +import { ParentSize } from '@visx/responsive' +import { twMerge } from 'tailwind-merge' +import { FaDiscord, FaGithub } from 'react-icons/fa' +import { CgMusicSpeaker } from 'react-icons/cg' +import { Footer } from '~/components/Footer' +import SponsorPack from '~/components/SponsorPack' +import { fetchCached } from '~/utils/cache.server' +import { LinkOrA } from '~/components/LinkOrA' + +const gradients = [ + `from-rose-500 to-yellow-500`, + `from-yellow-500 to-teal-500`, + `from-teal-500 to-violet-500`, + `from-blue-500 to-pink-500`, +] + +const menu = [ + { + label: ( +
+ Blog +
+ ), + to: '/blog', + }, + { + label: ( +
+ GitHub +
+ ), + to: 'https://github.com/tanstack', + }, + { + label: ( +
+ Discord +
+ ), + to: 'https://tlinz.com/discord', + }, +] + +const libraries = [ + { + name: 'TanStack Query', + getStyles: () => + `shadow-xl shadow-red-700/20 dark:shadow-lg dark:shadow-red-500/30 text-red-500 border-2 border-transparent hover:border-current`, + to: '/query', + tagline: `Powerful asynchronous state management, server-state utilities and data fetching`, + description: `Fetch, cache, update, and wrangle all forms of async data in your TS/JS, React, Vue, Solid & Svelte applications all without touching any "global state".`, + }, + { + name: 'TanStack Table', + getStyles: () => + `shadow-xl shadow-blue-700/20 dark:shadow-lg dark:shadow-blue-500/30 text-blue-500 border-2 border-transparent hover:border-current`, + to: '/table', + tagline: `Headless UI for building powerful tables & datagrids`, + description: `Supercharge your tables or build a datagrid from scratch for TS/JS, React, Vue, Solid & Svelte while retaining 100% control over markup and styles.`, + }, + { + name: 'TanStack Router', + getStyles: () => + `shadow-xl shadow-emerald-700/20 dark:shadow-lg dark:shadow-emerald-500/30 text-emerald-500 dark:text-emerald-400 border-2 border-transparent hover:border-current`, + to: '/router', + tagline: `Type-safe Routing for React applications.`, + description: `Powerful routing for your React applications including a fully type-safe API and first-class search-param for managing state in the URL.`, + badge: ( +
+ New +
+ ), + }, + { + name: 'TanStack Start', + getStyles: () => + ` + text-transparent bg-clip-text bg-[linear-gradient(to_right,#59b8ff,#e331d8,#ff9600,red)] + shadow-xl shadow-amber-700/20 dark:shadow-lg dark:shadow-amber-500/30 border-2 border-transparent hover:border-current + `, + // to: 'https://github.com/tannerlinsley/react-ranger', + tagline: `Type-safe, SSR-friendly meta-framework for React & Preact.`, + description: `A meta-framework for building modern React & Preact applications, powered by TanStack Router, Astro & Bling.`, + badge: ( +
+ Soon +
+ ), + }, + { + name: 'TanStack Virtual', + getStyles: () => + `shadow-xl shadow-purple-700/20 dark:shadow-lg dark:shadow-purple-500/30 text-purple-500 border-2 border-transparent hover:border-current`, + to: '/virtual', + tagline: `Headless UI for Virtualizing Large Element Lists`, + description: `Virtualize only the visible content for massive scrollable DOM nodes at 60FPS in TS/JS, React, Vue, Solid & Svelte while retaining 100% control over markup and styles.`, + badge: ( +
+ New +
+ ), + }, + { + name: 'React Charts', + getStyles: () => + `shadow-xl shadow-orange-700/20 dark:shadow-lg dark:shadow-orange-500/30 text-orange-500 border-2 border-transparent hover:border-current`, + to: 'https://react-charts.tanstack.com', + tagline: `Simple, immersive & interactive charts for React`, + description: `Flexible, declarative, and highly configurable charts designed to pragmatically display dynamic data.`, + }, + { + name: 'React Ranger', + getStyles: () => + `shadow-xl shadow-pink-700/20 dark:shadow-lg dark:shadow-pink-500/30 text-pink-500 border-2 border-transparent hover:border-current`, + to: 'https://github.com/tannerlinsley/react-ranger', + tagline: `Headless range and multi-range slider utilities.`, + description: `React ranger supplies the primitive range and multi-range slider logic as a headless API that can be attached to any styles or markup for that perfect design.`, + badge: ( +
+ New +
+ ), + }, + { + name: 'TanStack Loaders', + getStyles: () => + `shadow-xl shadow-amber-700/20 dark:shadow-lg dark:shadow-amber-500/30 text-amber-500 border-2 border-transparent hover:border-current`, + // to: 'https://github.com/tannerlinsley/react-ranger', + tagline: `Simple data loading and caching utilities for apps`, + description: `Simple and lightweight cached data loading designed for fetch-as-you-render patterns in React, Vue, Solid & Svelte. It's basically React Query lite!`, + badge: ( +
+ Soon +
+ ), + }, + { + name: 'TanStack Actions', + getStyles: () => + `shadow-xl shadow-lime-700/20 dark:shadow-lg dark:shadow-lime-500/30 text-lime-500 border-2 border-transparent hover:border-current`, + // to: 'https://github.com/tannerlinsley/react-ranger', + tagline: `Simple mutation management utilities for apps`, + description: `Simple and lightweight action/mutation management utility for frameworks like React, Vue, Solid & Svelte.`, + badge: ( +
+ Soon +
+ ), + }, +] + +const courses = [ + { + name: 'The Official TanStack React Query Course', + getStyles: () => `border-t-4 border-red-500 hover:(border-green-500)`, + href: 'https://ui.dev/checkout/react-query?from=tanstack', + description: `Learn how to build enterprise quality apps with TanStack's React Query the easy way with our brand new course.`, + price: `$195`, + }, +] + +export const loader = async () => { + const { getSponsorsForSponsorPack } = require('../server/sponsors') + + const sponsors = await getSponsorsForSponsorPack() + + return json({ + sponsors, + randomNumber: Math.random(), + }) +} + +export const action: ActionFunction = async ({ request }) => { + const formData = await request.formData() + return fetch(`https://bytes.dev/api/bytes-optin-cors`, { + method: 'POST', + body: JSON.stringify({ + email: formData.get('email_address'), + influencer: 'tanstack', + }), + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }) +} + +function sample(arr: any[], random = Math.random()) { + return arr[Math.floor(random * arr.length)] +} + +export default function Index() { + const data = useActionData() + const { sponsors, randomNumber } = useLoaderData() + const transition = useTransition() + const isLoading = transition.state === 'submitting' + const hasSubmitted = data?.status === 'success' + const hasError = data?.status === 'error' + + const gradient = sample(gradients, randomNumber) + + return ( + <> +
+ {menu?.map((item, i) => { + const label = ( +
{item.label}
+ ) + + return ( +
+ {item.to.startsWith('http') ? ( + + {label} + + ) : ( + + {label} + + )} +
+ ) + })} +
+
+

+ + TanStack + +

+

+ High-quality open-source software for{' '} + + web developers. + +

+

+ Headless, type-safe, & powerful utilities for State Management, + Routing, Data Visualization, Charts, Tables, and more. +

+
+
+
+

Open Source Libraries

+
+ {libraries.map((library, i) => { + return ( + +
+
+ {library.name} +
+ {library.badge ?? null} +
+
+ {library.tagline} +
+
+ {library.description} +
+
+ ) + })} +
+
+
+
+
+
+

Partners

+
+
+ + + +
+
+
+ TanStack Table and AG Grid are respectfully the{' '} + best table/datagrid libraries around. Instead + of competing, we're working together to ensure the highest + quality table/datagrid options are available for the entire + JS/TS ecosystem and every use-case. +
+ + Read More + +
+
+
+
+

Affiliates

+
+ +
+ + This ad helps us keep the lights on 😉 + +
+
+
+
+ +
+
+

OSS Sponsors

+
+
+ + {/* return ( + +
+ )} + +
+
+ Wow, you've come a long way! +
+
+ Only one thing left to do... +
+
+ + Get Started! + +
+
+
+
+ + ); +} diff --git a/app/routes/router.tsx b/app/routes/router.tsx new file mode 100644 index 000000000..1d5b6a5d2 --- /dev/null +++ b/app/routes/router.tsx @@ -0,0 +1,26 @@ +import type { LoaderFunction, MetaFunction } from '@remix-run/node' +import { redirect } from '@remix-run/node' +import { Outlet } from '@remix-run/react' +import { seo } from '~/utils/seo' + +export let meta: MetaFunction = () => { + return seo({ + title: + 'TanStack Router | React Router, Solid Router, Svelte Router, Vue Router', + description: + 'Powerful routing and first-class search-param APIs for JS/TS, React, Solid, Vue and Svelte', + image: 'https://github.com/tanstack/router/raw/beta/media/header.png', + }) +} + +export const loader: LoaderFunction = async (context) => { + if (!context.request.url.includes('/router/v')) { + return redirect(`${new URL(context.request.url).origin}/router/v1`) + } + + return new Response('OK') +} + +export default function RouteReactTable() { + return +} diff --git a/app/routes/router/$.tsx b/app/routes/router/$.tsx new file mode 100644 index 000000000..5ca9fab70 --- /dev/null +++ b/app/routes/router/$.tsx @@ -0,0 +1,35 @@ +import type { LoaderFunction } from '@remix-run/node' +import { redirect } from '@remix-run/node' + +export const loader: LoaderFunction = (context) => { + handleRedirects(context) + + return redirect('/router/v1') +} + +function handleRedirects(context: Parameters[0]) { + const url = new URL(context.request.url) + // prettier-ignore + const reactLocationv2List = [ + {from: 'docs/overview',to: 'docs/guide/introduction',}, + {from: 'docs/installation',to: 'docs/guide/installation',}, + {from: 'docs/api',to: 'docs/api/virtualizer',}, + {from: 'examples/fixed',to: 'docs/examples/react/fixed',}, + {from: 'examples/variable',to: 'docs/examples/react/variable',}, + {from: 'examples/dynamic',to: 'docs/examples/react/dynamic',}, + {from: 'examples/infinite-scroll',to: 'docs/examples/react/infinite-scroll',}, + {from: 'examples/padding',to: 'docs/examples/react/padding',}, + {from: 'examples/smooth-scroll',to: 'docs/examples/react/smooth-scroll',}, + {from: 'examples/sticky',to: 'docs/examples/react/sticky',}, + {from: '',to: '',}, + ] + + reactLocationv2List.forEach((item) => { + if (url.pathname.startsWith(`/router/react-location/${item.from}`)) { + throw redirect( + `/router/v1/${item.to}?from=reactLocationV2&original=https://react-location-v2.tanstack.com/${item.from}`, + 301 + ) + } + }) +} diff --git a/app/routes/router/v1.tsx b/app/routes/router/v1.tsx new file mode 100644 index 000000000..241ea4d0e --- /dev/null +++ b/app/routes/router/v1.tsx @@ -0,0 +1,67 @@ +import { Link, Outlet, useLocation, useSearchParams } from '@remix-run/react' +import type { LoaderFunction } from '@remix-run/node' +import { json } from '@remix-run/node' +import { DefaultErrorBoundary } from '~/components/DefaultErrorBoundary' +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' +import type { DocsConfig } from '~/components/Docs' +import { fetchRepoFile } from '~/utils/documents.server' +import { useMatchesData } from '~/utils/utils' + +export const v1branch = 'beta' + +export const loader: LoaderFunction = async () => { + const config = await fetchRepoFile( + 'tanstack/router', + v1branch, + `docs/config.json` + ) + + const parsedConfig = JSON.parse(config ?? '') + + if (!parsedConfig) { + throw new Error('Repo docs/config.json not found!') + } + + return json(parsedConfig) +} + +export const ErrorBoundary = DefaultErrorBoundary +export const CatchBoundary = DefaultCatchBoundary + +export const useRouterV1Config = () => + useMatchesData('/router/v1') as DocsConfig + +export default function RouteReactRouter() { + const [params] = useSearchParams() + const location = useLocation() + + const show = params.get('from') === 'reactLocationV2' + const original = params.get('original') + + return ( + <> + {show ? ( +
+
+ Looking for the{' '} + + React Location v2 documentation + + ? +
+ + Hide + +
+ ) : null} + + + ) +} diff --git a/app/routes/router/v1/docs.tsx b/app/routes/router/v1/docs.tsx new file mode 100644 index 000000000..44892e074 --- /dev/null +++ b/app/routes/router/v1/docs.tsx @@ -0,0 +1,79 @@ +import * as React from 'react' +import { FaDiscord, FaGithub } from 'react-icons/fa' +import { Link } from '@remix-run/react' +import type { MetaFunction } from '@remix-run/node' +import { useRouterV1Config } from '../v1' +import { gradientText } from './index' +import { seo } from '~/utils/seo' +import type { DocsConfig } from '~/components/Docs' +import { Docs } from '~/components/Docs' + +const logo = ( + <> + + TanStack + + + Router{' '} + BETA + + +) + +const localMenu = { + label: 'Menu', + children: [ + { + label: 'Home', + to: '..', + }, + { + label: ( +
+ GitHub +
+ ), + to: 'https://github.com/tanstack/router', + }, + { + label: ( +
+ Discord +
+ ), + to: 'https://tlinz.com/discord', + }, + ], +} + +export let meta: MetaFunction = () => { + return seo({ + title: 'TanStack Router Docs | React Router', + description: 'Modern and scalable routing for React applications', + }) +} + +export default function DocsRoute() { + let config = useRouterV1Config() + + config = React.useMemo( + () => + ({ + ...config, + menu: [localMenu, ...config.menu], + } as DocsConfig), + [config] + ) + + return ( + + ) +} diff --git a/app/routes/router/v1/docs/$.tsx b/app/routes/router/v1/docs/$.tsx new file mode 100644 index 000000000..11ac882e2 --- /dev/null +++ b/app/routes/router/v1/docs/$.tsx @@ -0,0 +1,91 @@ +import { useLoaderData } from '@remix-run/react' +import type { LoaderFunction, MetaFunction } from '@remix-run/node' +import { json } from '@remix-run/node' +import { + Doc, + extractFrontMatter, + fetchRepoFile, + markdownToMdx, +} from '~/utils/documents.server' +import { v1branch } from '../../v1' +import { FaEdit } from 'react-icons/fa' +import { DocTitle } from '~/components/DocTitle' +import { Mdx } from '~/components/Mdx' +import { DefaultErrorBoundary } from '~/components/DefaultErrorBoundary' +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' +import { seo } from '~/utils/seo' +import removeMarkdown from 'remove-markdown' + +export const loader: LoaderFunction = async (context) => { + const { '*': docsPath } = context.params + + if (!docsPath) { + throw new Error('Invalid docs path') + } + + const filePath = `docs/${docsPath}.md` + + const file = await fetchRepoFile('tanstack/router', v1branch, filePath) + + if (!file) { + throw new Response('Not Found', { + status: 404, + }) + } + + const frontMatter = extractFrontMatter(file) + const description = removeMarkdown(frontMatter.excerpt ?? '') + + const mdx = await markdownToMdx(frontMatter.content) + + return json( + { + title: frontMatter.data.title, + description, + filePath, + code: mdx.code, + }, + { + headers: { + 'Cache-Control': 's-maxage=1, stale-while-revalidate=300', + }, + } + ) +} + +export let meta: MetaFunction = ({ data }) => { + return seo({ + title: `${data?.title ?? 'Docs'} | TanStack Router Docs`, + description: data?.description, + }) +} + +export const ErrorBoundary = DefaultErrorBoundary +export const CatchBoundary = DefaultCatchBoundary + +export default function RouteReactTableDocs() { + const { title, code, filePath } = useLoaderData() + + return ( +
+ {title ?? ''} +
+
+
+
+ +
+
+
+ +
+
+ ) +} diff --git a/app/routes/router/v1/docs/examples.tsx b/app/routes/router/v1/docs/examples.tsx new file mode 100644 index 000000000..7bc8bc772 --- /dev/null +++ b/app/routes/router/v1/docs/examples.tsx @@ -0,0 +1,5 @@ +import { Outlet } from '@remix-run/react' + +export default function ExamplesRoute() { + return +} diff --git a/app/routes/router/v1/docs/examples/$.tsx b/app/routes/router/v1/docs/examples/$.tsx new file mode 100644 index 000000000..6c604798f --- /dev/null +++ b/app/routes/router/v1/docs/examples/$.tsx @@ -0,0 +1,60 @@ +import * as React from "react"; +import type { LoaderFunction, MetaFunction } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { DocTitle } from "~/components/DocTitle"; +import { v1branch } from "~/routes/router/v1"; +import { seo } from "~/utils/seo"; +import { capitalize, slugToTitle } from "~/utils/utils"; + +export const loader: LoaderFunction = async (context) => { + const { "*": examplePath } = context.params; + const [kind, _name] = (examplePath ?? "").split("/"); + const [name, search] = _name.split("?"); + + return json({ kind, name, search: search ?? "" }); +}; + +export let meta: MetaFunction = ({ data }) => { + return seo({ + title: `${capitalize(data.kind)} Router ${slugToTitle( + data.name + )} Example | TanStack Router Docs`, + description: `An example showing how to implement ${slugToTitle( + data.name + )} in ${capitalize(data.kind)} Router`, + }); +}; + +export default function RouteReactTableDocs() { + const { kind, name, search } = useLoaderData(); + + const examplePath = [kind, name].join("/"); + + const [isDark, setIsDark] = React.useState(true); + + React.useEffect(() => { + setIsDark(window.matchMedia?.(`(prefers-color-scheme: dark)`).matches); + }, []); + + return ( +
+
+ + {capitalize(kind)} Example: {slugToTitle(name)} + +
+
+ +
+ {/* )} */} + +
+
+ Wow, you've come a long way! +
+
+ Only one thing left to do... +
+
+ + Get Started! + +
+
+
+
+ ) +} diff --git a/app/routes/sponsor-login.tsx b/app/routes/sponsor-login.tsx new file mode 100644 index 000000000..59b53782d --- /dev/null +++ b/app/routes/sponsor-login.tsx @@ -0,0 +1,201 @@ +import React from 'react' +import * as qss from 'qss' +import { FaGithub, FaDiscord, FaPlug, FaCheck } from 'react-icons/fa' + +import useSessionStorage from '../hooks/useSessionStorage' +import axios from 'axios' + +const discordClientID = '725855554362146899' +const githubClientID = 'Iv1.3aa8d13a4a3fde91' + +const discordOauthStateKey = 'discord_oauth_state' +const githubOauthStateKey = 'github_oauth_state' + +const discordOauthCodeKey = 'discord_oauth_token' +const githubOauthCodeKey = 'github_oauth_token' + +const discordScope = 'identify guilds.join' + +const githubScope = 'user' + +const getbuttonStyles = () => ` + px-4 py-2 + cursor-pointer + rounded-lg + flex items-center gap-2 +` + +export default function SponsorsLogin() { + const [loadingMessage, setIsLoading] = React.useState( + 'Loading...' + ) + const [error, setError] = React.useState(null) + const [message, setMessage] = React.useState(null) + const [discordState, setDiscordState] = + useSessionStorage(discordOauthStateKey) + const [githubState, setGithubState] = useSessionStorage(githubOauthStateKey) + const [discordCode, setDiscordCode] = useSessionStorage(discordOauthCodeKey) + const [githubCode, setGithubCode] = useSessionStorage(githubOauthCodeKey) + + const loginToDiscord = async () => { + setError(null) + const state = generateState() + setDiscordState(state) + window.location = `https://discord.com/oauth2/authorize?response_type=code&client_id=${discordClientID}&state=${state}&scope=${discordScope}&redirect_uri=${getRedirectUrl()}` + } + + const loginToGithub = async () => { + setError(null) + const state = generateState() + setGithubState(state) + window.location = `https://github.com/login/oauth/authorize?response_type=code&client_id=${githubClientID}&state=${state}&scope=${githubScope}&redirect_uri=${getRedirectUrl()}` + } + + const linkAccounts = async () => { + setIsLoading('Linking accounts...') + + try { + const { + data: { message, error }, + } = await axios.post('/api/link-discord-github', { + discordCode, + discordState, + githubCode, + githubState, + redirectUrl: getRedirectUrl(), + }) + + if (error) { + setError(error) + } else { + setMessage(message) + } + } catch (err) { + setError(err) + } finally { + setDiscordCode(null) + setGithubCode(null) + setIsLoading(false) + } + } + + React.useEffect(() => { + setIsLoading(false) + + const search = window.location.search.substring(1) + + if (search) { + let { code, state } = qss.decode(search) + + state = state + '' + + if (state === githubState) { + setGithubCode(code) + } else if (state === discordState) { + setDiscordCode(code) + } + + window.location = getRedirectUrl() + } + }, []) + + return ( +
+ {error ? ( +
+

{error}

+
+ ) : message ? ( +
+

{message}

+
+ ) : null} +
+
+

Sponsor Log-in

+
+
+ {loadingMessage ? ( +
{loadingMessage}
+ ) : ( +
+
+ + +
+ {githubCode && discordCode ? ( +
+ +
+ ) : null} +
+ )} +
+
+
+

+ Not a sponsor yet?{' '} + + Sign up here! + +

+
+
+
+ ) +} + +function generateState() { + return `st_${(Math.random() + '').replace('.', '')}` +} + +function getRedirectUrl() { + return window.location.origin + window.location.pathname +} diff --git a/app/routes/sponsors-embed.tsx b/app/routes/sponsors-embed.tsx new file mode 100644 index 000000000..06735acbb --- /dev/null +++ b/app/routes/sponsors-embed.tsx @@ -0,0 +1,58 @@ +import SponsorPack from '../components/SponsorPack' +import { json, LoaderArgs, MetaFunction } from '@remix-run/node' +import { useLoaderData } from '@remix-run/react' +import { DefaultErrorBoundary } from '~/components/DefaultErrorBoundary' +import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' + +export const handle = { + baseParent: true, +} + +export const loader = async () => { + const { getSponsorsForSponsorPack } = require('../server/sponsors') + + const sponsors = await getSponsorsForSponsorPack() + + return json( + { + sponsors, + }, + { + headers: { + 'Cache-Control': 'max-age=300, s-maxage=3600, stale-while-revalidate', + }, + } + ) +} + +export const ErrorBoundary = DefaultErrorBoundary +export const CatchBoundary = DefaultCatchBoundary + +export const headers = ({ loaderHeaders }: { loaderHeaders: Headers }) => { + return { + 'Cache-Control': loaderHeaders.get('Cache-Control') ?? '', + } +} + +export default function Sponsors() { + const { sponsors } = useLoaderData() + + return ( + <> +
+ \ No newline at end of file diff --git a/public/Algolia-logo-white.svg b/public/Algolia-logo-white.svg deleted file mode 100644 index f2cb9d3c5..000000000 --- a/public/Algolia-logo-white.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/_headers b/public/_headers deleted file mode 100644 index 45563990d..000000000 --- a/public/_headers +++ /dev/null @@ -1,10 +0,0 @@ -# WebContainer requires these headers for SharedArrayBuffer support -# Only apply to /builder route to avoid affecting other pages -/builder - Cross-Origin-Opener-Policy: same-origin - Cross-Origin-Embedder-Policy: require-corp - -# Vite emits fingerprinted files under /assets, so they can be cached until -# the filename changes on a later build. -/assets/* - Cache-Control: public, max-age=31536000, immutable diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png deleted file mode 100644 index 09c8324f8..000000000 Binary files a/public/android-chrome-192x192.png and /dev/null differ diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png deleted file mode 100644 index 11d626ea3..000000000 Binary files a/public/android-chrome-512x512.png and /dev/null differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png deleted file mode 100644 index 5a9423cc0..000000000 Binary files a/public/apple-touch-icon.png and /dev/null differ diff --git a/public/blog-assets/ag-ui-compliance/header.png b/public/blog-assets/ag-ui-compliance/header.png deleted file mode 100644 index b10bb5ef5..000000000 Binary files a/public/blog-assets/ag-ui-compliance/header.png and /dev/null differ diff --git a/public/blog-assets/announcing-tanstack-form-v1/form_header.png b/public/blog-assets/announcing-tanstack-form-v1/form_header.png deleted file mode 100644 index 913beca3b..000000000 Binary files a/public/blog-assets/announcing-tanstack-form-v1/form_header.png and /dev/null differ diff --git a/public/blog-assets/announcing-tanstack-form-v1/tanstack_form_bluesky_announce.png b/public/blog-assets/announcing-tanstack-form-v1/tanstack_form_bluesky_announce.png deleted file mode 100644 index 724ae0173..000000000 Binary files a/public/blog-assets/announcing-tanstack-form-v1/tanstack_form_bluesky_announce.png and /dev/null differ diff --git a/public/blog-assets/announcing-tanstack-start-v1/header.png b/public/blog-assets/announcing-tanstack-start-v1/header.png deleted file mode 100644 index cb8eec9f5..000000000 Binary files a/public/blog-assets/announcing-tanstack-start-v1/header.png and /dev/null differ diff --git a/public/blog-assets/debug-logging-for-tanstack-ai/header.png b/public/blog-assets/debug-logging-for-tanstack-ai/header.png deleted file mode 100644 index cb38d53be..000000000 Binary files a/public/blog-assets/debug-logging-for-tanstack-ai/header.png and /dev/null differ diff --git a/public/blog-assets/directives-and-the-platform-boundary/header.png b/public/blog-assets/directives-and-the-platform-boundary/header.png deleted file mode 100644 index e7d314f25..000000000 Binary files a/public/blog-assets/directives-and-the-platform-boundary/header.png and /dev/null differ diff --git a/public/blog-assets/from-docs-to-agents/diagram-discovery.svg b/public/blog-assets/from-docs-to-agents/diagram-discovery.svg deleted file mode 100644 index c74c331de..000000000 --- a/public/blog-assets/from-docs-to-agents/diagram-discovery.svg +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - npx intent install — DEPENDENCY GRAPH DISCOVERY - - - node_modules/ - - - - react - no skills - - - - - @tanstack/router - - - - tailwindcss - no skills - - - - - @tanstack/query - - - - zod - no skills - - - - - @tanstack/table - - - - typescript - no skills - - - - - - - - - intent install - discovers + wires - - - - - - - AGENT CONFIG - - - CLAUDE.md - - - .cursorrules - - - .windsurfrules - - Router skills ✓ - Query skills ✓ - Table skills ✓ - - - - intent-enabled package - - standard package (skipped) - diff --git a/public/blog-assets/from-docs-to-agents/diagram-lifecycle.svg b/public/blog-assets/from-docs-to-agents/diagram-lifecycle.svg deleted file mode 100644 index 83769c5b8..000000000 --- a/public/blog-assets/from-docs-to-agents/diagram-lifecycle.svg +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - THE @TANSTACK/INTENT LIFECYCLE - - - - YOUR DOCS - guides/ - migration/ - source of truth - - - - - - - CLI - intent scaffold - guided generation - + intent validate - - - - - - - NPM PACKAGE - skills/ - SKILL.md - versioned with code - - - - - - - CONSUMER PROJECT - npm install / update - intent install - auto-wires agent config - - - - - STALENESS - intent stale - CI catches drift - - - - - docs change? - - - - update skill - - - - FEEDBACK - intent feedback - structured reports - - - - - issue? - - - - - - - npm update = latest code + latest knowledge - Skills travel with the tool, not the model's training cutoff - diff --git a/public/blog-assets/from-docs-to-agents/diagram-split-brain.svg b/public/blog-assets/from-docs-to-agents/diagram-split-brain.svg deleted file mode 100644 index afd854ab5..000000000 --- a/public/blog-assets/from-docs-to-agents/diagram-split-brain.svg +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - THE VERSION SPLIT-BRAIN PROBLEM - - - - MODEL TRAINING DATA - - - - useQuery({ queryKey, queryFn }) - - v5 - - - useQuery(queryKey, queryFn) - - v4 - - - createFileRoute('/posts') - - v1 - - - new Route({ path: '/posts' }) - - pre-v1 - - - No mechanism to know which applies to your project - - - vs - - - - INTENT SKILLS - - - - @tanstack/query-intent@5.62.0 - Pinned to your installed version - - - @tanstack/router-intent@1.95.0 - Pinned to your installed version - - - npm update = latest skills - Knowledge always matches code - - Versioned, maintained, unambiguous - diff --git a/public/blog-assets/from-docs-to-agents/diagram-status-quo.svg b/public/blog-assets/from-docs-to-agents/diagram-status-quo.svg deleted file mode 100644 index 887e4dc2f..000000000 --- a/public/blog-assets/from-docs-to-agents/diagram-status-quo.svg +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - THE STATUS QUO - - - - - GITHUB REPO - awesome-cursorrules - Last updated: 4 months ago - - - - DISCORD MESSAGE - tanstack-router.md - Author: random_dev_42 - - - - GIST - query-v4-rules.md - ⚠ Written for v4, not v5 - - - - BLOG POST - table-tips.md - Author: unknown - - - - - - - - - - copy + paste - - - copy + paste - - - - YOUR PROJECT - .cursorrules / CLAUDE.md - Mixed versions · No update mechanism - No way to detect staleness - - - - No versioning - Which version is this for? - - - No updates - Silently goes stale - - - No ownership - Who maintains this? - - - No composition - Skills don't know each other - diff --git a/public/blog-assets/from-docs-to-agents/header.png b/public/blog-assets/from-docs-to-agents/header.png deleted file mode 100644 index bf1e74c3b..000000000 Binary files a/public/blog-assets/from-docs-to-agents/header.png and /dev/null differ diff --git a/public/blog-assets/generation-hooks/header.png b/public/blog-assets/generation-hooks/header.png deleted file mode 100644 index 29f2c9299..000000000 Binary files a/public/blog-assets/generation-hooks/header.png and /dev/null differ diff --git a/public/blog-assets/how-we-test-tanstack-ai-across-7-providers/cover.png b/public/blog-assets/how-we-test-tanstack-ai-across-7-providers/cover.png deleted file mode 100644 index 6988f3590..000000000 Binary files a/public/blog-assets/how-we-test-tanstack-ai-across-7-providers/cover.png and /dev/null differ diff --git a/public/blog-assets/multi-turn-structured-output/header.png b/public/blog-assets/multi-turn-structured-output/header.png deleted file mode 100644 index 7773c596e..000000000 Binary files a/public/blog-assets/multi-turn-structured-output/header.png and /dev/null differ diff --git a/public/blog-assets/netlify-partnership/header.jpg b/public/blog-assets/netlify-partnership/header.jpg deleted file mode 100644 index 16662df99..000000000 Binary files a/public/blog-assets/netlify-partnership/header.jpg and /dev/null differ diff --git a/public/blog-assets/openrouter-partnership/header.png b/public/blog-assets/openrouter-partnership/header.png deleted file mode 100644 index 7953e6c9c..000000000 Binary files a/public/blog-assets/openrouter-partnership/header.png and /dev/null differ diff --git a/public/blog-assets/power-in-pragmatism/header.jpg b/public/blog-assets/power-in-pragmatism/header.jpg deleted file mode 100644 index 67c24801f..000000000 Binary files a/public/blog-assets/power-in-pragmatism/header.jpg and /dev/null differ diff --git a/public/blog-assets/react-server-components/header.jpg b/public/blog-assets/react-server-components/header.jpg deleted file mode 100644 index e17f75bb1..000000000 Binary files a/public/blog-assets/react-server-components/header.jpg and /dev/null differ diff --git a/public/blog-assets/removing-third-party-ads/header.jpg b/public/blog-assets/removing-third-party-ads/header.jpg deleted file mode 100644 index b53c55c50..000000000 Binary files a/public/blog-assets/removing-third-party-ads/header.jpg and /dev/null differ diff --git a/public/blog-assets/run-coding-agents-in-a-sandbox/header.png b/public/blog-assets/run-coding-agents-in-a-sandbox/header.png deleted file mode 100644 index 0acda7405..000000000 Binary files a/public/blog-assets/run-coding-agents-in-a-sandbox/header.png and /dev/null differ diff --git a/public/blog-assets/search-params-are-state/search-params-are-state-header.jpg b/public/blog-assets/search-params-are-state/search-params-are-state-header.jpg deleted file mode 100644 index 4ec9a4c22..000000000 Binary files a/public/blog-assets/search-params-are-state/search-params-are-state-header.jpg and /dev/null differ diff --git a/public/blog-assets/start-adds-rsbuild-support/header.jpg b/public/blog-assets/start-adds-rsbuild-support/header.jpg deleted file mode 100644 index 1b32223d3..000000000 Binary files a/public/blog-assets/start-adds-rsbuild-support/header.jpg and /dev/null differ diff --git a/public/blog-assets/streaming-structured-output/header.png b/public/blog-assets/streaming-structured-output/header.png deleted file mode 100644 index 688b43187..000000000 Binary files a/public/blog-assets/streaming-structured-output/header.png and /dev/null differ diff --git a/public/blog-assets/tanstack-2-years/tanstack-2-years-header.jpg b/public/blog-assets/tanstack-2-years/tanstack-2-years-header.jpg deleted file mode 100644 index 89597f620..000000000 Binary files a/public/blog-assets/tanstack-2-years/tanstack-2-years-header.jpg and /dev/null differ diff --git a/public/blog-assets/tanstack-ai-alpha-2/header.jpeg b/public/blog-assets/tanstack-ai-alpha-2/header.jpeg deleted file mode 100644 index d88234067..000000000 Binary files a/public/blog-assets/tanstack-ai-alpha-2/header.jpeg and /dev/null differ diff --git a/public/blog-assets/tanstack-ai-alpha-your-ai-your-way/header.jpg b/public/blog-assets/tanstack-ai-alpha-your-ai-your-way/header.jpg deleted file mode 100644 index 1753fb653..000000000 Binary files a/public/blog-assets/tanstack-ai-alpha-your-ai-your-way/header.jpg and /dev/null differ diff --git a/public/blog-assets/tanstack-ai-audio-generation/header.png b/public/blog-assets/tanstack-ai-audio-generation/header.png deleted file mode 100644 index 68aea3162..000000000 Binary files a/public/blog-assets/tanstack-ai-audio-generation/header.png and /dev/null differ diff --git a/public/blog-assets/tanstack-ai-beta/devtools.png b/public/blog-assets/tanstack-ai-beta/devtools.png deleted file mode 100644 index 6dc3093cc..000000000 Binary files a/public/blog-assets/tanstack-ai-beta/devtools.png and /dev/null differ diff --git a/public/blog-assets/tanstack-ai-beta/header.png b/public/blog-assets/tanstack-ai-beta/header.png deleted file mode 100644 index 15ce1962c..000000000 Binary files a/public/blog-assets/tanstack-ai-beta/header.png and /dev/null differ diff --git a/public/blog-assets/tanstack-ai-code-mode/header.jpg b/public/blog-assets/tanstack-ai-code-mode/header.jpg deleted file mode 100644 index efe702196..000000000 Binary files a/public/blog-assets/tanstack-ai-code-mode/header.jpg and /dev/null differ diff --git a/public/blog-assets/tanstack-ai-lazy-tool-discovery/header.webp b/public/blog-assets/tanstack-ai-lazy-tool-discovery/header.webp deleted file mode 100644 index 34ed068ad..000000000 Binary files a/public/blog-assets/tanstack-ai-lazy-tool-discovery/header.webp and /dev/null differ diff --git a/public/blog-assets/tanstack-ai-middleware/header.webp b/public/blog-assets/tanstack-ai-middleware/header.webp deleted file mode 100644 index 163b24172..000000000 Binary files a/public/blog-assets/tanstack-ai-middleware/header.webp and /dev/null differ diff --git a/public/blog-assets/tanstack-ai-orchestration/header.png b/public/blog-assets/tanstack-ai-orchestration/header.png deleted file mode 100644 index 82824fe1f..000000000 Binary files a/public/blog-assets/tanstack-ai-orchestration/header.png and /dev/null differ diff --git a/public/blog-assets/tanstack-ai-realtime-voice-chat/header.webp b/public/blog-assets/tanstack-ai-realtime-voice-chat/header.webp deleted file mode 100644 index b0e0db9b0..000000000 Binary files a/public/blog-assets/tanstack-ai-realtime-voice-chat/header.webp and /dev/null differ diff --git a/public/blog-assets/tanstack-ai-the-ai-function-postmortem/header.jpg b/public/blog-assets/tanstack-ai-the-ai-function-postmortem/header.jpg deleted file mode 100644 index 9d0a02f81..000000000 Binary files a/public/blog-assets/tanstack-ai-the-ai-function-postmortem/header.jpg and /dev/null differ diff --git a/public/blog-assets/tanstack-ai-why-we-split-the-adapters/header.jpeg b/public/blog-assets/tanstack-ai-why-we-split-the-adapters/header.jpeg deleted file mode 100644 index d21ad6a90..000000000 Binary files a/public/blog-assets/tanstack-ai-why-we-split-the-adapters/header.jpeg and /dev/null differ diff --git a/public/blog-assets/tanstack-db-0.1/header.png b/public/blog-assets/tanstack-db-0.1/header.png deleted file mode 100644 index dacbb88c6..000000000 Binary files a/public/blog-assets/tanstack-db-0.1/header.png and /dev/null differ diff --git a/public/blog-assets/tanstack-db-0.5-query-driven-sync/header.png b/public/blog-assets/tanstack-db-0.5-query-driven-sync/header.png deleted file mode 100644 index 8e5d6fce0..000000000 Binary files a/public/blog-assets/tanstack-db-0.5-query-driven-sync/header.png and /dev/null differ diff --git a/public/blog-assets/tanstack-db-0.6-app-ready-with-persistence-and-includes/header.jpg b/public/blog-assets/tanstack-db-0.6-app-ready-with-persistence-and-includes/header.jpg deleted file mode 100644 index 18f46edeb..000000000 Binary files a/public/blog-assets/tanstack-db-0.6-app-ready-with-persistence-and-includes/header.jpg and /dev/null differ diff --git a/public/blog-assets/tanstack-open-source-awards-2026/header.jpg b/public/blog-assets/tanstack-open-source-awards-2026/header.jpg deleted file mode 100644 index 186e4fe5d..000000000 Binary files a/public/blog-assets/tanstack-open-source-awards-2026/header.jpg and /dev/null differ diff --git a/public/blog-assets/tanstack-router-route-matching-tree-rewrite/big-number.png b/public/blog-assets/tanstack-router-route-matching-tree-rewrite/big-number.png deleted file mode 100644 index 168492dbe..000000000 Binary files a/public/blog-assets/tanstack-router-route-matching-tree-rewrite/big-number.png and /dev/null differ diff --git a/public/blog-assets/tanstack-router-route-matching-tree-rewrite/buildlocation-evolution-benchmark.png b/public/blog-assets/tanstack-router-route-matching-tree-rewrite/buildlocation-evolution-benchmark.png deleted file mode 100644 index 7179240e4..000000000 Binary files a/public/blog-assets/tanstack-router-route-matching-tree-rewrite/buildlocation-evolution-benchmark.png and /dev/null differ diff --git a/public/blog-assets/tanstack-router-route-matching-tree-rewrite/header.jpg b/public/blog-assets/tanstack-router-route-matching-tree-rewrite/header.jpg deleted file mode 100644 index 5ba4bcedb..000000000 Binary files a/public/blog-assets/tanstack-router-route-matching-tree-rewrite/header.jpg and /dev/null differ diff --git a/public/blog-assets/tanstack-router-route-matching-tree-rewrite/lru-benchmark.png b/public/blog-assets/tanstack-router-route-matching-tree-rewrite/lru-benchmark.png deleted file mode 100644 index 6a391bb4c..000000000 Binary files a/public/blog-assets/tanstack-router-route-matching-tree-rewrite/lru-benchmark.png and /dev/null differ diff --git a/public/blog-assets/tanstack-router-route-matching-tree-rewrite/matching-evolution-benchmark.png b/public/blog-assets/tanstack-router-route-matching-tree-rewrite/matching-evolution-benchmark.png deleted file mode 100644 index ee637614f..000000000 Binary files a/public/blog-assets/tanstack-router-route-matching-tree-rewrite/matching-evolution-benchmark.png and /dev/null differ diff --git a/public/blog-assets/tanstack-router-signal-graph/after-granular-store-graph-2.mp4 b/public/blog-assets/tanstack-router-signal-graph/after-granular-store-graph-2.mp4 deleted file mode 100644 index 9c5438886..000000000 Binary files a/public/blog-assets/tanstack-router-signal-graph/after-granular-store-graph-2.mp4 and /dev/null differ diff --git a/public/blog-assets/tanstack-router-signal-graph/before-granular-store-graph-2.mp4 b/public/blog-assets/tanstack-router-signal-graph/before-granular-store-graph-2.mp4 deleted file mode 100644 index df593b122..000000000 Binary files a/public/blog-assets/tanstack-router-signal-graph/before-granular-store-graph-2.mp4 and /dev/null differ diff --git a/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-react.png b/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-react.png deleted file mode 100644 index af9c4c311..000000000 Binary files a/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-react.png and /dev/null differ diff --git a/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-solid.png b/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-solid.png deleted file mode 100644 index 93f7f4f41..000000000 Binary files a/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-solid.png and /dev/null differ diff --git a/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-vue.png b/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-vue.png deleted file mode 100644 index 81857b21b..000000000 Binary files a/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-vue.png and /dev/null differ diff --git a/public/blog-assets/tanstack-router-signal-graph/client-side-nav-react.png b/public/blog-assets/tanstack-router-signal-graph/client-side-nav-react.png deleted file mode 100644 index 4e45156f1..000000000 Binary files a/public/blog-assets/tanstack-router-signal-graph/client-side-nav-react.png and /dev/null differ diff --git a/public/blog-assets/tanstack-router-signal-graph/client-side-nav-solid.png b/public/blog-assets/tanstack-router-signal-graph/client-side-nav-solid.png deleted file mode 100644 index 6e6c72a8c..000000000 Binary files a/public/blog-assets/tanstack-router-signal-graph/client-side-nav-solid.png and /dev/null differ diff --git a/public/blog-assets/tanstack-router-signal-graph/client-side-nav-vue.png b/public/blog-assets/tanstack-router-signal-graph/client-side-nav-vue.png deleted file mode 100644 index 6992a4599..000000000 Binary files a/public/blog-assets/tanstack-router-signal-graph/client-side-nav-vue.png and /dev/null differ diff --git a/public/blog-assets/tanstack-router-signal-graph/header.jpg b/public/blog-assets/tanstack-router-signal-graph/header.jpg deleted file mode 100644 index 9c03d0be2..000000000 Binary files a/public/blog-assets/tanstack-router-signal-graph/header.jpg and /dev/null differ diff --git a/public/blog-assets/tanstack-router-signal-graph/store-updates-history-react.png b/public/blog-assets/tanstack-router-signal-graph/store-updates-history-react.png deleted file mode 100644 index 8eebbb72d..000000000 Binary files a/public/blog-assets/tanstack-router-signal-graph/store-updates-history-react.png and /dev/null differ diff --git a/public/blog-assets/tanstack-router-signal-graph/store-updates-history-solid.png b/public/blog-assets/tanstack-router-signal-graph/store-updates-history-solid.png deleted file mode 100644 index 8acaef188..000000000 Binary files a/public/blog-assets/tanstack-router-signal-graph/store-updates-history-solid.png and /dev/null differ diff --git a/public/blog-assets/tanstack-router-signal-graph/store-updates-history-vue.png b/public/blog-assets/tanstack-router-signal-graph/store-updates-history-vue.png deleted file mode 100644 index ad03c5070..000000000 Binary files a/public/blog-assets/tanstack-router-signal-graph/store-updates-history-vue.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/build-location-after.png b/public/blog-assets/tanstack-start-5x-ssr-throughput/build-location-after.png deleted file mode 100644 index 0b13aeebd..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/build-location-after.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/build-location-before.png b/public/blog-assets/tanstack-start-5x-ssr-throughput/build-location-before.png deleted file mode 100644 index 0a485cf35..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/build-location-before.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/delete-after.png b/public/blog-assets/tanstack-start-5x-ssr-throughput/delete-after.png deleted file mode 100644 index 116104df3..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/delete-after.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/delete-before.png b/public/blog-assets/tanstack-start-5x-ssr-throughput/delete-before.png deleted file mode 100644 index 1929a1455..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/delete-before.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/elu-empty.png b/public/blog-assets/tanstack-start-5x-ssr-throughput/elu-empty.png deleted file mode 100644 index 805778b5e..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/elu-empty.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/elu-links.png b/public/blog-assets/tanstack-start-5x-ssr-throughput/elu-links.png deleted file mode 100644 index 8fb0205a0..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/elu-links.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/elu-nested.png b/public/blog-assets/tanstack-start-5x-ssr-throughput/elu-nested.png deleted file mode 100644 index 6ac372f8f..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/elu-nested.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/header.jpg b/public/blog-assets/tanstack-start-5x-ssr-throughput/header.jpg deleted file mode 100644 index d4cb94a99..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/header.jpg and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/interpolate-after.png b/public/blog-assets/tanstack-start-5x-ssr-throughput/interpolate-after.png deleted file mode 100644 index 17fbd307e..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/interpolate-after.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/interpolate-before.png b/public/blog-assets/tanstack-start-5x-ssr-throughput/interpolate-before.png deleted file mode 100644 index cda31f05e..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/interpolate-before.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/links-after.png b/public/blog-assets/tanstack-start-5x-ssr-throughput/links-after.png deleted file mode 100644 index 216cb4a40..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/links-after.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/nested-after.png b/public/blog-assets/tanstack-start-5x-ssr-throughput/nested-after.png deleted file mode 100644 index 3b8ee4bf3..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/nested-after.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/nothing-after.png b/public/blog-assets/tanstack-start-5x-ssr-throughput/nothing-after.png deleted file mode 100644 index f3aa86a35..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/nothing-after.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/router-state-after.png b/public/blog-assets/tanstack-start-5x-ssr-throughput/router-state-after.png deleted file mode 100644 index 294319479..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/router-state-after.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-5x-ssr-throughput/router-state-before.png b/public/blog-assets/tanstack-start-5x-ssr-throughput/router-state-before.png deleted file mode 100644 index 38479c753..000000000 Binary files a/public/blog-assets/tanstack-start-5x-ssr-throughput/router-state-before.png and /dev/null differ diff --git a/public/blog-assets/tanstack-start-solid-v2/header.png b/public/blog-assets/tanstack-start-solid-v2/header.png deleted file mode 100644 index 4e3df9bcb..000000000 Binary files a/public/blog-assets/tanstack-start-solid-v2/header.png and /dev/null differ diff --git a/public/blog-assets/tanstack-table-v9-memory-performance/charts.png b/public/blog-assets/tanstack-table-v9-memory-performance/charts.png deleted file mode 100644 index 54feabaa0..000000000 Binary files a/public/blog-assets/tanstack-table-v9-memory-performance/charts.png and /dev/null differ diff --git a/public/blog-assets/tanstack-table-v9-memory-performance/header.png b/public/blog-assets/tanstack-table-v9-memory-performance/header.png deleted file mode 100644 index f6f545a2f..000000000 Binary files a/public/blog-assets/tanstack-table-v9-memory-performance/header.png and /dev/null differ diff --git a/public/blog-assets/tanstack-table-v9-reactivity/header.png b/public/blog-assets/tanstack-table-v9-reactivity/header.png deleted file mode 100644 index 369809c5b..000000000 Binary files a/public/blog-assets/tanstack-table-v9-reactivity/header.png and /dev/null differ diff --git a/public/blog-assets/tanstack-table-v9-taking-form/header.png b/public/blog-assets/tanstack-table-v9-taking-form/header.png deleted file mode 100644 index 34842c3e4..000000000 Binary files a/public/blog-assets/tanstack-table-v9-taking-form/header.png and /dev/null differ diff --git a/public/blog-assets/tanstack-table-v9-taking-form/screenshot-1.png b/public/blog-assets/tanstack-table-v9-taking-form/screenshot-1.png deleted file mode 100644 index d770ae671..000000000 Binary files a/public/blog-assets/tanstack-table-v9-taking-form/screenshot-1.png and /dev/null differ diff --git a/public/blog-assets/tanstack-table-v9-typescript-performance/header.png b/public/blog-assets/tanstack-table-v9-typescript-performance/header.png deleted file mode 100644 index e117dab42..000000000 Binary files a/public/blog-assets/tanstack-table-v9-typescript-performance/header.png and /dev/null differ diff --git a/public/blog-assets/tsr-perf-milestone/language-service-fast.mp4 b/public/blog-assets/tsr-perf-milestone/language-service-fast.mp4 deleted file mode 100644 index c3a6a4ccc..000000000 Binary files a/public/blog-assets/tsr-perf-milestone/language-service-fast.mp4 and /dev/null differ diff --git a/public/blog-assets/tsr-perf-milestone/language-service-slow.mp4 b/public/blog-assets/tsr-perf-milestone/language-service-slow.mp4 deleted file mode 100644 index d1b44bebb..000000000 Binary files a/public/blog-assets/tsr-perf-milestone/language-service-slow.mp4 and /dev/null differ diff --git a/public/blog-assets/tsr-perf-milestone/tracing-declare-route-tree.png b/public/blog-assets/tsr-perf-milestone/tracing-declare-route-tree.png deleted file mode 100644 index 8efa82a9a..000000000 Binary files a/public/blog-assets/tsr-perf-milestone/tracing-declare-route-tree.png and /dev/null differ diff --git a/public/blog-assets/tsr-perf-milestone/tracing-faster.png b/public/blog-assets/tsr-perf-milestone/tracing-faster.png deleted file mode 100644 index db4d45970..000000000 Binary files a/public/blog-assets/tsr-perf-milestone/tracing-faster.png and /dev/null differ diff --git a/public/blog-assets/tsr-perf-milestone/tracing-slow.png b/public/blog-assets/tsr-perf-milestone/tracing-slow.png deleted file mode 100644 index 4fb1898d3..000000000 Binary files a/public/blog-assets/tsr-perf-milestone/tracing-slow.png and /dev/null differ diff --git a/public/blog-assets/type-safe-provider-tools-tanstack-ai/header.png b/public/blog-assets/type-safe-provider-tools-tanstack-ai/header.png deleted file mode 100644 index 7b0e4cf68..000000000 Binary files a/public/blog-assets/type-safe-provider-tools-tanstack-ai/header.png and /dev/null differ diff --git a/public/blog-assets/who-owns-the-tree/header.jpg b/public/blog-assets/who-owns-the-tree/header.jpg deleted file mode 100644 index 56fd71c0a..000000000 Binary files a/public/blog-assets/who-owns-the-tree/header.jpg and /dev/null differ diff --git a/public/blog-assets/why-tanstack-start-and-router/tanstack-start-blog-header.jpg b/public/blog-assets/why-tanstack-start-and-router/tanstack-start-blog-header.jpg deleted file mode 100644 index 2e6f57dd7..000000000 Binary files a/public/blog-assets/why-tanstack-start-and-router/tanstack-start-blog-header.jpg and /dev/null differ diff --git a/public/blog-assets/why-tanstack-start-is-ditching-adapters/nitro.jpg b/public/blog-assets/why-tanstack-start-is-ditching-adapters/nitro.jpg deleted file mode 100644 index b2b8062c0..000000000 Binary files a/public/blog-assets/why-tanstack-start-is-ditching-adapters/nitro.jpg and /dev/null differ diff --git a/public/blog-assets/your-mcp-your-way/header.png b/public/blog-assets/your-mcp-your-way/header.png deleted file mode 100644 index 1599d6e71..000000000 Binary files a/public/blog-assets/your-mcp-your-way/header.png and /dev/null differ diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png deleted file mode 100644 index e3389b004..000000000 Binary files a/public/favicon-16x16.png and /dev/null differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png deleted file mode 100644 index 900c77d44..000000000 Binary files a/public/favicon-32x32.png and /dev/null differ diff --git a/public/favicon.ico b/public/favicon.ico index 1a1751676..27d3ef719 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/favicons/android-chrome-192x192.png b/public/favicons/android-chrome-192x192.png new file mode 100644 index 000000000..f2d180227 Binary files /dev/null and b/public/favicons/android-chrome-192x192.png differ diff --git a/public/favicons/android-chrome-512x512.png b/public/favicons/android-chrome-512x512.png new file mode 100644 index 000000000..493562a0f Binary files /dev/null and b/public/favicons/android-chrome-512x512.png differ diff --git a/public/favicons/apple-touch-icon.png b/public/favicons/apple-touch-icon.png new file mode 100644 index 000000000..53fcba768 Binary files /dev/null and b/public/favicons/apple-touch-icon.png differ diff --git a/public/favicons/favicon-16x16.png b/public/favicons/favicon-16x16.png new file mode 100644 index 000000000..b52a619f9 Binary files /dev/null and b/public/favicons/favicon-16x16.png differ diff --git a/public/favicons/favicon-32x32.png b/public/favicons/favicon-32x32.png new file mode 100644 index 000000000..6abe62acd Binary files /dev/null and b/public/favicons/favicon-32x32.png differ diff --git a/public/fonts/Inter-Black.ttf b/public/fonts/Inter-Black.ttf deleted file mode 100644 index 8e56c69d3..000000000 Binary files a/public/fonts/Inter-Black.ttf and /dev/null differ diff --git a/public/fonts/Inter-ExtraBold.ttf b/public/fonts/Inter-ExtraBold.ttf deleted file mode 100644 index 8a9a1bcb9..000000000 Binary files a/public/fonts/Inter-ExtraBold.ttf and /dev/null differ diff --git a/public/fonts/Inter-Regular.ttf b/public/fonts/Inter-Regular.ttf deleted file mode 100644 index 399a6e0c3..000000000 Binary files a/public/fonts/Inter-Regular.ttf and /dev/null differ diff --git a/public/fonts/Inter-latin-ext.woff2 b/public/fonts/Inter-latin-ext.woff2 deleted file mode 100644 index a0125fa05..000000000 Binary files a/public/fonts/Inter-latin-ext.woff2 and /dev/null differ diff --git a/public/fonts/Inter-latin.woff2 b/public/fonts/Inter-latin.woff2 deleted file mode 100644 index b0d0e2e5c..000000000 Binary files a/public/fonts/Inter-latin.woff2 and /dev/null differ diff --git a/public/images/logos/logo-black.svg b/public/images/logos/logo-black.svg deleted file mode 100644 index ae99f5fdf..000000000 --- a/public/images/logos/logo-black.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/images/logos/logo-color-100.png b/public/images/logos/logo-color-100.png deleted file mode 100644 index 41443b6c2..000000000 Binary files a/public/images/logos/logo-color-100.png and /dev/null differ diff --git a/public/images/logos/logo-color-600.png b/public/images/logos/logo-color-600.png deleted file mode 100644 index 9db3e67ba..000000000 Binary files a/public/images/logos/logo-color-600.png and /dev/null differ diff --git a/public/images/logos/logo-color-banner-100.png b/public/images/logos/logo-color-banner-100.png deleted file mode 100644 index 367cbcffc..000000000 Binary files a/public/images/logos/logo-color-banner-100.png and /dev/null differ diff --git a/public/images/logos/logo-color-banner-600.png b/public/images/logos/logo-color-banner-600.png deleted file mode 100644 index 568c2e8df..000000000 Binary files a/public/images/logos/logo-color-banner-600.png and /dev/null differ diff --git a/public/images/logos/logo-white.svg b/public/images/logos/logo-white.svg deleted file mode 100644 index 5e12bb860..000000000 --- a/public/images/logos/logo-white.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/images/logos/logo-word-black.svg b/public/images/logos/logo-word-black.svg deleted file mode 100644 index 2789c63d5..000000000 --- a/public/images/logos/logo-word-black.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/images/logos/logo-word-white.svg b/public/images/logos/logo-word-white.svg deleted file mode 100644 index b6ec5086c..000000000 --- a/public/images/logos/logo-word-white.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/images/logos/splash-dark.png b/public/images/logos/splash-dark.png deleted file mode 100644 index 3f43c689e..000000000 Binary files a/public/images/logos/splash-dark.png and /dev/null differ diff --git a/public/images/logos/splash-light.png b/public/images/logos/splash-light.png deleted file mode 100644 index 4e262a5eb..000000000 Binary files a/public/images/logos/splash-light.png and /dev/null differ diff --git a/public/images/logos/toy-palm-chair.png b/public/images/logos/toy-palm-chair.png deleted file mode 100644 index ddd146918..000000000 Binary files a/public/images/logos/toy-palm-chair.png and /dev/null differ diff --git a/public/images/ship.png b/public/images/ship.png deleted file mode 100644 index 1e3188cde..000000000 Binary files a/public/images/ship.png and /dev/null differ diff --git a/public/images/total-support-share.png b/public/images/total-support-share.png deleted file mode 100644 index 857f864c6..000000000 Binary files a/public/images/total-support-share.png and /dev/null differ diff --git a/public/models/Textures/colormap.png b/public/models/Textures/colormap.png deleted file mode 100644 index d070357c2..000000000 Binary files a/public/models/Textures/colormap.png and /dev/null differ diff --git a/public/models/beach-chair.glb b/public/models/beach-chair.glb deleted file mode 100644 index 10f4ac366..000000000 Binary files a/public/models/beach-chair.glb and /dev/null differ diff --git a/public/models/boat.glb b/public/models/boat.glb deleted file mode 100644 index 08282c980..000000000 Binary files a/public/models/boat.glb and /dev/null differ diff --git a/public/models/colormap.png b/public/models/colormap.png deleted file mode 100644 index d070357c2..000000000 Binary files a/public/models/colormap.png and /dev/null differ diff --git a/public/models/palm-tree.glb b/public/models/palm-tree.glb deleted file mode 100644 index 454429375..000000000 Binary files a/public/models/palm-tree.glb and /dev/null differ diff --git a/public/models/palm-trees.glb b/public/models/palm-trees.glb deleted file mode 100644 index d3e903eeb..000000000 Binary files a/public/models/palm-trees.glb and /dev/null differ diff --git a/public/models/rowboat.glb b/public/models/rowboat.glb deleted file mode 100644 index e301aaf2f..000000000 Binary files a/public/models/rowboat.glb and /dev/null differ diff --git a/public/models/sailboat.glb b/public/models/sailboat.glb deleted file mode 100644 index 4401373de..000000000 Binary files a/public/models/sailboat.glb and /dev/null differ diff --git a/public/models/ship.glb b/public/models/ship.glb deleted file mode 100644 index e8e190edf..000000000 Binary files a/public/models/ship.glb and /dev/null differ diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index 8880c3462..000000000 --- a/public/robots.txt +++ /dev/null @@ -1,4 +0,0 @@ -User-agent: * -Allow: / - -Sitemap: https://tanstack.com/sitemap.xml \ No newline at end of file diff --git a/public/shop/shirt.glb b/public/shop/shirt.glb deleted file mode 100644 index 9c7609edd..000000000 Binary files a/public/shop/shirt.glb and /dev/null differ diff --git a/public/site.webmanifest b/public/site.webmanifest index 45dc8a206..7c8c2af76 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -1 +1,19 @@ -{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file +{ + "name": "TanStack", + "short_name": "TanStack", + "icons": [ + { + "src": "/favicons/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/favicons/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#111827", + "background_color": "#111827", + "display": "standalone" +} diff --git a/remix.config.js b/remix.config.js new file mode 100644 index 000000000..3a84dd6cc --- /dev/null +++ b/remix.config.js @@ -0,0 +1,13 @@ +/** @type {import('@remix-run/dev').AppConfig} */ +module.exports = { + serverBuildTarget: "vercel", + // When running locally in development mode, we use the built in remix + // server. This does not understand the vercel lambda module format, + // so we default back to the standard build output. + server: process.env.NODE_ENV === "development" ? undefined : "./server.js", + ignoredRouteFiles: ["**/.*"], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "api/index.js", + // publicPath: "/build/", +}; diff --git a/remix.env.d.ts b/remix.env.d.ts new file mode 100644 index 000000000..72e2affe3 --- /dev/null +++ b/remix.env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/scripts/auth-login.ts b/scripts/auth-login.ts deleted file mode 100644 index 12ca3bd5a..000000000 --- a/scripts/auth-login.ts +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env tsx -/** - * TanStack CLI Auth Login - * - * Opens a browser for OAuth on tanstack.com, mints a session token, - * and saves it as DEV_SESSION_TOKEN in .env.local for local development. - * - * Usage: - * pnpm auth:login - * pnpm auth:login --url http://localhost:3000 # auth against local server - */ - -import { spawn } from 'child_process' -import { readFileSync, writeFileSync, existsSync } from 'fs' -import { resolve } from 'path' - -// --------------------------------------------------------------------------- -// Config -// --------------------------------------------------------------------------- - -const args = process.argv.slice(2) -const urlFlagIdx = args.indexOf('--url') -const BASE_URL = - urlFlagIdx !== -1 && args[urlFlagIdx + 1] - ? args[urlFlagIdx + 1] - : 'https://tanstack.com' - -const POLL_INTERVAL_MS = 2000 -const TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes -const ENV_FILE = resolve(process.cwd(), '.env.local') -const ENV_KEY = 'DEV_SESSION_TOKEN' - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function openBrowser(url: string) { - const platform = process.platform - const cmd = - platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open' - spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref() -} - -function upsertEnvFile(key: string, value: string) { - let contents = existsSync(ENV_FILE) ? readFileSync(ENV_FILE, 'utf8') : '' - const line = `${key}=${value}` - const pattern = new RegExp(`^${key}=.*$`, 'm') - - if (pattern.test(contents)) { - contents = contents.replace(pattern, line) - } else { - contents = - contents.endsWith('\n') || contents === '' - ? contents + line + '\n' - : contents + '\n' + line + '\n' - } - - writeFileSync(ENV_FILE, contents, 'utf8') -} - -async function fetchJson(url: string, options?: RequestInit): Promise { - const res = await fetch(url, options) - if (!res.ok) { - throw new Error(`HTTP ${res.status} from ${url}`) - } - return res.json() as Promise -} - -// --------------------------------------------------------------------------- -// Main flow -// --------------------------------------------------------------------------- - -async function main() { - console.log(`\nAuthenticating with ${BASE_URL}...\n`) - - // 1. Create a ticket - const { ticketId } = await fetchJson<{ ticketId: string }>( - `${BASE_URL}/api/auth/cli/create-ticket`, - { method: 'POST' }, - ) - - // 2. Open browser - const authUrl = `${BASE_URL}/auth/cli?ticket=${ticketId}` - console.log(`Opening browser to:\n ${authUrl}\n`) - console.log('Waiting for you to complete sign-in...') - openBrowser(authUrl) - - // 3. Poll for authorization - const deadline = Date.now() + TIMEOUT_MS - while (Date.now() < deadline) { - await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS)) - - const result = await fetchJson< - | { authorized: false } - | { authorized: true; sessionToken: string } - | { error: string } - >(`${BASE_URL}/api/auth/cli/status/${ticketId}`).catch(() => null) - - if (!result) continue - - if ('error' in result) { - console.error(`\nError: ${result.error}`) - process.exit(1) - } - - if (result.authorized) { - // 4. Save token - upsertEnvFile(ENV_KEY, result.sessionToken) - console.log(`\nSuccess! Token saved to .env.local as ${ENV_KEY}.`) - console.log('Restart your dev server to pick up the new session.\n') - process.exit(0) - } - } - - console.error('\nTimed out waiting for authorization. Please try again.') - process.exit(1) -} - -main().catch((err) => { - console.error('\nFailed:', err instanceof Error ? err.message : err) - process.exit(1) -}) diff --git a/scripts/build-content-collections.mjs b/scripts/build-content-collections.mjs deleted file mode 100644 index d89662b90..000000000 --- a/scripts/build-content-collections.mjs +++ /dev/null @@ -1,5 +0,0 @@ -import { createBuilder } from '@content-collections/core' -import path from 'node:path' - -const builder = await createBuilder(path.resolve('content-collections.ts')) -await builder.build() diff --git a/scripts/check-docs-menu-links.ts b/scripts/check-docs-menu-links.ts deleted file mode 100644 index 7d5300bf5..000000000 --- a/scripts/check-docs-menu-links.ts +++ /dev/null @@ -1,769 +0,0 @@ -import { execFileSync } from 'node:child_process' -import { mkdirSync, writeFileSync } from 'node:fs' -import { dirname } from 'node:path' -import { getBranch, publicLibraries } from '../src/libraries' -import { resolveDocsPathRedirect } from '../src/utils/docs-redirects' -import type { DocsRedirectManifest } from '../src/utils/docs-redirects' -import type { LibrarySlim } from '../src/libraries' - -type ConfigItem = { - label: string - to: string -} - -type ConfigFrameworkSection = { - label: string - children: Array -} - -type ConfigSection = { - label: string - children: Array - frameworks?: Array -} - -type DocsConfig = { - sections: Array -} - -type GitHubTreeItem = { - path: string - type: string -} - -type MenuEntry = { - framework?: string - item: ConfigItem - section: string -} - -type LinkTarget = - | { - kind: 'docs' - path: string - } - | { - kind: 'example' - examplePath: string - framework: string - } - | { - kind: 'internal' - path: string - } - | { - kind: 'external' - } - -type Candidate = { - branch: string - docsRoot: string - entry: MenuEntry - library: LibrarySlim - reason: string - target: Exclude - url: string -} - -type BrokenLink = { - branch: string - docsRoot: string - finalUrl: string - framework?: string - label: string - library: string - reason: string - repo: string - section: string - status: number | 'error' | 'timeout' - target: string - targetKind: string - to: string - url: string -} - -type SourceCandidate = Omit - -type FetchStatus = { - finalUrl: string - status: BrokenLink['status'] -} - -type CliOptions = { - baseUrl: string - jsonPath?: string - verifySite: boolean -} - -const defaultBaseUrl = 'https://tanstack.com' -const requestTimeoutMs = 30_000 -const httpConcurrency = 3 -const statusFetchAttempts = 2 - -const cliOptions = parseCliOptions(process.argv.slice(2)) -const githubToken = getGitHubToken() - -main().catch((error: unknown) => { - console.error(error instanceof Error ? error.message : error) - process.exitCode = 1 -}) - -async function main() { - const candidates: Array = [] - let checkedDocs = 0 - let checkedExamples = 0 - let checkedInternal = 0 - - for (const library of publicLibraries) { - const branch = getBranch(library, 'latest') - const docsRoot = library.docsRoot || 'docs' - const [config, tree] = await Promise.all([ - fetchDocsConfig(library.repo, branch, docsRoot), - fetchGitHubTree(library.repo, branch), - ]) - - if (!config) { - console.warn( - `${library.id}: skipped because ${docsRoot}/config.json was not found`, - ) - continue - } - - const manifest = buildDocsManifest(tree, docsRoot) - const entries = getMenuEntries(config) - let libraryCandidates = 0 - - for (const entry of entries) { - const target = getLinkTarget(entry.item.to, library) - - if (target.kind === 'external') { - continue - } - - if (target.kind === 'internal') { - checkedInternal += 1 - continue - } - - const url = buildDocsUrl(library.id, target) - - if (target.kind === 'example') { - checkedExamples += 1 - - if ( - !library.frameworks.some( - (framework) => framework === target.framework, - ) || - !hasExample(tree, target) - ) { - libraryCandidates += 1 - candidates.push({ - branch, - docsRoot, - entry, - library, - reason: 'example directory was not found', - target, - url, - }) - } - - continue - } - - checkedDocs += 1 - - const resolution = resolveDocsPathRedirect({ - defaultDocs: library.defaultDocs ?? 'overview', - docsPath: target.path, - frameworks: library.frameworks, - manifest, - }) - - if (resolution.type === 'not-found') { - libraryCandidates += 1 - candidates.push({ - branch, - docsRoot, - entry, - library, - reason: 'docs markdown path was not found', - target, - url, - }) - } - } - - console.log( - [ - library.id, - `${entries.length} menu items`, - `${manifest.paths.length} docs paths`, - `${libraryCandidates} candidates`, - ].join(': '), - ) - } - - const brokenLinks = cliOptions.verifySite - ? await verifyCandidates(candidates, cliOptions.baseUrl) - : candidates.map((candidate) => candidateToBrokenLink(candidate)) - - const report = { - generatedAt: new Date().toISOString(), - baseUrl: cliOptions.baseUrl, - checked: { - docs: checkedDocs, - examples: checkedExamples, - internal: checkedInternal, - candidates: candidates.length, - broken: brokenLinks.length, - }, - sourceCandidates: candidates.map(candidateToSourceCandidate), - brokenLinks, - } - - if (cliOptions.jsonPath) { - mkdirSync(dirname(cliOptions.jsonPath), { recursive: true }) - writeFileSync(cliOptions.jsonPath, `${JSON.stringify(report, null, 2)}\n`) - } - - console.log('') - console.log( - `checked ${checkedDocs} docs links, ${checkedExamples} example links, and ${checkedInternal} internal links`, - ) - - if (brokenLinks.length === 0) { - console.log('no broken docs menu links found') - return - } - - console.log(`found ${brokenLinks.length} broken docs menu links`) - for (const link of brokenLinks) { - console.log( - [ - `- ${link.status}`, - link.library, - link.section, - link.framework ? `(${link.framework})` : '', - `"${link.label}"`, - link.url, - ] - .filter(Boolean) - .join(' '), - ) - } - - process.exitCode = 1 -} - -function parseCliOptions(args: Array): CliOptions { - const options: CliOptions = { - baseUrl: process.env.TANSTACK_DOCS_LINK_BASE_URL ?? defaultBaseUrl, - verifySite: true, - } - - for (let index = 0; index < args.length; index += 1) { - const arg = args[index] - - if (arg === '--') { - continue - } - - if (arg === '--json') { - const value = args[index + 1] - if (!value) { - throw new Error('--json requires a file path') - } - options.jsonPath = value - index += 1 - continue - } - - if (arg === '--base-url') { - const value = args[index + 1] - if (!value) { - throw new Error('--base-url requires a URL') - } - options.baseUrl = value - index += 1 - continue - } - - if (arg === '--no-site') { - options.verifySite = false - continue - } - - throw new Error(`Unknown argument: ${arg}`) - } - - options.baseUrl = options.baseUrl.replace(/\/+$/g, '') - - return options -} - -function getGitHubToken() { - const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN - - if (envToken) { - return envToken - } - - try { - return execFileSync('gh', ['auth', 'token'], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }).trim() - } catch { - return undefined - } -} - -async function verifyCandidates(candidates: Array, baseUrl: string) { - const results = await mapWithConcurrency( - candidates, - httpConcurrency, - async (candidate) => { - const status = await fetchStatus(new URL(candidate.url, baseUrl).href) - - if ( - typeof status.status === 'number' && - status.status >= 200 && - status.status < 400 - ) { - return undefined - } - - return candidateToBrokenLink(candidate, status) - }, - ) - - return results.filter(isDefined) -} - -async function fetchStatus(url: string): Promise { - let lastStatus: FetchStatus = { - finalUrl: url, - status: 'error', - } - - for (let attempt = 0; attempt < statusFetchAttempts; attempt += 1) { - try { - const response = await fetch(url, { - headers: { - 'User-Agent': 'tanstack-docs-menu-link-check', - }, - signal: AbortSignal.timeout(requestTimeoutMs), - }) - await response.body?.cancel() - - return { - finalUrl: response.url, - status: response.status, - } - } catch (error) { - lastStatus = - error instanceof DOMException && error.name === 'TimeoutError' - ? { - finalUrl: url, - status: 'timeout', - } - : { - finalUrl: url, - status: 'error', - } - } - } - - return lastStatus -} - -function candidateToBrokenLink( - candidate: Candidate, - status: FetchStatus = { - finalUrl: candidate.url, - status: 'error', - }, -): BrokenLink { - return { - finalUrl: status.finalUrl, - status: status.status, - ...candidateToSourceCandidate(candidate), - } -} - -function candidateToSourceCandidate(candidate: Candidate): SourceCandidate { - return { - branch: candidate.branch, - docsRoot: candidate.docsRoot, - framework: candidate.entry.framework, - label: candidate.entry.item.label, - library: candidate.library.id, - reason: candidate.reason, - repo: candidate.library.repo, - section: candidate.entry.section, - target: getTargetPath(candidate.target), - targetKind: candidate.target.kind, - to: candidate.entry.item.to, - url: candidate.url, - } -} - -function getTargetPath(target: Exclude) { - if (target.kind === 'docs') { - return target.path - } - - if (target.kind === 'example') { - return `examples/${target.framework}/${target.examplePath}` - } - - return target.path -} - -async function fetchGitHubTree(repo: string, branch: string) { - const value = await fetchGitHubJson( - `https://api.github.com/repos/${repo}/git/trees/${encodeURIComponent( - branch, - )}?recursive=1`, - ) - - if (!isRecord(value) || !Array.isArray(value.tree)) { - throw new Error(`Invalid GitHub tree response for ${repo}@${branch}`) - } - - return value.tree.filter(isGitHubTreeItem) -} - -async function fetchDocsConfig(repo: string, branch: string, docsRoot: string) { - const file = await fetchGitHubFile(repo, branch, `${docsRoot}/config.json`) - - if (!file) { - return undefined - } - - const parsed: unknown = JSON.parse(file) - - if (!isDocsConfig(parsed)) { - throw new Error(`Invalid docs config for ${repo}@${branch}:${docsRoot}`) - } - - return parsed -} - -async function fetchGitHubFile(repo: string, branch: string, filePath: string) { - const value = await fetchGitHubJson( - `https://api.github.com/repos/${repo}/contents/${encodeURIComponent( - filePath, - ).replace(/%2F/g, '/')}?ref=${encodeURIComponent(branch)}`, - true, - ) - - if (!value) { - return undefined - } - - if (!isRecord(value) || typeof value.content !== 'string') { - throw new Error(`Invalid GitHub file response for ${repo}:${filePath}`) - } - - const encoding = - typeof value.encoding === 'string' ? value.encoding : 'base64' - - return Buffer.from( - value.content, - encoding === 'base64' ? 'base64' : 'utf8', - ).toString('utf8') -} - -async function fetchGitHubJson(url: string, allowNotFound = false) { - const response = await fetch(url, { - headers: { - Accept: 'application/vnd.github+json', - 'User-Agent': 'tanstack-docs-menu-link-check', - ...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}), - }, - }) - - if (allowNotFound && response.status === 404) { - await response.body?.cancel() - return undefined - } - - if (!response.ok) { - throw new Error( - `GitHub request failed: ${response.status} ${response.statusText} ${url}`, - ) - } - - return response.json() -} - -function buildDocsManifest( - tree: Array, - docsRoot: string, -): DocsRedirectManifest { - return { - paths: tree.flatMap((item) => { - if ( - item.type !== 'blob' || - !item.path.startsWith(`${docsRoot}/`) || - !item.path.endsWith('.md') - ) { - return [] - } - - const canonicalPath = getCanonicalDocsPath(item.path, docsRoot) - return canonicalPath ? [canonicalPath] : [] - }), - redirects: {}, - } -} - -function getCanonicalDocsPath(filePath: string, docsRoot: string) { - const normalizedFilePath = removeLeadingSlash(filePath) - const normalizedDocsRoot = removeLeadingSlash(docsRoot).replace(/\/+$/g, '') - const prefix = `${normalizedDocsRoot}/` - - if (!normalizedFilePath.startsWith(prefix)) { - return undefined - } - - return normalizedFilePath - .slice(prefix.length) - .replace(/\.md$/, '') - .replace(/\/index$/, '') -} - -function getMenuEntries(config: DocsConfig): Array { - return config.sections.flatMap((section) => [ - ...section.children.map((item) => ({ - item, - section: section.label, - })), - ...(section.frameworks ?? []).flatMap((framework) => - framework.children.map((item) => ({ - framework: framework.label, - item, - section: section.label, - })), - ), - ]) -} - -function getLinkTarget(to: string, library: LibrarySlim): LinkTarget { - const trimmed = to.trim() - - if (!trimmed || isExternalUrl(trimmed)) { - return { kind: 'external' } - } - - const path = removeSearchAndHash(trimmed) - - if (!path) { - return { kind: 'external' } - } - - if (path === '..') { - return { kind: 'internal', path: `/${library.id}/latest` } - } - - if (path === './framework') { - return { kind: 'internal', path: `/${library.id}/latest/docs/framework` } - } - - const docsPath = getDocsPath(path) - - if (!docsPath) { - return { kind: 'internal', path: replaceRouteParams(path, library.id) } - } - - if (isSpecialDocsPath(docsPath)) { - return { - kind: 'internal', - path: `/${library.id}/latest/docs/${docsPath}`, - } - } - - const exampleTarget = getExampleTarget(docsPath) - - if (exampleTarget) { - return exampleTarget - } - - return { - kind: 'docs', - path: docsPath, - } -} - -function getDocsPath(path: string) { - if (path.startsWith('/')) { - const normalized = replaceRouteParams(path, '__library__') - const docsIndex = normalized.indexOf('/docs/') - - if (docsIndex === -1) { - return undefined - } - - return normalized.slice(docsIndex + '/docs/'.length) - } - - if (path.startsWith('../')) { - return undefined - } - - return path.replace(/^\.\//, '') -} - -function getExampleTarget(docsPath: string): LinkTarget | undefined { - const match = /^framework\/([^/]+)\/examples\/(.+)$/.exec(docsPath) - - if (!match) { - return undefined - } - - const framework = match[1] - const examplePath = match[2] - - if (!framework || !examplePath) { - return undefined - } - - return { - examplePath, - framework, - kind: 'example', - } -} - -function buildDocsUrl(libraryId: string, target: LinkTarget) { - switch (target.kind) { - case 'docs': - return `/${libraryId}/latest/docs/${target.path}` - case 'example': - return `/${libraryId}/latest/docs/framework/${target.framework}/examples/${target.examplePath}` - case 'internal': - return target.path - case 'external': - return '' - } -} - -function hasExample( - tree: Array, - target: Extract, -) { - const prefix = `examples/${target.framework}/${target.examplePath}/` - - return tree.some( - (item) => item.type === 'blob' && item.path.startsWith(prefix), - ) -} - -function isSpecialDocsPath(path: string) { - return ['blog', 'contributors', 'npm-stats', 'community-resources'].includes( - path, - ) -} - -function removeSearchAndHash(value: string) { - return value.split('#')[0]?.split('?')[0] ?? '' -} - -function replaceRouteParams(path: string, libraryId: string) { - return path - .replaceAll('$libraryId', libraryId) - .replaceAll('$version', 'latest') -} - -function isExternalUrl(value: string) { - return /^[a-z][a-z0-9+.-]*:/i.test(value) -} - -function removeLeadingSlash(value: string) { - return value.replace(/^\/+/, '') -} - -async function mapWithConcurrency( - values: Array, - concurrency: number, - fn: (value: T) => Promise, -) { - const results = new Array(values.length) - let index = 0 - - const workers = Array.from( - { length: Math.min(concurrency, values.length) }, - async () => { - while (index < values.length) { - const currentIndex = index - index += 1 - results[currentIndex] = await fn(values[currentIndex]) - } - }, - ) - - await Promise.all(workers) - - return results -} - -function isDocsConfig(value: unknown): value is DocsConfig { - if (!isRecord(value) || !Array.isArray(value.sections)) { - return false - } - - return value.sections.every(isConfigSection) -} - -function isConfigSection(value: unknown): value is ConfigSection { - if (!isRecord(value)) { - return false - } - - if (typeof value.label !== 'string' || !Array.isArray(value.children)) { - return false - } - - const frameworks = value.frameworks - - return ( - value.children.every(isConfigItem) && - (frameworks === undefined || - (Array.isArray(frameworks) && frameworks.every(isFrameworkSection))) - ) -} - -function isFrameworkSection(value: unknown): value is ConfigFrameworkSection { - return ( - isRecord(value) && - typeof value.label === 'string' && - Array.isArray(value.children) && - value.children.every(isConfigItem) - ) -} - -function isConfigItem(value: unknown): value is ConfigItem { - return ( - isRecord(value) && - typeof value.label === 'string' && - typeof value.to === 'string' - ) -} - -function isGitHubTreeItem(value: unknown): value is GitHubTreeItem { - return ( - isRecord(value) && - typeof value.path === 'string' && - typeof value.type === 'string' - ) -} - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null -} - -function isDefined(value: T | undefined): value is T { - return value !== undefined -} diff --git a/scripts/inspect-content-cache-paths.ts b/scripts/inspect-content-cache-paths.ts deleted file mode 100644 index c0217ff24..000000000 --- a/scripts/inspect-content-cache-paths.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { and, eq, sql } from 'drizzle-orm' -import { db } from '../src/db/client' -import { githubContentCache } from '../src/db/schema' - -async function main() { - const repo = 'tanstack/router' - const gitRef = 'main' - - console.log(`\n=== ${repo}@${gitRef} ===`) - - const [{ total }] = await db - .select({ total: sql`count(*)::int` }) - .from(githubContentCache) - .where( - and( - eq(githubContentCache.repo, repo), - eq(githubContentCache.gitRef, gitRef), - ), - ) - console.log(`total: ${total}`) - - console.log('\nby contentKind:') - const byKind = await db - .select({ - kind: githubContentCache.contentKind, - rows: sql`count(*)::int`, - present: sql`count(*) filter (where ${githubContentCache.isPresent})::int`, - absent: sql`count(*) filter (where not ${githubContentCache.isPresent})::int`, - }) - .from(githubContentCache) - .where( - and( - eq(githubContentCache.repo, repo), - eq(githubContentCache.gitRef, gitRef), - ), - ) - .groupBy(githubContentCache.contentKind) - console.table(byKind) - - console.log('\ntop-level path segment distribution:') - const byTopSegment = await db.execute(sql` - select - content_kind, - split_part(path, '/', 1) as top, - count(*)::int as rows - from github_content_cache - where repo = ${repo} and git_ref = ${gitRef} - group by content_kind, top - order by count(*) desc - limit 30 - `) - console.table(byTopSegment) - - console.log('\nfile extension distribution (file kind only):') - const byExt = await db.execute(sql` - select - case - when path ~ '\\.[a-zA-Z0-9]+$' then regexp_replace(path, '.*\\.([a-zA-Z0-9]+)$', '\\1') - else '(none)' - end as ext, - count(*)::int as rows - from github_content_cache - where repo = ${repo} and git_ref = ${gitRef} and content_kind = 'file' - group by ext - order by count(*) desc - limit 30 - `) - console.table(byExt) - - console.log('\nsample 20 random file paths:') - const samples = await db.execute(sql` - select path, is_present, length(coalesce(text_content, '')) as text_len - from github_content_cache - where repo = ${repo} and git_ref = ${gitRef} and content_kind = 'file' - order by random() - limit 20 - `) - console.table(samples) - - console.log('\nsample 10 random dir paths:') - const dirSamples = await db.execute(sql` - select path, is_present, jsonb_array_length(coalesce(json_content, '[]'::jsonb)) as entries - from github_content_cache - where repo = ${repo} and git_ref = ${gitRef} and content_kind = 'dir' - order by random() - limit 10 - `) - console.table(dirSamples) - - console.log('\npath length distribution:') - const lenBuckets = await db.execute(sql` - select - case - when length(path) < 50 then '< 50' - when length(path) < 100 then '50-100' - when length(path) < 200 then '100-200' - when length(path) < 500 then '200-500' - else '500+' - end as bucket, - count(*)::int as rows - from github_content_cache - where repo = ${repo} and git_ref = ${gitRef} - group by bucket - order by min(length(path)) - `) - console.table(lenBuckets) -} - -main() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err) - process.exit(1) - }) diff --git a/scripts/inspect-content-cache.ts b/scripts/inspect-content-cache.ts deleted file mode 100644 index c72904944..000000000 --- a/scripts/inspect-content-cache.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { sql } from 'drizzle-orm' -import { db } from '../src/db/client' -import { docsArtifactCache, githubContentCache } from '../src/db/schema' - -async function main() { - console.log('\n=== github_content_cache ===') - - const [{ total }] = await db - .select({ total: sql`count(*)::int` }) - .from(githubContentCache) - console.log(`total rows: ${total}`) - - console.log('\nby repo:') - const byRepo = await db - .select({ - repo: githubContentCache.repo, - rows: sql`count(*)::int`, - refs: sql`count(distinct ${githubContentCache.gitRef})::int`, - }) - .from(githubContentCache) - .groupBy(githubContentCache.repo) - .orderBy(sql`count(*) desc`) - console.table(byRepo) - - console.log('\ntop 20 (repo, gitRef) by row count:') - const byRef = await db - .select({ - repo: githubContentCache.repo, - gitRef: githubContentCache.gitRef, - rows: sql`count(*)::int`, - oldestCreated: sql`min(${githubContentCache.createdAt})`, - newestCreated: sql`max(${githubContentCache.createdAt})`, - newestUpdated: sql`max(${githubContentCache.updatedAt})`, - }) - .from(githubContentCache) - .groupBy(githubContentCache.repo, githubContentCache.gitRef) - .orderBy(sql`count(*) desc`) - .limit(20) - console.table(byRef) - - console.log('\ncreatedAt age buckets:') - const ageBuckets = await db.execute(sql` - select - case - when created_at > now() - interval '1 day' then '0-1d' - when created_at > now() - interval '7 days' then '1-7d' - when created_at > now() - interval '30 days' then '7-30d' - when created_at > now() - interval '90 days' then '30-90d' - when created_at > now() - interval '180 days' then '90-180d' - else '180d+' - end as bucket, - count(*)::int as rows - from github_content_cache - group by bucket - order by min(created_at) - `) - console.table(ageBuckets) - - console.log('\n=== docs_artifact_cache ===') - const [{ totalArt }] = await db - .select({ totalArt: sql`count(*)::int` }) - .from(docsArtifactCache) - console.log(`total rows: ${totalArt}`) -} - -main() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err) - process.exit(1) - }) diff --git a/scripts/link-packages.mjs b/scripts/link-packages.mjs deleted file mode 100644 index 5499124dc..000000000 --- a/scripts/link-packages.mjs +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env node -/** - * Link local packages in parallel for faster dev startup - */ - -import { spawn } from 'node:child_process' - -const packages = [ - '../create-tsrouter-app/packages/cta-engine', - '../create-tsrouter-app/frameworks/react-cra', - '../create-tsrouter-app/frameworks/solid', -] - -async function linkPackage(pkg) { - return new Promise((resolve, reject) => { - const proc = spawn('pnpm', ['link', pkg], { stdio: 'inherit' }) - proc.on('close', (code) => { - if (code === 0) resolve() - else reject(new Error(`Failed to link ${pkg}`)) - }) - proc.on('error', reject) - }) -} - -try { - await Promise.all(packages.map(linkPackage)) -} catch (e) { - console.error(e.message) - process.exit(1) -} diff --git a/scripts/mcp-eval/README.md b/scripts/mcp-eval/README.md deleted file mode 100644 index b6536b319..000000000 --- a/scripts/mcp-eval/README.md +++ /dev/null @@ -1,106 +0,0 @@ -# MCP Documentation Discoverability Evaluation - -This tool tests how well AI assistants can find the right TanStack documentation using the MCP server. - -## Why This Exists - -When an AI uses the TanStack MCP to answer questions, it needs to: - -1. Search for relevant docs -2. Find the RIGHT docs (not just related ones) -3. Do so efficiently (fewer searches = better) - -This evaluation suite helps us "train" our docs to be more discoverable by: - -- Identifying search queries that fail to surface important docs -- Finding gaps in doc titles, descriptions, and content -- Measuring improvement over time - -## Running the Tests - -```bash -# Run all tests (requires dev server running on port 3001) -pnpm dev # in another terminal -npx tsx scripts/mcp-eval/run-eval.ts - -# Run a specific test -npx tsx scripts/mcp-eval/run-eval.ts --test router-query-ssr-integration - -# Run tests by tag -npx tsx scripts/mcp-eval/run-eval.ts --tag start -npx tsx scripts/mcp-eval/run-eval.ts --tag query - -# Use a different MCP endpoint -MCP_URL=https://tanstack.com/api/mcp npx tsx scripts/mcp-eval/run-eval.ts - -# Authenticate with an API key (required for production) -MCP_API_KEY=your-api-key MCP_URL=http://localhost:3000/api/mcp npx tsx scripts/mcp-eval/run-eval.ts -``` - -## Test Case Structure - -Each test case in `test-cases.json` includes: - -```json -{ - "id": "unique-id", - "question": "The question an AI might receive", - "difficulty": "easy | medium | hard", - "tags": ["library", "topic"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/data-loading", - "required": true, - "reason": "Why this doc should be found" - } - ], - "idealSearchQueries": ["queries that SHOULD find the doc"], - "badSearchQueries": ["queries that surprisingly DON'T work"], - "correctAnswerMustInclude": ["key terms the answer needs"], - "notes": "Any additional context" -} -``` - -## Scoring - -Each test is scored 0-100: - -- **50%** - Finding required docs -- **30%** - Search efficiency (fewer searches = better) -- **20%** - Doc appearing in top 3 results - -A test passes if: - -- All required docs are found -- Score >= 70 - -## Adding New Tests - -1. Think of a question users/AIs commonly ask -2. Search for it yourself using the MCP tools -3. Document which docs SHOULD be found -4. Add the test case to `test-cases.json` -5. Run the eval to see if it passes -6. If it fails, either: - - Fix the test (wrong expectations) - - Fix the docs (improve discoverability) - -## Improving Doc Discoverability - -When a test fails, consider: - -1. **Doc titles** - Does the title include key search terms? -2. **Doc descriptions** - Is there frontmatter that Algolia indexes? -3. **Cross-references** - Do related docs link to each other? -4. **Canonical terms** - Are you using the terms users search for? - -Example: The "useQuery" search returns API reference, not the guide. -Solution: Either rename the guide or add "useQuery" prominently to it. - -## CI Integration - -```bash -# Exit code is 0 if all tests pass, 1 otherwise -npx tsx scripts/mcp-eval/run-eval.ts || echo "Some tests failed" -``` diff --git a/scripts/mcp-eval/mine-questions.ts b/scripts/mcp-eval/mine-questions.ts deleted file mode 100644 index f478d2409..000000000 --- a/scripts/mcp-eval/mine-questions.ts +++ /dev/null @@ -1,407 +0,0 @@ -/** - * Question Mining Script for MCP Eval Test Cases - * - * This script helps systematically gather real user questions from various sources - * to improve our test coverage. - * - * Sources: - * 1. Stack Overflow - API for tagged questions - * 2. GitHub Issues/Discussions - GraphQL API - * 3. Reddit - API for subreddit search - * - * Usage: - * npx tsx scripts/mcp-eval/mine-questions.ts --source stackoverflow --library query - * npx tsx scripts/mcp-eval/mine-questions.ts --source github --library router - * npx tsx scripts/mcp-eval/mine-questions.ts --all - */ - -const STACK_OVERFLOW_TAGS: Record = { - query: ['tanstackreact-query', 'react-query'], - router: ['tanstack-router'], - table: ['tanstack-table', 'react-table'], - form: ['tanstack-form', 'react-hook-form'], // react-hook-form for comparison - virtual: ['tanstack-virtual', 'react-virtual'], -} - -const GITHUB_REPOS: Record = { - query: 'TanStack/query', - router: 'TanStack/router', - table: 'TanStack/table', - form: 'TanStack/form', - virtual: 'TanStack/virtual', - start: 'TanStack/start', - store: 'TanStack/store', -} - -interface QuestionCandidate { - source: 'stackoverflow' | 'github' | 'reddit' - title: string - url: string - score: number - tags: string[] - library: string - createdAt: string -} - -async function fetchStackOverflowQuestions( - tags: string[], - library: string, -): Promise { - const questions: QuestionCandidate[] = [] - - for (const tag of tags) { - try { - // Stack Overflow API - get questions sorted by votes - const url = `https://api.stackexchange.com/2.3/questions?order=desc&sort=votes&tagged=${tag}&site=stackoverflow&pagesize=50&filter=withbody` - const response = await fetch(url) - const data = await response.json() - - if (data.items) { - for (const item of data.items) { - questions.push({ - source: 'stackoverflow', - title: item.title, - url: item.link, - score: item.score, - tags: item.tags, - library, - createdAt: new Date(item.creation_date * 1000).toISOString(), - }) - } - } - } catch (error) { - console.error(`Error fetching SO questions for ${tag}:`, error) - } - } - - return questions -} - -async function fetchGitHubDiscussions( - repo: string, - library: string, -): Promise { - const questions: QuestionCandidate[] = [] - - // GitHub GraphQL API for discussions - // Note: Requires GITHUB_TOKEN env var - const token = process.env.GITHUB_TOKEN - if (!token) { - console.warn('GITHUB_TOKEN not set, skipping GitHub discussions') - return questions - } - - const query = ` - query($repo: String!, $owner: String!) { - repository(name: $repo, owner: $owner) { - discussions(first: 50, orderBy: {field: CREATED_AT, direction: DESC}, categoryId: null) { - nodes { - title - url - upvoteCount - createdAt - category { - name - } - } - } - } - } - ` - - const [owner, repoName] = repo.split('/') - - try { - const response = await fetch('https://api.github.com/graphql', { - method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query, - variables: { repo: repoName, owner }, - }), - }) - - const data = await response.json() - - if (data.data?.repository?.discussions?.nodes) { - for (const node of data.data.repository.discussions.nodes) { - // Filter to Q&A category - if ( - node.category?.name?.toLowerCase().includes('q&a') || - node.category?.name?.toLowerCase().includes('help') - ) { - questions.push({ - source: 'github', - title: node.title, - url: node.url, - score: node.upvoteCount, - tags: [node.category?.name || 'discussion'], - library, - createdAt: node.createdAt, - }) - } - } - } - } catch (error) { - console.error(`Error fetching GitHub discussions for ${repo}:`, error) - } - - return questions -} - -async function fetchGitHubIssues( - repo: string, - library: string, -): Promise { - const questions: QuestionCandidate[] = [] - - const token = process.env.GITHUB_TOKEN - const headers: Record = { - Accept: 'application/vnd.github.v3+json', - } - if (token) { - headers.Authorization = `Bearer ${token}` - } - - try { - // Get issues labeled as questions or with "how" in title - const url = `https://api.github.com/repos/${repo}/issues?state=all&per_page=100&sort=comments&direction=desc` - const response = await fetch(url, { headers }) - const issues = await response.json() - - if (Array.isArray(issues)) { - for (const issue of issues) { - // Filter to question-like issues - const isQuestion = - issue.title.toLowerCase().includes('how') || - issue.title.toLowerCase().includes('?') || - issue.labels?.some( - (l: { name: string }) => - l.name.toLowerCase().includes('question') || - l.name.toLowerCase().includes('help'), - ) - - if (isQuestion && !issue.pull_request) { - questions.push({ - source: 'github', - title: issue.title, - url: issue.html_url, - score: issue.comments + (issue.reactions?.['+1'] || 0), - tags: issue.labels?.map((l: { name: string }) => l.name) || [], - library, - createdAt: issue.created_at, - }) - } - } - } - } catch (error) { - console.error(`Error fetching GitHub issues for ${repo}:`, error) - } - - return questions -} - -function categorizeQuestion(title: string): string[] { - const categories: string[] = [] - const lower = title.toLowerCase() - - // Topic detection - if (lower.includes('mutation') || lower.includes('mutate')) - categories.push('mutations') - if (lower.includes('cache') || lower.includes('invalidat')) - categories.push('cache') - if (lower.includes('infinite') || lower.includes('pagination')) - categories.push('pagination') - if (lower.includes('ssr') || lower.includes('server')) categories.push('ssr') - if (lower.includes('typescript') || lower.includes('type')) - categories.push('typescript') - if (lower.includes('test')) categories.push('testing') - if (lower.includes('error') || lower.includes('retry')) - categories.push('error-handling') - if (lower.includes('prefetch') || lower.includes('preload')) - categories.push('prefetching') - if (lower.includes('suspense')) categories.push('suspense') - if (lower.includes('devtools')) categories.push('devtools') - if (lower.includes('optimistic')) categories.push('optimistic') - if (lower.includes('dependent') || lower.includes('serial')) - categories.push('dependent') - if (lower.includes('parallel')) categories.push('parallel') - if (lower.includes('refetch') || lower.includes('stale')) - categories.push('refetching') - if (lower.includes('auth')) categories.push('auth') - if (lower.includes('loading') || lower.includes('pending')) - categories.push('loading-states') - if (lower.includes('route') || lower.includes('navigation')) - categories.push('routing') - if (lower.includes('param')) categories.push('params') - if (lower.includes('search')) categories.push('search-params') - if (lower.includes('loader')) categories.push('loaders') - if (lower.includes('sort')) categories.push('sorting') - if (lower.includes('filter')) categories.push('filtering') - if (lower.includes('select')) categories.push('selection') - if (lower.includes('virtual')) categories.push('virtualization') - if (lower.includes('form') || lower.includes('submit')) - categories.push('forms') - if (lower.includes('valid')) categories.push('validation') - - return categories.length > 0 ? categories : ['general'] -} - -function convertToTestCase(q: QuestionCandidate): object { - const categories = categorizeQuestion(q.title) - - return { - id: `mined-${q.library}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - question: q.title.replace(/'/g, "'").replace(/"/g, '"'), - difficulty: 'medium', - tags: [q.library, ...categories], - source: { - type: q.source, - url: q.url, - score: q.score, - }, - expectedDocs: [ - { - library: q.library, - path: 'TODO: Fill in the correct doc path', - required: true, - reason: 'TODO: Explain why this doc answers the question', - }, - ], - idealSearchQueries: ['TODO: Add ideal search queries'], - correctAnswerMustInclude: ['TODO: Add key terms'], - notes: `Mined from ${q.source} on ${new Date().toISOString()}. Original score: ${q.score}`, - } -} - -async function main() { - const args = process.argv.slice(2) - let source = 'all' as string - let library: string | null = null - - for (let i = 0; i < args.length; i++) { - if (args[i] === '--source' && args[i + 1]) { - source = args[i + 1] - } - if (args[i] === '--library' && args[i + 1]) { - library = args[i + 1] - } - } - - console.log(`\n🔍 Mining questions from ${source}...`) - if (library) console.log(` Filtering to library: ${library}`) - console.log('') - - const allQuestions: QuestionCandidate[] = [] - - const libraries = library ? [library] : Object.keys(GITHUB_REPOS) - - for (const lib of libraries) { - console.log(`\n📚 Processing ${lib}...`) - - if (source === 'stackoverflow' || source === 'all') { - const tags = STACK_OVERFLOW_TAGS[lib] - if (tags) { - console.log(` Stack Overflow tags: ${tags.join(', ')}`) - const soQuestions = await fetchStackOverflowQuestions(tags, lib) - console.log(` Found ${soQuestions.length} SO questions`) - allQuestions.push(...soQuestions) - } - } - - if (source === 'github' || source === 'all') { - const repo = GITHUB_REPOS[lib] - if (repo) { - console.log(` GitHub repo: ${repo}`) - const ghDiscussions = await fetchGitHubDiscussions(repo, lib) - const ghIssues = await fetchGitHubIssues(repo, lib) - console.log( - ` Found ${ghDiscussions.length} discussions, ${ghIssues.length} issues`, - ) - allQuestions.push(...ghDiscussions, ...ghIssues) - } - } - - // Rate limiting - await new Promise((r) => setTimeout(r, 1000)) - } - - // Sort by score and dedupe - const sortedQuestions = allQuestions - .sort((a, b) => b.score - a.score) - .filter( - (q, i, arr) => - arr.findIndex( - (x) => x.title.toLowerCase() === q.title.toLowerCase(), - ) === i, - ) - - console.log(`\n\n📊 Results Summary`) - console.log(` Total unique questions: ${sortedQuestions.length}`) - - // Group by library - const byLibrary: Record = {} - for (const q of sortedQuestions) { - byLibrary[q.library] = (byLibrary[q.library] || 0) + 1 - } - console.log(` By library:`) - for (const [lib, count] of Object.entries(byLibrary)) { - console.log(` ${lib}: ${count}`) - } - - // Group by category - const byCategory: Record = {} - for (const q of sortedQuestions) { - for (const cat of categorizeQuestion(q.title)) { - byCategory[cat] = (byCategory[cat] || 0) + 1 - } - } - console.log(` By category:`) - const sortedCategories = Object.entries(byCategory).sort( - (a, b) => b[1] - a[1], - ) - for (const [cat, count] of sortedCategories.slice(0, 15)) { - console.log(` ${cat}: ${count}`) - } - - // Output top questions as potential test cases - console.log(`\n\n🎯 Top 20 Questions (by score):`) - console.log('='.repeat(80)) - - const testCaseCandidates = sortedQuestions.slice(0, 20).map(convertToTestCase) - - for (const q of sortedQuestions.slice(0, 20)) { - console.log(`\n[${q.library}] ${q.title}`) - console.log(` Score: ${q.score} | Source: ${q.source}`) - console.log(` Categories: ${categorizeQuestion(q.title).join(', ')}`) - console.log(` URL: ${q.url}`) - } - - // Save candidates to file - const outputPath = './scripts/mcp-eval/mined-questions.json' - const fs = await import('fs') - fs.writeFileSync( - outputPath, - JSON.stringify( - { - minedAt: new Date().toISOString(), - totalQuestions: sortedQuestions.length, - topCandidates: testCaseCandidates, - allQuestions: sortedQuestions, - }, - null, - 2, - ), - ) - console.log( - `\n\n💾 Saved ${sortedQuestions.length} questions to ${outputPath}`, - ) - console.log( - ` Review the file and add promising questions to test-cases.json`, - ) -} - -main().catch(console.error) diff --git a/scripts/mcp-eval/results.json b/scripts/mcp-eval/results.json deleted file mode 100644 index 71a07bdba..000000000 --- a/scripts/mcp-eval/results.json +++ /dev/null @@ -1,3051 +0,0 @@ -{ - "timestamp": "2026-01-11T23:15:57.600Z", - "summary": { - "passed": 110, - "total": 115, - "avgScore": 95 - }, - "results": [ - { - "testId": "router-query-ssr-integration", - "question": "In TanStack Start, how do I prefetch data for a route that uses both a loader and TanStack Query, ensuring the data is dehydrated to the client without double-fetching?", - "difficulty": "hard", - "searchesPerformed": [ - { - "query": "query integration", - "resultsCount": 63, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/integrations/query", - "https://tanstack.com/router/latest/docs/integrations/query#what-you-get", - "https://tanstack.com/router/latest/docs/integrations/query#installation", - "https://tanstack.com/router/latest/docs/integrations/query#setup", - "https://tanstack.com/router/latest/docs/integrations/query#ssr-behavior-and-streaming" - ] - } - ], - "expectedDocsFound": ["router:integrations/query"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "createfileroute-basic", - "question": "How do I create a file-based route in TanStack Router?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "createFileRoute", - "resultsCount": 18, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/api/router/createFileRouteFunction", - "https://tanstack.com/router/latest/docs/framework/react/api/router/createFileRouteFunction#createfileroute-options", - "https://tanstack.com/router/latest/docs/framework/react/api/router/createFileRouteFunction#createfileroute-returns", - "https://tanstack.com/router/latest/docs/framework/react/api/router/createFileRouteFunction#examples", - "https://tanstack.com/router/latest/docs/framework/react/api/router/createFileRouteFunction#path-option" - ] - } - ], - "expectedDocsFound": [ - "router:framework/react/api/router/createFileRouteFunction" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-usequery-basic", - "question": "How do I fetch data with TanStack Query in React?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "queries guide", - "resultsCount": 87, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/angular/reference/functions/injectQuery#see", - "https://tanstack.com/query/latest/docs/framework/angular/reference/functions/injectQuery#see-1", - "https://tanstack.com/query/latest/docs/framework/angular/reference/functions/injectQuery#see-2", - "https://tanstack.com/query/latest/docs/framework/angular/reference/functions/injectQuery#see-3", - "https://tanstack.com/query/latest/docs/framework/react/guides/ssr#prefetching-dependent-queries" - ] - }, - { - "query": "query basics", - "resultsCount": 106, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/guides/queries#query-basics", - "https://tanstack.com/query/latest/docs/framework/react/guides/queries#fetchstatus", - "https://tanstack.com/query/latest/docs/framework/react/guides/queries#why-two-different-states", - "https://tanstack.com/query/latest/docs/framework/vue/guides/queries#query-basics", - "https://tanstack.com/query/latest/docs/framework/solid/guides/queries#query-basics" - ] - } - ], - "expectedDocsFound": ["query:framework/react/guides/queries"], - "expectedDocsMissed": [], - "totalSearches": 2, - "passed": true, - "score": 85, - "notes": [] - }, - { - "testId": "start-server-functions", - "question": "How do I create a server function in TanStack Start that can be called from the client?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "server function", - "resultsCount": 290, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/streaming-data-from-server-functions", - "https://tanstack.com/start/latest/docs/framework/react/guide/static-server-functions", - "https://tanstack.com/start/latest/docs/framework/react/guide/server-functions", - "https://tanstack.com/start/latest/docs/framework/react/guide/server-functions#what-are-server-functions", - "https://tanstack.com/start/latest/docs/framework/react/guide/streaming-data-from-server-functions#typed-readable-streams" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/server-functions"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-search-params-validation", - "question": "How do I validate and type search params in TanStack Router?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "search params validation", - "resultsCount": 34, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/code-splitting#how-does-tanstack-router-split-code", - "https://tanstack.com/router/latest/docs/framework/solid/guide/code-splitting#how-does-tanstack-router-split-code", - "https://tanstack.com/router/latest/docs/framework/react/routing/routing-concepts#the-root-route", - "https://tanstack.com/router/latest/docs/framework/solid/routing/routing-concepts#the-root-route", - "https://tanstack.com/router/latest/docs/framework/react/decisions-on-dx#why-is-the-routers-configuration-done-this-way" - ] - }, - { - "query": "validateSearch", - "resultsCount": 16, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/api/router/RouteOptionsType#validatesearch-method", - "https://tanstack.com/router/latest/docs/framework/react/guide/search-params#validating-search-params", - "https://tanstack.com/router/latest/docs/framework/solid/guide/search-params#validating-search-params", - "https://tanstack.com/router/latest/docs/framework/react/guide/search-params#adapters", - "https://tanstack.com/router/latest/docs/framework/solid/guide/search-params#adapters" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/search-params"], - "expectedDocsMissed": [], - "totalSearches": 2, - "passed": true, - "score": 85, - "notes": [] - }, - { - "testId": "table-column-definitions", - "question": "How do I define columns for TanStack Table?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "column definitions", - "resultsCount": 25, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/table/latest/docs/guide/column-defs#column-definitions-guide", - "https://tanstack.com/table/latest/docs/guide/migrating#update-column-definitions", - "https://tanstack.com/table/latest/docs/guide/tables#defining-columns", - "https://tanstack.com/table/latest/docs/api/core/column-def", - "https://tanstack.com/table/latest/docs/guide/data#data-guide" - ] - } - ], - "expectedDocsFound": ["table:guide/column-defs"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "form-validation-zod", - "question": "How do I use Zod validation with TanStack Form?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "form zod validation", - "resultsCount": 4, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/form/latest/docs/framework/react/guides/dynamic-validation#standard-schema-validation", - "https://tanstack.com/form/latest/docs/framework/react/guides/basic-concepts#validation-with-standard-schema-libraries", - "https://tanstack.com/form/latest/docs/framework/react/guides/validation#standard-schema-libraries", - "https://tanstack.com/form/latest/docs/philosophy#forms-need-flexibility" - ] - } - ], - "expectedDocsFound": ["form:framework/react/guides/validation"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "virtual-dynamic-row-heights", - "question": "How do I handle dynamic/variable row heights in TanStack Virtual?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "dynamic size virtualizer", - "resultsCount": 456, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/virtual/latest/docs/api/virtual-item#size", - "https://tanstack.com/table/latest/docs/guide/virtualization#examples", - "https://tanstack.com/virtual/latest/docs/api/virtualizer#resizeitem", - "https://tanstack.com/virtual/latest/docs/api/virtualizer#measureelement", - "https://tanstack.com/virtual/latest/docs/api/virtualizer#estimatesize" - ] - }, - { - "query": "measureElement", - "resultsCount": 5, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/virtual/latest/docs/api/virtualizer#measureelement", - "https://tanstack.com/virtual/latest/docs/api/virtualizer#measureelement-1", - "https://tanstack.com/virtual/latest/docs/api/virtual-item#size", - "https://tanstack.com/virtual/latest/docs/api/virtualizer#resizeitem", - "https://tanstack.com/virtual/latest/docs/api/virtualizer#estimatesize" - ] - } - ], - "expectedDocsFound": ["virtual:api/virtualizer"], - "expectedDocsMissed": ["virtual:framework/react/examples/dynamic"], - "totalSearches": 2, - "passed": true, - "score": 85, - "notes": [] - }, - { - "testId": "query-mutations", - "question": "How do I update data on the server with TanStack Query?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "mutation", - "resultsCount": 694, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/guides/mutations", - "https://tanstack.com/query/latest/docs/framework/react/guides/mutations#resetting-mutation-state", - "https://tanstack.com/query/latest/docs/framework/react/guides/mutations#mutation-side-effects", - "https://tanstack.com/query/latest/docs/framework/react/guides/mutations#promises", - "https://tanstack.com/query/latest/docs/framework/react/guides/mutations#retry" - ] - } - ], - "expectedDocsFound": ["query:framework/react/guides/mutations"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-loaderDeps", - "question": "How do I make a TanStack Router loader depend on search params so it reloads when they change?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "loaderDeps", - "resultsCount": 33, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/api/router/useLoaderDepsHook", - "https://tanstack.com/router/latest/docs/framework/react/api/router/useLoaderDepsHook#useloaderdepshook-options", - "https://tanstack.com/router/latest/docs/framework/react/api/router/useLoaderDepsHook#useloaderdeps-returns", - "https://tanstack.com/router/latest/docs/framework/react/api/router/useLoaderDepsHook#examples", - "https://tanstack.com/router/latest/docs/framework/react/api/router/useLoaderDepsHook#optsfrom-option" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/data-loading"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "query-optimistic-updates", - "question": "How do I implement optimistic updates with TanStack Query so the UI updates immediately before the server responds?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "optimistic updates", - "resultsCount": 107, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates", - "https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates#via-the-ui", - "https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates#via-the-cache", - "https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates#when-to-use-what", - "https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates#further-reading" - ] - } - ], - "expectedDocsFound": ["query:framework/react/guides/optimistic-updates"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-infinite-scroll", - "question": "How do I implement infinite scrolling with TanStack Query?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "infinite query", - "resultsCount": 355, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries", - "https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries#example", - "https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries#what-happens-when-an-infinite-query-needs-to-be-refetched", - "https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries#what-if-i-want-to-implement-a-bi-directional-infinite-list", - "https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries#what-if-i-want-to-show-the-pages-in-reversed-order" - ] - } - ], - "expectedDocsFound": ["query:framework/react/guides/infinite-queries"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-cache-invalidation", - "question": "How do I invalidate and refetch queries after a mutation in TanStack Query?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "invalidate queries", - "resultsCount": 65, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation#query-matching-with-invalidatequeries", - "https://tanstack.com/query/latest/docs/framework/vue/guides/query-invalidation#query-matching-with-invalidatequeries", - "https://tanstack.com/query/latest/docs/framework/solid/guides/query-invalidation#query-matching-with-invalidatequeries", - "https://tanstack.com/query/latest/docs/framework/angular/guides/query-invalidation#query-matching-with-invalidatequeries", - "https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientinvalidatequeries" - ] - } - ], - "expectedDocsFound": [ - "query:framework/react/guides/invalidations-from-mutations" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "query-dependent-queries", - "question": "How do I make one query depend on the result of another query in TanStack Query?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "dependent queries", - "resultsCount": 100, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries", - "https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries#usequery-dependent-query", - "https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries#usequeries-dependent-query", - "https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries#a-note-about-performance", - "https://tanstack.com/query/latest/docs/framework/vue/guides/dependent-queries" - ] - } - ], - "expectedDocsFound": ["query:framework/react/guides/dependent-queries"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-prefetching", - "question": "How do I prefetch data before a user navigates to a page with TanStack Query?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "prefetch query", - "resultsCount": 200, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/reference/usePrefetchQuery", - "https://tanstack.com/query/latest/docs/framework/react/reference/usePrefetchInfiniteQuery", - "https://tanstack.com/query/latest/docs/framework/react/guides/prefetching#prefetchquery--prefetchinfinitequery", - "https://tanstack.com/query/latest/docs/framework/solid/guides/prefetching#prefetchquery--prefetchinfinitequery", - "https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientprefetchquery" - ] - } - ], - "expectedDocsFound": ["query:framework/react/guides/prefetching"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-suspense", - "question": "How do I use TanStack Query with React Suspense?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "suspense guide", - "resultsCount": 11, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/guides/suspense#suspense-on-the-server-with-streaming", - "https://tanstack.com/query/latest/docs/framework/react/guides/ssr#a-quick-note-on-suspense", - "https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#handling-slow-loaders", - "https://tanstack.com/router/latest/docs/framework/solid/guide/data-loading#handling-slow-loaders", - "https://tanstack.com/query/latest/docs/framework/react/guides/request-waterfalls#nested-component-waterfalls" - ] - } - ], - "expectedDocsFound": ["query:framework/react/guides/suspense"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-devtools", - "question": "How do I set up TanStack Query DevTools to debug my queries?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "query devtools", - "resultsCount": 50, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/guides/network-mode#devtools", - "https://tanstack.com/query/latest/docs/framework/vue/guides/network-mode#devtools", - "https://tanstack.com/query/latest/docs/framework/solid/guides/network-mode#devtools", - "https://tanstack.com/query/latest/docs/framework/angular/guides/network-mode#devtools", - "https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-react-query-3#devtools-are-now-part-of-the-main-repo-and-npm-package" - ] - } - ], - "expectedDocsFound": ["query:framework/react/devtools"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "router-code-splitting", - "question": "How do I implement code splitting and lazy loading routes in TanStack Router?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "code splitting", - "resultsCount": 73, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/code-splitting", - "https://tanstack.com/router/latest/docs/framework/react/guide/automatic-code-splitting", - "https://tanstack.com/router/latest/docs/framework/react/guide/code-splitting#how-does-tanstack-router-split-code", - "https://tanstack.com/router/latest/docs/framework/react/guide/automatic-code-splitting#how-does-it-work", - "https://tanstack.com/router/latest/docs/framework/react/guide/code-splitting#encapsulating-a-routes-files-into-a-directory" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/code-splitting"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-navigation-blocking", - "question": "How do I prevent navigation when a form has unsaved changes in TanStack Router?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "navigation blocking", - "resultsCount": 17, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/navigation-blocking", - "https://tanstack.com/router/latest/docs/framework/react/guide/navigation-blocking#how-does-navigation-blocking-work", - "https://tanstack.com/router/latest/docs/framework/react/guide/navigation-blocking#how-do-i-use-navigation-blocking", - "https://tanstack.com/router/latest/docs/framework/react/guide/navigation-blocking#hooklogical-based-blocking", - "https://tanstack.com/router/latest/docs/framework/react/guide/navigation-blocking#component-based-blocking" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/navigation-blocking"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-authenticated-routes", - "question": "How do I protect routes that require authentication in TanStack Router?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "authenticated routes", - "resultsCount": 35, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes", - "https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes#the-routebeforeload-option", - "https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes#redirecting", - "https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes#non-redirected-authentication", - "https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes#authentication-using-react-contexthooks" - ] - } - ], - "expectedDocsFound": [ - "router:framework/react/guide/authenticated-routes" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-not-found", - "question": "How do I handle 404 not found pages in TanStack Router?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "not found", - "resultsCount": 443, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/not-found-errors", - "https://tanstack.com/router/latest/docs/framework/react/guide/not-found-errors#overview", - "https://tanstack.com/router/latest/docs/framework/react/guide/not-found-errors#the-notfoundmode-option", - "https://tanstack.com/router/latest/docs/framework/react/guide/not-found-errors#configuring-a-routes-notfoundcomponent", - "https://tanstack.com/router/latest/docs/framework/react/guide/not-found-errors#default-router-wide-not-found-handling" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/not-found-errors"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-path-params", - "question": "How do I access dynamic path parameters like /posts/$postId in TanStack Router?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "path params", - "resultsCount": 124, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/path-params", - "https://tanstack.com/router/latest/docs/framework/react/guide/path-params#path-params-can-be-used-by-child-routes", - "https://tanstack.com/router/latest/docs/framework/react/guide/path-params#path-params-in-loaders", - "https://tanstack.com/router/latest/docs/framework/react/guide/path-params#path-params-in-components", - "https://tanstack.com/router/latest/docs/framework/react/guide/path-params#path-params-outside-of-routes" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/path-params"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-preloading", - "question": "How do I preload route data on hover in TanStack Router?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "preload route", - "resultsCount": 84, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/api/router/RouterType#preloadroute-method", - "https://tanstack.com/router/latest/docs/framework/react/guide/preloading#preloading-manually", - "https://tanstack.com/router/latest/docs/framework/solid/guide/preloading#preloading-manually", - "https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#opting-out-of-caching-while-still-preloading", - "https://tanstack.com/router/latest/docs/framework/solid/guide/data-loading#opting-out-of-caching-while-still-preloading" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/preloading"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-devtools", - "question": "How do I set up TanStack Router DevTools?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "router devtools", - "resultsCount": 31, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/installation/manual#install-tanstack-router-vite-plugin-and-the-router-devtools", - "https://tanstack.com/router/latest/docs/framework/solid/installation/manual#install-tanstack-router-vite-plugin-and-the-router-devtools", - "https://tanstack.com/router/latest/docs/framework/react/devtools#using-devtools-in-production", - "https://tanstack.com/router/latest/docs/framework/solid/devtools#using-devtools-in-production", - "https://tanstack.com/router/latest/docs/framework/react/devtools#fixed-mode" - ] - } - ], - "expectedDocsFound": ["router:framework/react/devtools"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "table-sorting", - "question": "How do I implement column sorting in TanStack Table?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "table sorting", - "resultsCount": 97, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/table/latest/docs/api/features/sorting#getsortedrowmodel-1", - "https://tanstack.com/table/latest/docs/api/features/sorting#using-sorting-functions", - "https://tanstack.com/table/latest/docs/guide/sorting#sorting-apis", - "https://tanstack.com/table/latest/docs/guide/sorting#manual-server-side-sorting", - "https://tanstack.com/table/latest/docs/api/features/sorting#clearsorting" - ] - } - ], - "expectedDocsFound": ["table:guide/sorting"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "table-filtering", - "question": "How do I add filtering to TanStack Table?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "table filtering", - "resultsCount": 132, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/table/latest/docs/api/features/global-filtering#getfilteredrowmodel-1", - "https://tanstack.com/table/latest/docs/api/features/column-filtering#getfilteredrowmodel-1", - "https://tanstack.com/table/latest/docs/api/features/global-filtering#getprefilteredrowmodel", - "https://tanstack.com/table/latest/docs/api/features/column-filtering#getprefilteredrowmodel", - "https://tanstack.com/table/latest/docs/guide/column-filtering#max-leaf-row-filter-depth" - ] - } - ], - "expectedDocsFound": ["table:guide/column-filtering"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "table-pagination", - "question": "How do I implement pagination in TanStack Table?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "table pagination", - "resultsCount": 67, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/table/latest/docs/guide/pagination#pagination-apis", - "https://tanstack.com/table/latest/docs/api/features/pagination#getpaginationrowmodel-1", - "https://tanstack.com/table/latest/docs/api/features/pagination#getprepaginationrowmodel", - "https://tanstack.com/table/latest/docs/api/features/pagination#onpaginationchange", - "https://tanstack.com/table/latest/docs/guide/pagination#manual-server-side-pagination" - ] - } - ], - "expectedDocsFound": ["table:guide/pagination"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "table-row-selection", - "question": "How do I enable row selection with checkboxes in TanStack Table?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "row selection", - "resultsCount": 60, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/table/latest/docs/guide/row-selection", - "https://tanstack.com/table/latest/docs/guide/row-selection#examples", - "https://tanstack.com/table/latest/docs/guide/row-selection#api", - "https://tanstack.com/table/latest/docs/guide/row-selection#row-selection-guide", - "https://tanstack.com/table/latest/docs/guide/row-selection#access-row-selection-state" - ] - } - ], - "expectedDocsFound": ["table:guide/row-selection"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "table-virtualization", - "question": "How do I virtualize a large table with TanStack Table and TanStack Virtual?", - "difficulty": "hard", - "searchesPerformed": [ - { - "query": "table virtualization", - "resultsCount": 6, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/table/latest/docs/guide/virtualization#virtualization-guide", - "https://tanstack.com/table/latest/docs/guide/pagination#should-you-use-virtualization-instead", - "https://tanstack.com/table/latest/docs/guide/virtualization", - "https://tanstack.com/table/latest/docs/guide/virtualization#examples", - "https://tanstack.com/table/latest/docs/guide/features" - ] - }, - { - "query": "virtualized rows", - "resultsCount": 6, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/table/latest/docs/framework/react/examples/virtualized-rows-experimental", - "https://tanstack.com/table/latest/docs/framework/react/examples/virtualized-rows", - "https://tanstack.com/table/latest/docs/framework/vue/examples/virtualized-rows", - "https://tanstack.com/table/latest/docs/guide/virtualization#examples", - "https://tanstack.com/virtual/latest/docs/api/virtualizer#lanes" - ] - } - ], - "expectedDocsFound": ["table:framework/react/examples/virtualized-rows"], - "expectedDocsMissed": [], - "totalSearches": 2, - "passed": true, - "score": 85, - "notes": [] - }, - { - "testId": "table-server-side", - "question": "How do I implement server-side pagination and sorting with TanStack Table?", - "difficulty": "hard", - "searchesPerformed": [ - { - "query": "server side table", - "resultsCount": 43, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/ssr", - "https://tanstack.com/router/latest/docs/framework/solid/guide/ssr", - "https://tanstack.com/table/latest/docs/guide/grouping#manual-grouping", - "https://tanstack.com/table/latest/docs/guide/global-faceting#custom-global-server-side-faceting", - "https://tanstack.com/table/latest/docs/framework/react/guide/table-state#individual-controlled-state" - ] - }, - { - "query": "manual pagination", - "resultsCount": 13, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/table/latest/docs/api/features/pagination#manualpagination", - "https://tanstack.com/table/latest/docs/guide/pagination#pagination-options", - "https://tanstack.com/table/latest/docs/guide/pagination#auto-reset-page-index", - "https://tanstack.com/table/latest/docs/api/features/pagination#autoresetpageindex", - "https://tanstack.com/table/latest/docs/guide/pagination#manual-server-side-pagination" - ] - } - ], - "expectedDocsFound": ["table:guide/pagination"], - "expectedDocsMissed": [], - "totalSearches": 2, - "passed": true, - "score": 85, - "notes": [] - }, - { - "testId": "form-field-arrays", - "question": "How do I handle dynamic arrays of fields in TanStack Form?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "form arrays", - "resultsCount": 75, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/form/latest/docs/framework/react/guides/arrays", - "https://tanstack.com/form/latest/docs/reference/index#type-aliases", - "https://tanstack.com/form/latest/docs/reference/type-aliases/DerivedFormState#errors", - "https://tanstack.com/form/latest/docs/reference/interfaces/FormState#errors", - "https://tanstack.com/form/latest/docs/reference/classes/FormApi#validatearrayfieldsstartingfrom" - ] - } - ], - "expectedDocsFound": ["form:framework/react/guides/arrays"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "form-async-validation", - "question": "How do I implement async validation that checks the server in TanStack Form?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "async validation", - "resultsCount": 283, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/form/latest/docs/framework/react/guides/dynamic-validation#async-validation", - "https://tanstack.com/form/latest/docs/reference/interfaces/FormOptions#asyncalways", - "https://tanstack.com/form/latest/docs/reference/interfaces/FieldOptions#asyncalways", - "https://tanstack.com/form/latest/docs/reference/interfaces/FieldApiOptions#asyncalways", - "https://tanstack.com/form/latest/docs/reference/interfaces/FieldOptions#asyncdebouncems" - ] - } - ], - "expectedDocsFound": ["form:framework/react/guides/dynamic-validation"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "form-submission", - "question": "How do I handle form submission with TanStack Form?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "form submit", - "resultsCount": 261, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/form/latest/docs/framework/react/guides/validation#preventing-invalid-forms-from-being-submitted", - "https://tanstack.com/form/latest/docs/reference/interfaces/FormOptions#onsubmit", - "https://tanstack.com/form/latest/docs/framework/react/guides/submission-handling#passing-additional-data-to-submission-handling", - "https://tanstack.com/form/latest/docs/framework/react/guides/ssr#remix-integration", - "https://tanstack.com/form/latest/docs/framework/react/guides/basic-concepts#form-instance" - ] - } - ], - "expectedDocsFound": ["form:framework/react/guides/basic-concepts"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "virtual-horizontal-scroll", - "question": "How do I create a horizontal virtualized list with TanStack Virtual?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "horizontal virtual", - "resultsCount": 5, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/virtual/latest/docs/introduction#the-virtualizer", - "https://tanstack.com/virtual/latest/docs/api/virtualizer#horizontal", - "https://tanstack.com/virtual/latest/docs/api/virtualizer#measureelement", - "https://tanstack.com/virtual/latest/docs/api/virtualizer#lanes", - "https://tanstack.com/virtual/latest/docs/api/virtualizer#isrtl" - ] - } - ], - "expectedDocsFound": ["virtual:api/virtualizer"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "virtual-window-scroll", - "question": "How do I virtualize a list that uses window scrolling instead of a container?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "useWindowVirtualizer", - "resultsCount": 5, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/virtual/latest/docs/framework/react/react-virtual#usewindowvirtualizer", - "https://tanstack.com/virtual/latest/docs/framework/react/react-virtual#useflushsync", - "https://tanstack.com/virtual/latest/docs/api/virtualizer#observeelementoffset", - "https://tanstack.com/virtual/latest/docs/api/virtualizer#observeelementrect", - "https://tanstack.com/virtual/latest/docs/api/virtualizer#scrolltofn" - ] - } - ], - "expectedDocsFound": ["virtual:framework/react/react-virtual"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "virtual-grid", - "question": "How do I create a virtualized grid with TanStack Virtual?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "virtualizer grid", - "resultsCount": 1, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/virtual/latest/docs/introduction#the-virtualizer" - ] - }, - { - "query": "virtual rows columns", - "resultsCount": 1, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/virtual/latest/docs/api/virtualizer#lanes" - ] - } - ], - "expectedDocsFound": ["virtual:api/virtualizer"], - "expectedDocsMissed": [], - "totalSearches": 2, - "passed": true, - "score": 85, - "notes": [] - }, - { - "testId": "start-deployment-vercel", - "question": "How do I deploy a TanStack Start app to Vercel?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "hosting vercel", - "resultsCount": 5, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/hosting#vercel", - "https://tanstack.com/start/latest/docs/framework/react/guide/hosting#deployment", - "https://tanstack.com/start/latest/docs/framework/solid/guide/hosting#vercel", - "https://tanstack.com/start/latest/docs/framework/solid/guide/hosting#deployment", - "https://tanstack.com/start/latest/docs/framework/react/comparison#deployment-flexibility" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/hosting"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-deployment-netlify", - "question": "How do I deploy a TanStack Start app to Netlify?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "hosting netlify", - "resultsCount": 13, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/hosting#what-should-i-use", - "https://tanstack.com/start/latest/docs/framework/solid/guide/hosting#what-should-i-use", - "https://tanstack.com/start/latest/docs/framework/react/guide/hosting#netlify--official-partner", - "https://tanstack.com/start/latest/docs/framework/react/guide/hosting#netlify", - "https://tanstack.com/start/latest/docs/framework/react/guide/hosting#manual-configuration" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/hosting"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-middleware", - "question": "How do I add middleware to TanStack Start for things like authentication?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "start middleware", - "resultsCount": 112, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/comparison#server-functions-vs-server-actions", - "https://tanstack.com/start/latest/docs/framework/react/guide/middleware#global-server-function-middleware", - "https://tanstack.com/start/latest/docs/framework/solid/guide/middleware#global-server-function-middleware", - "https://tanstack.com/start/latest/docs/framework/react/comparison#middleware-architecture", - "https://tanstack.com/start/latest/docs/framework/react/guide/middleware#global-request-middleware" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/middleware"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-api-routes", - "question": "How do I create API routes in TanStack Start?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "server routes", - "resultsCount": 263, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/server-routes", - "https://tanstack.com/start/latest/docs/framework/react/guide/server-routes#server-routes-and-app-routes", - "https://tanstack.com/start/latest/docs/framework/react/guide/server-routes#file-route-conventions", - "https://tanstack.com/start/latest/docs/framework/react/guide/server-routes#unique-route-paths", - "https://tanstack.com/start/latest/docs/framework/react/guide/server-routes#escaped-matching" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/server-routes"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-static-prerendering", - "question": "How do I statically prerender pages at build time in TanStack Start?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "static prerender", - "resultsCount": 23, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/static-prerendering#prerendering", - "https://tanstack.com/start/latest/docs/framework/react/guide/static-prerendering#crawling-links", - "https://tanstack.com/start/latest/docs/framework/solid/guide/static-prerendering#prerendering", - "https://tanstack.com/start/latest/docs/framework/solid/guide/static-prerendering#crawling-links", - "https://tanstack.com/start/latest/docs/framework/react/guide/static-prerendering" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/static-prerendering"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "store-basics", - "question": "How do I create and use a store with TanStack Store?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "store quick start", - "resultsCount": 11, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/store/latest/docs/framework/react/quick-start", - "https://tanstack.com/store/latest/docs/quick-start", - "https://tanstack.com/store/latest/docs/quick-start#store", - "https://tanstack.com/store/latest/docs/quick-start#derived", - "https://tanstack.com/store/latest/docs/quick-start#effects" - ] - } - ], - "expectedDocsFound": ["store:framework/react/quick-start"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "store-derived", - "question": "How do I create derived state from a TanStack Store?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "derived", - "resultsCount": 213, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/store/latest/docs/reference/classes/Derived", - "https://tanstack.com/store/latest/docs/reference/classes/Derived#type-parameters", - "https://tanstack.com/store/latest/docs/reference/classes/Derived#constructors", - "https://tanstack.com/store/latest/docs/reference/classes/Derived#properties", - "https://tanstack.com/store/latest/docs/reference/classes/Derived#methods" - ] - } - ], - "expectedDocsFound": ["store:reference/classes/Derived"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-vue-basics", - "question": "How do I use TanStack Query with Vue?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "vue query", - "resultsCount": 316, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/vue", - "https://tanstack.com/query/latest/docs/framework/vue/guides/migrating-to-v5#vue-query-breaking-changes", - "https://tanstack.com/query/latest/docs/framework/vue/guides/migrating-to-v5#usequeries-composable-returns-ref-instead-of-reactive", - "https://tanstack.com/query/latest/docs/framework/vue/guides/migrating-to-v5#vue-v33-is-now-required", - "https://tanstack.com/query/latest/docs/framework/vue/installation#vue-query-initialization" - ] - }, - { - "query": "tanstack query vue", - "resultsCount": 86, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/vue/guides/does-this-replace-client-state", - "https://tanstack.com/query/latest/docs/framework/vue/guides/does-this-replace-client-state#a-contrived-example", - "https://tanstack.com/query/latest/docs/framework/vue", - "https://tanstack.com/query/latest/docs/framework/vue/overview", - "https://tanstack.com/query/latest/docs/framework/vue/overview#motivation" - ] - } - ], - "expectedDocsFound": ["query:framework/vue/overview"], - "expectedDocsMissed": [], - "totalSearches": 2, - "passed": false, - "score": 65, - "notes": [] - }, - { - "testId": "query-solid-basics", - "question": "How do I use TanStack Query with SolidJS?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "solid query", - "resultsCount": 379, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/solid/examples/start-basic-solid-query", - "https://tanstack.com/router/latest/docs/framework/solid/examples/router-monorepo-solid-query", - "https://tanstack.com/router/latest/docs/framework/solid/examples/kitchen-sink-solid-query-file-based", - "https://tanstack.com/router/latest/docs/framework/solid/examples/kitchen-sink-solid-query", - "https://tanstack.com/router/latest/docs/framework/solid/examples/basic-solid-query-file-based" - ] - }, - { - "query": "tanstack query solid", - "resultsCount": 74, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/solid/quick-start", - "https://tanstack.com/query/latest/docs/framework/solid", - "https://tanstack.com/query/latest/docs/framework/solid/plugins/createPersister#installation", - "https://tanstack.com/query/latest/docs/framework/solid/overview", - "https://tanstack.com/router/latest/docs/framework/solid/overview" - ] - } - ], - "expectedDocsFound": ["query:framework/solid/overview"], - "expectedDocsMissed": [], - "totalSearches": 2, - "passed": false, - "score": 65, - "notes": [] - }, - { - "testId": "router-vue-basics", - "question": "How do I use TanStack Router with Vue?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "router overview", - "resultsCount": 44, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/routing", - "https://tanstack.com/start/latest/docs/framework/solid/guide/routing", - "https://tanstack.com/router/latest/docs/framework/react/overview", - "https://tanstack.com/router/latest/docs/framework/react/overview#a-fork-in-the-route", - "https://tanstack.com/start/latest/docs/framework/react/overview#should-i-use-tanstack-start-or-just-tanstack-router" - ] - } - ], - "expectedDocsFound": ["router:framework/react/overview"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-testing", - "question": "How do I test components that use TanStack Query?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "testing query", - "resultsCount": 22, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/guides/testing#further-reading", - "https://tanstack.com/db/latest/docs/guides/live-queries#complex-nested-subqueries", - "https://tanstack.com/query/latest/docs/framework/react/guides/testing", - "https://tanstack.com/query/latest/docs/framework/react/guides/testing#our-first-test", - "https://tanstack.com/query/latest/docs/framework/react/guides/testing#turn-off-retries" - ] - } - ], - "expectedDocsFound": ["query:framework/react/guides/testing"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-typescript", - "question": "How do I properly type TanStack Query hooks with TypeScript?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "query typescript", - "resultsCount": 138, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/typescript#further-reading", - "https://tanstack.com/query/latest/docs/framework/react/reference/queryOptions", - "https://tanstack.com/query/latest/docs/framework/vue/reference/queryOptions", - "https://tanstack.com/query/latest/docs/framework/solid/reference/queryOptions", - "https://tanstack.com/query/latest/docs/framework/react/reference/infiniteQueryOptions" - ] - } - ], - "expectedDocsFound": ["query:framework/react/typescript"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-scroll-restoration", - "question": "How do I handle scroll position restoration in TanStack Router?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "scroll restoration", - "resultsCount": 25, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/scroll-restoration", - "https://tanstack.com/query/latest/docs/framework/react/guides/scroll-restoration", - "https://tanstack.com/router/latest/docs/framework/react/guide/scroll-restoration#hashtop-of-page-scrolling", - "https://tanstack.com/router/latest/docs/framework/react/guide/scroll-restoration#scroll-to-top--nested-scrollable-areas", - "https://tanstack.com/router/latest/docs/framework/react/guide/scroll-restoration#scroll-restoration" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/scroll-restoration"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-error-handling", - "question": "How do I handle route errors and show error boundaries in TanStack Router?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "router error boundary", - "resultsCount": 34, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultoncatch-property", - "https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#handling-errors-with-routeoptionserrorcomponent", - "https://tanstack.com/router/latest/docs/framework/solid/guide/data-loading#handling-errors-with-routeoptionserrorcomponent", - "https://tanstack.com/start/latest/docs/framework/react/guide/error-boundaries#error-boundaries-react-start", - "https://tanstack.com/start/latest/docs/framework/solid/guide/error-boundaries#error-boundaries-solid-start" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/data-loading"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-pending-ui", - "question": "How do I show loading states during navigation in TanStack Router?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "pending component", - "resultsCount": 70, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#showing-a-pending-component", - "https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#avoiding-pending-component-flash", - "https://tanstack.com/router/latest/docs/framework/solid/guide/data-loading#showing-a-pending-component", - "https://tanstack.com/router/latest/docs/framework/solid/guide/data-loading#avoiding-pending-component-flash", - "https://tanstack.com/router/latest/docs/framework/react/api/router/RouteOptionsType#pendingcomponent-property" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/data-loading"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-forms", - "question": "How do I handle form submissions with server actions in TanStack Start?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "server functions", - "resultsCount": 290, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/streaming-data-from-server-functions", - "https://tanstack.com/start/latest/docs/framework/react/guide/static-server-functions", - "https://tanstack.com/start/latest/docs/framework/react/guide/server-functions", - "https://tanstack.com/start/latest/docs/framework/react/guide/server-functions#what-are-server-functions", - "https://tanstack.com/start/latest/docs/framework/react/guide/streaming-data-from-server-functions#typed-readable-streams" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/server-functions"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-head-meta", - "question": "How do I set page titles and meta tags in TanStack Start?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "document head", - "resultsCount": 46, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/document-head-management", - "https://tanstack.com/router/latest/docs/framework/react/guide/document-head-management#managing-the-document-head", - "https://tanstack.com/router/latest/docs/framework/react/guide/document-head-management#managing-body-scripts", - "https://tanstack.com/router/latest/docs/framework/react/guide/document-head-management#deduping", - "https://tanstack.com/router/latest/docs/framework/react/guide/document-head-management#headcontent-" - ] - } - ], - "expectedDocsFound": [ - "router:framework/react/guide/document-head-management" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-enabled-button-click", - "question": "How do I trigger a query only when a button is clicked instead of on component mount?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "disable query", - "resultsCount": 72, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/react-native#disable-queries-on-out-of-focus-screens", - "https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries", - "https://tanstack.com/query/latest/docs/framework/vue/guides/disabling-queries", - "https://tanstack.com/query/latest/docs/framework/solid/guides/disabling-queries", - "https://tanstack.com/query/latest/docs/framework/angular/guides/disabling-queries" - ] - } - ], - "expectedDocsFound": ["query:framework/react/guides/disabling-queries"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-stale-cache-time", - "question": "What is the difference between staleTime and gcTime (cacheTime) in TanStack Query?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "staleTime gcTime", - "resultsCount": 23, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/comparison#built-in-client-side-caching", - "https://tanstack.com/db/latest/docs/reference/interfaces/BaseCollectionConfig#startsync", - "https://tanstack.com/db/latest/docs/reference/interfaces/LocalStorageCollectionConfig#startsync", - "https://tanstack.com/db/latest/docs/reference/interfaces/CollectionConfig#startsync", - "https://tanstack.com/query/latest/docs/reference/timeoutManager" - ] - } - ], - "expectedDocsFound": ["query:framework/react/guides/caching"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "query-conditional-enabled", - "question": "How do I conditionally run a query based on some state or prop in TanStack Query?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "dependent queries", - "resultsCount": 100, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries", - "https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries#usequery-dependent-query", - "https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries#usequeries-dependent-query", - "https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries#a-note-about-performance", - "https://tanstack.com/query/latest/docs/framework/vue/guides/dependent-queries" - ] - } - ], - "expectedDocsFound": ["query:framework/react/guides/dependent-queries"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-callbacks-deprecated", - "question": "The onSuccess, onError, and onSettled callbacks are deprecated in TanStack Query. What should I use instead?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "migrate v5", - "resultsCount": 3, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#codemod", - "https://tanstack.com/query/latest/docs/framework/vue/guides/migrating-to-v5#codemod", - "https://tanstack.com/query/latest/docs/framework/svelte/migrate-from-v5-to-v6" - ] - } - ], - "expectedDocsFound": ["query:framework/react/guides/migrating-to-v5"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-provider-error", - "question": "I'm getting 'No QueryClient set, use QueryClientProvider to set one'. How do I fix this?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "QueryClientProvider", - "resultsCount": 23, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/reference/QueryClientProvider", - "https://tanstack.com/query/latest/docs/framework/svelte/reference/type-aliases/QueryClientProviderProps", - "https://tanstack.com/query/latest/docs/framework/svelte/reference/type-aliases/QueryClientProviderProps#properties", - "https://tanstack.com/query/latest/docs/framework/svelte/reference/type-aliases/QueryClientProviderProps#children", - "https://tanstack.com/query/latest/docs/framework/svelte/reference/type-aliases/QueryClientProviderProps#client" - ] - }, - { - "query": "quick start query", - "resultsCount": 19, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/quick-start", - "https://tanstack.com/start/latest/docs/framework/react/quick-start#examples", - "https://tanstack.com/start/latest/docs/framework/react/quick-start#other-router-examples", - "https://tanstack.com/query/latest/docs/framework/vue/quick-start", - "https://tanstack.com/query/latest/docs/framework/solid/quick-start" - ] - } - ], - "expectedDocsFound": ["query:framework/react/quick-start"], - "expectedDocsMissed": [], - "totalSearches": 2, - "passed": true, - "score": 85, - "notes": [] - }, - { - "testId": "table-default-sorting", - "question": "How do I set a default/initial sort order when the table first loads in TanStack Table?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "initial sorting", - "resultsCount": 4, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/table/latest/docs/guide/sorting#initial-sorting-state", - "https://tanstack.com/table/latest/docs/api/features/sorting#resetsorting", - "https://tanstack.com/table/latest/docs/guide/sorting#sorting-apis", - "https://tanstack.com/table/latest/docs/guide/custom-features#getdefaultcolumndef" - ] - } - ], - "expectedDocsFound": ["table:guide/sorting"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "table-column-visibility", - "question": "How do I hide or show columns dynamically in TanStack Table?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "column visibility", - "resultsCount": 48, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/table/latest/docs/guide/column-visibility", - "https://tanstack.com/table/latest/docs/guide/column-visibility#examples", - "https://tanstack.com/table/latest/docs/guide/column-visibility#api", - "https://tanstack.com/table/latest/docs/guide/column-visibility#column-visibility-guide", - "https://tanstack.com/table/latest/docs/guide/column-visibility#other-examples" - ] - } - ], - "expectedDocsFound": ["table:guide/column-visibility"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "table-row-click-handler", - "question": "How do I handle row click events and select a row when clicked in TanStack Table?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "row selection", - "resultsCount": 60, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/table/latest/docs/guide/row-selection", - "https://tanstack.com/table/latest/docs/guide/row-selection#examples", - "https://tanstack.com/table/latest/docs/guide/row-selection#api", - "https://tanstack.com/table/latest/docs/guide/row-selection#row-selection-guide", - "https://tanstack.com/table/latest/docs/guide/row-selection#access-row-selection-state" - ] - } - ], - "expectedDocsFound": ["table:guide/row-selection"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "virtual-scroll-to-index", - "question": "How do I programmatically scroll to a specific item/index in TanStack Virtual?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "scrollToIndex", - "resultsCount": 1, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/virtual/latest/docs/api/virtualizer#scrolltoindex" - ] - } - ], - "expectedDocsFound": ["virtual:api/virtualizer"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-parallel-queries", - "question": "How do I run multiple queries in parallel with TanStack Query?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "parallel queries", - "resultsCount": 46, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/guides/parallel-queries", - "https://tanstack.com/query/latest/docs/framework/react/guides/parallel-queries#manual-parallel-queries", - "https://tanstack.com/query/latest/docs/framework/react/guides/parallel-queries#dynamic-parallel-queries-with-usequeries", - "https://tanstack.com/query/latest/docs/framework/vue/guides/parallel-queries", - "https://tanstack.com/query/latest/docs/framework/solid/guides/parallel-queries" - ] - } - ], - "expectedDocsFound": ["query:framework/react/guides/parallel-queries"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "query-placeholder-data", - "question": "How do I show placeholder or initial data while my query is loading in TanStack Query?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "placeholder data", - "resultsCount": 70, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/guides/placeholder-query-data", - "https://tanstack.com/query/latest/docs/framework/react/guides/placeholder-query-data#what-is-placeholder-data", - "https://tanstack.com/query/latest/docs/framework/react/guides/placeholder-query-data#placeholder-data-as-a-value", - "https://tanstack.com/query/latest/docs/framework/react/guides/placeholder-query-data#placeholder-data-as-a-function", - "https://tanstack.com/query/latest/docs/framework/react/guides/placeholder-query-data#further-reading" - ] - } - ], - "expectedDocsFound": [ - "query:framework/react/guides/placeholder-query-data" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-link-active-state", - "question": "How do I style the active link differently when on that route in TanStack Router?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "Link active", - "resultsCount": 22, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/api/router/ActiveLinkOptionsType", - "https://tanstack.com/router/latest/docs/framework/react/api/router/ActiveLinkOptionsType#activelinkoptions-properties", - "https://tanstack.com/router/latest/docs/framework/react/api/router/ActiveLinkOptionsType#activeprops", - "https://tanstack.com/router/latest/docs/framework/react/api/router/ActiveLinkOptionsType#inactiveprops", - "https://tanstack.com/router/latest/docs/framework/react/api/router/useLinkPropsHook#uselinkprops-options" - ] - } - ], - "expectedDocsFound": [ - "router:framework/react/api/router/ActiveLinkOptionsType" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-layout-routes", - "question": "How do I create layout routes that wrap child routes with shared UI in TanStack Router?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "layout routes", - "resultsCount": 69, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/server-routes#pathless-layout-routes-and-break-out-routes", - "https://tanstack.com/start/latest/docs/framework/solid/guide/server-routes#pathless-layout-routes-and-break-out-routes", - "https://tanstack.com/router/latest/docs/framework/react/routing/routing-concepts#layout-routes", - "https://tanstack.com/router/latest/docs/framework/react/routing/routing-concepts#pathless-layout-routes", - "https://tanstack.com/router/latest/docs/framework/react/routing/code-based-routing#layout-routes" - ] - } - ], - "expectedDocsFound": ["router:framework/react/routing/routing-concepts"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-pathless-layout", - "question": "How do I create a pathless layout route that groups routes without adding to the URL path?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "pathless layout", - "resultsCount": 29, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/server-routes#pathless-layout-routes-and-break-out-routes", - "https://tanstack.com/start/latest/docs/framework/solid/guide/server-routes#pathless-layout-routes-and-break-out-routes", - "https://tanstack.com/router/latest/docs/framework/react/routing/routing-concepts#pathless-layout-routes", - "https://tanstack.com/router/latest/docs/framework/react/routing/code-based-routing#pathless-layout-routes", - "https://tanstack.com/router/latest/docs/framework/solid/routing/routing-concepts#pathless-layout-routes" - ] - } - ], - "expectedDocsFound": ["router:framework/react/routing/routing-concepts"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-route-context", - "question": "How do I pass data through route context to child routes in TanStack Router?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "route context", - "resultsCount": 188, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/api/router/useRouteContextHook", - "https://tanstack.com/router/latest/docs/framework/react/api/router/rootRouteWithContextFunction", - "https://tanstack.com/router/latest/docs/framework/react/api/router/createRootRouteWithContextFunction", - "https://tanstack.com/router/latest/docs/framework/react/api/router/useRouteContextHook#useroutecontext-options", - "https://tanstack.com/router/latest/docs/framework/react/api/router/rootRouteWithContextFunction#rootroutewithcontext-generics" - ] - } - ], - "expectedDocsFound": [ - "router:framework/react/api/router/createRootRouteWithContextFunction" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-use-navigate", - "question": "How do I programmatically navigate to a route in TanStack Router?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "useNavigate", - "resultsCount": 36, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/api/router/useNavigateHook", - "https://tanstack.com/router/latest/docs/framework/react/api/router/useNavigateHook#usenavigate-options", - "https://tanstack.com/router/latest/docs/framework/react/api/router/useNavigateHook#usenavigate-returns", - "https://tanstack.com/router/latest/docs/framework/react/api/router/useNavigateHook#navigate-function", - "https://tanstack.com/router/latest/docs/framework/react/api/router/useNavigateHook#examples" - ] - } - ], - "expectedDocsFound": [ - "router:framework/react/api/router/useNavigateHook" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-catch-all-splat", - "question": "How do I create a catch-all or splat route that matches any path in TanStack Router?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "splat route", - "resultsCount": 73, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/routing/routing-concepts#splat--catch-all-routes", - "https://tanstack.com/router/latest/docs/framework/solid/routing/routing-concepts#splat--catch-all-routes", - "https://tanstack.com/start/latest/docs/framework/react/guide/routing#types-of-routes", - "https://tanstack.com/start/latest/docs/framework/solid/guide/routing#types-of-routes", - "https://tanstack.com/start/latest/docs/framework/react/migrate-from-next-js#dynamic-and-catch-all-routes" - ] - } - ], - "expectedDocsFound": ["router:framework/react/routing/routing-concepts"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-file-based-routing", - "question": "How does file-based routing work in TanStack Router?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "file based routing", - "resultsCount": 139, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/routing/file-based-routing", - "https://tanstack.com/router/latest/docs/framework/react/routing/file-based-routing#what-is-file-based-routing", - "https://tanstack.com/router/latest/docs/framework/react/routing/file-based-routing#s-or-s", - "https://tanstack.com/router/latest/docs/framework/react/routing/file-based-routing#directory-routes", - "https://tanstack.com/router/latest/docs/framework/react/routing/file-based-routing#flat-routes" - ] - } - ], - "expectedDocsFound": [ - "router:framework/react/routing/file-based-routing" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-navigate-search-params", - "question": "How do I navigate while preserving or modifying search params in TanStack Router?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "search params navigation", - "resultsCount": 39, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/overview", - "https://tanstack.com/router/latest/docs/framework/solid/overview", - "https://tanstack.com/start/latest/docs/framework/react/comparison#-tanstack-router-comparison-vs-react-router--nextjs-", - "https://tanstack.com/router/latest/docs/framework/react/guide/route-masking", - "https://tanstack.com/router/latest/docs/framework/solid/guide/route-masking" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/search-params"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "router-redirect-beforeload", - "question": "How do I redirect users in TanStack Router before the route loads?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "redirect beforeLoad", - "resultsCount": 13, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes#redirecting", - "https://tanstack.com/router/latest/docs/framework/solid/guide/authenticated-routes#redirecting", - "https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes#authentication-using-react-contexthooks", - "https://tanstack.com/router/latest/docs/framework/solid/guide/authenticated-routes#authentication-using-react-contexthooks", - "https://tanstack.com/router/latest/docs/framework/react/api/router/redirectFunction" - ] - } - ], - "expectedDocsFound": [ - "router:framework/react/guide/authenticated-routes" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-route-masking", - "question": "How do I show a different URL in the browser than the actual route in TanStack Router?", - "difficulty": "hard", - "searchesPerformed": [ - { - "query": "route masking", - "resultsCount": 48, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/route-masking", - "https://tanstack.com/router/latest/docs/framework/react/guide/route-masking#how-does-route-masking-work", - "https://tanstack.com/router/latest/docs/framework/react/guide/route-masking#how-do-i-use-route-masking", - "https://tanstack.com/router/latest/docs/framework/react/guide/route-masking#unmasking-when-sharing-the-url", - "https://tanstack.com/router/latest/docs/framework/react/guide/route-masking#local-unmasking-defaults" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/route-masking"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-ssr-streaming", - "question": "How does SSR and streaming work in TanStack Router?", - "difficulty": "hard", - "searchesPerformed": [ - { - "query": "ssr router guide", - "resultsCount": 23, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#to-router-cache-or-not-to-router-cache", - "https://tanstack.com/router/latest/docs/framework/solid/guide/data-loading#to-router-cache-or-not-to-router-cache", - "https://tanstack.com/router/latest/docs/framework/react/guide/ssr", - "https://tanstack.com/router/latest/docs/framework/solid/guide/ssr", - "https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/ssr"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-external-data-loading", - "question": "How do I use TanStack Router with an external data fetching library like TanStack Query?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "external data loading", - "resultsCount": 32, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading", - "https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#to-store-or-to-coordinate", - "https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#what-data-fetching-libraries-are-supported", - "https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#using-loaders-to-ensure-data-is-loaded", - "https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#a-more-realistic-example-using-tanstack-query" - ] - } - ], - "expectedDocsFound": [ - "router:framework/react/guide/external-data-loading" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-execution-model", - "question": "How do I understand where my code runs in TanStack Start (server vs client)?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "execution model", - "resultsCount": 71, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/execution-model", - "https://tanstack.com/start/latest/docs/framework/react/guide/execution-model#core-principle-isomorphic-by-default", - "https://tanstack.com/start/latest/docs/framework/react/guide/execution-model#the-execution-boundary", - "https://tanstack.com/start/latest/docs/framework/react/guide/execution-model#execution-control-apis", - "https://tanstack.com/start/latest/docs/framework/react/guide/execution-model#architectural-patterns" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/execution-model"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-server-only-code", - "question": "How do I ensure code only runs on the server in TanStack Start?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "server only code", - "resultsCount": 32, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/execution-model#bundle-analysis", - "https://tanstack.com/start/latest/docs/framework/solid/guide/execution-model#bundle-analysis", - "https://tanstack.com/start/latest/docs/framework/react/guide/code-execution-patterns#production-checklist", - "https://tanstack.com/start/latest/docs/framework/solid/guide/code-execution-patterns#production-checklist", - "https://tanstack.com/start/latest/docs/framework/react/guide/code-execution-patterns" - ] - } - ], - "expectedDocsFound": [ - "start:framework/react/guide/code-execution-patterns" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-authentication", - "question": "How do I implement authentication in TanStack Start?", - "difficulty": "hard", - "searchesPerformed": [ - { - "query": "authentication start", - "resultsCount": 133, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/how-to/setup-authentication#user-logged-out-on-page-refresh", - "https://tanstack.com/router/latest/docs/framework/react/how-to/setup-authentication#add-authentication-persistence", - "https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes#authentication-using-react-contexthooks", - "https://tanstack.com/router/latest/docs/framework/solid/guide/authenticated-routes#authentication-using-react-contexthooks", - "https://tanstack.com/start/latest/docs/framework/react/guide/authentication" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/authentication"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "start-auth-overview", - "question": "What authentication options are available for TanStack Start?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "authentication overview", - "resultsCount": 16, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/authentication", - "https://tanstack.com/start/latest/docs/framework/solid/guide/authentication", - "https://tanstack.com/start/latest/docs/framework/react/guide/authentication#next-steps", - "https://tanstack.com/start/latest/docs/framework/solid/guide/authentication#next-steps", - "https://tanstack.com/router/latest/docs/framework/react/overview#inherited-route-context" - ] - } - ], - "expectedDocsFound": [ - "start:framework/react/guide/authentication-overview" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "start-streaming-server-functions", - "question": "How do I stream data from a server function in TanStack Start?", - "difficulty": "hard", - "searchesPerformed": [ - { - "query": "streaming server function", - "resultsCount": 15, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/overview", - "https://tanstack.com/start/latest/docs/framework/solid/overview", - "https://tanstack.com/start/latest/docs/framework/react/guide/streaming-data-from-server-functions", - "https://tanstack.com/start/latest/docs/framework/react/guide/streaming-data-from-server-functions#typed-readable-streams", - "https://tanstack.com/start/latest/docs/framework/react/guide/streaming-data-from-server-functions#async-generators-in-server-functions" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/server-functions"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "start-tailwind", - "question": "How do I set up Tailwind CSS with TanStack Start?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "tailwind start", - "resultsCount": 28, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/tailwind-integration", - "https://tanstack.com/start/latest/docs/framework/solid/guide/tailwind-integration", - "https://tanstack.com/start/latest/docs/framework/react/quick-start#impatient", - "https://tanstack.com/router/latest/docs/framework/react/quick-start#impatient", - "https://tanstack.com/start/latest/docs/framework/solid/quick-start#impatient" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/tailwind-integration"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-database", - "question": "How do I connect to a database in TanStack Start?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "database start", - "resultsCount": 45, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/databases#what-should-i-use", - "https://tanstack.com/start/latest/docs/framework/solid/guide/databases#what-should-i-use", - "https://tanstack.com/start/latest/docs/framework/react/guide/databases#how-simple-is-it-to-use-a-database-with-tanstack-start", - "https://tanstack.com/start/latest/docs/framework/solid/guide/databases#how-simple-is-it-to-use-a-database-with-tanstack-start", - "https://tanstack.com/start/latest/docs/framework/react/guide/databases#documentation--apis" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/databases"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-migrate-nextjs", - "question": "How do I migrate from Next.js to TanStack Start?", - "difficulty": "hard", - "searchesPerformed": [ - { - "query": "migrate nextjs", - "resultsCount": 23, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/migrate-from-next-js", - "https://tanstack.com/start/latest/docs/framework/react/migrate-from-next-js#step-by-step-basics", - "https://tanstack.com/start/latest/docs/framework/react/migrate-from-next-js#next-steps-advanced", - "https://tanstack.com/start/latest/docs/framework/react/migrate-from-next-js#prerequisites", - "https://tanstack.com/start/latest/docs/framework/react/migrate-from-next-js#1-remove-nextjs" - ] - } - ], - "expectedDocsFound": ["start:framework/react/migrate-from-next-js"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-build-from-scratch", - "question": "How do I build a TanStack Start project from scratch without using a template?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "build from scratch", - "resultsCount": 24, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/build-from-scratch", - "https://tanstack.com/start/latest/docs/framework/react/build-from-scratch#typescript-configuration", - "https://tanstack.com/start/latest/docs/framework/react/build-from-scratch#install-dependencies", - "https://tanstack.com/start/latest/docs/framework/react/build-from-scratch#update-configuration-files", - "https://tanstack.com/start/latest/docs/framework/react/build-from-scratch#add-the-basic-templating" - ] - } - ], - "expectedDocsFound": ["start:framework/react/build-from-scratch"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-rendering-markdown", - "question": "How do I render markdown content in TanStack Start?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "render markdown", - "resultsCount": 2, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/rendering-markdown#method-2-dynamic-markdown-from-remote-sources", - "https://tanstack.com/start/latest/docs/framework/react/guide/rendering-markdown#creating-a-markdown-component" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/rendering-markdown"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-cloudflare-workers", - "question": "How do I deploy TanStack Start to Cloudflare Workers?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "cloudflare workers", - "resultsCount": 9, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/hosting#cloudflare-workers--official-partner", - "https://tanstack.com/start/latest/docs/framework/react/guide/isr#cloudflare-workers", - "https://tanstack.com/start/latest/docs/framework/solid/guide/hosting#cloudflare-workers--official-partner", - "https://tanstack.com/start/latest/docs/framework/solid/guide/hosting#cloudflare-workers", - "https://tanstack.com/start/latest/docs/framework/react/comparison#deployment-flexibility" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/hosting"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-vs-nextjs", - "question": "How does TanStack Start compare to Next.js?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "start vs nextjs", - "resultsCount": 22, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/comparison", - "https://tanstack.com/router/latest/docs/framework/react/comparison", - "https://tanstack.com/start/latest/docs/framework/react/comparison#key-philosophical-differences", - "https://tanstack.com/start/latest/docs/framework/react/comparison#when-to-choose-each-framework", - "https://tanstack.com/start/latest/docs/framework/react/comparison#feature-deep-dives" - ] - } - ], - "expectedDocsFound": ["start:framework/react/comparison"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-providers-location", - "question": "Where do I put providers like QueryClientProvider in TanStack Start?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "query integration setup", - "resultsCount": 6, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/integrations/query#setup", - "https://tanstack.com/router/latest/docs/integrations/query#works-with-tanstack-start", - "https://tanstack.com/query/latest/docs/framework/angular/guides/testing", - "https://tanstack.com/query/latest/docs/framework/react/guides/prefetching#router-integration", - "https://tanstack.com/query/latest/docs/framework/solid/guides/prefetching#router-integration" - ] - } - ], - "expectedDocsFound": ["router:integrations/query"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-routetree-gen-error", - "question": "I'm getting 'Cannot find module routeTree.gen' error. How do I fix it?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "routeTree.gen", - "resultsCount": 12, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/faq#should-i-commit-my-routetreegents-file-into-git", - "https://tanstack.com/router/latest/docs/framework/solid/faq#should-i-commit-my-routetreegents-file-into-git", - "https://tanstack.com/start/latest/docs/framework/react/guide/routing#route-tree-generation", - "https://tanstack.com/start/latest/docs/framework/solid/guide/routing#route-tree-generation", - "https://tanstack.com/router/latest/docs/framework/react/guide/creating-a-router#filesystem-route-tree" - ] - } - ], - "expectedDocsFound": ["router:framework/react/faq"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-exclude-layout", - "question": "How do I exclude specific routes from a parent layout in TanStack Router?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "non-nested routes", - "resultsCount": 8, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/routing/routing-concepts#non-nested-routes", - "https://tanstack.com/router/latest/docs/framework/react/routing/code-based-routing#non-nested-routes", - "https://tanstack.com/router/latest/docs/framework/solid/routing/routing-concepts#non-nested-routes", - "https://tanstack.com/router/latest/docs/framework/solid/routing/code-based-routing#non-nested-routes", - "https://tanstack.com/router/latest/docs/framework/react/routing/code-based-routing#routing-concepts-for-code-based-routing" - ] - } - ], - "expectedDocsFound": ["router:framework/react/routing/routing-concepts"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-dynamic-nested-routes", - "question": "How do I create dynamic nested routes like /users/$userId/posts/$postId in TanStack Router?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "dynamic route segments", - "resultsCount": 16, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/routing/routing-concepts#dynamic-route-segments", - "https://tanstack.com/router/latest/docs/framework/react/routing/code-based-routing#dynamic-route-segments", - "https://tanstack.com/router/latest/docs/framework/solid/routing/routing-concepts#dynamic-route-segments", - "https://tanstack.com/router/latest/docs/framework/solid/routing/code-based-routing#dynamic-route-segments", - "https://tanstack.com/router/latest/docs/framework/react/routing/code-based-routing#routing-concepts-for-code-based-routing" - ] - } - ], - "expectedDocsFound": ["router:framework/react/routing/routing-concepts"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-search-params-no-rerender", - "question": "How do I update search params without causing a full page re-render in TanStack Router?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "search params shallow", - "resultsCount": 547, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/search-params", - "https://tanstack.com/router/latest/docs/framework/react/guide/custom-search-param-serialization", - "https://tanstack.com/router/latest/docs/framework/react/guide/search-params#why-not-just-use-urlsearchparams", - "https://tanstack.com/router/latest/docs/framework/react/guide/custom-search-param-serialization#using-base64", - "https://tanstack.com/router/latest/docs/framework/react/guide/search-params#search-params-the-og-state-manager" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/search-params"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-link-typesafe-wrapper", - "question": "How do I wrap TanStack Router's Link component while keeping type safety?", - "difficulty": "hard", - "searchesPerformed": [ - { - "query": "custom link", - "resultsCount": 23, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/guide/custom-link", - "https://tanstack.com/router/latest/docs/framework/react/guide/custom-link#createlink-for-cross-cutting-concerns", - "https://tanstack.com/router/latest/docs/framework/react/guide/custom-link#createlink-with-third-party-libraries", - "https://tanstack.com/router/latest/docs/framework/react/guide/custom-link#basic-example", - "https://tanstack.com/router/latest/docs/framework/react/guide/custom-link#react-aria-components-example" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/custom-link"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-error-boundary-disable", - "question": "How do I disable or customize the error boundary in TanStack Router?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "error boundary router", - "resultsCount": 34, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultoncatch-property", - "https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#handling-errors-with-routeoptionserrorcomponent", - "https://tanstack.com/router/latest/docs/framework/solid/guide/data-loading#handling-errors-with-routeoptionserrorcomponent", - "https://tanstack.com/router/latest/docs/framework/react/overview#why-tanstack-router", - "https://tanstack.com/router/latest/docs/framework/solid/overview#why-tanstack-router" - ] - } - ], - "expectedDocsFound": ["router:framework/react/guide/data-loading"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "router-generate-href", - "question": "How do I generate a URL/href string for a route without navigating in TanStack Router?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "buildLocation", - "resultsCount": 1, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/router/latest/docs/framework/react/api/router/RouterType#buildlocation-method" - ] - } - ], - "expectedDocsFound": ["router:framework/react/api/router/RouterType"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "start-server-fn-validation", - "question": "How do I validate input data in a TanStack Start server function?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "server function validation", - "resultsCount": 21, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/authentication-overview#route-protection-architecture", - "https://tanstack.com/start/latest/docs/framework/solid/guide/authentication-overview#route-protection-architecture", - "https://tanstack.com/start/latest/docs/framework/react/tutorial/reading-writing-file#form-submission-issues", - "https://tanstack.com/start/latest/docs/framework/react/guide/server-functions#parameters--validation", - "https://tanstack.com/start/latest/docs/framework/react/guide/server-functions#basic-parameters" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/server-functions"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "start-env-variables", - "question": "How do I use environment variables in TanStack Start?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "environment variables", - "resultsCount": 85, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/environment-variables", - "https://tanstack.com/start/latest/docs/framework/react/guide/environment-variables#quick-start", - "https://tanstack.com/start/latest/docs/framework/react/guide/environment-variables#environment-variable-contexts", - "https://tanstack.com/start/latest/docs/framework/react/guide/environment-variables#environment-file-setup", - "https://tanstack.com/start/latest/docs/framework/react/guide/environment-variables#common-patterns" - ] - } - ], - "expectedDocsFound": [ - "start:framework/react/guide/environment-variables" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "partner-database-recommendation", - "question": "What database should I use with TanStack Start?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "database start", - "resultsCount": 45, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/databases#what-should-i-use", - "https://tanstack.com/start/latest/docs/framework/solid/guide/databases#what-should-i-use", - "https://tanstack.com/start/latest/docs/framework/react/guide/databases#how-simple-is-it-to-use-a-database-with-tanstack-start", - "https://tanstack.com/start/latest/docs/framework/solid/guide/databases#how-simple-is-it-to-use-a-database-with-tanstack-start", - "https://tanstack.com/start/latest/docs/framework/react/guide/databases#documentation--apis" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/databases"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "partner-auth-recommendation", - "question": "What authentication solution should I use with TanStack Start?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "authentication overview", - "resultsCount": 16, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/authentication", - "https://tanstack.com/start/latest/docs/framework/solid/guide/authentication", - "https://tanstack.com/start/latest/docs/framework/react/guide/authentication#next-steps", - "https://tanstack.com/start/latest/docs/framework/solid/guide/authentication#next-steps", - "https://tanstack.com/router/latest/docs/framework/react/overview#inherited-route-context" - ] - } - ], - "expectedDocsFound": [ - "start:framework/react/guide/authentication-overview" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "partner-hosting-recommendation", - "question": "Where should I deploy my TanStack Start application?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "hosting start", - "resultsCount": 44, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/hosting#what-should-i-use", - "https://tanstack.com/start/latest/docs/framework/solid/guide/hosting#what-should-i-use", - "https://tanstack.com/start/latest/docs/framework/react/guide/hosting#deployment", - "https://tanstack.com/start/latest/docs/framework/solid/guide/hosting#deployment", - "https://tanstack.com/start/latest/docs/framework/react/overview" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/hosting"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "partner-table-upgrade", - "question": "I need more advanced features than TanStack Table provides. What should I use?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "table ag grid", - "resultsCount": 9, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/table/latest/docs/enterprise/ag-grid#conclusion", - "https://tanstack.com/table/latest/docs/introduction#component-based-table-libraries", - "https://tanstack.com/table/latest/docs/enterprise/ag-grid", - "https://tanstack.com/table/latest/docs/enterprise/ag-grid#why-choose-ag-grid", - "https://tanstack.com/table/latest/docs/enterprise/ag-grid#comprehensive-feature-set" - ] - } - ], - "expectedDocsFound": ["table:introduction"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "partner-neon-setup", - "question": "How do I connect Neon database to my TanStack Start app?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "neon database", - "resultsCount": 4, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/databases#recommended-database-providers", - "https://tanstack.com/start/latest/docs/framework/solid/guide/databases#recommended-database-providers", - "https://tanstack.com/start/latest/docs/framework/react/guide/databases#what-is-neon", - "https://tanstack.com/start/latest/docs/framework/solid/guide/databases#what-is-neon" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/databases"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "partner-clerk-setup", - "question": "How do I add Clerk authentication to TanStack Start?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "clerk authentication", - "resultsCount": 25, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/authentication-overview#clerk---complete-authentication-platform", - "https://tanstack.com/start/latest/docs/framework/solid/guide/authentication-overview#clerk---complete-authentication-platform", - "https://tanstack.com/start/latest/docs/framework/react/guide/authentication#authentication-approaches", - "https://tanstack.com/start/latest/docs/framework/solid/guide/authentication#authentication-approaches", - "https://tanstack.com/router/latest/docs/framework/react/how-to/setup-auth-providers" - ] - } - ], - "expectedDocsFound": [ - "start:framework/react/guide/authentication-overview" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "partner-error-monitoring", - "question": "How do I set up error monitoring for my TanStack app?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "error monitoring", - "resultsCount": 13, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/observability#other-popular-tools", - "https://tanstack.com/start/latest/docs/framework/solid/guide/observability#other-popular-tools", - "https://tanstack.com/start/latest/docs/framework/react/guide/observability#partner-solution-sentry", - "https://tanstack.com/start/latest/docs/framework/solid/guide/observability#partner-solution-sentry", - "https://tanstack.com/pacer/latest/docs/framework/react/reference/functions/useAsyncBatcher" - ] - }, - { - "query": "sentry start", - "resultsCount": 42, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/observability#partner-solution-sentry", - "https://tanstack.com/start/latest/docs/framework/solid/guide/observability#partner-solution-sentry", - "https://tanstack.com/start/latest/docs/framework/react/guide/server-routes#handling-server-route-requests", - "https://tanstack.com/start/latest/docs/framework/solid/guide/server-routes#handling-server-route-requests", - "https://tanstack.com/start/latest/docs/framework/react/guide/server-entry-point" - ] - } - ], - "expectedDocsFound": [], - "expectedDocsMissed": ["start:framework/react/guide/error-handling"], - "totalSearches": 2, - "passed": false, - "score": 65, - "notes": [] - }, - { - "testId": "partner-cms-recommendation", - "question": "What CMS should I use with TanStack Start for content management?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "cms start", - "resultsCount": 3160, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/store/latest/docs/framework/react/quick-start", - "https://tanstack.com/start/latest/docs/framework/react/quick-start", - "https://tanstack.com/start/latest/docs/framework/react/overview", - "https://tanstack.com/router/latest/docs/framework/react/quick-start", - "https://tanstack.com/query/latest/docs/framework/react/quick-start" - ] - }, - { - "query": "headless cms tanstack", - "resultsCount": 1778, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/form/latest/docs/framework/react/guides/react-native", - "https://tanstack.com/table/latest/docs/overview#headless", - "https://tanstack.com/form/latest/docs/framework/react/guides/ui-libraries#usage-of-tanstack-form-with-ui-libraries", - "https://tanstack.com/table/latest/docs/guide/tables#table-instance-guide", - "https://tanstack.com/virtual/latest/docs/introduction" - ] - } - ], - "expectedDocsFound": [], - "expectedDocsMissed": ["start:framework/react/guide/rendering-markdown"], - "totalSearches": 2, - "passed": false, - "score": 65, - "notes": [] - }, - { - "testId": "ecosystem-postgres-alias", - "question": "I need a postgres database for my TanStack Start app", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "databases guide", - "resultsCount": 4, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/databases#documentation--apis", - "https://tanstack.com/pacer/latest/docs/guides/async-batching#when-to-use-async-batching", - "https://tanstack.com/pacer/latest/docs/guides/batching#when-to-use-batching", - "https://tanstack.com/start/latest/docs/framework/solid/guide/databases#documentation--apis" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/databases"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "ecosystem-login-alias", - "question": "How do I add login functionality to my app?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "authentication overview", - "resultsCount": 16, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/authentication", - "https://tanstack.com/start/latest/docs/framework/solid/guide/authentication", - "https://tanstack.com/start/latest/docs/framework/react/guide/authentication#next-steps", - "https://tanstack.com/start/latest/docs/framework/solid/guide/authentication#next-steps", - "https://tanstack.com/router/latest/docs/framework/react/overview#inherited-route-context" - ] - } - ], - "expectedDocsFound": [ - "start:framework/react/guide/authentication-overview" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "ecosystem-serverless-alias", - "question": "What serverless hosting options work with TanStack Start?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "hosting guide", - "resultsCount": 7, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/build-from-scratch#writing-your-first-route", - "https://tanstack.com/start/latest/docs/framework/solid/build-from-scratch#writing-your-first-route", - "https://tanstack.com/start/latest/docs/framework/react/guide/hosting#deployment", - "https://tanstack.com/start/latest/docs/framework/solid/guide/hosting#deployment", - "https://tanstack.com/start/latest/docs/framework/react/guide/hosting#bun" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/hosting"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "ecosystem-error-tracking-alias", - "question": "I need error tracking for production", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "observability", - "resultsCount": 50, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/observability", - "https://tanstack.com/start/latest/docs/framework/react/guide/observability#partner-solution-sentry", - "https://tanstack.com/start/latest/docs/framework/react/guide/observability#built-in-observability-patterns", - "https://tanstack.com/start/latest/docs/framework/react/guide/observability#external-observability-tools", - "https://tanstack.com/start/latest/docs/framework/react/guide/observability#best-practices" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/observability"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "ecosystem-library-filter", - "question": "What ecosystem integrations are available for TanStack Table?", - "difficulty": "easy", - "searchesPerformed": [ - { - "query": "table introduction", - "resultsCount": 12, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/query/latest/docs/framework/react/guides/request-waterfalls#single-component-waterfalls--serial-queries", - "https://tanstack.com/query/latest/docs/framework/solid/guides/request-waterfalls#single-component-waterfalls--serial-queries", - "https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries#a-note-about-performance", - "https://tanstack.com/query/latest/docs/framework/vue/guides/dependent-queries#a-note-about-performance", - "https://tanstack.com/query/latest/docs/framework/solid/guides/dependent-queries#a-note-about-performance" - ] - } - ], - "expectedDocsFound": ["table:introduction"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "ecosystem-sso-enterprise", - "question": "I need SSO and enterprise authentication features", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "authentication overview", - "resultsCount": 16, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/authentication", - "https://tanstack.com/start/latest/docs/framework/solid/guide/authentication", - "https://tanstack.com/start/latest/docs/framework/react/guide/authentication#next-steps", - "https://tanstack.com/start/latest/docs/framework/solid/guide/authentication#next-steps", - "https://tanstack.com/router/latest/docs/framework/react/overview#inherited-route-context" - ] - } - ], - "expectedDocsFound": [ - "start:framework/react/guide/authentication-overview" - ], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 80, - "notes": [] - }, - { - "testId": "ecosystem-realtime-database", - "question": "I need a real-time database with live queries", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "databases guide", - "resultsCount": 4, - "foundExpectedDoc": true, - "topResults": [ - "https://tanstack.com/start/latest/docs/framework/react/guide/databases#documentation--apis", - "https://tanstack.com/pacer/latest/docs/guides/async-batching#when-to-use-async-batching", - "https://tanstack.com/pacer/latest/docs/guides/batching#when-to-use-batching", - "https://tanstack.com/start/latest/docs/framework/solid/guide/databases#documentation--apis" - ] - } - ], - "expectedDocsFound": ["start:framework/react/guide/databases"], - "expectedDocsMissed": [], - "totalSearches": 1, - "passed": true, - "score": 100, - "notes": [] - }, - { - "testId": "ecosystem-rate-limiting", - "question": "How do I add rate limiting to my API?", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "rate limiting", - "resultsCount": 87, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/pacer/latest/docs/guides/rate-limiting", - "https://tanstack.com/pacer/latest/docs/guides/async-rate-limiting", - "https://tanstack.com/pacer/latest/docs/guides/rate-limiting#rate-limiting-concept", - "https://tanstack.com/pacer/latest/docs/guides/async-rate-limiting#when-to-use-async-rate-limiting", - "https://tanstack.com/pacer/latest/docs/guides/async-rate-limiting#async-rate-limiting-in-tanstack-pacer" - ] - }, - { - "query": "pacer", - "resultsCount": 2176, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/pacer/latest/docs/guides/which-pacer-utility-should-i-choose", - "https://tanstack.com/pacer/latest/docs/guides/which-pacer-utility-should-i-choose#synchronous-vs-asynchronous", - "https://tanstack.com/pacer/latest/docs/guides/which-pacer-utility-should-i-choose#pacer-lite-vs-pacer", - "https://tanstack.com/pacer/latest/docs/guides/which-pacer-utility-should-i-choose#which-hook-variation-should-i-use", - "https://tanstack.com/pacer/latest/docs/guides/which-pacer-utility-should-i-choose#when-to-use-the-asynchronous-version" - ] - } - ], - "expectedDocsFound": [], - "expectedDocsMissed": ["pacer:overview"], - "totalSearches": 2, - "passed": false, - "score": 65, - "notes": [] - }, - { - "testId": "ecosystem-offline-sync", - "question": "I need offline-first sync capabilities for my app", - "difficulty": "medium", - "searchesPerformed": [ - { - "query": "tanstack db", - "resultsCount": 363, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/db/latest/docs/framework/react/overview", - "https://tanstack.com/db/latest/docs/framework/react/overview#installation", - "https://tanstack.com/db/latest/docs/framework/react/overview#react-hooks", - "https://tanstack.com/db/latest/docs/framework/react/overview#basic-usage", - "https://tanstack.com/db/latest/docs/framework/react/overview#uselivequery" - ] - }, - { - "query": "sync", - "resultsCount": 857, - "foundExpectedDoc": false, - "topResults": [ - "https://tanstack.com/db/latest/docs/reference/interfaces/SyncConfig#sync", - "https://tanstack.com/db/latest/docs/reference/interfaces/SyncConfig#parameters", - "https://tanstack.com/db/latest/docs/reference/interfaces/SyncConfig#returns-1", - "https://tanstack.com/db/latest/docs/reference/interfaces/SyncConfig#params", - "https://tanstack.com/db/latest/docs/reference/interfaces/SyncConfig#begin" - ] - } - ], - "expectedDocsFound": [], - "expectedDocsMissed": ["db:overview"], - "totalSearches": 2, - "passed": true, - "score": 85, - "notes": [] - } - ] -} diff --git a/scripts/mcp-eval/run-eval.ts b/scripts/mcp-eval/run-eval.ts deleted file mode 100644 index 649181c1f..000000000 --- a/scripts/mcp-eval/run-eval.ts +++ /dev/null @@ -1,330 +0,0 @@ -/** - * MCP Documentation Discoverability Evaluation Runner - * - * This script tests how well the TanStack MCP server helps AI assistants - * find the right documentation to answer questions. - * - * Usage: - * npx tsx scripts/mcp-eval/run-eval.ts - * npx tsx scripts/mcp-eval/run-eval.ts --test router-query-ssr-integration - * npx tsx scripts/mcp-eval/run-eval.ts --tag start - */ - -import testCases from './test-cases.json' - -const MCP_URL = process.env.MCP_URL || 'http://localhost:3001/api/mcp' -const MCP_API_KEY = process.env.MCP_API_KEY - -interface SearchResult { - title: string - url: string - library: string - breadcrumb: string[] -} - -interface McpResponse { - result?: { - content: Array<{ type: string; text: string }> - } - error?: { - code: number - message: string - } -} - -interface TestResult { - testId: string - question: string - difficulty: string - searchesPerformed: Array<{ - query: string - resultsCount: number - foundExpectedDoc: boolean - topResults: string[] - }> - expectedDocsFound: string[] - expectedDocsMissed: string[] - totalSearches: number - passed: boolean - score: number - notes: string[] -} - -async function callMcp(method: string, params: object): Promise { - const headers: Record = { - 'Content-Type': 'application/json', - Accept: 'application/json, text/event-stream', - } - - if (MCP_API_KEY) { - headers['Authorization'] = `Bearer ${MCP_API_KEY}` - } - - // First initialize - await fetch(MCP_URL, { - method: 'POST', - headers, - body: JSON.stringify({ - jsonrpc: '2.0', - id: 0, - method: 'initialize', - params: { - protocolVersion: '2024-11-05', - capabilities: {}, - clientInfo: { name: 'mcp-eval', version: '1.0.0' }, - }, - }), - }) - - // Then call the tool - const response = await fetch(MCP_URL, { - method: 'POST', - headers, - body: JSON.stringify({ - jsonrpc: '2.0', - id: 1, - method: 'tools/call', - params: { - name: method, - arguments: params, - }, - }), - }) - - return response.json() -} - -async function searchDocs( - query: string, - library?: string, -): Promise<{ results: SearchResult[]; totalHits: number }> { - const response = await callMcp('search_docs', { - query, - library, - limit: 10, - }) - - if (response.error) { - throw new Error(response.error.message) - } - - const text = response.result?.content[0]?.text || '{}' - return JSON.parse(text) -} -function extractPathFromUrl(url: string): string { - // Extract path from URL like https://tanstack.com/router/latest/docs/integrations/query - // Also handle anchors like #some-section - const match = url.match(/\/docs\/([^#]+)/) - return match ? match[1] : '' -} - -async function runTestCase( - testCase: (typeof testCases.testCases)[0], -): Promise { - const result: TestResult = { - testId: testCase.id, - question: testCase.question, - difficulty: testCase.difficulty, - searchesPerformed: [], - expectedDocsFound: [], - expectedDocsMissed: [], - totalSearches: 0, - passed: false, - score: 0, - notes: [], - } - - const expectedPaths = new Set( - testCase.expectedDocs.map((d) => `${d.library}:${d.path}`), - ) - const foundPaths = new Set() - - // Try ideal search queries first - for (const query of testCase.idealSearchQueries || []) { - result.totalSearches++ - - try { - const searchResult = await searchDocs(query) - const topResults = searchResult.results.slice(0, 5).map((r) => r.url) - - let foundExpected = false - for (const r of searchResult.results) { - const path = extractPathFromUrl(r.url) - const key = `${r.library}:${path}` - - if (expectedPaths.has(key)) { - foundPaths.add(key) - foundExpected = true - } - } - - result.searchesPerformed.push({ - query, - resultsCount: searchResult.totalHits, - foundExpectedDoc: foundExpected, - topResults, - }) - - // If we found all expected docs, stop searching - if (foundPaths.size === expectedPaths.size) { - break - } - } catch (error) { - result.notes.push(`Search failed for "${query}": ${error}`) - } - } - - // Record which docs were found vs missed - for (const doc of testCase.expectedDocs) { - const key = `${doc.library}:${doc.path}` - if (foundPaths.has(key)) { - result.expectedDocsFound.push(key) - } else { - result.expectedDocsMissed.push(key) - } - } - - // Calculate score - const requiredDocs = testCase.expectedDocs.filter((d) => d.required) - const requiredFound = requiredDocs.filter((d) => - foundPaths.has(`${d.library}:${d.path}`), - ) - - // Score breakdown: - // - 50% for finding required docs - // - 30% for finding them in fewer searches - // - 20% for finding them in top results - - const requiredScore = - requiredDocs.length > 0 ? requiredFound.length / requiredDocs.length : 1 - - const searchEfficiency = Math.max( - 0, - 1 - (result.totalSearches - 1) / (testCase.idealSearchQueries?.length || 3), - ) - - // Check if expected doc appeared in top 3 results - const topResultScore = result.searchesPerformed.some((s) => { - return testCase.expectedDocs.some((doc) => - s.topResults - .slice(0, 3) - .some((url) => url.includes(doc.path.replace(/\//g, '/'))), - ) - }) - ? 1 - : 0 - - result.score = Math.round( - (requiredScore * 0.5 + searchEfficiency * 0.3 + topResultScore * 0.2) * 100, - ) - - result.passed = - requiredFound.length === requiredDocs.length && result.score >= 70 - - return result -} - -async function main() { - const args = process.argv.slice(2) - let testFilter: string | undefined - let tagFilter: string | undefined - - for (let i = 0; i < args.length; i++) { - if (args[i] === '--test' && args[i + 1]) { - testFilter = args[i + 1] - } - if (args[i] === '--tag' && args[i + 1]) { - tagFilter = args[i + 1] - } - } - - let cases = testCases.testCases - - if (testFilter) { - cases = cases.filter((c) => c.id === testFilter) - } - - if (tagFilter) { - cases = cases.filter((c) => c.tags.includes(tagFilter)) - } - - console.log(`\n🧪 Running ${cases.length} MCP evaluation test(s)...\n`) - console.log('='.repeat(60)) - - const results: TestResult[] = [] - - for (const testCase of cases) { - console.log(`\n📋 Test: ${testCase.id}`) - console.log(` Question: ${testCase.question.slice(0, 60)}...`) - - try { - const result = await runTestCase(testCase) - results.push(result) - - const status = result.passed ? '✅ PASS' : '❌ FAIL' - console.log(` ${status} (Score: ${result.score}/100)`) - console.log(` Searches: ${result.totalSearches}`) - - if (result.expectedDocsMissed.length > 0) { - console.log(` ⚠️ Missed: ${result.expectedDocsMissed.join(', ')}`) - } - - if (result.notes.length > 0) { - result.notes.forEach((n) => console.log(` 📝 ${n}`)) - } - } catch (error) { - console.log(` ❌ ERROR: ${error}`) - } - } - - console.log('\n' + '='.repeat(60)) - console.log('\n📊 Summary\n') - - const passed = results.filter((r) => r.passed).length - const total = results.length - const avgScore = Math.round( - results.reduce((sum, r) => sum + r.score, 0) / total, - ) - - console.log(` Passed: ${passed}/${total}`) - console.log(` Average Score: ${avgScore}/100`) - - // Group by difficulty - const byDifficulty = { - easy: results.filter((r) => r.difficulty === 'easy'), - medium: results.filter((r) => r.difficulty === 'medium'), - hard: results.filter((r) => r.difficulty === 'hard'), - } - - for (const [diff, tests] of Object.entries(byDifficulty)) { - if (tests.length > 0) { - const p = tests.filter((t) => t.passed).length - const avg = Math.round( - tests.reduce((s, t) => s + t.score, 0) / tests.length, - ) - console.log(` ${diff}: ${p}/${tests.length} passed (avg: ${avg})`) - } - } - - // Output detailed results as JSON - const outputPath = './scripts/mcp-eval/results.json' - const fs = await import('fs') - fs.writeFileSync( - outputPath, - JSON.stringify( - { - timestamp: new Date().toISOString(), - summary: { passed, total, avgScore }, - results, - }, - null, - 2, - ), - ) - console.log(`\n Detailed results: ${outputPath}`) - - // Exit with error if any tests failed - process.exit(passed === total ? 0 : 1) -} - -main().catch(console.error) diff --git a/scripts/mcp-eval/test-cases.json b/scripts/mcp-eval/test-cases.json deleted file mode 100644 index 52b91ad52..000000000 --- a/scripts/mcp-eval/test-cases.json +++ /dev/null @@ -1,1957 +0,0 @@ -{ - "$schema": "./test-cases.schema.json", - "version": "1.0.0", - "description": "MCP documentation discoverability test cases", - "testCases": [ - { - "id": "router-query-ssr-integration", - "question": "In TanStack Start, how do I prefetch data for a route that uses both a loader and TanStack Query, ensuring the data is dehydrated to the client without double-fetching?", - "difficulty": "hard", - "tags": ["start", "router", "query", "ssr", "integration"], - "expectedDocs": [ - { - "library": "router", - "path": "integrations/query", - "required": true, - "reason": "This is THE official integration guide" - } - ], - "idealSearchQueries": [ - "query integration", - "router query ssr", - "react-router-ssr-query" - ], - "badSearchQueries": [ - "prefetch dehydrate SSR", - "loader prefetch dehydrate" - ], - "correctAnswerMustInclude": [ - "@tanstack/react-router-ssr-query", - "setupRouterSsrQueryIntegration" - ], - "notes": "The integration package handles all dehydration/hydration automatically. Manual setup is possible but not recommended." - }, - { - "id": "createfileroute-basic", - "question": "How do I create a file-based route in TanStack Router?", - "difficulty": "easy", - "tags": ["router", "basics"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/api/router/createFileRouteFunction", - "required": true, - "reason": "Primary API reference" - } - ], - "idealSearchQueries": ["createFileRoute"], - "correctAnswerMustInclude": ["createFileRoute", "export const Route"] - }, - { - "id": "query-usequery-basic", - "question": "How do I fetch data with TanStack Query in React?", - "difficulty": "easy", - "tags": ["query", "basics", "react"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/queries", - "required": true, - "reason": "Primary guide for queries" - } - ], - "idealSearchQueries": ["queries guide", "query basics"], - "badSearchQueries": ["useQuery"], - "correctAnswerMustInclude": ["useQuery", "queryKey", "queryFn"], - "notes": "Searching 'useQuery' returns the API reference, not the guide. Search for 'queries guide' instead." - }, - { - "id": "start-server-functions", - "question": "How do I create a server function in TanStack Start that can be called from the client?", - "difficulty": "medium", - "tags": ["start", "server-functions"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/server-functions", - "required": true, - "reason": "Primary server functions guide" - } - ], - "idealSearchQueries": ["server function", "createServerFn"], - "correctAnswerMustInclude": ["createServerFn"] - }, - { - "id": "router-search-params-validation", - "question": "How do I validate and type search params in TanStack Router?", - "difficulty": "medium", - "tags": ["router", "search-params", "validation", "typescript"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/search-params", - "required": true, - "reason": "Primary search params guide" - } - ], - "idealSearchQueries": ["search params validation", "validateSearch"], - "correctAnswerMustInclude": ["validateSearch"] - }, - { - "id": "table-column-definitions", - "question": "How do I define columns for TanStack Table?", - "difficulty": "easy", - "tags": ["table", "basics"], - "expectedDocs": [ - { - "library": "table", - "path": "guide/column-defs", - "required": true, - "reason": "Primary column definitions guide" - } - ], - "idealSearchQueries": ["column definitions", "createColumnHelper"], - "correctAnswerMustInclude": ["columnHelper", "accessorKey"] - }, - { - "id": "form-validation-zod", - "question": "How do I use Zod validation with TanStack Form?", - "difficulty": "medium", - "tags": ["form", "validation", "zod"], - "expectedDocs": [ - { - "library": "form", - "path": "framework/react/guides/validation", - "required": true, - "reason": "Validation guide covering Zod" - } - ], - "idealSearchQueries": ["form zod validation", "form validation"], - "correctAnswerMustInclude": ["zod", "validators"] - }, - { - "id": "virtual-dynamic-row-heights", - "question": "How do I handle dynamic/variable row heights in TanStack Virtual?", - "difficulty": "medium", - "tags": ["virtual", "dynamic-sizing"], - "expectedDocs": [ - { - "library": "virtual", - "path": "api/virtualizer", - "required": true, - "reason": "API docs for measureElement and estimateSize" - }, - { - "library": "virtual", - "path": "framework/react/examples/dynamic", - "required": false, - "reason": "Example showing dynamic sizing" - } - ], - "idealSearchQueries": ["dynamic size virtualizer", "measureElement"], - "correctAnswerMustInclude": ["estimateSize", "measureElement"] - }, - { - "id": "query-mutations", - "question": "How do I update data on the server with TanStack Query?", - "difficulty": "easy", - "tags": ["query", "mutations"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/mutations", - "required": true, - "reason": "Primary mutations guide" - } - ], - "idealSearchQueries": ["mutation", "useMutation"], - "correctAnswerMustInclude": ["useMutation", "mutate"] - }, - { - "id": "router-loaderDeps", - "question": "How do I make a TanStack Router loader depend on search params so it reloads when they change?", - "difficulty": "medium", - "tags": ["router", "loaders", "search-params"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/data-loading", - "required": true, - "reason": "Data loading guide covers loaderDeps" - } - ], - "idealSearchQueries": ["loaderDeps", "loader search params"], - "correctAnswerMustInclude": ["loaderDeps"] - }, - - { - "id": "query-optimistic-updates", - "question": "How do I implement optimistic updates with TanStack Query so the UI updates immediately before the server responds?", - "difficulty": "medium", - "tags": ["query", "mutations", "optimistic"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/optimistic-updates", - "required": true, - "reason": "Dedicated optimistic updates guide" - } - ], - "idealSearchQueries": ["optimistic updates", "optimistic mutation"], - "correctAnswerMustInclude": ["onMutate", "onError", "onSettled"] - }, - { - "id": "query-infinite-scroll", - "question": "How do I implement infinite scrolling with TanStack Query?", - "difficulty": "medium", - "tags": ["query", "pagination", "infinite"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/infinite-queries", - "required": true, - "reason": "Infinite queries guide" - } - ], - "idealSearchQueries": ["infinite query", "useInfiniteQuery"], - "correctAnswerMustInclude": [ - "useInfiniteQuery", - "getNextPageParam", - "fetchNextPage" - ] - }, - { - "id": "query-cache-invalidation", - "question": "How do I invalidate and refetch queries after a mutation in TanStack Query?", - "difficulty": "easy", - "tags": ["query", "cache", "invalidation"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/invalidations-from-mutations", - "required": true, - "reason": "Guide on invalidating queries from mutations" - } - ], - "idealSearchQueries": [ - "invalidate queries", - "invalidateQueries mutation" - ], - "correctAnswerMustInclude": ["invalidateQueries", "queryClient"] - }, - { - "id": "query-dependent-queries", - "question": "How do I make one query depend on the result of another query in TanStack Query?", - "difficulty": "medium", - "tags": ["query", "dependent", "serial"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/dependent-queries", - "required": true, - "reason": "Dependent queries guide" - } - ], - "idealSearchQueries": ["dependent queries", "enabled option"], - "correctAnswerMustInclude": ["enabled"] - }, - { - "id": "query-prefetching", - "question": "How do I prefetch data before a user navigates to a page with TanStack Query?", - "difficulty": "medium", - "tags": ["query", "prefetch", "performance"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/prefetching", - "required": true, - "reason": "Prefetching guide" - } - ], - "idealSearchQueries": ["prefetch query", "prefetchQuery"], - "correctAnswerMustInclude": ["prefetchQuery"] - }, - { - "id": "query-suspense", - "question": "How do I use TanStack Query with React Suspense?", - "difficulty": "medium", - "tags": ["query", "suspense", "react"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/suspense", - "required": true, - "reason": "Suspense guide" - } - ], - "idealSearchQueries": ["suspense guide", "suspense query guide"], - "badSearchQueries": ["useSuspenseQuery"], - "correctAnswerMustInclude": ["useSuspenseQuery"], - "notes": "Searching 'useSuspenseQuery' returns API reference first. Once Algolia pageRank fix is deployed, guide should rank higher." - }, - { - "id": "query-devtools", - "question": "How do I set up TanStack Query DevTools to debug my queries?", - "difficulty": "easy", - "tags": ["query", "devtools", "debugging"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/devtools", - "required": true, - "reason": "DevTools setup guide" - } - ], - "idealSearchQueries": ["query devtools", "ReactQueryDevtools"], - "correctAnswerMustInclude": ["ReactQueryDevtools"] - }, - - { - "id": "router-code-splitting", - "question": "How do I implement code splitting and lazy loading routes in TanStack Router?", - "difficulty": "medium", - "tags": ["router", "code-splitting", "lazy", "performance"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/code-splitting", - "required": true, - "reason": "Code splitting guide" - } - ], - "idealSearchQueries": ["code splitting", "lazy route"], - "correctAnswerMustInclude": ["lazyRouteComponent", "lazy"] - }, - { - "id": "router-navigation-blocking", - "question": "How do I prevent navigation when a form has unsaved changes in TanStack Router?", - "difficulty": "medium", - "tags": ["router", "navigation", "blocking", "forms"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/navigation-blocking", - "required": true, - "reason": "Navigation blocking guide" - } - ], - "idealSearchQueries": ["navigation blocking", "useBlocker"], - "correctAnswerMustInclude": ["useBlocker"] - }, - { - "id": "router-authenticated-routes", - "question": "How do I protect routes that require authentication in TanStack Router?", - "difficulty": "medium", - "tags": ["router", "auth", "protected-routes"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/authenticated-routes", - "required": true, - "reason": "Authenticated routes guide" - } - ], - "idealSearchQueries": [ - "authenticated routes", - "protected routes", - "auth redirect" - ], - "correctAnswerMustInclude": ["beforeLoad", "redirect"] - }, - { - "id": "router-not-found", - "question": "How do I handle 404 not found pages in TanStack Router?", - "difficulty": "easy", - "tags": ["router", "404", "not-found"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/not-found-errors", - "required": true, - "reason": "Not found errors guide" - } - ], - "idealSearchQueries": ["not found", "404", "notFoundComponent"], - "correctAnswerMustInclude": ["notFoundComponent"] - }, - { - "id": "router-path-params", - "question": "How do I access dynamic path parameters like /posts/$postId in TanStack Router?", - "difficulty": "easy", - "tags": ["router", "params", "dynamic-routes"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/path-params", - "required": true, - "reason": "Path params guide" - } - ], - "idealSearchQueries": ["path params", "useParams", "route params"], - "correctAnswerMustInclude": ["useParams", "$"] - }, - { - "id": "router-preloading", - "question": "How do I preload route data on hover in TanStack Router?", - "difficulty": "easy", - "tags": ["router", "preload", "performance"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/preloading", - "required": true, - "reason": "Preloading guide" - } - ], - "idealSearchQueries": ["preload route", "preloading"], - "correctAnswerMustInclude": ["preload"] - }, - { - "id": "router-devtools", - "question": "How do I set up TanStack Router DevTools?", - "difficulty": "easy", - "tags": ["router", "devtools", "debugging"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/devtools", - "required": true, - "reason": "DevTools guide" - } - ], - "idealSearchQueries": ["router devtools", "TanStackRouterDevtools"], - "correctAnswerMustInclude": ["TanStackRouterDevtools"] - }, - - { - "id": "table-sorting", - "question": "How do I implement column sorting in TanStack Table?", - "difficulty": "easy", - "tags": ["table", "sorting"], - "expectedDocs": [ - { - "library": "table", - "path": "guide/sorting", - "required": true, - "reason": "Sorting guide" - } - ], - "idealSearchQueries": ["table sorting", "getSortedRowModel"], - "correctAnswerMustInclude": ["getSortedRowModel", "sorting"] - }, - { - "id": "table-filtering", - "question": "How do I add filtering to TanStack Table?", - "difficulty": "medium", - "tags": ["table", "filtering"], - "expectedDocs": [ - { - "library": "table", - "path": "guide/column-filtering", - "required": true, - "reason": "Column filtering guide" - } - ], - "idealSearchQueries": ["table filtering", "getFilteredRowModel"], - "correctAnswerMustInclude": ["getFilteredRowModel", "columnFilters"] - }, - { - "id": "table-pagination", - "question": "How do I implement pagination in TanStack Table?", - "difficulty": "easy", - "tags": ["table", "pagination"], - "expectedDocs": [ - { - "library": "table", - "path": "guide/pagination", - "required": true, - "reason": "Pagination guide" - } - ], - "idealSearchQueries": ["table pagination", "getPaginationRowModel"], - "correctAnswerMustInclude": [ - "getPaginationRowModel", - "pageIndex", - "pageSize" - ] - }, - { - "id": "table-row-selection", - "question": "How do I enable row selection with checkboxes in TanStack Table?", - "difficulty": "medium", - "tags": ["table", "selection"], - "expectedDocs": [ - { - "library": "table", - "path": "guide/row-selection", - "required": true, - "reason": "Row selection guide" - } - ], - "idealSearchQueries": ["row selection", "table checkbox"], - "correctAnswerMustInclude": ["rowSelection", "enableRowSelection"] - }, - { - "id": "table-virtualization", - "question": "How do I virtualize a large table with TanStack Table and TanStack Virtual?", - "difficulty": "hard", - "tags": ["table", "virtual", "performance"], - "expectedDocs": [ - { - "library": "table", - "path": "framework/react/examples/virtualized-rows", - "required": true, - "reason": "Virtualized rows example" - } - ], - "idealSearchQueries": ["table virtualization", "virtualized rows"], - "correctAnswerMustInclude": ["useVirtualizer", "virtualRows"] - }, - { - "id": "table-server-side", - "question": "How do I implement server-side pagination and sorting with TanStack Table?", - "difficulty": "hard", - "tags": ["table", "server-side", "pagination"], - "expectedDocs": [ - { - "library": "table", - "path": "guide/pagination", - "required": true, - "reason": "Pagination guide covers manual mode" - } - ], - "idealSearchQueries": ["server side table", "manual pagination"], - "correctAnswerMustInclude": ["manualPagination", "pageCount"] - }, - - { - "id": "form-field-arrays", - "question": "How do I handle dynamic arrays of fields in TanStack Form?", - "difficulty": "medium", - "tags": ["form", "arrays", "dynamic"], - "expectedDocs": [ - { - "library": "form", - "path": "framework/react/guides/arrays", - "required": true, - "reason": "Array fields guide" - } - ], - "idealSearchQueries": ["form arrays", "field array"], - "correctAnswerMustInclude": ["pushValue", "removeValue"] - }, - { - "id": "form-async-validation", - "question": "How do I implement async validation that checks the server in TanStack Form?", - "difficulty": "medium", - "tags": ["form", "validation", "async"], - "expectedDocs": [ - { - "library": "form", - "path": "framework/react/guides/dynamic-validation", - "required": true, - "reason": "Dynamic validation guide covers async validation" - } - ], - "idealSearchQueries": ["async validation", "async form validation"], - "correctAnswerMustInclude": ["async"] - }, - { - "id": "form-submission", - "question": "How do I handle form submission with TanStack Form?", - "difficulty": "easy", - "tags": ["form", "submit"], - "expectedDocs": [ - { - "library": "form", - "path": "framework/react/guides/basic-concepts", - "required": true, - "reason": "Basic concepts covers submission" - } - ], - "idealSearchQueries": ["form submit", "handleSubmit form"], - "correctAnswerMustInclude": ["onSubmit", "handleSubmit"] - }, - - { - "id": "virtual-horizontal-scroll", - "question": "How do I create a horizontal virtualized list with TanStack Virtual?", - "difficulty": "easy", - "tags": ["virtual", "horizontal"], - "expectedDocs": [ - { - "library": "virtual", - "path": "api/virtualizer", - "required": true, - "reason": "API docs cover horizontal option" - } - ], - "idealSearchQueries": ["horizontal virtual", "horizontal virtualizer"], - "correctAnswerMustInclude": ["horizontal"] - }, - { - "id": "virtual-window-scroll", - "question": "How do I virtualize a list that uses window scrolling instead of a container?", - "difficulty": "medium", - "tags": ["virtual", "window-scroll"], - "expectedDocs": [ - { - "library": "virtual", - "path": "framework/react/react-virtual", - "required": true, - "reason": "React Virtual docs cover useWindowVirtualizer" - } - ], - "idealSearchQueries": ["useWindowVirtualizer", "window virtualizer"], - "correctAnswerMustInclude": ["useWindowVirtualizer"] - }, - { - "id": "virtual-grid", - "question": "How do I create a virtualized grid with TanStack Virtual?", - "difficulty": "medium", - "tags": ["virtual", "grid"], - "expectedDocs": [ - { - "library": "virtual", - "path": "api/virtualizer", - "required": true, - "reason": "Virtualizer API covers grid setup" - } - ], - "idealSearchQueries": ["virtualizer grid", "virtual rows columns"], - "correctAnswerMustInclude": ["virtualizer"] - }, - - { - "id": "start-deployment-vercel", - "question": "How do I deploy a TanStack Start app to Vercel?", - "difficulty": "easy", - "tags": ["start", "deployment", "vercel"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/hosting", - "required": true, - "reason": "Hosting guide contains Vercel section" - } - ], - "idealSearchQueries": ["hosting vercel", "deploy start"], - "correctAnswerMustInclude": ["vercel"] - }, - { - "id": "start-deployment-netlify", - "question": "How do I deploy a TanStack Start app to Netlify?", - "difficulty": "easy", - "tags": ["start", "deployment", "netlify"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/hosting", - "required": true, - "reason": "Hosting guide contains Netlify section" - } - ], - "idealSearchQueries": ["hosting netlify", "deploy start"], - "correctAnswerMustInclude": ["netlify"] - }, - { - "id": "start-middleware", - "question": "How do I add middleware to TanStack Start for things like authentication?", - "difficulty": "medium", - "tags": ["start", "middleware", "auth"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/middleware", - "required": true, - "reason": "Middleware guide" - } - ], - "idealSearchQueries": ["start middleware", "createMiddleware"], - "correctAnswerMustInclude": ["middleware"] - }, - { - "id": "start-api-routes", - "question": "How do I create API routes in TanStack Start?", - "difficulty": "medium", - "tags": ["start", "api", "routes"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/server-routes", - "required": true, - "reason": "Server routes guide covers API routes" - } - ], - "idealSearchQueries": ["server routes", "api routes"], - "correctAnswerMustInclude": ["server", "routes"] - }, - { - "id": "start-static-prerendering", - "question": "How do I statically prerender pages at build time in TanStack Start?", - "difficulty": "medium", - "tags": ["start", "static", "prerender", "ssg"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/static-prerendering", - "required": true, - "reason": "Static prerendering guide" - } - ], - "idealSearchQueries": ["static prerender", "prerender start"], - "correctAnswerMustInclude": ["prerender"] - }, - - { - "id": "store-basics", - "question": "How do I create and use a store with TanStack Store?", - "difficulty": "easy", - "tags": ["store", "basics"], - "expectedDocs": [ - { - "library": "store", - "path": "framework/react/quick-start", - "required": true, - "reason": "Quick start guide" - } - ], - "idealSearchQueries": ["store quick start", "useStore"], - "correctAnswerMustInclude": ["Store", "useStore"] - }, - { - "id": "store-derived", - "question": "How do I create derived state from a TanStack Store?", - "difficulty": "medium", - "tags": ["store", "derived", "computed"], - "expectedDocs": [ - { - "library": "store", - "path": "reference/classes/Derived", - "required": true, - "reason": "Derived class reference" - } - ], - "idealSearchQueries": ["derived", "Derived store"], - "correctAnswerMustInclude": ["Derived"] - }, - - { - "id": "query-vue-basics", - "question": "How do I use TanStack Query with Vue?", - "difficulty": "easy", - "tags": ["query", "vue", "basics"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/vue/overview", - "required": true, - "reason": "Vue overview" - } - ], - "idealSearchQueries": ["vue query", "tanstack query vue"], - "correctAnswerMustInclude": ["useQuery", "VueQueryPlugin"] - }, - { - "id": "query-solid-basics", - "question": "How do I use TanStack Query with SolidJS?", - "difficulty": "easy", - "tags": ["query", "solid", "basics"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/solid/overview", - "required": true, - "reason": "Solid overview" - } - ], - "idealSearchQueries": ["solid query", "tanstack query solid"], - "correctAnswerMustInclude": ["createQuery", "QueryClientProvider"] - }, - { - "id": "router-vue-basics", - "question": "How do I use TanStack Router with Vue?", - "difficulty": "easy", - "tags": ["router", "vue", "basics"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/overview", - "required": true, - "reason": "Router overview (Vue support is experimental)" - } - ], - "idealSearchQueries": ["router overview", "tanstack router"], - "correctAnswerMustInclude": ["router"], - "notes": "Vue support for Router is experimental and docs may be limited" - }, - - { - "id": "query-testing", - "question": "How do I test components that use TanStack Query?", - "difficulty": "medium", - "tags": ["query", "testing"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/testing", - "required": true, - "reason": "Testing guide" - } - ], - "idealSearchQueries": ["testing query", "test useQuery"], - "correctAnswerMustInclude": ["QueryClientProvider", "test"] - }, - { - "id": "query-typescript", - "question": "How do I properly type TanStack Query hooks with TypeScript?", - "difficulty": "medium", - "tags": ["query", "typescript"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/typescript", - "required": true, - "reason": "TypeScript guide" - } - ], - "idealSearchQueries": ["query typescript", "type useQuery"], - "correctAnswerMustInclude": ["TypeScript", "TData", "TError"] - }, - - { - "id": "router-scroll-restoration", - "question": "How do I handle scroll position restoration in TanStack Router?", - "difficulty": "medium", - "tags": ["router", "scroll"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/scroll-restoration", - "required": true, - "reason": "Scroll restoration guide" - } - ], - "idealSearchQueries": ["scroll restoration", "ScrollRestoration"], - "correctAnswerMustInclude": ["ScrollRestoration", "scroll"] - }, - { - "id": "router-error-handling", - "question": "How do I handle route errors and show error boundaries in TanStack Router?", - "difficulty": "medium", - "tags": ["router", "errors"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/data-loading", - "required": true, - "reason": "Data loading covers error handling" - } - ], - "idealSearchQueries": ["router error boundary", "errorComponent"], - "correctAnswerMustInclude": ["errorComponent", "onError"] - }, - { - "id": "router-pending-ui", - "question": "How do I show loading states during navigation in TanStack Router?", - "difficulty": "easy", - "tags": ["router", "loading", "pending"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/data-loading", - "required": true, - "reason": "Data loading covers pending states" - } - ], - "idealSearchQueries": ["pending component", "loading navigation"], - "correctAnswerMustInclude": ["pendingComponent"] - }, - - { - "id": "start-forms", - "question": "How do I handle form submissions with server actions in TanStack Start?", - "difficulty": "medium", - "tags": ["start", "forms", "server-actions"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/server-functions", - "required": true, - "reason": "Server functions cover form handling" - } - ], - "idealSearchQueries": ["server functions", "createServerFn"], - "correctAnswerMustInclude": ["createServerFn"] - }, - { - "id": "start-head-meta", - "question": "How do I set page titles and meta tags in TanStack Start?", - "difficulty": "easy", - "tags": ["start", "meta", "seo"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/document-head-management", - "required": true, - "reason": "Document head management guide" - } - ], - "idealSearchQueries": ["document head", "meta title"], - "correctAnswerMustInclude": ["head"] - }, - - { - "id": "query-enabled-button-click", - "question": "How do I trigger a query only when a button is clicked instead of on component mount?", - "difficulty": "medium", - "tags": ["query", "enabled", "manual-fetch"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/disabling-queries", - "required": true, - "reason": "Disabling queries guide covers enabled option and refetch" - } - ], - "idealSearchQueries": ["disable query", "enabled false", "manual query"], - "correctAnswerMustInclude": ["enabled", "refetch"], - "notes": "Mined from Stack Overflow - score 227. Very common question about lazy/on-demand queries." - }, - { - "id": "query-stale-cache-time", - "question": "What is the difference between staleTime and gcTime (cacheTime) in TanStack Query?", - "difficulty": "medium", - "tags": ["query", "cache", "stale"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/caching", - "required": true, - "reason": "Caching guide explains staleTime and gcTime" - } - ], - "idealSearchQueries": ["staleTime gcTime", "caching query", "cache time"], - "correctAnswerMustInclude": ["staleTime", "gcTime"], - "notes": "Mined from Stack Overflow - score 71. Common confusion about cache timing options." - }, - { - "id": "query-conditional-enabled", - "question": "How do I conditionally run a query based on some state or prop in TanStack Query?", - "difficulty": "easy", - "tags": ["query", "enabled", "conditional"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/dependent-queries", - "required": true, - "reason": "Dependent queries guide covers enabled option" - } - ], - "idealSearchQueries": [ - "dependent queries", - "enabled query", - "conditional query" - ], - "correctAnswerMustInclude": ["enabled"], - "notes": "Mined from Stack Overflow - score 68. Very common pattern for conditional fetching." - }, - { - "id": "query-callbacks-deprecated", - "question": "The onSuccess, onError, and onSettled callbacks are deprecated in TanStack Query. What should I use instead?", - "difficulty": "medium", - "tags": ["query", "callbacks", "migration"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/migrating-to-v5", - "required": true, - "reason": "Migration guide covers callback changes" - } - ], - "idealSearchQueries": [ - "migrate v5", - "onSuccess deprecated", - "query callbacks" - ], - "correctAnswerMustInclude": ["useEffect"], - "notes": "Mined from Stack Overflow - score 54. Important migration question for v5 users." - }, - { - "id": "query-provider-error", - "question": "I'm getting 'No QueryClient set, use QueryClientProvider to set one'. How do I fix this?", - "difficulty": "easy", - "tags": ["query", "setup", "error"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/quick-start", - "required": true, - "reason": "Quick start shows proper QueryClientProvider setup" - } - ], - "idealSearchQueries": ["QueryClientProvider", "quick start query"], - "correctAnswerMustInclude": ["QueryClientProvider", "QueryClient"], - "notes": "Mined from Stack Overflow - score 113. Common setup error for new users." - }, - { - "id": "table-default-sorting", - "question": "How do I set a default/initial sort order when the table first loads in TanStack Table?", - "difficulty": "easy", - "tags": ["table", "sorting", "initial-state"], - "expectedDocs": [ - { - "library": "table", - "path": "guide/sorting", - "required": true, - "reason": "Sorting guide covers initial sorting state" - } - ], - "idealSearchQueries": ["initial sorting", "default sort table"], - "correctAnswerMustInclude": ["sorting", "initialState"], - "notes": "Mined from Stack Overflow - score 50. Common question about initial table state." - }, - { - "id": "table-column-visibility", - "question": "How do I hide or show columns dynamically in TanStack Table?", - "difficulty": "easy", - "tags": ["table", "columns", "visibility"], - "expectedDocs": [ - { - "library": "table", - "path": "guide/column-visibility", - "required": true, - "reason": "Column visibility guide" - } - ], - "idealSearchQueries": ["column visibility", "hide column table"], - "correctAnswerMustInclude": ["columnVisibility"], - "notes": "Mined from GitHub - score 54. Users often confused about show/hide columns." - }, - { - "id": "table-row-click-handler", - "question": "How do I handle row click events and select a row when clicked in TanStack Table?", - "difficulty": "medium", - "tags": ["table", "selection", "events"], - "expectedDocs": [ - { - "library": "table", - "path": "guide/row-selection", - "required": true, - "reason": "Row selection guide" - } - ], - "idealSearchQueries": ["row selection", "row click table"], - "correctAnswerMustInclude": ["rowSelection", "onClick"], - "notes": "Mined from Stack Overflow - score 65. Common interaction pattern." - }, - { - "id": "virtual-scroll-to-index", - "question": "How do I programmatically scroll to a specific item/index in TanStack Virtual?", - "difficulty": "medium", - "tags": ["virtual", "scroll", "navigation"], - "expectedDocs": [ - { - "library": "virtual", - "path": "api/virtualizer", - "required": true, - "reason": "Virtualizer API covers scrollToIndex" - } - ], - "idealSearchQueries": ["scrollToIndex", "scroll to item virtual"], - "correctAnswerMustInclude": ["scrollToIndex"], - "notes": "Common need for navigating virtualized lists programmatically." - }, - { - "id": "query-parallel-queries", - "question": "How do I run multiple queries in parallel with TanStack Query?", - "difficulty": "medium", - "tags": ["query", "parallel", "multiple"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/parallel-queries", - "required": true, - "reason": "Parallel queries guide" - } - ], - "idealSearchQueries": ["parallel queries", "useQueries"], - "correctAnswerMustInclude": ["useQueries"], - "notes": "Common pattern for fetching multiple resources at once." - }, - { - "id": "query-placeholder-data", - "question": "How do I show placeholder or initial data while my query is loading in TanStack Query?", - "difficulty": "medium", - "tags": ["query", "placeholder", "loading"], - "expectedDocs": [ - { - "library": "query", - "path": "framework/react/guides/placeholder-query-data", - "required": true, - "reason": "Placeholder query data guide" - } - ], - "idealSearchQueries": ["placeholder data", "initialData query"], - "correctAnswerMustInclude": ["placeholderData"], - "notes": "Important UX pattern for perceived performance." - }, - { - "id": "router-link-active-state", - "question": "How do I style the active link differently when on that route in TanStack Router?", - "difficulty": "easy", - "tags": ["router", "link", "active", "styling"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/api/router/ActiveLinkOptionsType", - "required": true, - "reason": "ActiveLinkOptionsType API covers activeProps and inactiveProps" - } - ], - "idealSearchQueries": ["Link active", "activeProps router"], - "correctAnswerMustInclude": ["activeProps", "Link"], - "notes": "Common navigation styling pattern." - }, - - { - "id": "router-layout-routes", - "question": "How do I create layout routes that wrap child routes with shared UI in TanStack Router?", - "difficulty": "medium", - "tags": ["router", "layout", "nested"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/routing/routing-concepts", - "required": true, - "reason": "Routing concepts covers layout routes and Outlet" - } - ], - "idealSearchQueries": ["layout routes", "Outlet router"], - "correctAnswerMustInclude": ["Outlet", "layout"] - }, - { - "id": "router-pathless-layout", - "question": "How do I create a pathless layout route that groups routes without adding to the URL path?", - "difficulty": "medium", - "tags": ["router", "layout", "pathless"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/routing/routing-concepts", - "required": true, - "reason": "Routing concepts covers pathless layout routes with underscore prefix" - } - ], - "idealSearchQueries": ["pathless layout", "route grouping"], - "correctAnswerMustInclude": ["_"] - }, - { - "id": "router-route-context", - "question": "How do I pass data through route context to child routes in TanStack Router?", - "difficulty": "medium", - "tags": ["router", "context", "data"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/api/router/createRootRouteWithContextFunction", - "required": true, - "reason": "createRootRouteWithContext for typed context" - } - ], - "idealSearchQueries": ["route context", "createRootRouteWithContext"], - "correctAnswerMustInclude": ["context"] - }, - { - "id": "router-use-navigate", - "question": "How do I programmatically navigate to a route in TanStack Router?", - "difficulty": "easy", - "tags": ["router", "navigation", "programmatic"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/api/router/useNavigateHook", - "required": true, - "reason": "useNavigate hook API" - } - ], - "idealSearchQueries": ["useNavigate", "programmatic navigation"], - "correctAnswerMustInclude": ["useNavigate"] - }, - { - "id": "router-catch-all-splat", - "question": "How do I create a catch-all or splat route that matches any path in TanStack Router?", - "difficulty": "easy", - "tags": ["router", "splat", "catch-all"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/routing/routing-concepts", - "required": true, - "reason": "Routing concepts covers splat routes" - } - ], - "idealSearchQueries": ["splat route", "catch all route"], - "correctAnswerMustInclude": ["$"] - }, - { - "id": "router-file-based-routing", - "question": "How does file-based routing work in TanStack Router?", - "difficulty": "easy", - "tags": ["router", "file-based", "basics"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/routing/file-based-routing", - "required": true, - "reason": "File-based routing guide" - } - ], - "idealSearchQueries": ["file based routing", "route file"], - "correctAnswerMustInclude": ["routes", "file"] - }, - { - "id": "router-navigate-search-params", - "question": "How do I navigate while preserving or modifying search params in TanStack Router?", - "difficulty": "medium", - "tags": ["router", "navigation", "search-params"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/search-params", - "required": true, - "reason": "Search params guide covers navigation with search" - } - ], - "idealSearchQueries": ["search params navigation", "navigate search"], - "correctAnswerMustInclude": ["search"] - }, - { - "id": "router-redirect-beforeload", - "question": "How do I redirect users in TanStack Router before the route loads?", - "difficulty": "medium", - "tags": ["router", "redirect", "beforeLoad"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/authenticated-routes", - "required": true, - "reason": "Authenticated routes guide shows redirect pattern" - } - ], - "idealSearchQueries": ["redirect beforeLoad", "redirect router"], - "correctAnswerMustInclude": ["redirect", "beforeLoad"] - }, - { - "id": "router-route-masking", - "question": "How do I show a different URL in the browser than the actual route in TanStack Router?", - "difficulty": "hard", - "tags": ["router", "masking", "url"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/route-masking", - "required": true, - "reason": "Route masking guide" - } - ], - "idealSearchQueries": ["route masking", "mask url"], - "correctAnswerMustInclude": ["mask"] - }, - { - "id": "router-ssr-streaming", - "question": "How does SSR and streaming work in TanStack Router?", - "difficulty": "hard", - "tags": ["router", "ssr", "streaming"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/ssr", - "required": true, - "reason": "SSR guide covers streaming" - } - ], - "idealSearchQueries": ["ssr router guide", "server side rendering"], - "correctAnswerMustInclude": ["SSR", "streaming"] - }, - { - "id": "router-external-data-loading", - "question": "How do I use TanStack Router with an external data fetching library like TanStack Query?", - "difficulty": "medium", - "tags": ["router", "query", "integration", "data"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/external-data-loading", - "required": true, - "reason": "External data loading guide" - } - ], - "idealSearchQueries": [ - "external data loading", - "router query integration" - ], - "correctAnswerMustInclude": ["external", "data"] - }, - - { - "id": "start-execution-model", - "question": "How do I understand where my code runs in TanStack Start (server vs client)?", - "difficulty": "medium", - "tags": ["start", "ssr", "execution"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/execution-model", - "required": true, - "reason": "Execution model guide explains server/client code" - } - ], - "idealSearchQueries": ["execution model", "server client start"], - "correctAnswerMustInclude": ["server", "client"] - }, - { - "id": "start-server-only-code", - "question": "How do I ensure code only runs on the server in TanStack Start?", - "difficulty": "medium", - "tags": ["start", "server", "code-patterns"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/code-execution-patterns", - "required": true, - "reason": "Code execution patterns guide" - } - ], - "idealSearchQueries": ["server only code", "code execution patterns"], - "correctAnswerMustInclude": ["server"] - }, - { - "id": "start-authentication", - "question": "How do I implement authentication in TanStack Start?", - "difficulty": "hard", - "tags": ["start", "auth", "security"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/authentication", - "required": true, - "reason": "Authentication guide" - } - ], - "idealSearchQueries": ["authentication start", "auth tanstack start"], - "correctAnswerMustInclude": ["auth"] - }, - { - "id": "start-auth-overview", - "question": "What authentication options are available for TanStack Start?", - "difficulty": "easy", - "tags": ["start", "auth", "overview"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/authentication-overview", - "required": true, - "reason": "Authentication overview with partner solutions" - } - ], - "idealSearchQueries": ["authentication overview", "clerk workos start"], - "correctAnswerMustInclude": ["Clerk", "WorkOS"] - }, - { - "id": "start-streaming-server-functions", - "question": "How do I stream data from a server function in TanStack Start?", - "difficulty": "hard", - "tags": ["start", "streaming", "server-functions"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/server-functions", - "required": true, - "reason": "Server functions guide covers streaming" - } - ], - "idealSearchQueries": ["streaming server function", "stream data start"], - "correctAnswerMustInclude": ["stream"] - }, - { - "id": "start-tailwind", - "question": "How do I set up Tailwind CSS with TanStack Start?", - "difficulty": "easy", - "tags": ["start", "tailwind", "css"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/tailwind-integration", - "required": true, - "reason": "Tailwind CSS integration guide" - } - ], - "idealSearchQueries": ["tailwind start", "css tailwind tanstack"], - "correctAnswerMustInclude": ["tailwind"] - }, - { - "id": "start-database", - "question": "How do I connect to a database in TanStack Start?", - "difficulty": "medium", - "tags": ["start", "database", "data"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/databases", - "required": true, - "reason": "Databases guide" - } - ], - "idealSearchQueries": ["database start", "drizzle prisma start"], - "correctAnswerMustInclude": ["database"] - }, - { - "id": "start-migrate-nextjs", - "question": "How do I migrate from Next.js to TanStack Start?", - "difficulty": "hard", - "tags": ["start", "migration", "nextjs"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/migrate-from-next-js", - "required": true, - "reason": "Next.js migration guide" - } - ], - "idealSearchQueries": ["migrate nextjs", "next.js to start"], - "correctAnswerMustInclude": ["migrate", "Next"] - }, - { - "id": "start-build-from-scratch", - "question": "How do I build a TanStack Start project from scratch without using a template?", - "difficulty": "medium", - "tags": ["start", "setup", "manual"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/build-from-scratch", - "required": true, - "reason": "Build from scratch guide" - } - ], - "idealSearchQueries": ["build from scratch", "manual setup start"], - "correctAnswerMustInclude": ["scratch"] - }, - { - "id": "start-rendering-markdown", - "question": "How do I render markdown content in TanStack Start?", - "difficulty": "medium", - "tags": ["start", "markdown", "content"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/rendering-markdown", - "required": true, - "reason": "Rendering markdown guide" - } - ], - "idealSearchQueries": ["render markdown", "markdown start"], - "correctAnswerMustInclude": ["markdown"] - }, - { - "id": "start-cloudflare-workers", - "question": "How do I deploy TanStack Start to Cloudflare Workers?", - "difficulty": "medium", - "tags": ["start", "deployment", "cloudflare"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/hosting", - "required": true, - "reason": "Hosting guide covers Cloudflare Workers" - } - ], - "idealSearchQueries": ["cloudflare workers", "deploy cloudflare start"], - "correctAnswerMustInclude": ["cloudflare"] - }, - { - "id": "start-vs-nextjs", - "question": "How does TanStack Start compare to Next.js?", - "difficulty": "easy", - "tags": ["start", "comparison", "nextjs"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/comparison", - "required": true, - "reason": "Comparison guide" - } - ], - "idealSearchQueries": ["start vs nextjs", "comparison next"], - "correctAnswerMustInclude": ["Next"] - }, - { - "id": "router-providers-location", - "question": "Where do I put providers like QueryClientProvider in TanStack Start?", - "difficulty": "medium", - "tags": ["start", "router", "providers", "setup"], - "expectedDocs": [ - { - "library": "router", - "path": "integrations/query", - "required": true, - "reason": "Query integration guide shows provider setup" - } - ], - "idealSearchQueries": [ - "query integration setup", - "QueryClientProvider router" - ], - "correctAnswerMustInclude": ["provider"], - "notes": "Mined from Stack Overflow. Common setup question." - }, - { - "id": "router-routetree-gen-error", - "question": "I'm getting 'Cannot find module routeTree.gen' error. How do I fix it?", - "difficulty": "easy", - "tags": ["router", "setup", "error"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/faq", - "required": true, - "reason": "FAQ covers routeTree.gen questions" - } - ], - "idealSearchQueries": ["routeTree.gen", "route tree faq"], - "correctAnswerMustInclude": ["routeTree"], - "notes": "Mined from Stack Overflow. Common setup error." - }, - { - "id": "router-exclude-layout", - "question": "How do I exclude specific routes from a parent layout in TanStack Router?", - "difficulty": "medium", - "tags": ["router", "layout", "nested"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/routing/routing-concepts", - "required": true, - "reason": "Routing concepts covers non-nested routes" - } - ], - "idealSearchQueries": ["non-nested routes", "exclude layout"], - "correctAnswerMustInclude": ["_"], - "notes": "Mined from Stack Overflow. Users want to break out of layouts." - }, - { - "id": "router-dynamic-nested-routes", - "question": "How do I create dynamic nested routes like /users/$userId/posts/$postId in TanStack Router?", - "difficulty": "medium", - "tags": ["router", "dynamic", "nested", "params"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/routing/routing-concepts", - "required": true, - "reason": "Routing concepts covers dynamic route segments" - } - ], - "idealSearchQueries": ["dynamic route segments", "routing concepts"], - "correctAnswerMustInclude": ["$"] - }, - { - "id": "router-search-params-no-rerender", - "question": "How do I update search params without causing a full page re-render in TanStack Router?", - "difficulty": "medium", - "tags": ["router", "search-params", "performance"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/search-params", - "required": true, - "reason": "Search params guide covers shallow navigation" - } - ], - "idealSearchQueries": [ - "search params shallow", - "update search no rerender" - ], - "correctAnswerMustInclude": ["search"], - "notes": "Mined from Stack Overflow. Common performance concern." - }, - { - "id": "router-link-typesafe-wrapper", - "question": "How do I wrap TanStack Router's Link component while keeping type safety?", - "difficulty": "hard", - "tags": ["router", "typescript", "link"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/custom-link", - "required": true, - "reason": "Custom link guide covers createLink" - } - ], - "idealSearchQueries": ["custom link", "createLink wrapper"], - "correctAnswerMustInclude": ["createLink"], - "notes": "Mined from Stack Overflow. TypeScript users want custom Link components." - }, - { - "id": "router-error-boundary-disable", - "question": "How do I disable or customize the error boundary in TanStack Router?", - "difficulty": "medium", - "tags": ["router", "errors", "error-boundary"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/guide/data-loading", - "required": true, - "reason": "Data loading covers error handling" - } - ], - "idealSearchQueries": ["error boundary router", "errorComponent"], - "correctAnswerMustInclude": ["errorComponent"], - "notes": "Mined from Stack Overflow. Users want custom error handling." - }, - { - "id": "router-generate-href", - "question": "How do I generate a URL/href string for a route without navigating in TanStack Router?", - "difficulty": "medium", - "tags": ["router", "url", "href"], - "expectedDocs": [ - { - "library": "router", - "path": "framework/react/api/router/RouterType", - "required": true, - "reason": "Router API covers buildLocation" - } - ], - "idealSearchQueries": ["buildLocation", "generate href router"], - "correctAnswerMustInclude": ["buildLocation"], - "notes": "Mined from Stack Overflow. Need URLs for sharing/copying." - }, - { - "id": "start-server-fn-validation", - "question": "How do I validate input data in a TanStack Start server function?", - "difficulty": "medium", - "tags": ["start", "server-functions", "validation"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/server-functions", - "required": true, - "reason": "Server functions guide covers validation" - } - ], - "idealSearchQueries": [ - "server function validation", - "validate server fn" - ], - "correctAnswerMustInclude": ["validator", "zod"] - }, - { - "id": "start-env-variables", - "question": "How do I use environment variables in TanStack Start?", - "difficulty": "easy", - "tags": ["start", "env", "config"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/environment-variables", - "required": true, - "reason": "Environment variables guide" - } - ], - "idealSearchQueries": ["environment variables", "env tanstack start"], - "correctAnswerMustInclude": ["env", "VITE"] - }, - - { - "id": "partner-database-recommendation", - "question": "What database should I use with TanStack Start?", - "difficulty": "easy", - "tags": ["start", "database", "partner", "ecosystem"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/databases", - "required": true, - "reason": "Databases guide with partner recommendations" - } - ], - "idealSearchQueries": ["database start", "databases guide"], - "correctAnswerMustInclude": ["Neon", "Convex"], - "notes": "Partner discovery test. Should surface recommended database partners." - }, - { - "id": "partner-auth-recommendation", - "question": "What authentication solution should I use with TanStack Start?", - "difficulty": "easy", - "tags": ["start", "auth", "partner", "ecosystem"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/authentication-overview", - "required": true, - "reason": "Authentication overview with partner solutions" - } - ], - "idealSearchQueries": ["authentication overview", "auth start"], - "correctAnswerMustInclude": ["Clerk", "WorkOS"], - "notes": "Partner discovery test. Should surface recommended auth partners." - }, - { - "id": "partner-hosting-recommendation", - "question": "Where should I deploy my TanStack Start application?", - "difficulty": "easy", - "tags": ["start", "hosting", "deployment", "partner", "ecosystem"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/hosting", - "required": true, - "reason": "Hosting guide with deployment partners" - } - ], - "idealSearchQueries": ["hosting start", "deploy start"], - "correctAnswerMustInclude": ["Cloudflare", "Netlify"], - "notes": "Partner discovery test. Should surface recommended hosting partners." - }, - { - "id": "partner-table-upgrade", - "question": "I need more advanced features than TanStack Table provides. What should I use?", - "difficulty": "medium", - "tags": ["table", "partner", "ecosystem", "data-grid"], - "expectedDocs": [ - { - "library": "table", - "path": "introduction", - "required": true, - "reason": "Table introduction mentions AG Grid partnership" - } - ], - "idealSearchQueries": ["table ag grid", "enterprise data grid"], - "correctAnswerMustInclude": ["AG Grid"], - "notes": "Partner discovery test. AG Grid is the enterprise upgrade path for Table users." - }, - { - "id": "partner-neon-setup", - "question": "How do I connect Neon database to my TanStack Start app?", - "difficulty": "medium", - "tags": ["start", "database", "neon", "partner"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/databases", - "required": true, - "reason": "Databases guide covers Neon setup" - } - ], - "idealSearchQueries": ["neon database", "neon start"], - "correctAnswerMustInclude": ["Neon", "PostgreSQL"], - "notes": "Partner-specific test. Users searching for specific partner integration." - }, - { - "id": "partner-clerk-setup", - "question": "How do I add Clerk authentication to TanStack Start?", - "difficulty": "medium", - "tags": ["start", "auth", "clerk", "partner"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/authentication-overview", - "required": true, - "reason": "Auth overview links to Clerk integration" - } - ], - "idealSearchQueries": ["clerk authentication", "clerk start"], - "correctAnswerMustInclude": ["Clerk"], - "notes": "Partner-specific test. Users searching for specific partner integration." - }, - { - "id": "partner-error-monitoring", - "question": "How do I set up error monitoring for my TanStack app?", - "difficulty": "medium", - "tags": ["start", "monitoring", "sentry", "partner", "ecosystem"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/error-handling", - "required": false, - "reason": "Error handling may mention monitoring solutions" - } - ], - "idealSearchQueries": ["error monitoring", "sentry start"], - "correctAnswerMustInclude": ["Sentry"], - "notes": "Partner discovery test. Sentry is the error monitoring partner." - }, - { - "id": "partner-cms-recommendation", - "question": "What CMS should I use with TanStack Start for content management?", - "difficulty": "medium", - "tags": ["start", "cms", "strapi", "partner", "ecosystem"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/rendering-markdown", - "required": false, - "reason": "Content/markdown guide may reference CMS options" - } - ], - "idealSearchQueries": ["cms start", "headless cms tanstack"], - "correctAnswerMustInclude": ["Strapi"], - "notes": "Partner discovery test. Strapi is the CMS partner." - }, - - { - "id": "ecosystem-postgres-alias", - "question": "I need a postgres database for my TanStack Start app", - "difficulty": "easy", - "tags": ["start", "database", "ecosystem", "mcp-tool"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/databases", - "required": true, - "reason": "Databases guide" - } - ], - "idealSearchQueries": ["databases guide"], - "correctAnswerMustInclude": ["Neon"], - "notes": "Tests category alias resolution. 'postgres' should resolve to 'database' category. MCP tool: get_ecosystem_recommendations({ category: 'postgres' })" - }, - { - "id": "ecosystem-login-alias", - "question": "How do I add login functionality to my app?", - "difficulty": "easy", - "tags": ["start", "auth", "ecosystem", "mcp-tool"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/authentication-overview", - "required": true, - "reason": "Auth overview" - } - ], - "idealSearchQueries": ["authentication overview"], - "correctAnswerMustInclude": ["Clerk", "WorkOS"], - "notes": "Tests category alias resolution. 'login' should resolve to 'auth' category. MCP tool: get_ecosystem_recommendations({ category: 'login' })" - }, - { - "id": "ecosystem-serverless-alias", - "question": "What serverless hosting options work with TanStack Start?", - "difficulty": "easy", - "tags": ["start", "deployment", "ecosystem", "mcp-tool"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/hosting", - "required": true, - "reason": "Hosting guide" - } - ], - "idealSearchQueries": ["hosting guide"], - "correctAnswerMustInclude": ["Cloudflare", "Netlify"], - "notes": "Tests category alias resolution. 'serverless' should resolve to 'deployment' category. MCP tool: get_ecosystem_recommendations({ category: 'serverless' })" - }, - { - "id": "ecosystem-error-tracking-alias", - "question": "I need error tracking for production", - "difficulty": "easy", - "tags": ["start", "monitoring", "ecosystem", "mcp-tool"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/observability", - "required": false, - "reason": "Observability guide" - } - ], - "idealSearchQueries": ["observability", "error monitoring"], - "correctAnswerMustInclude": ["Sentry"], - "notes": "Tests category alias resolution. 'error tracking' should resolve to 'monitoring' category. MCP tool: get_ecosystem_recommendations({ category: 'error-tracking' })" - }, - { - "id": "ecosystem-library-filter", - "question": "What ecosystem integrations are available for TanStack Table?", - "difficulty": "easy", - "tags": ["table", "ecosystem", "mcp-tool"], - "expectedDocs": [ - { - "library": "table", - "path": "introduction", - "required": true, - "reason": "Table introduction" - } - ], - "idealSearchQueries": ["table introduction"], - "correctAnswerMustInclude": ["AG Grid"], - "notes": "Tests library filter. MCP tool: get_ecosystem_recommendations({ library: 'table' })" - }, - { - "id": "ecosystem-sso-enterprise", - "question": "I need SSO and enterprise authentication features", - "difficulty": "medium", - "tags": ["start", "auth", "enterprise", "ecosystem", "mcp-tool"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/authentication-overview", - "required": true, - "reason": "Auth overview with WorkOS for enterprise" - } - ], - "idealSearchQueries": ["authentication overview", "workos sso"], - "correctAnswerMustInclude": ["WorkOS"], - "notes": "Tests SSO/enterprise auth. 'sso' alias should resolve to 'auth'. WorkOS specifically handles enterprise features." - }, - { - "id": "ecosystem-realtime-database", - "question": "I need a real-time database with live queries", - "difficulty": "medium", - "tags": ["start", "database", "realtime", "ecosystem", "mcp-tool"], - "expectedDocs": [ - { - "library": "start", - "path": "framework/react/guide/databases", - "required": true, - "reason": "Databases guide" - } - ], - "idealSearchQueries": ["databases guide", "convex realtime"], - "correctAnswerMustInclude": ["Convex"], - "notes": "Tests real-time database needs. Convex specifically offers real-time features." - }, - { - "id": "ecosystem-rate-limiting", - "question": "How do I add rate limiting to my API?", - "difficulty": "medium", - "tags": ["api", "rate-limiting", "ecosystem", "mcp-tool"], - "expectedDocs": [ - { - "library": "pacer", - "path": "overview", - "required": false, - "reason": "Pacer overview for rate limiting" - } - ], - "idealSearchQueries": ["rate limiting", "pacer"], - "correctAnswerMustInclude": ["Unkey"], - "notes": "Tests API/rate-limiting category. Unkey is the API management partner." - }, - { - "id": "ecosystem-offline-sync", - "question": "I need offline-first sync capabilities for my app", - "difficulty": "medium", - "tags": ["db", "database", "sync", "ecosystem", "mcp-tool"], - "expectedDocs": [ - { - "library": "db", - "path": "overview", - "required": false, - "reason": "TanStack DB overview" - } - ], - "idealSearchQueries": ["tanstack db", "sync"], - "correctAnswerMustInclude": ["Electric", "PowerSync"], - "notes": "Tests sync/offline capabilities. Electric and PowerSync are TanStack DB partners." - } - ] -} diff --git a/scripts/og-preview.ts b/scripts/og-preview.ts deleted file mode 100644 index 30e9cb1b9..000000000 --- a/scripts/og-preview.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Renders one OG image per library to `.og-preview/`. - * Run with: pnpm exec tsx scripts/og-preview.ts - */ -import { mkdirSync, writeFileSync } from 'node:fs' -import { resolve } from 'node:path' -import { libraries } from '../src/libraries/libraries' -import { generateOgImageResponse } from '../src/server/og/generate.server' - -const OUT_DIR = resolve(process.cwd(), '.og-preview') - -async function renderToFile( - outPath: string, - input: Parameters[0], -) { - const result = await generateOgImageResponse(input) - if ('kind' in result) { - console.warn(`[skip] ${input.libraryId}: ${result.kind}`) - return - } - const buf = Buffer.from(await result.arrayBuffer()) - writeFileSync(outPath, buf) - console.log(`[ok] ${outPath}`) -} - -async function main() { - mkdirSync(OUT_DIR, { recursive: true }) - - for (const lib of libraries) { - if (!lib.to) continue // skip entries without a landing page (react-charts, create-tsrouter-app) - - await renderToFile(resolve(OUT_DIR, `${lib.id}.png`), { libraryId: lib.id }) - await renderToFile(resolve(OUT_DIR, `${lib.id}-docs.png`), { - libraryId: lib.id, - title: 'Overview', - description: `${lib.tagline} Guides, API reference and examples in one place.`, - }) - } -} - -main().catch((err) => { - console.error(err) - process.exit(1) -}) diff --git a/scripts/preview-content-cache-prune.ts b/scripts/preview-content-cache-prune.ts deleted file mode 100644 index 0b4a960e8..000000000 --- a/scripts/preview-content-cache-prune.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { sql } from 'drizzle-orm' -import { db } from '../src/db/client' - -async function main() { - console.log('\n=== preview prune impact ===') - - const before = await db.execute(sql` - select - count(*)::int as total_rows, - count(*) filter (where is_present)::int as present_rows, - count(*) filter (where not is_present)::int as absent_rows, - count(*) filter (where not is_present and updated_at < now() - interval '1 day')::int as absent_older_than_1d, - count(*) filter (where updated_at < now() - interval '30 days')::int as any_older_than_30d - from github_content_cache - `) - console.table(before) - - console.log('\nbreakdown of absent rows by repo (top 20):') - const byRepo = await db.execute(sql` - select repo, count(*)::int as absent_rows - from github_content_cache - where not is_present and updated_at < now() - interval '1 day' - group by repo - order by count(*) desc - limit 20 - `) - console.table(byRepo) -} - -main() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err) - process.exit(1) - }) diff --git a/scripts/prune-content-cache-negatives.ts b/scripts/prune-content-cache-negatives.ts deleted file mode 100644 index 8bf3b4e98..000000000 --- a/scripts/prune-content-cache-negatives.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { eq, sql } from 'drizzle-orm' -import { db } from '../src/db/client' -import { githubContentCache } from '../src/db/schema' - -/** - * One-shot cleanup: delete every negative cache row (is_present = false). - * - * The daily prune fn keeps negatives for 1 day going forward, but the - * existing rows had their updated_at bumped by mark*Stale so they all - * look fresh. They're safe to drop wholesale — anything still being - * requested will repopulate within 15 minutes (the negative TTL), and - * most are bogus paths that won't be requested again. - */ -async function main() { - const startTime = Date.now() - - const [{ before }] = await db - .select({ before: sql`count(*)::int` }) - .from(githubContentCache) - console.log(`[prune-negatives] rows before: ${before}`) - - const deleted = await db - .delete(githubContentCache) - .where(eq(githubContentCache.isPresent, false)) - .returning({ id: githubContentCache.id }) - - const [{ after }] = await db - .select({ after: sql`count(*)::int` }) - .from(githubContentCache) - - const duration = Date.now() - startTime - console.log( - `[prune-negatives] ✓ Deleted ${deleted.length} negative rows in ${duration}ms (rows after: ${after})`, - ) -} - -main() - .then(() => process.exit(0)) - .catch((err) => { - console.error('[prune-negatives] ✗ Failed:', err) - process.exit(1) - }) diff --git a/scripts/prune-content-cache.ts b/scripts/prune-content-cache.ts deleted file mode 100644 index bdac0d3ba..000000000 --- a/scripts/prune-content-cache.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { pruneStaleCacheRows } from '../src/utils/github-content-cache.server' - -async function main() { - const startTime = Date.now() - console.log('[prune-content-cache] Starting prune...') - - const result = await pruneStaleCacheRows() - const duration = Date.now() - startTime - - console.log( - `[prune-content-cache] ✓ Completed in ${duration}ms - deleted ${result.githubContentDeleted} content entries, ${result.docsArtifactDeleted} artifact entries (cutoff: ${result.cutoff.toISOString()})`, - ) -} - -main() - .then(() => process.exit(0)) - .catch((err) => { - console.error('[prune-content-cache] ✗ Failed:', err) - process.exit(1) - }) diff --git a/scripts/purge-cloudflare-cache.mjs b/scripts/purge-cloudflare-cache.mjs deleted file mode 100644 index 89e8a9142..000000000 --- a/scripts/purge-cloudflare-cache.mjs +++ /dev/null @@ -1,105 +0,0 @@ -const ZONE_NAME = process.env.CLOUDFLARE_ZONE_NAME || 'tanstack.com' - -const token = - process.env.CLOUDFLARE_CACHE_PURGE_TOKEN || - process.env.CLOUDFLARE_API_TOKEN || - process.env.CF_API_TOKEN - -if (!token) { - fail( - 'Missing CLOUDFLARE_CACHE_PURGE_TOKEN, CLOUDFLARE_API_TOKEN, or CF_API_TOKEN.', - ) -} - -const zoneId = - process.env.CLOUDFLARE_ZONE_ID || - process.env.CF_ZONE_ID || - (await findZoneId(token, ZONE_NAME)) - -if (!zoneId) { - fail( - `Missing CLOUDFLARE_ZONE_ID. Set it directly, or use a token with Zone:Read so ${ZONE_NAME} can be discovered.`, - ) -} - -const response = await fetch( - `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`, - { - body: JSON.stringify({ purge_everything: true }), - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - method: 'POST', - }, -) - -const payload = await response.json().catch(() => undefined) - -if (!isCloudflareResponse(payload) || !response.ok || !payload.success) { - const message = - isCloudflareResponse(payload) && payload.errors.length - ? payload.errors - .map((error) => error.message) - .filter(Boolean) - .join('; ') - : `Cloudflare purge failed with status ${response.status}` - - fail(message) -} - -console.log(`Purged Cloudflare cache for ${ZONE_NAME}.`) - -async function findZoneId(apiToken, zoneName) { - const response = await fetch( - `https://api.cloudflare.com/client/v4/zones?name=${encodeURIComponent( - zoneName, - )}&status=active`, - { - headers: { - Authorization: `Bearer ${apiToken}`, - 'Content-Type': 'application/json', - }, - }, - ) - - if (!response.ok) { - return undefined - } - - const payload = await response.json().catch(() => undefined) - if (!isZoneListResponse(payload)) { - return undefined - } - - const zone = payload.result.find((candidate) => candidate.name === zoneName) - return zone?.id -} - -function isCloudflareResponse(value) { - return ( - typeof value === 'object' && - value !== null && - typeof value.success === 'boolean' && - Array.isArray(value.errors) - ) -} - -function isZoneListResponse(value) { - return ( - isCloudflareResponse(value) && - Array.isArray(value.result) && - value.result.every( - (zone) => - typeof zone === 'object' && - zone !== null && - typeof zone.id === 'string' && - typeof zone.name === 'string', - ) - ) -} - -function fail(message) { - console.error(message) - process.exit(1) -} diff --git a/scripts/register-discord-commands.ts b/scripts/register-discord-commands.ts deleted file mode 100644 index 6ce9d0d22..000000000 --- a/scripts/register-discord-commands.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Register Discord slash commands with the Discord API. - * - * Run this script after adding new commands or modifying existing ones: - * pnpm tsx scripts/register-discord-commands.ts - * - * Required environment variables: - * DISCORD_APPLICATION_ID - Your Discord application ID - * DISCORD_BOT_TOKEN - Your Discord bot token - */ - -const DISCORD_APPLICATION_ID = process.env.DISCORD_APPLICATION_ID -const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN - -if (!DISCORD_APPLICATION_ID || !DISCORD_BOT_TOKEN) { - console.error('Missing required environment variables:') - if (!DISCORD_APPLICATION_ID) console.error(' - DISCORD_APPLICATION_ID') - if (!DISCORD_BOT_TOKEN) console.error(' - DISCORD_BOT_TOKEN') - process.exit(1) -} - -const commands = [ - { - name: 'tanstack', - description: 'Check TanStack Bot status', - type: 1, // CHAT_INPUT - }, -] - -async function registerCommands() { - const url = `https://discord.com/api/v10/applications/${DISCORD_APPLICATION_ID}/commands` - - console.log('Registering commands with Discord API...') - console.log(`Application ID: ${DISCORD_APPLICATION_ID}`) - console.log(`Commands to register: ${commands.map((c) => c.name).join(', ')}`) - - const response = await fetch(url, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bot ${DISCORD_BOT_TOKEN}`, - }, - body: JSON.stringify(commands), - }) - - if (!response.ok) { - const error = await response.text() - console.error(`Failed to register commands: ${response.status}`) - console.error(error) - process.exit(1) - } - - const registered = await response.json() - console.log('\nSuccessfully registered commands:') - for (const cmd of registered) { - console.log(` - /${cmd.name} (ID: ${cmd.id})`) - } -} - -registerCommands().catch((error) => { - console.error('Failed to register commands:', error) - process.exit(1) -}) diff --git a/scripts/sync-docs-webhooks.ts b/scripts/sync-docs-webhooks.ts deleted file mode 100644 index 833561b9d..000000000 --- a/scripts/sync-docs-webhooks.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { execFileSync } from 'node:child_process' -import { docsWebhookSources } from '../src/utils/docs-webhook-sources' - -type GitHubHook = { - active: boolean - config?: { - url?: string - } - events?: Array - id: number -} - -type DocsWebhookSource = { - refs: Array - repo: string -} - -const dryRun = process.argv.includes('--dry-run') -const siteUrl = (process.env.SITE_URL || 'https://tanstack.com').replace( - /\/+$/g, - '', -) -const webhookUrl = `${siteUrl}/api/github/webhook` -const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET - -if (!dryRun && !webhookSecret) { - throw new Error( - 'GITHUB_WEBHOOK_SECRET is required to create or update repository webhooks.', - ) -} - -if (!dryRun && webhookSecret) { - if (webhookSecret.length < 16 || /\s/.test(webhookSecret)) { - throw new Error( - 'GITHUB_WEBHOOK_SECRET must be a raw secret value, not a secret manager placeholder.', - ) - } -} - -function runGh(args: Array) { - return execFileSync('gh', args, { - cwd: process.cwd(), - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - }).trim() -} - -function listHooks(repo: string) { - const output = runGh(['api', `repos/${repo}/hooks`]) - - return JSON.parse(output) as Array -} - -function createWebhook(repo: string) { - if (dryRun) { - console.log(`[dry-run] create webhook for ${repo} -> ${webhookUrl}`) - return - } - - runGh([ - 'api', - `repos/${repo}/hooks`, - '--method', - 'POST', - '-F', - 'name=web', - '-F', - 'active=true', - '-F', - 'events[]=push', - '-F', - `config[url]=${webhookUrl}`, - '-F', - 'config[content_type]=json', - '-F', - `config[secret]=${webhookSecret}`, - '-F', - 'config[insecure_ssl]=0', - ]) - - console.log(`created webhook for ${repo}`) -} - -function updateWebhook(repo: string, hookId: number) { - if (dryRun) { - console.log( - `[dry-run] update webhook ${hookId} for ${repo} -> ${webhookUrl}`, - ) - return - } - - runGh([ - 'api', - `repos/${repo}/hooks/${hookId}`, - '--method', - 'PATCH', - '-F', - 'active=true', - '-F', - 'events[]=push', - '-F', - `config[url]=${webhookUrl}`, - '-F', - 'config[content_type]=json', - '-F', - `config[secret]=${webhookSecret}`, - '-F', - 'config[insecure_ssl]=0', - ]) - - console.log(`updated webhook ${hookId} for ${repo}`) -} - -const webhookSources = docsWebhookSources as Array - -console.log(`docs webhook target: ${webhookUrl}`) -console.log('watched repos/refs:') - -for (const source of webhookSources) { - console.log(`- ${source.repo} (${source.refs.join(', ')})`) -} - -for (const source of webhookSources) { - const hooks = listHooks(source.repo) - const existingHook = hooks.find((hook) => hook.config?.url === webhookUrl) - - if (!existingHook) { - createWebhook(source.repo) - continue - } - - updateWebhook(source.repo, existingHook.id) -} diff --git a/scripts/test-chunk-consistency.ts b/scripts/test-chunk-consistency.ts deleted file mode 100644 index d5a1fddcb..000000000 --- a/scripts/test-chunk-consistency.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Test that chunk boundaries are consistent across multiple runs - * Verifies that the same package will always generate the same chunk keys - */ - -import { getNormalizedNpmDownloadChunks } from '../src/utils/npm-download-ranges' - -console.log('\n' + '='.repeat(80)) -console.log('🧪 Testing Chunk Boundary Consistency') -console.log('='.repeat(80) + '\n') - -// Simulate @tanstack/react-query (created Oct 25, 2019) -const packageName = '@tanstack/react-query' -const createdDate = '2019-10-25' - -console.log(`Package: ${packageName}`) -console.log(`Created: ${createdDate}`) -console.log('') - -// Simulate running on different days -const testDates = ['2025-12-06', '2025-12-07', '2025-12-08'] - -console.log('Testing chunk consistency across different "today" dates:\n') - -const allChunks: Record> = {} - -for (const today of testDates) { - const chunks = getNormalizedNpmDownloadChunks({ - startDate: createdDate, - endDate: today, - today, - }) - allChunks[today] = chunks - - console.log(`📅 If run on ${today}:`) - console.log(` Total chunks: ${chunks.length}`) - console.log(` Last 3 chunks:`) - chunks.slice(-3).forEach((chunk, idx) => { - const chunkNum = chunks.length - 3 + idx + 1 - console.log(` ${chunkNum}. ${chunk.from} → ${chunk.to}`) - }) - console.log('') -} - -// Check consistency -console.log('='.repeat(80)) -console.log('🔍 Consistency Analysis') -console.log('='.repeat(80) + '\n') - -const dates = Object.keys(allChunks) -const firstRun = allChunks[dates[0]] -const secondRun = allChunks[dates[1]] -const thirdRun = allChunks[dates[2]] - -// Historical chunks should be identical -const historicalChunksToCompare = Math.min( - firstRun.length - 1, - secondRun.length - 1, - thirdRun.length - 1, -) - -let allHistoricalMatch = true -for (let i = 0; i < historicalChunksToCompare; i++) { - if ( - firstRun[i].from !== secondRun[i].from || - firstRun[i].to !== secondRun[i].to || - firstRun[i].from !== thirdRun[i].from || - firstRun[i].to !== thirdRun[i].to - ) { - console.log(`❌ Chunk ${i + 1} differs between runs`) - console.log(` Run 1: ${firstRun[i].from} → ${firstRun[i].to}`) - console.log(` Run 2: ${secondRun[i].from} → ${secondRun[i].to}`) - console.log(` Run 3: ${thirdRun[i].from} → ${thirdRun[i].to}`) - allHistoricalMatch = false - } -} - -if (allHistoricalMatch) { - console.log( - `✅ Historical chunks (${historicalChunksToCompare} chunks) are consistent across all runs`, - ) - console.log( - ` All historical chunks will have identical cache keys regardless of when fetched`, - ) -} - -// Current chunk should differ (expected behavior) -console.log('') -console.log('Current (last) chunk analysis:') -const lastChunks = dates.map((date) => { - const chunks = allChunks[date] - return chunks[chunks.length - 1] -}) - -console.log( - ` Run 1 (${dates[0]}): ${lastChunks[0].from} → ${lastChunks[0].to}`, -) -console.log( - ` Run 2 (${dates[1]}): ${lastChunks[1].from} → ${lastChunks[1].to}`, -) -console.log( - ` Run 3 (${dates[2]}): ${lastChunks[2].from} → ${lastChunks[2].to}`, -) - -const currentChunksDiffer = lastChunks[0].to !== lastChunks[1].to -console.log('') -if (currentChunksDiffer) { - console.log( - '✅ Current chunks differ (expected - they track "today" and will be marked mutable)', - ) - console.log( - ' These chunks expire in 6 hours and get refreshed with updated data', - ) -} else { - console.log('⚠️ Current chunks are identical (might be same day)') -} - -// Cache key example -console.log('\n' + '='.repeat(80)) -console.log('📦 Example Cache Keys') -console.log('='.repeat(80) + '\n') - -const exampleChunks = firstRun.slice(0, 3) -exampleChunks.forEach((chunk, idx) => { - const cacheKey = `${packageName}|${chunk.from}|${chunk.to}|daily` - const isHistorical = chunk.to < dates[0] - console.log(`Chunk ${idx + 1}: ${cacheKey}`) - console.log( - ` ${isHistorical ? '🔒 Immutable (cached forever)' : '⏱️ Mutable (6-hour TTL)'}`, - ) -}) - -console.log('\n' + '='.repeat(80)) -console.log( - '✅ Chunk consistency verified - cache deduplication will work correctly', -) -console.log('='.repeat(80) + '\n') diff --git a/scripts/test-npm-cache.ts b/scripts/test-npm-cache.ts deleted file mode 100644 index a1bbced9d..000000000 --- a/scripts/test-npm-cache.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Test script for NPM download chunk caching - * Tests that chunks are properly cached and retrieved - */ - -import { - getCachedNpmDownloadChunk, - setCachedNpmDownloadChunk, -} from '../src/utils/npm-download-cache.server' - -async function testCache() { - console.log('\n' + '='.repeat(80)) - console.log('🧪 Testing NPM Download Chunk Cache') - console.log('='.repeat(80) + '\n') - - const testPackage = '@tanstack/react-query' - const testDateFrom = '2024-01-01' - const testDateTo = '2024-06-30' - - // Test 1: Cache miss - console.log('Test 1: Cache miss (should return null)') - const miss = await getCachedNpmDownloadChunk( - testPackage, - testDateFrom, - testDateTo, - ) - console.log( - ` Result: ${miss === null ? '✅ NULL (as expected)' : '❌ Got data (unexpected)'}`, - ) - - // Test 2: Write to cache - console.log('\nTest 2: Write to cache') - await setCachedNpmDownloadChunk({ - packageName: testPackage, - dateFrom: testDateFrom, - dateTo: testDateTo, - binSize: 'daily', - totalDownloads: 100000, - dailyData: [ - { day: '2024-01-01', downloads: 1000 }, - { day: '2024-01-02', downloads: 1100 }, - ], - isImmutable: false, // Will be calculated - }) - console.log(' ✅ Write completed') - - // Test 3: Cache hit - console.log('\nTest 3: Cache hit (should return data)') - const hit = await getCachedNpmDownloadChunk( - testPackage, - testDateFrom, - testDateTo, - ) - if (hit) { - console.log(` ✅ Got cached data:`) - console.log(` Package: ${hit.packageName}`) - console.log(` Range: ${hit.dateFrom} to ${hit.dateTo}`) - console.log(` Total downloads: ${hit.totalDownloads.toLocaleString()}`) - console.log(` Daily data points: ${hit.dailyData.length}`) - console.log(` Is immutable: ${hit.isImmutable}`) - } else { - console.log(' ❌ Cache miss (unexpected)') - } - - // Test 4: Historical chunk (should be immutable) - console.log('\nTest 4: Historical chunk (should be marked immutable)') - const historicalDateFrom = '2023-01-01' - const historicalDateTo = '2023-06-30' - await setCachedNpmDownloadChunk({ - packageName: testPackage, - dateFrom: historicalDateFrom, - dateTo: historicalDateTo, - binSize: 'daily', - totalDownloads: 50000, - dailyData: [ - { day: '2023-01-01', downloads: 500 }, - { day: '2023-01-02', downloads: 550 }, - ], - isImmutable: false, // Will be calculated based on dateTo - }) - - const historical = await getCachedNpmDownloadChunk( - testPackage, - historicalDateFrom, - historicalDateTo, - ) - if (historical) { - console.log( - ` Is immutable: ${historical.isImmutable ? '✅ YES (as expected)' : '❌ NO (unexpected)'}`, - ) - } else { - console.log(' ❌ Failed to retrieve historical chunk') - } - - console.log('\n' + '='.repeat(80)) - console.log('✅ Cache tests completed') - console.log('='.repeat(80) + '\n') - - process.exit(0) -} - -testCache().catch((error) => { - console.error('❌ Test failed:', error) - process.exit(1) -}) diff --git a/server.js b/server.js new file mode 100644 index 000000000..4e2a0e00e --- /dev/null +++ b/server.js @@ -0,0 +1,4 @@ +import { createRequestHandler } from "@remix-run/vercel"; +import * as build from "@remix-run/dev/server-build"; + +export default createRequestHandler({ build, mode: process.env.NODE_ENV }); diff --git a/src/auth/auth.server.ts b/src/auth/auth.server.ts deleted file mode 100644 index 74794c930..000000000 --- a/src/auth/auth.server.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Auth Service - * - * Main authentication service that coordinates session validation - * and user retrieval. Uses inversion of control for all dependencies. - */ - -import type { - AuthUser, - Capability, - DbUser, - IAuthService, - ICapabilitiesRepository, - ISessionService, - IUserRepository, - SessionCookieData, -} from './types' -import { AuthError } from './types' - -// ============================================================================ -// Auth Service Implementation -// ============================================================================ - -export class AuthService implements IAuthService { - constructor( - private sessionService: ISessionService, - private userRepository: IUserRepository, - private capabilitiesRepository: ICapabilitiesRepository, - ) {} - - /** - * Get current user from request - * Returns null if not authenticated - */ - async getCurrentUser(request: Request): Promise { - const signedCookie = this.sessionService.getSessionCookie(request) - - if (!signedCookie) { - return null - } - - try { - const cookieData = await this.sessionService.verifyCookie(signedCookie) - - if (!cookieData) { - console.error( - '[AuthService] Session cookie verification failed - invalid signature or expired', - ) - return null - } - - const result = await this.validateSession(cookieData) - if (!result) { - return null - } - - return this.mapDbUserToAuthUser(result.user, result.capabilities) - } catch (error) { - console.error('[AuthService] Failed to get user from session:', { - error: error instanceof Error ? error.message : 'Unknown error', - stack: error instanceof Error ? error.stack : undefined, - }) - return null - } - } - - /** - * Validate session data against the database - */ - async validateSession( - sessionData: SessionCookieData, - ): Promise<{ user: DbUser; capabilities: Capability[] } | null> { - const user = await this.userRepository.findById(sessionData.userId) - - if (!user) { - console.error( - `[AuthService] Session cookie references non-existent user ${sessionData.userId}`, - ) - return null - } - - // Verify session version matches (for session revocation) - if (user.sessionVersion !== sessionData.version) { - console.error( - `[AuthService] Session version mismatch for user ${user.id} - expected ${user.sessionVersion}, got ${sessionData.version}`, - ) - return null - } - - // Get effective capabilities - const capabilities = - await this.capabilitiesRepository.getEffectiveCapabilities(user.id) - - return { user, capabilities } - } - - /** - * Map database user to AuthUser type - */ - private mapDbUserToAuthUser( - user: DbUser, - capabilities: Capability[], - ): AuthUser { - return { - userId: user.id, - email: user.email, - name: user.name, - image: user.image, - oauthImage: user.oauthImage, - displayUsername: user.displayUsername, - capabilities, - adsDisabled: user.adsDisabled, - interestedInHidingAds: user.interestedInHidingAds, - lastUsedFramework: user.lastUsedFramework, - signupSources: user.signupSources, - } - } -} - -// ============================================================================ -// Auth Guard Functions -// ============================================================================ - -/** - * Require authentication - throws if not authenticated - */ -export async function requireAuthentication( - authService: IAuthService, - request: Request, -): Promise { - const user = await authService.getCurrentUser(request) - if (!user) { - throw new AuthError('Not authenticated', 'NOT_AUTHENTICATED') - } - return user -} - -/** - * Require specific capability - throws if not authorized - */ -export async function requireCapability( - authService: IAuthService, - request: Request, - capability: Capability, -): Promise { - const user = await requireAuthentication(authService, request) - - const hasAccess = - user.capabilities.includes('admin') || - user.capabilities.includes(capability) - - if (!hasAccess) { - throw new AuthError( - `Missing required capability: ${capability}`, - 'MISSING_CAPABILITY', - ) - } - - return user -} - -/** - * Require admin capability - throws if not admin - */ -export async function requireAdmin( - authService: IAuthService, - request: Request, -): Promise { - return requireCapability(authService, request, 'admin') -} diff --git a/src/auth/capabilities.server.ts b/src/auth/capabilities.server.ts deleted file mode 100644 index ccbe43124..000000000 --- a/src/auth/capabilities.server.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Capabilities Service - * - * Handles authorization via capability-based access control. - * Uses inversion of control for data access. - */ - -import type { ICapabilitiesRepository, Capability } from './types' - -export { - hasCapability, - hasAllCapabilities, - hasAnyCapability, - isAdmin, - userHasCapability, - userIsAdmin, -} from './capabilities' - -// ============================================================================ -// Capabilities Service -// ============================================================================ - -export class CapabilitiesService { - constructor(private repository: ICapabilitiesRepository) {} - - /** - * Get effective capabilities for a user (direct + role-based) - */ - async getEffectiveCapabilities(userId: string): Promise { - return this.repository.getEffectiveCapabilities(userId) - } - - /** - * Get effective capabilities for multiple users efficiently - */ - async getBulkEffectiveCapabilities( - userIds: string[], - ): Promise> { - return this.repository.getBulkEffectiveCapabilities(userIds) - } -} diff --git a/src/auth/capabilities.ts b/src/auth/capabilities.ts deleted file mode 100644 index 12ea9bad1..000000000 --- a/src/auth/capabilities.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { AuthUser } from './types' -import { - type Capability, - hasCapability, - hasAllCapabilities, - hasAnyCapability, - isAdmin, -} from '~/db/types' - -export { hasCapability, hasAllCapabilities, hasAnyCapability, isAdmin } - -export function userHasCapability( - user: AuthUser | null | undefined, - capability: Capability, -): boolean { - if (!user) return false - return hasCapability(user.capabilities, capability) -} - -export function userIsAdmin(user: AuthUser | null | undefined): boolean { - if (!user) return false - return isAdmin(user.capabilities) -} diff --git a/src/auth/cli-tickets.server.ts b/src/auth/cli-tickets.server.ts deleted file mode 100644 index b7987a290..000000000 --- a/src/auth/cli-tickets.server.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * CLI Auth Ticket Store - * - * In-memory store for short-lived CLI authentication tickets. - * Tickets are created by the CLI, then authorized in the browser after - * the user completes OAuth. The CLI polls for the token. - * - * TTL: 5 minutes. Single-use (consumed on exchange). - */ - -const TICKET_TTL_MS = 5 * 60 * 1000 -const MAX_TICKETS = 1_000 - -interface CliTicket { - userId: string | null - sessionToken: string | null - expiresAt: number - authorized: boolean -} - -// Server handlers and server-function handlers can load this module through -// different bundled entry points. Pin the Map to globalThis so those callers -// share one ticket store instead of fragmenting module-level state. -const TICKETS_KEY = Symbol.for('tanstack.cli-auth.tickets') -const TICKETS_INTERVAL_KEY = Symbol.for('tanstack.cli-auth.cleanup') - -type GlobalWithTickets = typeof globalThis & { - [TICKETS_KEY]?: Map - [TICKETS_INTERVAL_KEY]?: ReturnType -} - -const globalScope = globalThis as GlobalWithTickets - -const tickets: Map = (globalScope[TICKETS_KEY] ??= new Map< - string, - CliTicket ->()) - -if (!globalScope[TICKETS_INTERVAL_KEY]) { - globalScope[TICKETS_INTERVAL_KEY] = setInterval( - () => { - cleanupExpiredTickets() - }, - 60 * 1000, // every minute - ) -} - -function cleanupExpiredTickets() { - const now = Date.now() - for (const [id, ticket] of tickets) { - if (ticket.expiresAt < now) { - tickets.delete(id) - } - } -} - -function trimTicketStore() { - cleanupExpiredTickets() - - while (tickets.size >= MAX_TICKETS) { - const oldestId = tickets.keys().next().value - if (!oldestId) break - tickets.delete(oldestId) - } -} - -export function createCliTicket(): string { - trimTicketStore() - - const id = crypto.randomUUID() - tickets.set(id, { - userId: null, - sessionToken: null, - expiresAt: Date.now() + TICKET_TTL_MS, - authorized: false, - }) - return id -} - -export function getCliTicket(id: string): CliTicket | null { - const ticket = tickets.get(id) - if (!ticket) return null - if (ticket.expiresAt < Date.now()) { - tickets.delete(id) - return null - } - return ticket -} - -export function authorizeCliTicket( - id: string, - userId: string, - sessionToken: string, -): boolean { - const ticket = getCliTicket(id) - if (!ticket) return false - ticket.userId = userId - ticket.sessionToken = sessionToken - ticket.authorized = true - return true -} - -export function consumeCliTicket(id: string): string | null { - const ticket = getCliTicket(id) - if (!ticket || !ticket.authorized || !ticket.sessionToken) return null - const { sessionToken } = ticket - tickets.delete(id) - return sessionToken -} diff --git a/src/auth/client.ts b/src/auth/client.ts deleted file mode 100644 index 2090ddf25..000000000 --- a/src/auth/client.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Auth Client Module - * - * Client-side authentication utilities and navigation helpers. - * This module is safe to import in browser code. - */ - -import type { OAuthProvider } from './types' -import { openCenteredPopupWindow } from '~/utils/browser-effects' - -// ============================================================================ -// Auth Client -// ============================================================================ - -/** - * Client-side auth utilities for OAuth flows - */ -export const authClient = { - signIn: { - /** - * Initiate OAuth sign-in with a social provider (full page redirect) - */ - social: ({ - provider, - returnTo, - }: { - provider: OAuthProvider - returnTo?: string - }) => { - const url = returnTo - ? `/auth/${provider}/start?returnTo=${encodeURIComponent(returnTo)}` - : `/auth/${provider}/start` - window.location.href = url - }, - - /** - * Initiate OAuth sign-in in a popup window (for modal-based login) - */ - socialPopup: ({ provider }: { provider: OAuthProvider }) => { - return openCenteredPopupWindow({ - url: `/auth/${provider}/start?popup=true`, - target: 'tanstack-oauth', - width: 500, - height: 600, - features: ['popup=yes'], - }) - }, - }, - - /** - * Sign out the current user - */ - signOut: async () => { - window.location.href = '/auth/signout' - }, -} - -// ============================================================================ -// Navigation Helpers -// ============================================================================ - -/** - * Navigate to sign-in page - */ -export function navigateToSignIn( - provider?: OAuthProvider, - returnTo?: string, -): void { - if (provider) { - const url = returnTo - ? `/auth/${provider}/start?returnTo=${encodeURIComponent(returnTo)}` - : `/auth/${provider}/start` - window.location.href = url - } else { - const url = returnTo - ? `/login?redirect=${encodeURIComponent(returnTo)}` - : '/login' - window.location.href = url - } -} - -/** - * Navigate to sign-out - */ -export function navigateToSignOut(): void { - window.location.href = '/auth/signout' -} - -/** - * Get current URL path for return-to parameter - */ -export function getCurrentPath(): string { - return window.location.pathname + window.location.search -} diff --git a/src/auth/context.server.ts b/src/auth/context.server.ts deleted file mode 100644 index 23d26db36..000000000 --- a/src/auth/context.server.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Auth Context Setup - * - * Creates and configures the auth services with their dependencies. - * This is the composition root for the auth module in this application. - */ - -import { AuthService } from './auth.server' -import { CapabilitiesService } from './capabilities.server' -import { OAuthService } from './oauth.server' -import { SessionService } from './session.server' -import { createAuthGuards } from './guards.server' -import { - DrizzleUserRepository, - DrizzleOAuthAccountRepository, - DrizzleCapabilitiesRepository, -} from './repositories.server' - -// ============================================================================ -// Environment Configuration -// ============================================================================ - -function getSessionSecret(): string { - const secret = process.env.SESSION_SECRET - if (!secret) { - if (process.env.NODE_ENV === 'production') { - throw new Error( - 'SESSION_SECRET environment variable is required in production', - ) - } - // In development, require explicit opt-in to use insecure default - if (process.env.ALLOW_INSECURE_SESSION_SECRET !== 'true') { - throw new Error( - 'SESSION_SECRET environment variable is required. ' + - 'Set ALLOW_INSECURE_SESSION_SECRET=true to use insecure default in development.', - ) - } - console.warn( - '[Auth] WARNING: Using insecure session secret for development. Do NOT use in production.', - ) - return 'dev-secret-key-change-in-production' - } - return secret -} - -function isProduction(): boolean { - return process.env.NODE_ENV === 'production' -} - -// ============================================================================ -// Service Instances (Singleton pattern) -// ============================================================================ - -// Repositories -let _userRepository: DrizzleUserRepository | null = null -let _oauthAccountRepository: DrizzleOAuthAccountRepository | null = null -let _capabilitiesRepository: DrizzleCapabilitiesRepository | null = null - -// Services -let _sessionService: SessionService | null = null -let _authService: AuthService | null = null -let _capabilitiesService: CapabilitiesService | null = null -let _oauthService: OAuthService | null = null - -// ============================================================================ -// Repository Getters -// ============================================================================ - -export function getUserRepository(): DrizzleUserRepository { - if (!_userRepository) { - _userRepository = new DrizzleUserRepository() - } - return _userRepository -} - -export function getOAuthAccountRepository(): DrizzleOAuthAccountRepository { - if (!_oauthAccountRepository) { - _oauthAccountRepository = new DrizzleOAuthAccountRepository() - } - return _oauthAccountRepository -} - -export function getCapabilitiesRepository(): DrizzleCapabilitiesRepository { - if (!_capabilitiesRepository) { - _capabilitiesRepository = new DrizzleCapabilitiesRepository() - } - return _capabilitiesRepository -} - -// ============================================================================ -// Service Getters -// ============================================================================ - -export function getSessionService(): SessionService { - if (!_sessionService) { - _sessionService = new SessionService(getSessionSecret(), isProduction()) - } - return _sessionService -} - -export function getAuthService(): AuthService { - if (!_authService) { - _authService = new AuthService( - getSessionService(), - getUserRepository(), - getCapabilitiesRepository(), - ) - } - return _authService -} - -export function getCapabilitiesService(): CapabilitiesService { - if (!_capabilitiesService) { - _capabilitiesService = new CapabilitiesService(getCapabilitiesRepository()) - } - return _capabilitiesService -} - -export function getOAuthService(): OAuthService { - if (!_oauthService) { - _oauthService = new OAuthService( - getOAuthAccountRepository(), - getUserRepository(), - ) - } - return _oauthService -} - -// ============================================================================ -// Auth Guards (bound to auth service) -// ============================================================================ - -let _authGuards: ReturnType | null = null - -export function getAuthGuards() { - if (!_authGuards) { - _authGuards = createAuthGuards(getAuthService()) - } - return _authGuards -} - -// ============================================================================ -// Convenience Exports -// ============================================================================ - -/** - * Get all auth services configured for this application - */ -export function getAuthContext() { - return { - sessionService: getSessionService(), - authService: getAuthService(), - capabilitiesService: getCapabilitiesService(), - oauthService: getOAuthService(), - guards: getAuthGuards(), - repositories: { - user: getUserRepository(), - oauthAccount: getOAuthAccountRepository(), - capabilities: getCapabilitiesRepository(), - }, - } -} diff --git a/src/auth/github.server.ts b/src/auth/github.server.ts deleted file mode 100644 index 11fff55d5..000000000 --- a/src/auth/github.server.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * GitHub Auth Utilities - * - * Helper functions for checking GitHub OAuth scopes and tokens. - */ - -import { env } from '~/utils/env' -import { - buildGitHubAuthUrl, - generateOAuthState, - getOAuthAccountRepository, -} from './index.server' - -const REPO_SCOPE = 'public_repo' - -function hasRepoScopeInString(tokenScope: string | null): boolean { - if (!tokenScope) return false - const scopes = tokenScope.split(/[,\s]+/) - return scopes.includes(REPO_SCOPE) || scopes.includes('repo') -} - -/** - * Check if a user has the public_repo scope for GitHub - */ -export async function hasGitHubRepoScope(userId: string): Promise { - const repo = getOAuthAccountRepository() - const account = await repo.findByUserId(userId, 'github') - return hasRepoScopeInString(account?.tokenScope ?? null) -} - -/** - * Get a user's GitHub access token - */ -export async function getGitHubToken(userId: string): Promise { - const repo = getOAuthAccountRepository() - const account = await repo.findByUserId(userId, 'github') - return account?.accessToken ?? null -} - -/** - * Get a user's GitHub username from their access token - */ -export async function getGitHubUsername( - accessToken: string, -): Promise { - const response = await fetch('https://api.github.com/user', { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: 'application/vnd.github.v3+json', - 'User-Agent': 'TanStack.com', - }, - }) - - if (!response.ok) return null - - const profile = await response.json() - return profile.login ?? null -} - -export interface GitHubAuthState { - hasGitHubAccount: boolean - hasRepoScope: boolean - accessToken: string | null -} - -/** - * Get complete GitHub auth state for a user - */ -export async function getGitHubAuthState( - userId: string, -): Promise { - const repo = getOAuthAccountRepository() - const account = await repo.findByUserId(userId, 'github') - - if (!account) { - return { - hasGitHubAccount: false, - hasRepoScope: false, - accessToken: null, - } - } - - return { - hasGitHubAccount: true, - hasRepoScope: hasRepoScopeInString(account.tokenScope), - accessToken: account.accessToken, - } -} - -/** - * Build a GitHub re-auth URL with additional scopes - */ -export function buildGitHubReAuthUrl( - returnTo: string, - additionalScopes: Array, -): string { - const clientId = env.GITHUB_OAUTH_CLIENT_ID - if (!clientId) { - throw new Error('GITHUB_OAUTH_CLIENT_ID is not configured') - } - - const state = generateOAuthState() - - // Build auth URL with additional scopes - return buildGitHubAuthUrl(clientId, undefined, state, additionalScopes) -} diff --git a/src/auth/guards.server.ts b/src/auth/guards.server.ts deleted file mode 100644 index 1824c8b55..000000000 --- a/src/auth/guards.server.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Auth Guards Module - * - * Provides guard functions for protecting routes and server functions. - * These are convenience wrappers around the auth service. - */ - -import type { AuthUser, Capability, IAuthService } from './types' -import { AuthError } from './types' - -// ============================================================================ -// Guard Factory -// ============================================================================ - -/** - * Create guard functions bound to an auth service instance - */ -export function createAuthGuards(authService: IAuthService) { - return { - /** - * Get current user (non-blocking, returns null if not authenticated) - */ - async getCurrentUser(request: Request): Promise { - try { - return await authService.getCurrentUser(request) - } catch { - return null - } - }, - - /** - * Require authentication (throws if not authenticated) - */ - async requireAuth(request: Request): Promise { - const user = await authService.getCurrentUser(request) - if (!user) { - throw new AuthError('Not authenticated', 'NOT_AUTHENTICATED') - } - return user - }, - - /** - * Require specific capability (throws if not authorized) - */ - async requireCapability( - request: Request, - capability: Capability, - ): Promise { - const user = await authService.getCurrentUser(request) - if (!user) { - throw new AuthError('Not authenticated', 'NOT_AUTHENTICATED') - } - - const hasAccess = - user.capabilities.includes('admin') || - user.capabilities.includes(capability) - - if (!hasAccess) { - throw new AuthError( - `Missing required capability: ${capability}`, - 'MISSING_CAPABILITY', - ) - } - - return user - }, - - /** - * Require admin access - */ - async requireAdmin(request: Request): Promise { - return this.requireCapability(request, 'admin') - }, - - /** - * Check if user has capability (non-throwing) - */ - async hasCapability( - request: Request, - capability: Capability, - ): Promise { - try { - await this.requireCapability(request, capability) - return true - } catch { - return false - } - }, - - /** - * Check if user is authenticated (non-throwing) - */ - async isAuthenticated(request: Request): Promise { - const user = await this.getCurrentUser(request) - return user !== null - }, - - /** - * Check if user is admin (non-throwing) - */ - async isAdmin(request: Request): Promise { - return this.hasCapability(request, 'admin') - }, - } -} - -// ============================================================================ -// Guard Types -// ============================================================================ - -export type AuthGuards = ReturnType - -// ============================================================================ -// Capability Guard Decorator Pattern (for server functions) -// ============================================================================ - -/** - * Create a guard that wraps a handler with capability check - */ -export function withCapability( - guards: AuthGuards, - capability: Capability, - getRequest: () => Request, - handler: (user: AuthUser, ...args: TArgs) => TReturn, -) { - return async (...args: TArgs): Promise> => { - const request = getRequest() - const user = await guards.requireCapability(request, capability) - return (await handler(user, ...args)) as Awaited - } -} - -/** - * Create a guard that wraps a handler with auth check - */ -export function withAuth( - guards: AuthGuards, - getRequest: () => Request, - handler: (user: AuthUser, ...args: TArgs) => TReturn, -) { - return async (...args: TArgs): Promise> => { - const request = getRequest() - const user = await guards.requireAuth(request) - return (await handler(user, ...args)) as Awaited - } -} diff --git a/src/auth/index.server.ts b/src/auth/index.server.ts deleted file mode 100644 index 78ccbd425..000000000 --- a/src/auth/index.server.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Auth Module - Server Entry Point - * - * This is the main entry point for server-side auth functionality. - * Import from '~/auth/index.server' for server-side code. - * - * Example usage: - * - * ```ts - * import { getAuthService, getAuthGuards } from '~/auth/index.server' - * - * // In a server function or loader - * const authService = getAuthService() - * const user = await authService.getCurrentUser(request) - * - * // Or use guards - * const guards = getAuthGuards() - * const user = await guards.requireAuth(request) - * ``` - */ - -// ============================================================================ -// Types (re-exported from types module) -// ============================================================================ - -export type { - // Core types - Capability, - OAuthProvider, - OAuthProfile, - OAuthResult, - SessionCookieData, - AuthUser, - DbUser, - // Interfaces for IoC - IUserRepository, - IOAuthAccountRepository, - ICapabilitiesRepository, - ISessionService, - IAuthService, - IOAuthService, - AuthContext, - // Error types - AuthErrorCode, -} from './types' - -export { VALID_CAPABILITIES, AuthError } from './types' - -// ============================================================================ -// Services -// ============================================================================ - -export { AuthService } from './auth.server' -export { SessionService } from './session.server' -export { CapabilitiesService } from './capabilities.server' -export { OAuthService } from './oauth.server' - -// ============================================================================ -// Session Utilities -// ============================================================================ - -export { - generateOAuthState, - createOAuthStateCookie, - clearOAuthStateCookie, - getOAuthStateCookie, - createOAuthPopupCookie, - clearOAuthPopupCookie, - isOAuthPopupMode, - createOAuthReturnToCookie, - clearOAuthReturnToCookie, - getOAuthReturnTo, - SESSION_DURATION_MS, - SESSION_MAX_AGE_SECONDS, -} from './session.server' - -// ============================================================================ -// Capability Utilities -// ============================================================================ - -export { - hasCapability, - hasAllCapabilities, - hasAnyCapability, - isAdmin, - userHasCapability, - userIsAdmin, -} from './capabilities' - -// ============================================================================ -// OAuth Utilities -// ============================================================================ - -export { - buildGitHubAuthUrl, - buildGoogleAuthUrl, - exchangeGitHubCode, - exchangeGoogleCode, - fetchGitHubProfile, - fetchGoogleProfile, -} from './oauth.server' - -// ============================================================================ -// Guards -// ============================================================================ - -export { createAuthGuards, withCapability, withAuth } from './guards.server' -export type { AuthGuards } from './guards.server' - -// ============================================================================ -// Repositories (for custom implementations) -// ============================================================================ - -export { - DrizzleUserRepository, - DrizzleOAuthAccountRepository, - DrizzleCapabilitiesRepository, - createRepositories, -} from './repositories.server' - -// ============================================================================ -// Context & Service Accessors (Application-specific) -// ============================================================================ - -export { - getAuthContext, - getAuthService, - getSessionService, - getCapabilitiesService, - getOAuthService, - getAuthGuards, - getUserRepository, - getOAuthAccountRepository, - getCapabilitiesRepository, -} from './context.server' diff --git a/src/auth/index.ts b/src/auth/index.ts deleted file mode 100644 index 45de08eda..000000000 --- a/src/auth/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Auth Module - Client Entry Point - * - * This is the main entry point for client-side auth functionality. - * Import from '~/auth' for client-side code. - * - * For server-side code, import from '~/auth/index.server' instead. - * - * Example usage: - * - * ```tsx - * import { authClient, navigateToSignIn } from '~/auth' - * - * // Sign in with GitHub - * authClient.signIn.social({ provider: 'github' }) - * - * // Sign out - * authClient.signOut() - * ``` - */ - -// ============================================================================ -// Types (client-safe types only) -// ============================================================================ - -export type { - Capability, - OAuthProvider, - AuthUser, - AuthErrorCode, -} from './types' - -export { VALID_CAPABILITIES, AuthError } from './types' - -// ============================================================================ -// Client Auth -// ============================================================================ - -export { - authClient, - navigateToSignIn, - navigateToSignOut, - getCurrentPath, -} from './client' - -// ============================================================================ -// Capability Utilities (client-safe) -// ============================================================================ - -export { - hasCapability, - hasAllCapabilities, - hasAnyCapability, - isAdmin, - userHasCapability, - userIsAdmin, -} from './capabilities' diff --git a/src/auth/oauth.server.ts b/src/auth/oauth.server.ts deleted file mode 100644 index f980169b2..000000000 --- a/src/auth/oauth.server.ts +++ /dev/null @@ -1,486 +0,0 @@ -/** - * OAuth Service - * - * Handles OAuth account management and user creation/linking. - * Uses inversion of control for database access. - */ - -import type { - IOAuthService, - IOAuthAccountRepository, - IUserRepository, - OAuthProfile, - OAuthProvider, - OAuthResult, -} from './types' -import { AuthError } from './types' - -const GITHUB_USER_AGENT = 'TanStack.com' - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null -} - -async function parseOAuthJson( - response: Response, - context: string, -): Promise { - const text = await response.text() - - try { - return JSON.parse(text) - } catch { - const body = text.trim().slice(0, 300) - console.error( - `[OAuth] ${context} returned non-JSON response: status=${response.status}, statusText=${response.statusText}, body=${body}`, - ) - throw new AuthError(`${context} returned non-JSON response`, 'OAUTH_ERROR') - } -} - -function getStringField(record: Record, key: string) { - const value = record[key] - return typeof value === 'string' ? value : undefined -} - -function getGitHubApiError(data: unknown) { - return isRecord(data) ? getStringField(data, 'message') : undefined -} - -// ============================================================================ -// OAuth Service Implementation -// ============================================================================ - -export class OAuthService implements IOAuthService { - constructor( - private oauthAccountRepository: IOAuthAccountRepository, - private userRepository: IUserRepository, - ) {} - - /** - * Upsert OAuth account and associated user - * - If OAuth account exists, updates user info and returns existing user - * - If user with email exists, links OAuth account to existing user - * - Otherwise, creates new user and OAuth account - */ - async upsertOAuthAccount( - provider: OAuthProvider, - profile: OAuthProfile, - tokenInfo?: { accessToken: string; scope: string }, - ): Promise { - try { - // Check if OAuth account already exists - const existingAccount = - await this.oauthAccountRepository.findByProviderAndAccountId( - provider, - profile.id, - ) - - if (existingAccount) { - // Account exists, update user info if needed - const user = await this.userRepository.findById(existingAccount.userId) - - if (!user) { - console.error( - `[OAuthService] OAuth account exists for ${provider}:${profile.id} but user ${existingAccount.userId} not found`, - ) - throw new AuthError( - 'User not found for existing OAuth account', - 'USER_NOT_FOUND', - ) - } - - const updates: { - email?: string - name?: string - oauthImage?: string - updatedAt?: Date - } = {} - - if (profile.email && user.email !== profile.email) { - updates.email = profile.email - } - if (profile.name && user.name !== profile.name) { - updates.name = profile.name - } - // Always update oauthImage from provider (it may have changed) - if (profile.image && user.oauthImage !== profile.image) { - updates.oauthImage = profile.image - } - - if (Object.keys(updates).length > 0) { - updates.updatedAt = new Date() - await this.userRepository.update(existingAccount.userId, updates) - } - - // Update token if provided - if (tokenInfo) { - await this.oauthAccountRepository.updateToken( - existingAccount.userId, - provider, - tokenInfo.accessToken, - tokenInfo.scope, - ) - } - - return { - userId: existingAccount.userId, - isNewUser: false, - } - } - - // Find user by email (for linking multiple OAuth providers) - const existingUser = await this.userRepository.findByEmail(profile.email) - - let userId: string - - if (existingUser) { - // Link OAuth account to existing user - console.log( - `[OAuthService] Linking ${provider} account to existing user ${existingUser.id} (${profile.email})`, - ) - userId = existingUser.id - - // Update user info if provided and not already set - const updates: { - name?: string - image?: string - oauthImage?: string - updatedAt?: Date - } = {} - - if (profile.name && !existingUser.name) { - updates.name = profile.name - } - if (profile.image && !existingUser.image) { - updates.image = profile.image - } - // Always update oauthImage from provider - if (profile.image && existingUser.oauthImage !== profile.image) { - updates.oauthImage = profile.image - } - - if (Object.keys(updates).length > 0) { - updates.updatedAt = new Date() - await this.userRepository.update(userId, updates) - } - } else { - // Create new user - console.log( - `[OAuthService] Creating new user for ${provider} login: ${profile.email}`, - ) - const newUser = await this.userRepository.create({ - email: profile.email, - name: profile.name, - image: profile.image, - oauthImage: profile.image, - displayUsername: profile.name, - capabilities: [], - }) - - userId = newUser.id - } - - // Create OAuth account link with token if provided - await this.oauthAccountRepository.create({ - userId, - provider, - providerAccountId: profile.id, - email: profile.email, - accessToken: tokenInfo?.accessToken, - tokenScope: tokenInfo?.scope, - }) - - return { - userId, - isNewUser: !existingUser, - } - } catch (error) { - if (error instanceof AuthError) { - throw error - } - - console.error( - `[OAuthService] Failed to upsert OAuth account for ${provider}:${profile.id} (${profile.email}):`, - { - error: error instanceof Error ? error.message : 'Unknown error', - stack: error instanceof Error ? error.stack : undefined, - }, - ) - throw new AuthError( - `OAuth account creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, - 'OAUTH_ERROR', - ) - } - } -} - -// ============================================================================ -// OAuth Provider Utilities -// ============================================================================ - -/** - * Build GitHub OAuth authorization URL - */ -export function buildGitHubAuthUrl( - clientId: string, - redirectUri: string | undefined, - state: string, - additionalScopes?: Array, -): string { - const scopes = ['user:email', ...(additionalScopes ?? [])].join(' ') - const params = new URLSearchParams({ - client_id: clientId, - scope: scopes, - state, - }) - - if (redirectUri) { - params.set('redirect_uri', redirectUri) - } - - return `https://github.com/login/oauth/authorize?${params.toString()}` -} - -/** - * Build Google OAuth authorization URL - */ -export function buildGoogleAuthUrl( - clientId: string, - redirectUri: string, - state: string, -): string { - return `https://accounts.google.com/o/oauth2/v2/auth?client_id=${encodeURIComponent( - clientId, - )}&redirect_uri=${encodeURIComponent( - redirectUri, - )}&response_type=code&scope=openid email profile&state=${encodeURIComponent(state)}` -} - -export interface GitHubTokenResult { - accessToken: string - scope: string -} - -/** - * Exchange GitHub authorization code for access token - */ -export async function exchangeGitHubCode( - code: string, - clientId: string, - clientSecret: string, - redirectUri?: string, -): Promise { - const body: { - client_id: string - client_secret: string - code: string - redirect_uri?: string - } = { - client_id: clientId, - client_secret: clientSecret, - code, - } - - if (redirectUri) { - body.redirect_uri = redirectUri - } - - const tokenResponse = await fetch( - 'https://github.com/login/oauth/access_token', - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': GITHUB_USER_AGENT, - }, - body: JSON.stringify(body), - }, - ) - - const tokenData = await parseOAuthJson(tokenResponse, 'GitHub token exchange') - if (!isRecord(tokenData)) { - throw new AuthError('Invalid GitHub token response', 'OAUTH_ERROR') - } - - const error = getStringField(tokenData, 'error') - if (error) { - const errorDescription = getStringField(tokenData, 'error_description') - console.error( - `[OAuth] GitHub token exchange failed: ${error}, description: ${errorDescription || 'none'}`, - ) - throw new AuthError(`GitHub OAuth error: ${error}`, 'OAUTH_ERROR') - } - - const accessToken = getStringField(tokenData, 'access_token') - if (!accessToken) { - console.error( - '[OAuth] GitHub token exchange succeeded but no access_token returned', - ) - throw new AuthError('No access token received from GitHub', 'OAUTH_ERROR') - } - - return { - accessToken, - scope: getStringField(tokenData, 'scope') ?? '', - } -} - -/** - * Fetch GitHub user profile - */ -export async function fetchGitHubProfile( - accessToken: string, -): Promise { - const profileResponse = await fetch('https://api.github.com/user', { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: 'application/vnd.github.v3+json', - 'User-Agent': GITHUB_USER_AGENT, - }, - }) - - const profile = await parseOAuthJson(profileResponse, 'GitHub profile API') - if (!isRecord(profile)) { - throw new AuthError('Invalid GitHub profile response', 'OAUTH_ERROR') - } - - if (!profileResponse.ok) { - const message = getGitHubApiError(profile) ?? 'Failed to fetch GitHub user' - throw new AuthError(message, 'OAUTH_ERROR') - } - - // Fetch email (may require separate call) - let email = getStringField(profile, 'email') - if (!email) { - const emailResponse = await fetch('https://api.github.com/user/emails', { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: 'application/vnd.github.v3+json', - 'User-Agent': GITHUB_USER_AGENT, - }, - }) - const emails = await parseOAuthJson(emailResponse, 'GitHub emails API') - - if (!Array.isArray(emails)) { - console.error( - `[OAuth] GitHub emails API returned non-array response:`, - emails, - ) - throw new AuthError( - getGitHubApiError(emails) || 'Failed to fetch GitHub emails', - 'OAUTH_ERROR', - ) - } - - const primaryEmail = emails.find( - (emailRecord): emailRecord is Record => - isRecord(emailRecord) && - emailRecord.primary === true && - emailRecord.verified === true && - typeof emailRecord.email === 'string', - ) - const verifiedEmail = emails.find( - (emailRecord): emailRecord is Record => - isRecord(emailRecord) && - emailRecord.verified === true && - typeof emailRecord.email === 'string', - ) - email = - getStringField(primaryEmail ?? {}, 'email') || - getStringField(verifiedEmail ?? {}, 'email') - } - - if (!email) { - console.error( - `[OAuth] No verified email found for GitHub user ${String( - profile.id, - )} (${getStringField(profile, 'login') ?? 'unknown'})`, - ) - throw new AuthError( - 'No verified email found for GitHub account', - 'OAUTH_ERROR', - ) - } - - return { - id: String(profile.id), - email, - name: getStringField(profile, 'name') || getStringField(profile, 'login'), - image: getStringField(profile, 'avatar_url'), - } -} - -/** - * Exchange Google authorization code for access token - */ -export async function exchangeGoogleCode( - code: string, - clientId: string, - clientSecret: string, - redirectUri: string, -): Promise { - const tokenResponse = await fetch('https://oauth2.googleapis.com/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - client_id: clientId, - client_secret: clientSecret, - code, - redirect_uri: redirectUri, - grant_type: 'authorization_code', - }), - }) - - const tokenData = await tokenResponse.json() - if (tokenData.error) { - console.error( - `[OAuth] Google token exchange failed: ${tokenData.error}, description: ${tokenData.error_description || 'none'}`, - ) - throw new AuthError(`Google OAuth error: ${tokenData.error}`, 'OAUTH_ERROR') - } - - if (!tokenData.access_token) { - console.error( - '[OAuth] Google token exchange succeeded but no access_token returned', - ) - throw new AuthError('No access token received from Google', 'OAUTH_ERROR') - } - - return tokenData.access_token -} - -/** - * Fetch Google user profile - */ -export async function fetchGoogleProfile( - accessToken: string, -): Promise { - const profileResponse = await fetch( - 'https://www.googleapis.com/oauth2/v2/userinfo', - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ) - - const profile = await profileResponse.json() - - if (!profile.verified_email) { - console.error( - `[OAuth] Google email not verified for user ${profile.id} (${profile.email})`, - ) - throw new AuthError('Google email not verified', 'OAUTH_ERROR') - } - - return { - id: profile.id, - email: profile.email, - name: profile.name, - image: profile.picture, - } -} diff --git a/src/auth/oauthClient.server.ts b/src/auth/oauthClient.server.ts deleted file mode 100644 index 1133db4b7..000000000 --- a/src/auth/oauthClient.server.ts +++ /dev/null @@ -1,607 +0,0 @@ -import { db } from '~/db/client' -import { - oauthAuthorizationCodes, - oauthAccessTokens, - oauthRefreshTokens, -} from '~/db/schema' -import { eq, and, sql, lt } from 'drizzle-orm' -import { sha256Hex } from '~/utils/hash' -import { envFunctions } from '~/utils/env.functions' - -// Token TTLs -const AUTH_CODE_TTL_MS = 10 * 60 * 1000 // 10 minutes -const ACCESS_TOKEN_TTL_MS = 60 * 60 * 1000 // 1 hour -const REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000 // 30 days - -// Token prefixes (new generic prefixes) -const ACCESS_TOKEN_PREFIX = 'oa_' -const REFRESH_TOKEN_PREFIX = 'oar_' -const REGISTERED_CLIENT_PREFIX = 'tsr_' -const LAST_USED_WRITE_INTERVAL_MS = 5 * 60 * 1000 -const MAX_REGISTERED_CLIENT_PAYLOAD_LENGTH = 8192 - -// Legacy prefixes for backwards compatibility -const LEGACY_ACCESS_TOKEN_PREFIX = 'mcp_' -const LEGACY_REFRESH_TOKEN_PREFIX = 'mcpr_' - -export type OAuthValidationResult = - | { - success: true - tokenId: string - userId: string - clientId: string - } - | { - success: false - error: string - status: number - } - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value) -} - -function isStringArray(value: unknown): value is Array { - return Array.isArray(value) && value.every((item) => typeof item === 'string') -} - -function normalizeRedirectUri(uri: string): string | null { - try { - return new URL(uri).toString() - } catch { - return null - } -} - -function bytesToBase64Url(bytes: Uint8Array) { - let binary = '' - for (const byte of bytes) { - binary += String.fromCharCode(byte) - } - - return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') -} - -function stringToBase64Url(value: string) { - return bytesToBase64Url(new TextEncoder().encode(value)) -} - -function base64UrlToString(value: string): string | null { - try { - const padded = value - .replace(/-/g, '+') - .replace(/_/g, '/') - .padEnd(Math.ceil(value.length / 4) * 4, '=') - const binary = atob(padded) - const bytes = new Uint8Array(binary.length) - for (let index = 0; index < binary.length; index++) { - bytes[index] = binary.charCodeAt(index) - } - return new TextDecoder().decode(bytes) - } catch { - return null - } -} - -function getOAuthClientSigningSecret() { - const secret = envFunctions.SESSION_SECRET - if (!secret && process.env.NODE_ENV === 'production') { - throw new Error('SESSION_SECRET is required for OAuth client registration') - } - - return secret ?? 'development-oauth-client-registration-secret' -} - -async function signOAuthClientPayload(payload: string) { - const key = await crypto.subtle.importKey( - 'raw', - new TextEncoder().encode(getOAuthClientSigningSecret()), - { name: 'HMAC', hash: 'SHA-256' }, - false, - ['sign'], - ) - const signature = await crypto.subtle.sign( - 'HMAC', - key, - new TextEncoder().encode(payload), - ) - return bytesToBase64Url(new Uint8Array(signature)) -} - -function parseRegisteredClientPayload( - value: unknown, -): { clientName: string; redirectUris: Array } | null { - if (!isRecord(value)) return null - - const clientName = value.clientName - const redirectUris = value.redirectUris - if (typeof clientName !== 'string' || !isStringArray(redirectUris)) { - return null - } - - if ( - clientName.length > 100 || - redirectUris.length === 0 || - redirectUris.length > 10 || - redirectUris.join('').length > 4096 || - redirectUris.some((uri) => uri.length > 2048 || !validateRedirectUri(uri)) - ) { - return null - } - - return { clientName, redirectUris } -} - -export async function createRegisteredClientId(params: { - clientName: string - redirectUris: Array -}) { - const payload = JSON.stringify({ - clientName: params.clientName, - redirectUris: params.redirectUris - .map(normalizeRedirectUri) - .filter((uri) => uri !== null), - }) - const encodedPayload = stringToBase64Url(payload) - const signature = await signOAuthClientPayload(encodedPayload) - - return `${REGISTERED_CLIENT_PREFIX}${encodedPayload}.${signature}` -} - -export async function validateOAuthClientRedirectUri( - clientId: string, - redirectUri: string, -) { - if (!clientId.startsWith(REGISTERED_CLIENT_PREFIX)) { - return false - } - - const body = clientId.slice(REGISTERED_CLIENT_PREFIX.length) - const [encodedPayload, signature] = body.split('.') - if ( - !encodedPayload || - !signature || - encodedPayload.length > MAX_REGISTERED_CLIENT_PAYLOAD_LENGTH - ) { - return false - } - - const expectedSignature = await signOAuthClientPayload(encodedPayload) - if (expectedSignature !== signature) { - return false - } - - const payloadText = base64UrlToString(encodedPayload) - if (!payloadText) { - return false - } - - let payload: unknown - try { - payload = JSON.parse(payloadText) - } catch { - return false - } - - const metadata = parseRegisteredClientPayload(payload) - const normalizedRedirectUri = normalizeRedirectUri(redirectUri) - return ( - !!metadata && - !!normalizedRedirectUri && - metadata.redirectUris.includes(normalizedRedirectUri) - ) -} - -/** - * Generate a secure random token with prefix - */ -export function generateToken(prefix: string): string { - const bytes = new Uint8Array(32) - crypto.getRandomValues(bytes) - const hex = Array.from(bytes) - .map((b) => b.toString(16).padStart(2, '0')) - .join('') - return prefix + hex -} - -/** - * Hash a token using SHA-256 - */ -export const hashToken = sha256Hex - -/** - * Validate redirect URI - must be localhost or HTTPS - */ -export function validateRedirectUri(uri: string): boolean { - try { - const url = new URL(uri) - if (url.username || url.password) { - return false - } - - // Allow localhost (any port) - if ( - url.hostname === 'localhost' || - url.hostname === '127.0.0.1' || - url.hostname === '[::1]' - ) { - return url.protocol === 'http:' || url.protocol === 'https:' - } - - // Allow HTTPS URLs - if (url.protocol === 'https:') { - return true - } - - return false - } catch { - return false - } -} - -/** - * Verify PKCE code challenge - * code_challenge = BASE64URL(SHA256(code_verifier)) - */ -export async function verifyCodeChallenge( - codeVerifier: string, - codeChallenge: string, -): Promise { - const encoder = new TextEncoder() - const data = encoder.encode(codeVerifier) - const hashBuffer = await crypto.subtle.digest('SHA-256', data) - const hashArray = new Uint8Array(hashBuffer) - - // Base64URL encode - const base64 = btoa(String.fromCharCode(...hashArray)) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, '') - - return base64 === codeChallenge -} - -/** - * Check if a token is an OAuth client token (new or legacy format) - */ -export function isOAuthClientToken(token: string): boolean { - return ( - token.startsWith(ACCESS_TOKEN_PREFIX) || - token.startsWith(LEGACY_ACCESS_TOKEN_PREFIX) - ) -} - -/** - * Check if a token is an OAuth refresh token (new or legacy format) - */ -export function isOAuthRefreshToken(token: string): boolean { - return ( - token.startsWith(REFRESH_TOKEN_PREFIX) || - token.startsWith(LEGACY_REFRESH_TOKEN_PREFIX) - ) -} - -/** - * Create an authorization code - */ -export async function createAuthorizationCode(params: { - userId: string - clientId: string - redirectUri: string - codeChallenge: string - codeChallengeMethod?: string - scope?: string -}): Promise { - if ( - !(await validateOAuthClientRedirectUri(params.clientId, params.redirectUri)) - ) { - throw new Error('Client is not registered for this redirect URI') - } - - const code = generateToken('') - const codeHash = await hashToken(code) - const expiresAt = new Date(Date.now() + AUTH_CODE_TTL_MS) - - await db.insert(oauthAuthorizationCodes).values({ - codeHash, - userId: params.userId, - clientId: params.clientId, - redirectUri: params.redirectUri, - codeChallenge: params.codeChallenge, - codeChallengeMethod: params.codeChallengeMethod || 'S256', - scope: params.scope || 'api', - expiresAt, - }) - - return code -} - -/** - * Exchange authorization code for tokens - */ -export async function exchangeAuthorizationCode(params: { - code: string - codeVerifier: string - redirectUri: string -}): Promise< - | { - success: true - accessToken: string - refreshToken: string - expiresIn: number - scope: string - } - | { success: false; error: string } -> { - const codeHash = await hashToken(params.code) - - // Atomically consume the authorization code before validation. Any exchange - // attempt makes the code single-use, even when the verifier is wrong. - const authCodes = await db - .delete(oauthAuthorizationCodes) - .where(eq(oauthAuthorizationCodes.codeHash, codeHash)) - .returning() - - const authCode = authCodes[0] - - if (!authCode) { - return { success: false, error: 'invalid_grant' } - } - - // Check expiration - if (authCode.expiresAt < new Date()) { - return { success: false, error: 'invalid_grant' } - } - - // Validate redirect URI matches - if (authCode.redirectUri !== params.redirectUri) { - return { success: false, error: 'invalid_grant' } - } - - // Verify PKCE - const pkceValid = await verifyCodeChallenge( - params.codeVerifier, - authCode.codeChallenge, - ) - if (!pkceValid) { - return { success: false, error: 'invalid_grant' } - } - - // Generate tokens (using new prefixes) - const accessToken = generateToken(ACCESS_TOKEN_PREFIX) - const refreshToken = generateToken(REFRESH_TOKEN_PREFIX) - const accessTokenHash = await hashToken(accessToken) - const refreshTokenHash = await hashToken(refreshToken) - - const accessExpiresAt = new Date(Date.now() + ACCESS_TOKEN_TTL_MS) - const refreshExpiresAt = new Date(Date.now() + REFRESH_TOKEN_TTL_MS) - - // Insert access token - const accessTokenResult = await db - .insert(oauthAccessTokens) - .values({ - tokenHash: accessTokenHash, - userId: authCode.userId, - clientId: authCode.clientId, - scope: authCode.scope, - expiresAt: accessExpiresAt, - }) - .returning({ id: oauthAccessTokens.id }) - - // Insert refresh token - await db.insert(oauthRefreshTokens).values({ - tokenHash: refreshTokenHash, - userId: authCode.userId, - clientId: authCode.clientId, - accessTokenId: accessTokenResult[0].id, - expiresAt: refreshExpiresAt, - }) - - return { - success: true, - accessToken, - refreshToken, - expiresIn: Math.floor(ACCESS_TOKEN_TTL_MS / 1000), - scope: authCode.scope, - } -} - -/** - * Refresh an access token - */ -export async function refreshAccessToken(refreshToken: string): Promise< - | { - success: true - accessToken: string - expiresIn: number - scope: string - } - | { success: false; error: string } -> { - const tokenHash = await hashToken(refreshToken) - - // Find refresh token - const refreshTokens = await db - .select() - .from(oauthRefreshTokens) - .where(eq(oauthRefreshTokens.tokenHash, tokenHash)) - .limit(1) - - const token = refreshTokens[0] - - if (!token) { - return { success: false, error: 'invalid_grant' } - } - - // Check expiration - if (token.expiresAt < new Date()) { - await db - .delete(oauthRefreshTokens) - .where(eq(oauthRefreshTokens.id, token.id)) - return { success: false, error: 'invalid_grant' } - } - - // Generate new access token (using new prefix) - const accessToken = generateToken(ACCESS_TOKEN_PREFIX) - const accessTokenHash = await hashToken(accessToken) - const accessExpiresAt = new Date(Date.now() + ACCESS_TOKEN_TTL_MS) - - // Insert new access token - const accessTokenResult = await db - .insert(oauthAccessTokens) - .values({ - tokenHash: accessTokenHash, - userId: token.userId, - clientId: token.clientId, - scope: 'api', - expiresAt: accessExpiresAt, - }) - .returning({ id: oauthAccessTokens.id }) - - // Update refresh token to point to new access token - await db - .update(oauthRefreshTokens) - .set({ accessTokenId: accessTokenResult[0].id }) - .where(eq(oauthRefreshTokens.id, token.id)) - - return { - success: true, - accessToken, - expiresIn: Math.floor(ACCESS_TOKEN_TTL_MS / 1000), - scope: 'api', - } -} - -/** - * Validate an OAuth access token - * Supports both new (oa_) and legacy (mcp_) token prefixes - */ -export async function validateOAuthToken( - token: string, -): Promise { - const tokenHash = await hashToken(token) - - const result = await db - .select({ - id: oauthAccessTokens.id, - userId: oauthAccessTokens.userId, - clientId: oauthAccessTokens.clientId, - expiresAt: oauthAccessTokens.expiresAt, - lastUsedAt: oauthAccessTokens.lastUsedAt, - }) - .from(oauthAccessTokens) - .where(eq(oauthAccessTokens.tokenHash, tokenHash)) - .limit(1) - - const accessToken = result[0] - - if (!accessToken) { - return { - success: false, - error: 'Invalid access token', - status: 401, - } - } - - // Check expiration - if (accessToken.expiresAt < new Date()) { - return { - success: false, - error: 'Access token has expired', - status: 401, - } - } - - if ( - !accessToken.lastUsedAt || - Date.now() - accessToken.lastUsedAt.getTime() > LAST_USED_WRITE_INTERVAL_MS - ) { - db.update(oauthAccessTokens) - .set({ lastUsedAt: new Date() }) - .where(eq(oauthAccessTokens.id, accessToken.id)) - .catch(() => {}) - } - - return { - success: true, - tokenId: accessToken.id, - userId: accessToken.userId, - clientId: accessToken.clientId, - } -} - -/** - * List connected OAuth apps for a user - * Uses refresh tokens since they persist longer than access tokens - */ -export async function listConnectedApps(userId: string): Promise< - Array<{ - clientId: string - createdAt: string - lastUsedAt: string | null - }> -> { - // Get unique clients from refresh tokens (more persistent than access tokens) - const result = await db - .select({ - clientId: oauthRefreshTokens.clientId, - createdAt: sql`MIN(${oauthRefreshTokens.createdAt})::text`, - }) - .from(oauthRefreshTokens) - .where(eq(oauthRefreshTokens.userId, userId)) - .groupBy(oauthRefreshTokens.clientId) - - return result.map((r) => ({ - clientId: r.clientId, - createdAt: r.createdAt, - lastUsedAt: null, - })) -} - -/** - * Revoke all tokens for a specific client - */ -export async function revokeTokensForClient( - userId: string, - clientId: string, -): Promise { - // Delete refresh tokens first (they reference access tokens) - await db - .delete(oauthRefreshTokens) - .where( - and( - eq(oauthRefreshTokens.userId, userId), - eq(oauthRefreshTokens.clientId, clientId), - ), - ) - - // Delete access tokens - await db - .delete(oauthAccessTokens) - .where( - and( - eq(oauthAccessTokens.userId, userId), - eq(oauthAccessTokens.clientId, clientId), - ), - ) -} - -/** - * Clean up expired tokens - */ -export async function cleanupExpiredTokens(): Promise { - const now = new Date() - - // Delete expired auth codes - await db - .delete(oauthAuthorizationCodes) - .where(lt(oauthAuthorizationCodes.expiresAt, now)) - - // Delete expired refresh tokens - await db - .delete(oauthRefreshTokens) - .where(lt(oauthRefreshTokens.expiresAt, now)) - - // Delete expired access tokens - await db.delete(oauthAccessTokens).where(lt(oauthAccessTokens.expiresAt, now)) -} diff --git a/src/auth/repositories.server.ts b/src/auth/repositories.server.ts deleted file mode 100644 index 225065bf2..000000000 --- a/src/auth/repositories.server.ts +++ /dev/null @@ -1,338 +0,0 @@ -/** - * Auth Repositories - * - * Implementation of repository interfaces using the application's database. - * This is the bridge between the auth module and the actual data layer. - * - * Note: This file imports from the application's database, making it - * application-specific. The auth module itself (types, services) remains - * database-agnostic through the repository interfaces. - */ - -import { db } from '~/db/client' -import { users, oauthAccounts, roles, roleAssignments } from '~/db/schema' -import { eq, and, inArray, sql } from 'drizzle-orm' -import type { - Capability, - DbUser, - ICapabilitiesRepository, - IOAuthAccountRepository, - IUserRepository, - OAuthProvider, - SignupSource, -} from './types' -import { encryptToken, decryptStoredToken } from '~/utils/crypto.server' - -// ============================================================================ -// User Repository Implementation -// ============================================================================ - -export class DrizzleUserRepository implements IUserRepository { - async findById(userId: string): Promise { - const user = await db.query.users.findFirst({ - where: eq(users.id, userId), - }) - - if (!user) return null - - return this.mapToDbUser(user) - } - - async findByEmail(email: string): Promise { - const user = await db.query.users.findFirst({ - where: eq(users.email, email), - }) - - if (!user) return null - - return this.mapToDbUser(user) - } - - async create(data: { - email: string - name?: string - image?: string - oauthImage?: string - displayUsername?: string - capabilities?: Capability[] - signupSources?: SignupSource[] - }): Promise { - const [newUser] = await db - .insert(users) - .values({ - email: data.email, - name: data.name, - image: data.image, - oauthImage: data.oauthImage, - displayUsername: data.displayUsername, - capabilities: data.capabilities || [], - signupSources: data.signupSources ?? [], - }) - .returning() - - if (!newUser) { - throw new Error('Failed to create user') - } - - return this.mapToDbUser(newUser) - } - - async update( - userId: string, - data: Partial<{ - email: string - name: string - image: string | null - oauthImage: string - displayUsername: string - capabilities: Capability[] - adsDisabled: boolean - interestedInHidingAds: boolean - lastUsedFramework: string - sessionVersion: number - signupSources: SignupSource[] - updatedAt: Date - }>, - ): Promise { - await db - .update(users) - .set({ ...data, updatedAt: data.updatedAt || new Date() }) - .where(eq(users.id, userId)) - } - - async incrementSessionVersion(userId: string): Promise { - await db - .update(users) - .set({ - sessionVersion: sql`${users.sessionVersion} + 1`, - updatedAt: new Date(), - }) - .where(eq(users.id, userId)) - } - - private mapToDbUser(user: typeof users.$inferSelect): DbUser { - return { - id: user.id, - email: user.email, - name: user.name, - image: user.image, - oauthImage: user.oauthImage, - displayUsername: user.displayUsername, - capabilities: user.capabilities as Capability[], - adsDisabled: user.adsDisabled, - interestedInHidingAds: user.interestedInHidingAds, - lastUsedFramework: user.lastUsedFramework, - signupSources: user.signupSources, - sessionVersion: user.sessionVersion, - createdAt: user.createdAt, - updatedAt: user.updatedAt, - } - } -} - -// ============================================================================ -// OAuth Account Repository Implementation -// ============================================================================ - -export class DrizzleOAuthAccountRepository implements IOAuthAccountRepository { - async findByProviderAndAccountId( - provider: OAuthProvider, - providerAccountId: string, - ): Promise<{ - userId: string - accessToken: string | null - tokenScope: string | null - } | null> { - const account = await db.query.oauthAccounts.findFirst({ - where: and( - eq(oauthAccounts.provider, provider), - eq(oauthAccounts.providerAccountId, providerAccountId), - ), - }) - - if (!account) return null - - return { - userId: account.userId, - accessToken: decryptStoredToken(account.accessToken), - tokenScope: account.tokenScope, - } - } - - async findByUserId( - userId: string, - provider: OAuthProvider, - ): Promise<{ accessToken: string | null; tokenScope: string | null } | null> { - const account = await db.query.oauthAccounts.findFirst({ - where: and( - eq(oauthAccounts.userId, userId), - eq(oauthAccounts.provider, provider), - ), - }) - - if (!account) return null - - return { - accessToken: decryptStoredToken(account.accessToken), - tokenScope: account.tokenScope, - } - } - - async create(data: { - userId: string - provider: OAuthProvider - providerAccountId: string - email: string - accessToken?: string - tokenScope?: string - }): Promise { - await db.insert(oauthAccounts).values({ - userId: data.userId, - provider: data.provider, - providerAccountId: data.providerAccountId, - email: data.email, - accessToken: data.accessToken - ? encryptToken(data.accessToken) - : undefined, - tokenScope: data.tokenScope, - }) - } - - async updateToken( - userId: string, - provider: OAuthProvider, - accessToken: string, - tokenScope: string, - ): Promise { - await db - .update(oauthAccounts) - .set({ - accessToken: encryptToken(accessToken), - tokenScope, - }) - .where( - and( - eq(oauthAccounts.userId, userId), - eq(oauthAccounts.provider, provider), - ), - ) - } -} - -// ============================================================================ -// Capabilities Repository Implementation -// ============================================================================ - -export class DrizzleCapabilitiesRepository implements ICapabilitiesRepository { - async getEffectiveCapabilities(userId: string): Promise { - // Single query to get both user capabilities and role capabilities - const result = await db - .select({ - userCapabilities: users.capabilities, - roleCapabilities: roles.capabilities, - }) - .from(users) - .leftJoin(roleAssignments, eq(roleAssignments.userId, users.id)) - .leftJoin(roles, eq(roles.id, roleAssignments.roleId)) - .where(eq(users.id, userId)) - - if (result.length === 0) { - return [] - } - - // Extract user capabilities (same for all rows) - const directCapabilities = (result[0]?.userCapabilities || - []) as Capability[] - - // Collect all role capabilities from all rows - const roleCapabilities = result - .map((r) => r.roleCapabilities) - .filter( - (caps): caps is Capability[] => caps !== null && Array.isArray(caps), - ) - .flat() as Capability[] - - // Union of direct capabilities and role capabilities - const effectiveCapabilities = Array.from( - new Set([...directCapabilities, ...roleCapabilities]), - ) - - return effectiveCapabilities - } - - async getBulkEffectiveCapabilities( - userIds: string[], - ): Promise> { - if (userIds.length === 0) { - return {} - } - - // Single query to get all user capabilities and role capabilities for all users - const result = await db - .select({ - userId: users.id, - userCapabilities: users.capabilities, - roleCapabilities: roles.capabilities, - }) - .from(users) - .leftJoin(roleAssignments, eq(roleAssignments.userId, users.id)) - .leftJoin(roles, eq(roles.id, roleAssignments.roleId)) - .where(inArray(users.id, userIds)) - - // Group results by userId - const userCapabilitiesMap: Record = {} - const userRoleCapabilitiesMap: Record = {} - - for (const row of result) { - const userId = row.userId - - // Store direct capabilities (same for all rows of the same user) - if (!userCapabilitiesMap[userId]) { - userCapabilitiesMap[userId] = (row.userCapabilities || - []) as Capability[] - } - - // Collect role capabilities - if (row.roleCapabilities && Array.isArray(row.roleCapabilities)) { - if (!userRoleCapabilitiesMap[userId]) { - userRoleCapabilitiesMap[userId] = [] - } - userRoleCapabilitiesMap[userId].push( - ...(row.roleCapabilities as Capability[]), - ) - } - } - - // Compute effective capabilities for each user - const effectiveCapabilitiesMap: Record = {} - - for (const userId of userIds) { - const directCapabilities = userCapabilitiesMap[userId] || [] - const roleCapabilities = userRoleCapabilitiesMap[userId] || [] - - // Union of direct capabilities and role capabilities - const effectiveCapabilities = Array.from( - new Set([...directCapabilities, ...roleCapabilities]), - ) - - effectiveCapabilitiesMap[userId] = effectiveCapabilities - } - - return effectiveCapabilitiesMap - } -} - -// ============================================================================ -// Repository Factory -// ============================================================================ - -/** - * Create all repository instances - */ -export function createRepositories() { - return { - userRepository: new DrizzleUserRepository(), - oauthAccountRepository: new DrizzleOAuthAccountRepository(), - capabilitiesRepository: new DrizzleCapabilitiesRepository(), - } -} diff --git a/src/auth/session.server.ts b/src/auth/session.server.ts deleted file mode 100644 index 2e6104b00..000000000 --- a/src/auth/session.server.ts +++ /dev/null @@ -1,249 +0,0 @@ -/** - * Session Management Module - * - * Handles cookie-based session management with HMAC-SHA256 signing. - * This module is framework-agnostic and uses Web Crypto API. - */ - -import type { SessionCookieData, ISessionService } from './types' - -// ============================================================================ -// Base64URL Utilities -// ============================================================================ - -function base64UrlEncode(str: string): string { - return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') -} - -function base64UrlDecode(str: string): string { - const normalized = str.replace(/-/g, '+').replace(/_/g, '/') - const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=') - return atob(padded) -} - -// ============================================================================ -// Session Service Implementation -// ============================================================================ - -export class SessionService implements ISessionService { - private secret: string - private isProduction: boolean - - constructor(secret: string, isProduction: boolean = false) { - if (isProduction && secret === 'dev-secret-key-change-in-production') { - throw new Error('SESSION_SECRET must be set in production') - } - this.secret = secret - this.isProduction = isProduction - } - - /** - * Sign cookie data using HMAC-SHA256 - */ - async signCookie(data: SessionCookieData): Promise { - const payload = `${data.userId}:${data.expiresAt}:${data.version}` - const payloadBase64 = base64UrlEncode(payload) - - const encoder = new TextEncoder() - const keyData = encoder.encode(this.secret) - const messageData = encoder.encode(payloadBase64) - - const key = await crypto.subtle.importKey( - 'raw', - keyData, - { name: 'HMAC', hash: 'SHA-256' }, - false, - ['sign'], - ) - - const signature = await crypto.subtle.sign('HMAC', key, messageData) - const signatureArray = new Uint8Array(signature) - - let signatureStr = '' - for (let i = 0; i < signatureArray.length; i++) { - signatureStr += String.fromCharCode(signatureArray[i]) - } - const signatureBase64 = base64UrlEncode(signatureStr) - - return `${payloadBase64}.${signatureBase64}` - } - - /** - * Verify and parse signed cookie - */ - async verifyCookie(signedCookie: string): Promise { - try { - const [payloadBase64, signatureBase64] = signedCookie.split('.') - - if (!payloadBase64 || !signatureBase64) { - return null - } - - const encoder = new TextEncoder() - const keyData = encoder.encode(this.secret) - const messageData = encoder.encode(payloadBase64) - - const key = await crypto.subtle.importKey( - 'raw', - keyData, - { name: 'HMAC', hash: 'SHA-256' }, - false, - ['verify'], - ) - - const signatureStr = base64UrlDecode(signatureBase64) - const signature = Uint8Array.from(signatureStr, (c) => c.charCodeAt(0)) - - const isValid = await crypto.subtle.verify( - 'HMAC', - key, - signature, - messageData, - ) - - if (!isValid) { - return null - } - - const payload = base64UrlDecode(payloadBase64) - const [userId, expiresAtStr, versionStr] = payload.split(':') - - if (!userId || !expiresAtStr || !versionStr) { - return null - } - - const expiresAt = parseInt(expiresAtStr, 10) - const version = parseInt(versionStr, 10) - - // Check expiration - if (Date.now() > expiresAt) { - return null - } - - return { - userId, - expiresAt, - version, - } - } catch (error) { - console.error( - '[SessionService] Error verifying cookie:', - error instanceof Error ? error.message : 'Unknown error', - ) - return null - } - } - - /** - * Read session cookie from request - */ - getSessionCookie(request: Request): string | null { - const cookies = request.headers.get('cookie') || '' - const sessionCookie = cookies - .split(';') - .find((c) => c.trim().startsWith('session_token=')) - - if (!sessionCookie) { - return null - } - - const tokenValue = sessionCookie.split('=').slice(1).join('=').trim() - const sessionToken = decodeURIComponent(tokenValue) - return sessionToken || null - } - - /** - * Create session cookie header value - */ - createSessionCookieHeader(signedCookie: string, maxAge: number): string { - return `session_token=${encodeURIComponent(signedCookie)}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax${this.isProduction ? '; Secure' : ''}` - } - - /** - * Create clear session cookie header value - */ - createClearSessionCookieHeader(): string { - return `session_token=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax${this.isProduction ? '; Secure' : ''}` - } -} - -// ============================================================================ -// OAuth State Cookie Utilities -// ============================================================================ - -export function generateOAuthState(): string { - const stateBytes = new Uint8Array(16) - crypto.getRandomValues(stateBytes) - const base64 = btoa(String.fromCharCode(...stateBytes)) - return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') -} - -export function createOAuthStateCookie( - state: string, - isProduction: boolean, -): string { - // Use SameSite=Lax to allow the cookie to be sent on OAuth redirects back from providers - // Strict would block the cookie since the redirect comes from an external domain (GitHub/Google) - return `oauth_state=${encodeURIComponent(state)}; HttpOnly; Path=/; Max-Age=${10 * 60}; SameSite=Lax${isProduction ? '; Secure' : ''}` -} - -export function clearOAuthStateCookie(isProduction: boolean): string { - return `oauth_state=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax${isProduction ? '; Secure' : ''}` -} - -export function getOAuthStateCookie(request: Request): string | null { - const cookies = request.headers.get('cookie') || '' - const stateCookie = cookies - .split(';') - .find((c) => c.trim().startsWith('oauth_state=')) - - if (!stateCookie) { - return null - } - - return decodeURIComponent(stateCookie.split('=').slice(1).join('=').trim()) -} - -export function createOAuthPopupCookie(isProduction: boolean): string { - return `oauth_popup=1; HttpOnly; Path=/; Max-Age=${10 * 60}; SameSite=Lax${isProduction ? '; Secure' : ''}` -} - -export function clearOAuthPopupCookie(isProduction: boolean): string { - return `oauth_popup=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax${isProduction ? '; Secure' : ''}` -} - -export function isOAuthPopupMode(request: Request): boolean { - const cookies = request.headers.get('cookie') || '' - return cookies.split(';').some((c) => c.trim().startsWith('oauth_popup=1')) -} - -export function createOAuthReturnToCookie( - returnTo: string, - isProduction: boolean, -): string { - return `oauth_return_to=${encodeURIComponent(returnTo)}; HttpOnly; Path=/; Max-Age=${10 * 60}; SameSite=Lax${isProduction ? '; Secure' : ''}` -} - -export function clearOAuthReturnToCookie(isProduction: boolean): string { - return `oauth_return_to=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax${isProduction ? '; Secure' : ''}` -} - -export function getOAuthReturnTo(request: Request): string | null { - const cookies = request.headers.get('cookie') || '' - const returnToCookie = cookies - .split(';') - .find((c) => c.trim().startsWith('oauth_return_to=')) - - if (!returnToCookie) { - return null - } - - return decodeURIComponent(returnToCookie.split('=').slice(1).join('=').trim()) -} - -// ============================================================================ -// Session Constants -// ============================================================================ - -export const SESSION_DURATION_MS = 30 * 24 * 60 * 60 * 1000 // 30 days -export const SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60 // 30 days diff --git a/src/auth/types.ts b/src/auth/types.ts deleted file mode 100644 index e4af8e21a..000000000 --- a/src/auth/types.ts +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Auth Module Types - * - * This file defines the core types and interfaces for the authentication module. - * These types are designed to be framework-agnostic and can be used by both - * server and client code. - */ - -// Re-export shared types from db/types.ts (single source of truth) -export type { Capability, OAuthProvider, SignupSource } from '~/db/types' -export { CAPABILITIES as VALID_CAPABILITIES } from '~/db/types' - -import type { Capability, OAuthProvider, SignupSource } from '~/db/types' - -export interface OAuthProfile { - id: string - email: string - name?: string - image?: string -} - -export interface OAuthResult { - userId: string - isNewUser: boolean -} - -// ============================================================================ -// Session Types -// ============================================================================ - -export interface SessionCookieData { - userId: string - expiresAt: number // Unix timestamp in milliseconds - version: number // sessionVersion from users table for revocation -} - -// ============================================================================ -// User Types -// ============================================================================ - -/** - * Authenticated user data returned from session validation - */ -export interface AuthUser { - userId: string - email: string - name: string | null - image: string | null - oauthImage: string | null - displayUsername: string | null - capabilities: Capability[] - adsDisabled: boolean | null - interestedInHidingAds: boolean | null - lastUsedFramework: string | null - signupSources: SignupSource[] -} - -/** - * Database user record (used by data access layer) - */ -export interface DbUser { - id: string - email: string - name: string | null - image: string | null - oauthImage: string | null - displayUsername: string | null - capabilities: Capability[] - adsDisabled: boolean | null - interestedInHidingAds: boolean | null - lastUsedFramework: string | null - signupSources: SignupSource[] - sessionVersion: number - createdAt: Date - updatedAt: Date -} - -// ============================================================================ -// Data Access Interfaces (for Inversion of Control) -// ============================================================================ - -/** - * User repository interface for database operations - * Implement this interface to inject database access into the auth module - */ -export interface IUserRepository { - findById(userId: string): Promise - findByEmail(email: string): Promise - create(data: { - email: string - name?: string - image?: string - oauthImage?: string - displayUsername?: string - capabilities?: Capability[] - signupSources?: SignupSource[] - }): Promise - update( - userId: string, - data: Partial<{ - email: string - name: string - image: string | null - oauthImage: string - displayUsername: string - capabilities: Capability[] - adsDisabled: boolean - interestedInHidingAds: boolean - lastUsedFramework: string - signupSources: SignupSource[] - sessionVersion: number - updatedAt: Date - }>, - ): Promise - incrementSessionVersion(userId: string): Promise -} - -/** - * OAuth account repository interface - */ -export interface IOAuthAccountRepository { - findByProviderAndAccountId( - provider: OAuthProvider, - providerAccountId: string, - ): Promise<{ - userId: string - accessToken: string | null - tokenScope: string | null - } | null> - findByUserId( - userId: string, - provider: OAuthProvider, - ): Promise<{ accessToken: string | null; tokenScope: string | null } | null> - create(data: { - userId: string - provider: OAuthProvider - providerAccountId: string - email: string - accessToken?: string - tokenScope?: string - }): Promise - updateToken( - userId: string, - provider: OAuthProvider, - accessToken: string, - tokenScope: string, - ): Promise -} - -/** - * Capabilities repository interface - */ -export interface ICapabilitiesRepository { - getEffectiveCapabilities(userId: string): Promise - getBulkEffectiveCapabilities( - userIds: string[], - ): Promise> -} - -// ============================================================================ -// Service Interfaces -// ============================================================================ - -/** - * Session service interface for cookie-based session management - */ -export interface ISessionService { - signCookie(data: SessionCookieData): Promise - verifyCookie(signedCookie: string): Promise - getSessionCookie(request: Request): string | null - createSessionCookieHeader(signedCookie: string, maxAge: number): string - createClearSessionCookieHeader(): string -} - -/** - * Auth service interface for user authentication - */ -export interface IAuthService { - getCurrentUser(request: Request): Promise - validateSession( - sessionData: SessionCookieData, - ): Promise<{ user: DbUser; capabilities: Capability[] } | null> -} - -/** - * OAuth service interface for OAuth operations - */ -export interface IOAuthService { - upsertOAuthAccount( - provider: OAuthProvider, - profile: OAuthProfile, - tokenInfo?: { accessToken: string; scope: string }, - ): Promise -} - -// ============================================================================ -// Auth Context (Dependency Injection Container) -// ============================================================================ - -/** - * Auth context contains all dependencies required by the auth module - * Use this to inject implementations at runtime - */ -export interface AuthContext { - userRepository: IUserRepository - oauthAccountRepository: IOAuthAccountRepository - capabilitiesRepository: ICapabilitiesRepository - sessionSecret: string - isProduction: boolean -} - -// ============================================================================ -// Error Types -// ============================================================================ - -export class AuthError extends Error { - constructor( - message: string, - public code: AuthErrorCode, - ) { - super(message) - this.name = 'AuthError' - } -} - -export type AuthErrorCode = - | 'NOT_AUTHENTICATED' - | 'MISSING_CAPABILITY' - | 'INVALID_SESSION' - | 'SESSION_EXPIRED' - | 'SESSION_REVOKED' - | 'USER_NOT_FOUND' - | 'OAUTH_ERROR' diff --git a/src/blog/ag-ui-compliance.md b/src/blog/ag-ui-compliance.md deleted file mode 100644 index 26f4f2e2a..000000000 --- a/src/blog/ag-ui-compliance.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -title: 'TanStack AI now fully speaks AG-UI' -published: 2026-05-17 -excerpt: 'Server-to-client AG-UI events already worked. TanStack AI now completes the round trip with client-to-server AG-UI compliance. Fully backward compatible.' -library: ai -authors: - - Alem Tuzlak ---- - -![TanStack AI now fully speaks AG-UI](/blog-assets/ag-ui-compliance/header.png) - -Half the protocol was already there. - -For a while now, endpoints built with `@tanstack/ai` have emitted [AG-UI](https://ag-ui.com) events on the wire going out. The streaming side of the conversation (`RUN_STARTED`, tool-call events, run finish, errors) was already a compliant AG-UI event stream. The piece that was still proprietary was the _other_ direction: the request body going from client to server. The TanStack client POSTed `{ messages, data }`, not AG-UI's `RunAgentInput`. - -That last half is what this release fixes. **TanStack AI is now fully AG-UI compliant in both directions.** Server-to-client events were AG-UI before. Client-to-server requests are AG-UI now. The round trip is done. - -The same `@tanstack/ai-client` can hit any AG-UI server. Any AG-UI client can hit an endpoint built with `@tanstack/ai`, wherever you host it (TanStack Start, Next.js, Hono, raw Node, Bun, anywhere). And nothing about your existing code breaks. - -## Why this matters - -AG-UI is an open protocol for agent-to-frontend communication. It defines a single wire format, `RunAgentInput`, that carries the conversation, the tools, the thread and run IDs, and arbitrary forwarded properties. Servers that speak AG-UI can be addressed by any compliant client. Clients that emit AG-UI can talk to any compliant server. - -With server-to-client AG-UI already in place, a `@tanstack/ai` endpoint could _stream_ to a compliant client. But the client-to-server side was a one-way mirror: only the TanStack client could _send_ requests that endpoint understood. The asymmetry meant true cross-vendor interop was still gated on rewriting your request layer. - -Closing that gap is what this release does. The whole ecosystem (CopilotKit, CrewAI, LangGraph adapters, and now TanStack AI) gets to share the same plumbing in both directions. - -## What changed on the wire - -Before this release, `@tanstack/ai-client` POSTed: - -```json -{ - "messages": [...], - "data": { ... } -} -``` - -After: - -```json -{ - "threadId": "thread-7f2a", - "runId": "run-a91", - "state": {}, - "messages": [...], - "tools": [...], - "context": [], - "forwardedProps": { ... }, - "data": { ... } -} -``` - -The new envelope is the full AG-UI `RunAgentInput`. The old `data` field is still emitted as a mirror of `forwardedProps` so legacy servers reading `body.data.X` keep working unchanged. `threadId` persists per session, `runId` is fresh per send, and `tools` carries the client's `clientTools` declarations so the server can dispatch tool calls without a static registry. - -Server-to-client events haven't changed shape, because they were already AG-UI compliant. They just now carry the matching `threadId` and `runId` you sent in. - -## What changed in the API - -Three new things to know about, all opt-in. - -### `chat()` accepts `threadId`, `runId`, `parentRunId` - -These were always part of the AG-UI event semantics on the way out. They're now first-class options on `chat()` and flow through every provider adapter into `RUN_STARTED` events for observability and run correlation. - -```ts -import { chat } from '@tanstack/ai' -import { openaiText } from '@tanstack/ai-openai/adapters' - -const stream = chat({ - adapter: openaiText('gpt-4o'), - threadId: 'thread-7f2a', - runId: 'run-a91', - messages: [...], -}) -``` - -If you don't pass them, the runtime auto-generates a stable `threadId` per request and a fresh `runId` per call. Existing code that didn't know about them keeps working. - -### `chatParamsFromRequest` for the server - -A one-import helper that reads `req.json()`, validates the body against the AG-UI `RunAgentInputSchema`, and gives you a clean params object. On invalid input it throws a `400 Response` that frameworks like TanStack Start, SolidStart, Remix, and React Router 7 return to the client automatically. - -```ts -import { - chat, - chatParamsFromRequest, - toServerSentEventsResponse, -} from '@tanstack/ai' -import { openaiText } from '@tanstack/ai-openai/adapters' - -export async function POST(req: Request) { - const params = await chatParamsFromRequest(req) - const stream = chat({ - adapter: openaiText('gpt-4o'), - messages: params.messages, - threadId: params.threadId, - tools: serverTools, - }) - return toServerSentEventsResponse(stream) -} -``` - -That's the whole server. No body shape to remember, no manual validation, and a typed `params.forwardedProps` if you want client-driven options like provider, model, or temperature. - -### `forwardedProps` replaces `body` on the client - -`useChat({ body: {...} })` still works, but `body` is now `@deprecated`. The canonical name is `forwardedProps`, which is what the new wire format calls the field. A jscodeshift codemod ships in the repo to flip every site: - -```bash -npx jscodeshift \ - --parser=tsx \ - -t https://raw.githubusercontent.com/TanStack/ai/main/codemods/ag-ui-compliance/transform.ts \ - "src/**/*.{ts,tsx}" -``` - -It's import-source gated, so files that don't import from `@tanstack/ai*` are left alone. - -## Nothing breaks - -This is the part most "wire format change" releases get wrong. The upgrade ships three compatibility bridges so old code keeps running: - -| Surface | Legacy (still works) | Canonical | -| ---------------------- | ---------------------------------------- | ------------------------- | -| Client option | `body: { ... }` | `forwardedProps: { ... }` | -| Server wire field | `body.data.X` (mirror of forwardedProps) | `body.forwardedProps.X` | -| Server `chat()` option | `conversationId` | `threadId` | - -An existing endpoint reading `body.data.provider` keeps reading `body.data.provider` because the client emits both `data` and `forwardedProps` with the same content. A `chat({ conversationId })` call keeps working because `conversationId` is now a deprecated alias of `threadId`. Mix old and new freely. The bridges will be removed in the next major release, so migrate at your convenience. - -## Bidirectional interop in practice - -With both halves of the protocol compliant, the boundaries between AI SDKs get a lot blurrier. - -**A pure AG-UI client (no TanStack code) hitting a `@tanstack/ai` endpoint** works end-to-end. Tool messages pass through as `ModelMessage` entries with `role: 'tool'`. AG-UI `reasoning` and `activity` messages with no TanStack equivalent are dropped at the boundary. `developer` messages collapse to `system` role. The outbound event stream was already AG-UI, so the foreign client renders it natively. - -**A TanStack client hitting a foreign AG-UI server** works for the common cases. Single-turn user messages mirror to AG-UI's `content` field. Server-emitted events stream and render. Multi-turn history with tool results from prior turns survives because the client sends AG-UI fan-out duplicates alongside the TanStack anchor messages. - -The practical upshot: if you've been waiting to try a different inference provider, a different framework's agent runtime, or a different orchestrator, the wire is no longer the thing standing in your way. Both directions speak the same language. - -## What's not in this release - -A few things were intentionally left out: - -- **Reasoning replay to LLM providers.** TanStack still drops `ThinkingPart` at the `UIMessage` → `ModelMessage` boundary. Providers like Anthropic that require thinking blocks to be replayed for extended thinking continuation are a separate track. -- **AG-UI `state` and `context` fields.** Surfaced on the params object but not yet wired into `chat()`. They're available for your endpoint to inspect or forward. -- **PHP and Python server packages.** No `chatParamsFromRequest` parity yet. Those examples temporarily lag on the old shape until the matching helpers ship. - -## Try it - -Upgrade `@tanstack/ai` and `@tanstack/ai-client` to the latest. If you're using one of the framework wrappers (`@tanstack/ai-react`, `-vue`, `-svelte`, `-solid`, `-preact`), bump those too so the client wire stays in lockstep. - -- [Migration guide](https://tanstack.com/ai/migration/ag-ui-compliance) walks through the three deprecation bridges and the codemod -- [Star the repo](https://github.com/TanStack/ai) if this saved you an adapter - -The AI stack is supposed to be the part you compose, not the part that locks you in. AG-UI is how that starts being true across vendors. With this release, TanStack AI is the first SDK to ship full bidirectional client-to-server _and_ server-to-client compliance against the AG-UI 0.0.52 spec. The next agent runtime you adopt should not be the one that finally forces you to rewrite your wire layer. diff --git a/src/blog/announcing-tanstack-form-v1.md b/src/blog/announcing-tanstack-form-v1.md deleted file mode 100644 index d26fcd08a..000000000 --- a/src/blog/announcing-tanstack-form-v1.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -title: Announcing TanStack Form v1 -published: 2025-03-03 -excerpt: The first stable version of TanStack Form is live and ready for production. We support five frameworks at launch — React, Vue, Angular, Solid, and Lit. -library: form -authors: - - Corbin Crutchley ---- - -![TanStack Form v1](/blog-assets/announcing-tanstack-form-v1/form_header.png) - -We're excited to announce the first stable version of [TanStack Form](/form/v1) is live and ready for usage in production! 🥳 - -We support five frameworks at launch: React, Vue, Angular, Solid, and Lit, as well as a myriad of features for each specific framework. - -# How to install - -```shell -$ npm i @tanstack/react-form -# or -$ npm i @tanstack/vue-form -# or -$ npm i @tanstack/angular-form -# or -$ npm i @tanstack/solid-form -# or -$ npm i @tanstack/lit-form -``` - -# A bit of history - -It was nearly two years ago when [I saw Tanner's BlueSky (an invite-only platform at the time) post announcing that he was working on a new project: TanStack Form](https://bsky.app/profile/tannerlinsley.com/post/3ju5z473w5525). - -![A back and forth between Tanner and myself on Bluesky about TanStack Form](/blog-assets/announcing-tanstack-form-v1/tanstack_form_bluesky_announce.png) - -At the time, I had just launched an alternative form library for React called "[HouseForm](https://web.archive.org/web/20240101000000*/houseform.dev)" and I was immediately enamored by some of the ideas Tanner's library brought to the table. - -I was fortunate enough to attend a hackathon that Tanner was also going to soon after and we were able to get some time to work on integrating some APIs from HouseForm into the project. - -Since that time, Tanner's handed much of the reigns of Form over to me and a wonderful group of additional maintainers. - -So, what have we built in that time? - -# Features - -One of the advantages of being in the oven for so long is that TanStack Form launches with a flurry of features you can leverage day one. - -Let's go over _just a few_ of them using React's adapter as examples. - -## Extreme type safety - -Like many all of the TanStack projects, Form has revolutionized what it means to be a "type-safe" form library. - -```tsx -const form = useForm({ - defaultValues: { - name: "", - age: 0 - } -}); - -// TypeScript will correctly tell you that `firstName` is not a valid field - - -// TypeScript will correctly tell you that `name`'s type is a `string`, not a `number` - }/> -``` - -We even support type-checking what errors are returned in ``: - -```tsx - (value < 12 ? { tooYoung: true } : undefined), - }} - children={(field) => ( - <> - - // TypeScript will correctly tell you that `errorMap.onChange` // is an - object, not a string -

{field.state.meta.errorMap.onChange}

- - )} -/> -``` - -> Oh, yeah, we support field-based validation as well as form validation. Mix-n-match them! - -The best part? [You won't need to pass any typescript generics to get this level of type safety](/form/latest/docs/philosophy#generics-are-grim). Everything is inferred from your runtime usage. - -## Schema validation - -Thanks to the awesome work by the creators of [Zod](http://zod.dev/), [Valibot](https://valibot.dev), and [ArkType](https://arktype.io/), we support [Standard Schema](https://github.com/standard-schema/standard-schema) out of the box; no other packages needed. - -```tsx -const userSchema = z.object({ - age: z.number().gte(13, 'You must be 13 to make an account'), -}) - -function App() { - const form = useForm({ - defaultValues: { - age: 0, - }, - validators: { - onChange: userSchema, - }, - }) - return ( -
- { - return <>{/* ... */} - }} - /> -
- ) -} -``` - -## Async validation - -That's not all, though! We also support async functions to validate your code; complete with built-in debouncing and [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)-based cancellation: - -```tsx - { - const currentAge = await fetchCurrentAgeOnProfile({ signal }) - return value < currentAge ? 'You can only increase the age' : undefined - }, - }} -/> -``` - -## Platform support - -Not only do we support multiple frameworks as we mentioned from the start; we support multiple runtimes. Whether you're using React Native, NativeScript, or even SSR solutions like Next.js or [TanStack Start](/start), we have you covered. - -In fact, if you're using SSR solutions, we even make server-side form validation a breeze: - -```typescript -// app/routes/index.tsx, but can be extracted to any other path -import { createServerValidate, getFormData } from '@tanstack/react-form/start' -import { yourSchemaHere } from '~/constants/forms' - -const serverValidate = createServerValidate({ - ...formOpts, - onServerValidate: yourSchemaHere, -}) - -export const getFormDataFromServer = createServerFn({ method: 'GET' }).handler( - async () => { - return getFormData() - }, -) -``` - -> This code sample excludes some of the relevant code to keep things glanceable. [For more details on our SSR integration, please check our docs.](/form/latest/docs/framework/react/guides/ssr) - -And boom, the exact same validation logic is running on both your frontend and backend. Your forms will even show errors when JavaScript is disabled on the user's browser! - -# A look forward - -We're not resting on our laurels, however - we have plans to add new features to v1 now that we're stable. These features include: - -- [Persistence APIs](https://github.com/TanStack/form/pull/561) -- [A Svelte 5 adapter](https://github.com/TanStack/form/issues/516) -- [Better DX for transforming values on submission](https://github.com/TanStack/form/issues/418) -- [Form Groups](https://github.com/TanStack/form/issues/419) - -And much more. - -# Thank **you** - -There's so many people I'd like to thank that once I'd gotten started I'd never end. Instead, I'll address each group of folks I want to thank. - -- Thank you to our contributors: So many people had to come together to make this happen. From maintainers of other TanStack projects giving us guidance, to drive-by PRs; it all helped us get across the line. - -- Thank you to our early adopters: The ones who took a risk on us and provided invaluable feedback on our APIs and functionality. -- Thank you to the content creators who covered our tools: You brought more eyes to our project - making it better through education and feedback. -- Thank you to the broader community: Your excitement to use our tools have driven the team immensely. - -And finally, thank **you** for taking the time to read and explore our newest tool. ❤️ diff --git a/src/blog/announcing-tanstack-query-v5.md b/src/blog/announcing-tanstack-query-v5.md deleted file mode 100644 index b061bdea3..000000000 --- a/src/blog/announcing-tanstack-query-v5.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Announcing TanStack Query v5 -published: 2023-10-17 -excerpt: After 91 alpha releases, 35 betas, and 16 release candidates, TanStack Query v5.0.0 is finally here — smaller, better, and more intuitive than ever. -library: query -authors: - - Dominik Dorfmeister ---- - -About one year ago, we announced the [TanStack Query v5 roadmap](https://github.com/TanStack/query/discussions/4252), and the whole team has been working hard on that version ever since. So we're super happy to announce that today is the day: After 91 alpha releases, 35 betas and 16 release candidates, TanStack Query [v5.0.0](https://github.com/TanStack/query/releases/tag/v5.0.0) is finally here! 🎉 - -v5 continues the journey of v4, trying to make TanStack Query smaller (v5 is ~20% smaller than v4), better and more intuitive to use. One of the main focus points for this release was around streamlining and simplifying the APIs we offer: - -## Breaking changes - -As a big breaking change, we've removed most overloads from the codebase, unifying how you use `useQuery` and other hooks. This is something we wanted to do for v4, but a [TypeScript limitation](https://github.com/microsoft/TypeScript/issues/43371) prevented us from doing that. TypeScript addressed this issue in TS 4.7, so we were able to remove all the overloads that we had for calling `useQuery` with a different amount of parameters. This is a huge DX win, because methods with overloads usually have quite bad TypeScript error messages. - -This is the biggest breaking change in v5, but we think it's worth it. The API is now much more consistent - you always just pass _one_ object. To alleviate the pain of changing all occurrences manually, we have tried to prepare everyone for this coming change for the last months. The documentation was changed to use the new API, and we released an auto-fixable [eslint rule](/query/v4/docs/eslint/prefer-query-object-syntax) in our eslint package. Additionally, v5 comes with [a codemod](/query/v5/docs/react/guides/migrating-to-v5#codemod) to help with the transition. - -Apart from that, we've renamed `cacheTime` to `gcTime` to better reflect what it is doing, merged `keepPreviousData` with `placeholderData`, renamed `loading` states to `pending` and [removed the callbacks](https://github.com/TanStack/query/discussions/5279) from `useQuery`. All these changes make v5 the most consistent and best version for new starters. - -To read more about the breaking changes, have a look at our [migration guide](/query/v5/docs/react/guides/migrating-to-v5). - -## New Features - -Of course, v5 comes loaded with amazing new features as well 🚀: - -### Simplified optimistic updates - -Enjoy a brand new, simplified way to perform optimistic updates by leveraging the returned `variables` from `useMutation`, without having to write code that updates the cache manually. For more details, have a look at the [optimistic updates documentation](/query/v5/docs/react/guides/optimistic-updates) - -### Sharable mutation state - -A frequently requested feature, as seen in this [two-year-old issue](https://github.com/TanStack/query/issues/2304), finally comes to life in v5: You can now get access to the state of all mutations, shared across components thanks to the new [useMutationState](/query/v5/docs/react/reference/useMutationState) hook. - -### 1st class `suspense` support - -That's right - `suspense` for data fetching is no longer experimental, but fully supported. React Query ships with new `useSuspenseQuery`, `useSuspenseInfiniteQuery` and `useSuspenseQueries` hooks. Have a look at the [suspense docs](/query/v5/docs/react/guides/suspense) to learn about the differences to the non-suspense versions. - -#### Streaming with React Server Components - -v5 also comes with an experimental integration for suspense on the server in nextJs, unifying the best of both worlds: The [react-query-next-experimental](/query/v5/docs/react/guides/advanced-ssr#experimental-streaming-without-prefetching-in-nextjs) adapter allows us to write a single `useSuspenseQuery`, which will initiate data fetching as early as possible: on the server, during SSR. It will then stream the result to the client, where it will be put into the cache automatically, giving us all the interactivity and data synchronization of React Query. - -### Improved Infinite Queries - -Infinite Queries can now [prefetch multiple pages](/query/v5/docs/react/guides/prefetching) at once, and you have the option to specify the [maximum amount of pages](/query/v5/docs/react/guides/infinite-queries#what-if-i-want-to-limit-the-number-of-pages) stored in the cache as well. - -### New Devtools - -The Query devtools have been re-written from scratch in a framework-agnostic way to make them available to all adapters. They also got a UI revamp and some new features like cache inline editing and light mode. - -### Fine-grained persistence - -Another long-standing [discussion from 2021](https://github.com/TanStack/query/discussions/2649) highlights the importance of fine-grained persistence with just-in-time restore capabilities (especially for mobile development) that the `PersistQueryClient` plugin doesn't have. With v5, we now have a new [experimental_createPersister](/query/v5/docs/react/plugins/createPersister) plugin that allows you to persist queries individually. - -### The `queryOptions` API - -Now that we have a unified way to call `useQuery` (with just one object as parameter), we can also build better abstractions on top of that. The new [queryOptions](/query/v5/docs/react/typescript#typing-query-options) function gives us a type-safe way to share our query definitions between `useQuery`and imperative methods like `queryClient.prefetchQuery`. On top of that, it can make `queryClient.getQueryData` type-safe as well. - ---- - -## THANK YOU - -We hope you're going to enjoy using v5 as much as we've enjoyed building it. What's left for us to say thanks to everyone who made this release possible. No matter if you're core contributor, implemented an issue from the roadmap, if you've fixed a typo in the docs or gave feedback on the alpha releases: Every contribution matters! It's the people that makes this library great, and we're blessed to have such an amazing community. ❤️ diff --git a/src/blog/announcing-tanstack-start-v1.md b/src/blog/announcing-tanstack-start-v1.md deleted file mode 100644 index eedc69561..000000000 --- a/src/blog/announcing-tanstack-start-v1.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: TanStack Start v1 Release Candidate -published: 2025-09-23 -excerpt: TanStack Start has officially reached a v1.0 Release Candidate. This is the build we expect to ship as 1.0, pending your final feedback, docs polish, and a few last-mile fixes. -library: start -authors: - - Tanner Linsley ---- - -![TanStack Start v1 Release Candidate](/blog-assets/announcing-tanstack-start-v1/header.png) - -TanStack Start has officially reached a **v1.0 Release Candidate**. This is the build we expect to ship as 1.0, pending your final feedback, docs polish, and a few last-mile fixes. Now’s the perfect time to kick the tires and help us validate the final stretch. When 1.0 ships, we’ll update this post (same URL) with any final notes. - -## Why this matters - -TanStack Start isn’t just another framework, it’s the next chapter in building type-safe, high-performance React apps without the heavy abstractions. Whether you’re coming from classic SPAs or dipping into SSR, Start gives you the flexibility to scale in the direction that fits your team. - -## What’s in v1 - -- **Type-safe, file-based routing** powered by TanStack Router -- **Server-first ergonomics** with isomorphic server functions -- **Built-in streaming**; **React Server Components** support is on the way -- **URL-as-state primitives** with runtime validation and full type-safety -- **Great SPA and SSR DX** with no lock-in or opaque magic -- **Deep Query integration** for prefetching, caching, and hydration - -Want the full backstory? Check out: - -- [Why choose TanStack Start and Router?](/blog/why-tanstack-start-and-router) -- [Why TanStack Start is ditching adapters](/blog/why-tanstack-start-is-ditching-adapters) - -## Get started or upgrade - -Follow the Getting Started guide to spin up a fresh app or upgrade an existing one: - -- [TanStack Start Docs](/start) - -That page will stay current with the exact commands for the RC and the final 1.0. - -## Path to 1.0 stable - -We plan to cut 1.0 shortly after collecting RC feedback. Expect a few small RC iterations; any breaking changes will be clearly documented. As we approach stable, only light polish remains. No major API shifts. - -> **Note:** React Server Components support is in active development and will land as a non-breaking v1.x addition. - -## Powered by partners - -We’re fortunate to build alongside an incredible lineup of partners whose support keeps TanStack open and moving fast: - -- [Code Rabbit](https://coderabbit.link/tanstack?utm_source=tanstack&via=tanstack) -- [Cloudflare](https://www.cloudflare.com?utm_source=tanstack) -- [AG Grid](https://ag-grid.com/react-data-grid/?utm_source=reacttable&utm_campaign=githubreacttable) -- [Netlify](https://netlify.com?utm_source=tanstack) -- [Neon](https://neon.tech?utm_source=tanstack) -- [Clerk](https://go.clerk.com/wOwHtuJ) -- [Convex](https://convex.dev?utm_source=tanstack) -- [Electric](https://electric-sql.com) -- [Sentry](https://sentry.io?utm_source=tanstack) -- [Prisma](https://www.prisma.io/?utm_source=tanstack&via=tanstack) -- [Strapi](https://strapi.link/tanstack-start) -- [Unkey](https://www.unkey.com/?utm_source=tanstack) -- [UI.dev / Bytes.dev](https://bytes.dev?utm_source-tanstack&utm_campaign=tanstack) -- [Nozzle](https://nozzle.io/?utm_source=tanstack&utm_campaign=tanstack) -- ...[Previous Partners](https://tanstack.com/partners?status=inactive) - -### Special thanks to Cloudflare and Netlify - -Their collaboration is a pivotal force driving the open web forward. Explore their announcements to see how we’re building the modern web together: [Cloudflare, Astro, and TanStack: building the modern web together](https://blog.cloudflare.com/cloudflare-astro-tanstack) and [Supporting an Open Web with Netlify + Cloudflare](https://www.netlify.com/blog/supporting-an-open-web-with-netlify-cloudflare/). - -## Your feedback counts - -This is the moment when your feedback matters most. Try the RC in a new or existing project and tell us what you think via the docs feedback links. If you hit an issue, the migration notes and examples in the docs will help. And we’ll be quick to respond. - -Thanks for building with us. Let’s ship 1.0, together. diff --git a/src/blog/debug-logging-for-tanstack-ai.md b/src/blog/debug-logging-for-tanstack-ai.md deleted file mode 100644 index a2b77690d..000000000 --- a/src/blog/debug-logging-for-tanstack-ai.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: 'One Flag, Every Chunk: Debug Logging Lands in TanStack AI' -published: 2026-04-22 -excerpt: "Your AI pipeline is a black box: a missing chunk, a middleware that doesn't fire, a tool call with mystery args. TanStack AI now ships pluggable, category-toggleable debug logging across every activity and adapter. Flip one flag and the pipeline prints itself." -library: ai -authors: - - Alem Tuzlak ---- - -![Debug Logging for TanStack AI](/blog-assets/debug-logging-for-tanstack-ai/header.png) - -You kick off a `chat()` call. A chunk goes missing. A middleware you wrote last week doesn't seem to fire. A tool gets called with arguments you can't explain. Your stream finishes, the UI looks wrong, and you have no idea which layer lied to you. - -Up until now your options were limited. You could wrap the SDK in a tracing platform, spend a day wiring OpenTelemetry, or sprinkle `console.log` into your own code and hope the problem lives where you can see it. Neither helps when the bug is **inside** the pipeline: a raw provider chunk that got dropped, a middleware that mutated config, a tool call the agent loop reissued. - -TanStack AI now has a built-in answer. **Flip one flag and the entire pipeline prints itself.** - -## Turn it on - -Add `debug: true` to any activity call: - -```typescript -import { chat } from '@tanstack/ai' -import { openaiText } from '@tanstack/ai-openai/adapters' - -const stream = chat({ - adapter: openaiText(), - model: 'gpt-4o', - messages: [{ role: 'user', content: 'Hello' }], - debug: true, -}) -``` - -Every internal event now prints to the console, prefixed with an emoji-tagged category so you can scan dense streaming logs without squinting: - -``` -📤 [tanstack-ai:request] 📤 activity=chat provider=openai model=gpt-4o messages=1 tools=0 stream=true -🔁 [tanstack-ai:agentLoop] 🔁 run started -📥 [tanstack-ai:provider] 📥 provider=openai type=response.output_text.delta -📨 [tanstack-ai:output] 📨 type=TEXT_MESSAGE_CONTENT -🧩 [tanstack-ai:middleware] 🧩 hook=onOutput -🔧 [tanstack-ai:tools] 🔧 tool=getTodos phase=before -``` - -That is the whole setup. No exporters, no sidecar, no dashboard. Just what your pipeline is actually doing, right now, in the terminal you already have open. - -## Eight categories, not a log level - -Most logging libraries give you `debug`, `info`, `warn`, `error` and ask you to pick one. That mapping is wrong for an AI pipeline. **The noise isn't a severity, it's a source.** When you're chasing a tool bug you don't want provider chunks. When you're chasing a provider bug you don't want middleware chatter. - -So `debug` accepts a config object where every category toggles independently: - -```typescript -chat({ - adapter: openaiText(), - model: 'gpt-4o', - messages, - debug: { middleware: false }, // everything except middleware -}) -``` - -Omitted categories default to `true`, so the common case is "turn off the one thing that's drowning you." Every category maps to a real pipeline surface: - -| Category | What it logs | -| ------------ | -------------------------------------------------------------- | -| `request` | Outgoing call to a provider (model, message count, tool count) | -| `provider` | Every raw chunk or frame from the provider SDK | -| `output` | Every chunk or result yielded to the caller | -| `middleware` | Inputs and outputs around every middleware hook | -| `tools` | Before and after tool call execution | -| `agentLoop` | Agent-loop iterations and phase transitions | -| `config` | Config transforms returned by middleware `onConfig` hooks | -| `errors` | Every caught error anywhere in the pipeline | - -Chat-only categories like `tools` and `agentLoop` just don't fire for `summarize()` or `generateImage()`, because they don't exist in those pipelines. You don't have to think about it. - -## Pipe it anywhere - -`console.log` is the right default for local work. It is the wrong default for production, where you want structured JSON going to a log shipper, not ANSI colors going to stdout. - -Pass your own `Logger` and the entire category system routes through it: - -```typescript -import type { Logger } from '@tanstack/ai' -import pino from 'pino' - -const pinoLogger = pino() -const logger: Logger = { - debug: (msg, meta) => pinoLogger.debug(meta, msg), - info: (msg, meta) => pinoLogger.info(meta, msg), - warn: (msg, meta) => pinoLogger.warn(meta, msg), - error: (msg, meta) => pinoLogger.error(meta, msg), -} - -chat({ - adapter: openaiText(), - model: 'gpt-4o', - messages, - debug: { logger }, -}) -``` - -The `Logger` interface is four methods. Anything that writes a line of text fits. Pino, winston, bunyan, a `fetch` to a logging service, a no-op that forwards to your existing observability layer. All valid. - -### Your logger can't break the pipeline - -This is the detail we lost sleep over. If your custom logger throws (a cyclic-meta `JSON.stringify`, a transport that rejects synchronously, a typo in a bound `this`), the exception should **not** bubble up and mask the real error that triggered the log call in the first place. - -Internally, every user-logger invocation is wrapped in a try/catch. A broken logger silently drops the log line. Your actual pipeline error still reaches you through thrown exceptions and `RUN_ERROR` chunks, exactly where you were looking for it. - -If you need to know when your own logger is failing, guard inside your implementation. The SDK will not guess how loud you want logger failures to be. - -## Every activity, every provider - -Debug logging isn't a chat-only feature. Every activity in TanStack AI accepts the same option: - -```typescript -summarize({ adapter, text, debug: true }) -generateImage({ adapter, prompt: 'a cat', debug: { logger } }) -generateSpeech({ adapter, text, debug: { request: true } }) -generateTranscription({ adapter, audio, debug: true }) -generateVideo({ adapter, prompt, debug: true }) -``` - -Realtime session adapters (`openaiRealtime`, `elevenlabsRealtime`) take it too. - -On the provider side, **every adapter in every provider package is wired through the structured logger**: OpenAI, Anthropic, Gemini, Grok, Groq, Ollama, OpenRouter, fal, and ElevenLabs. 25 adapters total. Zero ad-hoc `console.*` calls remain in adapter source code. Whether you're debugging an Anthropic text stream or an ElevenLabs realtime session, the output shape is the same. - -## The small decisions that add up - -A few calls that look cosmetic but matter once you're staring at a thousand-line log. - -**Emoji prefixes on both sides of the tag.** `📨 [tanstack-ai:output] 📨 ...` reads faster than raw brackets in a dense stream, and your eye can hop categories without parsing text. - -**`console.dir` with `depth: null`.** Node's default console formatting stops at depth 2, so nested provider chunks render as `[Object]` and you lose the thing you were trying to see. Debug logs surface the entire structure. In browsers, the raw object still lands in DevTools for interactive inspection. - -**Errors log unconditionally.** You don't have to remember to turn them on. If you really want total silence, `debug: false` or `debug: { errors: false }` does it. Otherwise errors flow through even when you haven't asked for any other category. - -**Internal devtools middleware is muted.** If you have the TanStack AI devtools middleware installed, its own hooks don't flood the `middleware` category. You see the middleware **you** wrote, not the plumbing. - -Each of these is a small call on its own. Together they're the difference between "debug output I actually read" and "debug output I pipe to `/dev/null` within ten seconds." - -## Getting it - -Debug logging ships in the latest `@tanstack/ai`. It's additive, backward-compatible, and available on every activity today. No config file, no exporter, no platform. - -Upgrade, add `debug: true` to the call you can't explain, and read the output. - -For the full reference, see the [Debug Logging guide](https://tanstack.com/ai/latest/docs/advanced/debug-logging). And if you want an even faster turnaround: TanStack AI ships an agent skill under `packages/typescript/ai/skills/` so your LLM-powered dev tools can discover the flag on their own. - -One flag. Every chunk. Your streams are no longer a black box. diff --git a/src/blog/directives-and-the-platform-boundary.md b/src/blog/directives-and-the-platform-boundary.md deleted file mode 100644 index 6d69b8bb2..000000000 --- a/src/blog/directives-and-the-platform-boundary.md +++ /dev/null @@ -1,298 +0,0 @@ ---- -title: Directives and the Platform Boundary -published: 2025-10-24 -excerpt: Frameworks are inventing top-level directives that look like language features but aren't standardized. A constructive look at portability and keeping a clear boundary between platform and library spaces. -authors: - - Tanner Linsley -description: A constructive look at framework directives, portability, and keeping a clear boundary between platform and library spaces. ---- - -![Header Image](/blog-assets/directives-and-the-platform-boundary/header.png) - -## A Quiet Trend in the JavaScript Ecosystem - -For years, JavaScript has had exactly one meaningful directive, `"use strict"`. It is standardized, enforced by runtimes, and behaves the same in every environment. It represents a clear contract between the language, the engines, and developers. - -But now we are watching a new trend emerge. Frameworks are inventing their own top level directives, `use client`, `use server`, `use cache`, `use workflow`, and more are appearing across the ecosystem. They look like language features. They sit where real language features sit. They affect how code is interpreted, bundled, and executed. - -There is an important distinction: these are not standardized JavaScript features. Runtimes don't understand them, there is no governing specification, and each framework is free to define its own meaning, rules, and edge cases. - -This can feel ergonomic today, but it also increases confusion, complicates debugging, and imposes costs on tooling and portability, patterns we’ve seen before. - ---- - -### When directives look like the platform, developers treat them like the platform - -A directive at the top of a file looks authoritative. It gives the impression of being a language level truth, not a framework hint. That creates a perception problem: - -- Developers assume directives are official -- Ecosystems begin to treat them as a shared API surface -- New learners struggle to distinguish JavaScript from framework magic -- The boundary between platform and vendor blurs -- Debuggability suffers and tooling must special‑case behaviors - -We’ve already seen confusion. Many developers now believe `use client` and `use server` are just how modern JavaScript works, unaware that they only exist inside specific build pipelines and server component semantics. That misunderstanding signals a deeper issue. - ---- - -### Credit where it's due: `use server` and `use client` - -Some directives exist because multiple tools needed a single, simple coordination point. In practice, `use server` and `use client` are pragmatic shims that tell bundlers and runtimes where code is allowed to execute in an RSC world. They have seen relatively broad support across bundlers precisely because the scope is narrow: execution location. - -That said, even these show the limits of directives once real-world needs appear. At scale, you often need parameters and policies that matter deeply to correctness and security: HTTP method, headers, middleware, auth context, tracing, caching behaviors, and more. Directives have no natural place to carry those options, which means they are frequently ignored, bolted on elsewhere, or re-encoded as new directive variants. - -### Where directives start to strain: options and directive-adjacent APIs - -When a directive immediately, or soon after creation, needs options or spawns siblings (e.g., `'use cache:remote'`) and helper calls like `cacheLife(...)`, that’s often a signal the feature wants to be an API, not a string at the top of a file. If you know you need a function anyway, just use a function for all of it. - -Examples: - -```js -'use cache:remote' -const fn = () => 'value' -``` - -```js -// explicit API with provenance and options -import { cache } from 'next/cache' -export const fn = cache(() => 'value', { - strategy: 'remote', - ttl: 60, -}) -``` - -And for server behavior where details matter: - -```js -import { server } from '@acme/runtime' - -export const action = server( - async (req) => { - return new Response('ok') - }, - { - method: 'POST', - headers: { 'x-foo': 'bar' }, - middleware: [requireAuth()], - }, -) -``` - -APIs carry provenance (imports), versioning (packages), composition (functions), and testability. Directives typically don’t, and trying to encode options into them can quickly become a design smell. - ---- - -### Shared syntax without a shared spec can be a fragile foundation - -Once multiple frameworks start adopting directives, we end up in the worst possible state: - -| Category | Shared Syntax | Shared Contract | Result | -| -------------------- | ------------- | --------------- | ---------------------- | -| ECMAScript | ✅ | ✅ | Stable and universal | -| Framework APIs | ❌ | ❌ | Isolated and fine | -| Framework Directives | ✅ | ❌ | Confusing and unstable | - -A shared surface area without a shared definition creates: - -- Interpretation drift, each framework defines its own semantics -- Portability issues, code that looks universal but is not -- Tooling burden, bundlers, linters, and IDEs must guess or chase behavior -- Platform friction, standards bodies get boxed in by ecosystem expectations - -An example of where we've seen these struggles before is with decorators. TypeScript normalized a non standard semantics, the community built on top of it, then TC39 went in a different direction. This was and continues to be a painful migration for many. - ---- - -### “Isn’t this just a Babel plugin/macro with different syntax?” - -Functionally, yes. Both directives and custom transforms can change behavior at compile time. The issue isn’t capability; it’s surface and optics. - -- Directives look like the platform. No import, no owner, no explicit source. They signal “this is JavaScript.” -- APIs/macros point to an owner. Imports provide provenance, versioning, and discoverability. - -At best, a directive is equivalent to calling a global, importless function like `window.useCache()` at the top of your file. That’s exactly why it’s risky: it hides the provider and moves framework semantics into what looks like language. - -Examples: - -```js -'use cache' -const fn = () => 'value' -``` - -```js -// explicit API (imported, ownable, discoverable) -import { createServerFn } from '@acme/runtime' -export const fn = createServerFn(() => 'value') -``` - -```js -// global magic (importless, hidden provider) -window.useCache() -const fn = () => 'value' -``` - -Why this matters: - -- Ownership and provenance: imports tell you who provides the behavior; directives do not. -- Tooling ergonomics: APIs live in package space; directives require ecosystem-wide special-casing. -- Portability and migration: replacing an imported API is straightforward; unwinding directive semantics across files is costly and ambiguous. -- Education and expectations: directives blur the platform boundary; APIs make the boundary explicit. - -So while a custom Babel plugin or macro can implement the same underlying feature, the import-based API keeps it clearly in framework space. Directives move that same behavior into what looks like language space, which is the core concern of this post. - -### “Does namespacing fix it?” (e.g., "use next.js cache") - -Namespacing helps human discoverability, but it doesn’t address the core problems: - -- It still looks like the platform. A top-level string literal implies language, not library. -- It still lacks provenance and versioning at the module level. Imports encode both; strings do not. -- It still requires special-casing across the toolchain (bundlers, linters, IDEs), rather than leveraging normal import resolution. -- It still encourages pseudo-standardization of syntax without a spec, just with vendor prefixes. -- It still increases migration cost compared to swapping an imported API. - -Examples: - -```js -'use next.js cache' -const fn = () => 'value' -``` - -```js -// explicit, ownable API with provenance and versioning -import { cache } from 'next/cache' -export const fn = cache(() => 'value') -``` - -If the goal is provenance, imports already solve that cleanly and work with today’s ecosystem. If the goal is a shared cross-framework primitive, that needs a real spec, not vendor strings that look like syntax. - ---- - -### Directives can drive competitive dynamics - -Once directives become a competitive surface, the incentives shift: - -1. One vendor ships a new directive -2. It becomes a visible feature -3. Developers expect it everywhere -4. Other frameworks feel pressure to adopt it -5. The syntax spreads without a spec - -This is how you get: - -```tsx -'use server' -'use client' -'use cache' -'use cache:remote' -'use workflow' -``` - -Even durable tasks, caching strategies, and execution locations are now being encoded as directives. These are runtime semantics, not syntax semantics. Encoding them as directives sets direction outside the standards process and merits caution. - ---- - -### Considering APIs instead of directives for option‑rich features - -Durable execution is a good example (e.g., `'use workflow'`, `'use step'`), but the point is general: directives can collapse behavior to a boolean, while many features benefit from options and room to evolve. Compilers and transforms can support either surface; this is about choosing the right one for longevity and clarity. - -```js -'use workflow' -'use step' -``` - -One option: an explicit API with provenance and options: - -```js -import { workflow, step } from '@workflows/workflow' - -export const sendEmail = workflow( - async (input) => { - /* ... */ - }, - { retries: 3, timeout: '1m' }, -) - -export const handle = step( - 'fetchUser', - async () => { - /* ... */ - }, - { cache: 60 }, -) -``` - -Function forms can be just as AST/transform‑friendly as directives, and they carry provenance (imports) and type‑safety. - -Another option is to inject a global once and type it: - -```ts -// bootstrap once -globalThis.workflow = createWorkflow() -// global types (e.g., global.d.ts) -declare global { - var workflow: typeof import('@workflows/workflow').workflow -} -``` - -Usage stays API‑shaped, without directives: - -```ts -export const task = workflow( - async () => { - /* ... */ - }, - { retries: 5 }, -) -``` - -Compilers that extend ergonomics are great. Just look at JSX is a useful precedent! We just need to do it carefully and responsibly: extend via APIs with clear provenance and types, not top‑level strings that look like the language. These are options, not prescriptions. - ---- - -### Subtle forms of lock‑in can emerge - -Even when there is no bad intent, directives create lock in by design: - -- Mental lock in, developers form muscle memory around a vendor's directive semantics -- Tooling lock in, IDEs, bundlers, and compilers must target a specific runtime -- Code lock in, directives sit at the syntax level, making them costly to remove or migrate - -Directives may not look proprietary, but they can behave more like proprietary features than an API would, because they reshape the grammar of the ecosystem. - ---- - -### If we want shared primitives, we should collaborate on specs and APIs - -There absolutely are real problems to solve: - -- Server execution boundaries -- Streaming and async workflows -- Distributed runtime primitives -- Durable tasks -- Caching semantics - -But those are problems for **APIs, capabilities, and future standards**, not for ungoverned pseudo syntax pushed through bundlers. - -If multiple frameworks truly want shared primitives, a responsible path is: - -- Collaborate on a cross framework spec -- Propose primitives to TC39 when appropriate -- Keep non standard features clearly scoped to API space, not language space - -Directives should be rare, stable, standardized and especially used judiciously rather than proliferating across vendors. - ---- - -### Why this differs from the JSX/virtual DOM moment - -It’s tempting to compare criticism of directives to the early skepticism around React’s JSX or the virtual DOM. The failure modes are different. JSX and the VDOM did not masquerade as language features; they came with explicit imports, provenance, and tooling boundaries. Directives, by contrast, live at the top-level of files and look like the platform, which creates ecosystem expectations and tooling burdens without a shared spec. - ---- - -### The bottom line - -Framework directives might feel like DX magic today, but the current trend risks a more fragmented future consisting of dialects defined not by standards, but by tools. - -We can aim for clearer boundaries. - -If frameworks want to innovate, they should, but they should also clearly distinguish **framework behavior** from **platform semantics**, instead of blurring that line for short term adoption. Clearer boundaries help the ecosystem. diff --git a/src/blog/from-docs-to-agents.md b/src/blog/from-docs-to-agents.md deleted file mode 100644 index 95ea15b6f..000000000 --- a/src/blog/from-docs-to-agents.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -title: 'Introducing TanStack Intent: Ship Agent Skills with your npm Packages' -published: 2026-03-04 -excerpt: Your docs are good. Your types are solid. Your agent still gets it wrong. TanStack Intent lets you ship agent skills alongside your npm packages so AI tools actually know how to use your library. -library: intent -authors: - - Sarah Gerrard - - Kyle Mathews ---- - -![From Docs to Agents](/blog-assets/from-docs-to-agents/header.png) - -Your docs are good. Your types are solid. Your agent still gets it wrong. - -Not because it's dumb — because nothing connects what you know about your tool to what agents know. Docs target humans who browse. Types check individual API calls but can't encode intent. Training data snapshots the ecosystem as it _was_, mixing versions with no way to tell which applies. The gap isn't content. It's lifecycle. - -## The copy-paste era - -The ecosystem already moves toward agent-readable knowledge. Cursor rules, CLAUDE.md files, skills directories — everyone agrees agents need more than docs and types. But delivery hasn't caught up. - -Today, if you want your agent to understand TanStack Router, you hunt for a community-maintained rules file on GitHub. Maybe it's in `awesome-cursorrules`. Maybe someone linked it in Discord. You copy it into `.cursorrules` or `CLAUDE.md`. Then you repeat for TanStack Query. And TanStack Table. Each from a different place, author, and point in time. - -Multiply that across every tool in your stack. You're managing copy-pasted knowledge files with no versioning, no update path, and no staleness signal. Did TanStack Router ship a breaking change last week? Your rules file doesn't know. Is that Query skill written for v4 or v5? Hope you checked. - -Finding skills is manual. Installing them is manual. Keeping them current is manual. When they drift — and they always drift — you discover it only when your agent produces subtly wrong code. - -Library maintainers already have the knowledge agents need — in docs, migration guides, "common mistakes" GitHub discussions, Discord answers. But none of it reaches agents through a channel the maintainer controls. The knowledge exists. The delivery mechanism doesn't. - -![The status quo: scattered rules files from different repos, authors, and versions, all manually copy-pasted into one project](/blog-assets/from-docs-to-agents/diagram-status-quo.svg) - -## Introducing `@tanstack/intent` - -`@tanstack/intent` is a CLI for library maintainers to generate, validate, and ship [Agent Skills](https://agentskills.io) alongside their npm packages. The [Agent Skills spec](https://agentskills.io) is an open standard already adopted by VS Code, GitHub Copilot, OpenAI Codex, Cursor, Claude Code, Goose, Amp, and others. - -**Skills ship inside your npm package.** They encode how your tool works, which patterns fit which goals, and what to avoid. Skills travel with the tool via `npm update` — not the model's training cutoff, not community-maintained rules files, not prompt snippets in READMEs. Versioned knowledge the maintainer owns, updated when the package updates. - -For popular, stable patterns — standard React hooks, Express middleware, Tailwind classes — agents do well. Training data is saturated with correct usage. But at the frontier — new tools, major version transitions, novel compositions across packages — agents hallucinate, confuse versions, and miss critical implications. The frontier is bigger than it sounds: every new library, every breaking change, every composition across tools that nobody has written about. And once a breaking change ships, models don't "catch up." They develop a permanent split-brain — training data contains _both_ versions forever with no way to disambiguate. Skills bypass this. They're pinned to the installed version. - -![Model training data mixes versions permanently vs. skills pinned to your installed version](/blog-assets/from-docs-to-agents/diagram-split-brain.svg) - -A skill is a short, versioned document that tells agents how to use a specific capability of your library — correct patterns, common mistakes, and when to apply them. Each skill declares which docs it was derived from: - -``` ---- -name: tanstack-router-search-params -description: Type-safe search param patterns for TanStack Router. Use when working with search params, query params, or validateSearch. -metadata: - sources: - - docs/framework/react/guide/search-params.md ---- -``` - -Inside the skill, you write what the agent needs to get right — including what NOT to do: - -```markdown -## Search Params - -Use `validateSearch` to define type-safe search params on a route: - -const Route = createFileRoute('/products')({ -validateSearch: z.object({ -page: z.number().default(1), -filter: z.string().optional(), -}), -}) - -## Common Mistakes - -❌ Don't access search params via `window.location` — use -`useSearch()` which is fully type-safe. - -❌ Don't parse search params manually. `validateSearch` handles -parsing, validation, and defaults. -``` - -That `metadata.sources` field is what keeps skills current. When those docs change, the CLI flags the skill for review. One source of truth, one derived artifact that stays in sync. - -## Generating and validating skills - -You don't author skills from scratch. `@tanstack/intent scaffold` generates them from your library: - -```bash -npx @tanstack/intent scaffold -``` - -The scaffold produces drafts you review, refine, and commit. Once committed, `@tanstack/intent validate` checks that they're well-formed: - -```bash -npx @tanstack/intent validate -``` - -`@tanstack/intent setup-github-actions` copies CI workflow templates into your repo so validation runs on every push: - -```bash -npx @tanstack/intent setup-github-actions -``` - -## The dependency graph does the discovery - -That's the maintainer side. For developers, the experience is simpler. - -When a developer runs `@tanstack/intent install`, the CLI discovers every intent-enabled package and wires skills into the agent configuration — CLAUDE.md, .cursorrules, whatever the tooling expects. - -```bash -npx @tanstack/intent install -``` - -![intent install discovers intent-enabled packages in node_modules and wires skills into agent config](/blog-assets/from-docs-to-agents/diagram-discovery.svg) - -No per-library setup. No hunting for rules files. Install the package, run `@tanstack/intent install`, and the agent understands the tool. Update the package, and skills update too. Knowledge travels the same channel as code. - -`@tanstack/intent list` shows what's available: - -```bash -npx @tanstack/intent list # See what's intent-enabled in your deps -npx @tanstack/intent list --json # Machine-readable output -``` - -For library maintainers, `@tanstack/intent meta` surfaces meta-skills — higher-level guidance on authoring and maintaining skills: - -```bash -npx @tanstack/intent meta -``` - -## Keeping it current - -The real risk with any derived artifact is staleness. You update your docs, ship a new API, and skills silently drift. `@tanstack/intent` treats staleness as a first-class problem. - -`@tanstack/intent stale` checks for version drift, flagging skills that have fallen behind their sources: - -```bash -npx @tanstack/intent stale # Human-readable report -npx @tanstack/intent stale --json # Machine-readable for CI -``` - -Run it in CI and you get a failing check when sources change. Skills become part of your release checklist — not something you remember to update, but something your pipeline catches. - -![The intent lifecycle: docs to skills to npm to agent config, with staleness checks and feedback loops](/blog-assets/from-docs-to-agents/diagram-lifecycle.svg) - -The feedback loop runs both directions. `@tanstack/intent feedback` lets users submit structured reports when a skill produces wrong output — which skill, which version, what broke. - -```bash -npx @tanstack/intent feedback -``` - -That context flows back to you as a maintainer, and the fix ships to everyone on the next `npm update`. Every support interaction produces an artifact that prevents the same class of problem for all future users — not just the one who reported it. This is what makes skills compound: each fix makes the skill better, and each `npm update` distributes the improvement. - -Skills that keep needing the same workaround signal something deeper. Sometimes the fix is a better skill. Sometimes it's a better API — the tool should absorb the lesson directly. A skill that persists forever means the tool has a design gap. A skill that disappears because the tool fixed the underlying problem is the system working exactly as intended. - -## Try it out - -We've started rolling out skills in [TanStack DB](https://github.com/TanStack/db/pull/1330) with other TanStack libraries following. If you maintain a library, tell your coding agent to run `npx @tanstack/intent scaffold` and let us know how it goes. We want feedback on the authoring workflow, the skill format, and what's missing. File issues on [GitHub](https://github.com/TanStack/intent) or find us on [Discord](https://tlinz.com/discord). - -The lifecycle: write your docs, generate skills, ship them with your package, validate and keep them current, learn from usage, improve your tool. Repeat. diff --git a/src/blog/generation-hooks.md b/src/blog/generation-hooks.md deleted file mode 100644 index f93b80d46..000000000 --- a/src/blog/generation-hooks.md +++ /dev/null @@ -1,265 +0,0 @@ ---- -title: 'Generation Hooks: Type-Safe AI Beyond Chat' -published: 2026-03-11 -excerpt: Chat is just the beginning. Your AI app needs image generation, text-to-speech, transcription, and more. TanStack AI now ships generation hooks — a unified set of React hooks for every non-chat AI activity. -library: ai -authors: - - Alem Tuzlak ---- - -![Generation Hooks](/blog-assets/generation-hooks/header.png) - -Chat is just the beginning. Your AI-powered app probably needs to generate images, convert text to speech, transcribe audio, summarize documents, or create videos. Until now, wiring up each of these activities meant writing custom fetch logic, managing loading states, handling errors, and juggling streaming protocols for every single one. - -Not anymore. - -## One Pattern to Rule Them All - -TanStack AI now ships **generation hooks**: a unified set of React hooks (with Solid, Vue, and Svelte support) that give you first-class primitives for every non-chat AI activity: - -- `useGenerateImage()` for image generation -- `useGenerateSpeech()` for text-to-speech -- `useTranscription()` for audio transcription -- `useSummarize()` for text summarization -- `useGenerateVideo()` for video generation - -Every hook follows the exact same API surface. Learn one, and you know them all: - -```tsx -const { generate, result, isLoading, error, stop, reset } = useGenerateImage({ - connection: fetchServerSentEvents('/api/generate/image'), -}) - -// That's it. Call generate() and your UI reacts. -generate({ prompt: 'A neon-lit cyberpunk cityscape at sunset' }) -``` - -The `result` is fully typed. The `error` is handled. Loading state is tracked. Abort is built in. No boilerplate, no `useEffect` spaghetti, no manual state management. - -## Three Ways to Connect - -Every generation hook supports three transport modes, so you can pick the one that fits your architecture: - -### 1. Streaming (Connection Adapter) - -The classic SSE approach. Your server wraps the generation in `toServerSentEventsResponse()`, and the client consumes it through `fetchServerSentEvents()`: - -```tsx -// Client -const { generate, result, isLoading } = useGenerateImage({ - connection: fetchServerSentEvents('/api/generate/image'), -}) -``` - -```typescript -// Server (API route) -const stream = generateImage({ - adapter: openaiImage('gpt-image-1'), - prompt: data.prompt, - stream: true, -}) -return toServerSentEventsResponse(stream) -``` - -This is the most flexible option. It works with any server framework, any hosting provider, any deployment model. - -### 2. Direct (Fetcher) - -Sometimes you don't need streaming. You just want to call a function and get a result. The fetcher mode does exactly that: - -```tsx -const { generate, result, isLoading } = useGenerateImage({ - fetcher: (input) => generateImageFn({ data: input }), -}) -``` - -The server function runs, returns JSON, and the hook updates your UI. Simple, synchronous from the user's perspective, and fully type-safe. - -### 3. Server Function Streaming (NEW) - -This is the one we're most excited about. It combines the **type safety of server functions** with the **real-time feedback of streaming**, and it works beautifully with TanStack Start. - -Here is the problem we solved: the `connection` approach uses a generic `Record` for its data payload. Great for flexibility, but your input loses all type information. The `fetcher` approach is fully typed, but it waits for the entire result before updating the UI. - -Server Function Streaming gives you both. Your fetcher returns a `Response` object (an SSE stream), and the client automatically detects it and parses the stream in real-time: - -```tsx -// Client - looks identical to the direct fetcher -const { generate, result, isLoading } = useGenerateImage({ - fetcher: (input) => generateImageStreamFn({ data: input }), -}) -``` - -```typescript -// Server - just add stream: true and wrap with toServerSentEventsResponse -export const generateImageStreamFn = createServerFn({ method: 'POST' }) - .validator( - z.object({ - prompt: z.string(), - numberOfImages: z.number().optional(), - size: z.string().optional(), - }), - ) - .handler(({ data }) => - toServerSentEventsResponse( - generateImage({ - adapter: openaiImage('gpt-image-1'), - prompt: data.prompt, - stream: true, - }), - ), - ) -``` - -From the client's perspective, the API is identical to a direct fetcher call. But behind the scenes, TanStack AI detects the `Response` object, reads the SSE stream, and feeds chunks through the same event pipeline used by the connection adapter. Progress events fire in real-time. Errors are reported as they happen. And your `input` parameter stays fully typed throughout. - -The detection is simple and zero-config: if your fetcher returns a `Response`, it's treated as an SSE stream. If it returns anything else, it's treated as a direct result. No flags, no configuration, no separate hook. - -## How It Works Under the Hood - -When a fetcher returns a `Response`, the `GenerationClient` runs a simple check: - -```typescript -const result = await this.fetcher(input, { signal }) - -if (result instanceof Response) { - // Parse as SSE stream - same pipeline as ConnectionAdapter - await this.processStream(parseSSEResponse(result, signal)) -} else { - // Use as direct result - this.setResult(result) -} -``` - -The `parseSSEResponse` utility reads the response body as a stream of newline-delimited SSE events, parses each `data:` line into a `StreamChunk`, and yields them into the same `processStream` method that the ConnectionAdapter uses. Same event types, same state transitions, same callbacks. - -This means every feature that works with streaming connections also works with server function streaming: progress reporting, chunk callbacks, abort signals, error handling. All of it. - -## Result Transforms - -Sometimes the raw result from the server isn't what you want to store in state. Every generation hook accepts an `onResult` callback that can transform the result before it's stored: - -```tsx -const { result } = useGenerateSpeech({ - fetcher: (input) => generateSpeechStreamFn({ data: input }), - onResult: (raw) => { - // Convert base64 audio to a blob URL for playback - const bytes = Uint8Array.from(atob(raw.audio), (c) => c.charCodeAt(0)) - const blob = new Blob([bytes], { type: raw.contentType ?? 'audio/mpeg' }) - return { - audioUrl: URL.createObjectURL(blob), - format: raw.format, - duration: raw.duration, - } - }, -}) - -// result is typed as { audioUrl: string; format?: string; duration?: number } | null -``` - -TypeScript infers the output type from your transform function. No explicit generics needed. - -## Video Generation: A First-Class Citizen - -Video generation is a different beast. Unlike image or speech generation, video providers like OpenAI's Sora use a jobs-based architecture: you submit a prompt, receive a job ID, then poll for status until the video is ready. This can take minutes. - -`useGenerateVideo()` handles all of this transparently: - -```tsx -const { generate, result, jobId, videoStatus, isLoading } = useGenerateVideo({ - fetcher: (input) => generateVideoStreamFn({ data: input }), -}) - -// In your JSX: -{ - videoStatus && ( -
-

Status: {videoStatus.status}

- {videoStatus.progress != null && ( -
- )} -
- ) -} -``` - -The hook exposes `jobId` and `videoStatus` as reactive state that updates in real-time as the server streams polling updates. Your users see "pending", "processing", progress percentages, and finally the completed video URL, all without you writing a single polling loop. - -## Every Activity, Same API - -Here's what makes this design special: the API is identical across all five generation types. Once you've built an image generation page, building a speech generation page is a matter of swapping the hook name and adjusting the input: - -| Hook | Input | Result | -| --------------------- | ------------------------------------ | ----------------------------------------------- | -| `useGenerateImage()` | `{ prompt, numberOfImages?, size? }` | `{ images: [{ url, b64Json, revisedPrompt }] }` | -| `useGenerateSpeech()` | `{ text, voice?, format? }` | `{ audio, contentType, format, duration }` | -| `useTranscription()` | `{ audio, language? }` | `{ text, segments, language, duration }` | -| `useSummarize()` | `{ text, style?, maxLength? }` | `{ summary }` | -| `useGenerateVideo()` | `{ prompt, size?, duration? }` | `{ jobId, status, url }` | - -Same `generate()`. Same `result`. Same `isLoading`. Same `error`. Same `stop()` and `reset()`. The consistency is intentional: we want AI features to be as easy to add to your app as a form submission. - -## Getting Started - -Install the packages: - -```bash -pnpm add @tanstack/ai @tanstack/ai-react @tanstack/ai-client @tanstack/ai-openai -``` - -Create a server function that streams: - -```typescript -import { createServerFn } from '@tanstack/react-start' -import { generateImage, toServerSentEventsResponse } from '@tanstack/ai' -import { openaiImage } from '@tanstack/ai-openai' - -export const generateImageStreamFn = createServerFn({ method: 'POST' }) - .validator(z.object({ prompt: z.string() })) - .handler(({ data }) => { - return toServerSentEventsResponse( - generateImage({ - adapter: openaiImage('gpt-image-1'), - prompt: data.prompt, - stream: true, - }), - ) - }) -``` - -Use it in your component: - -```tsx -import { useGenerateImage } from '@tanstack/ai-react' -import { generateImageStreamFn } from '../lib/server-fns' - -function ImageGenerator() { - const { generate, result, isLoading, error } = useGenerateImage({ - fetcher: (input) => generateImageStreamFn({ data: input }), - }) - - return ( -
- - {result?.images.map((img, i) => ( - Generated - ))} -
- ) -} -``` - -Three lines of hook setup. Type-safe input. Streaming progress. Error handling. Abort support. That's it. - -## What's Next - -Generation hooks are available now in `@tanstack/ai-client` and `@tanstack/ai-react`. Support for Solid, Vue, and Svelte is coming soon with the same API surface. - -We're also working on expanding the adapter ecosystem so you can use these hooks with providers beyond OpenAI. The generation functions are provider-agnostic by design, so swapping from OpenAI to Anthropic or a local model will be a single line change. - -Build something cool and let us know. We can't wait to see what you create. diff --git a/src/blog/how-we-test-tanstack-ai-across-7-providers.md b/src/blog/how-we-test-tanstack-ai-across-7-providers.md deleted file mode 100644 index 846b8e774..000000000 --- a/src/blog/how-we-test-tanstack-ai-across-7-providers.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: 'How We Test TanStack AI Across 7 Providers on Every PR' -published: 2026-04-13 -draft: false -excerpt: "TanStack AI runs 147 deterministic E2E tests across 7 LLM providers in under 2 minutes. Here's the testing infrastructure that makes it possible." -library: ai -authors: - - Alem Tuzlak ---- - -![E2E Testing - All Tests Passed](/blog-assets/how-we-test-tanstack-ai-across-7-providers/cover.png) - -LLM responses are non-deterministic. API calls cost money. And the thing that works perfectly with OpenAI might silently break with Anthropic. - -If you've ever built on an AI SDK, you know the feeling: you trust the library works because the README says it supports your provider. But does it? Has anyone actually verified that tool calling works the same way across OpenAI, Gemini, and Ollama? That streaming structured output doesn't break when you switch from Groq to Anthropic? - -We got tired of wondering. So we built an E2E testing infrastructure for TanStack AI that verifies every feature across every provider on every pull request. 137 tests. 7 providers. About 2 minutes. Zero API keys required. - -Here's how it works and why it matters for the long-term stability of the project. - -## The Problem with Testing AI Libraries - -Unit tests are great for business logic. They're terrible for verifying that your OpenAI adapter actually produces the same streaming behavior as your Gemini adapter. - -The typical approach for AI SDKs is to unit-test the adapter layer with mocked HTTP responses, maybe run a handful of integration tests against a real API in CI, and call it a day. This breaks down in three predictable ways: - -**Mocks lie.** When you mock at the HTTP level, you're testing your assumptions about the API, not the API itself. Provider response formats change. Streaming chunk boundaries differ. Edge cases in tool call serialization only surface with real payloads. - -**Real API tests are flaky and expensive.** Rate limits, network timeouts, non-deterministic responses, and per-token costs all conspire to make CI unreliable. Most teams end up skipping these tests or running them manually before releases. - -**Provider parity is assumed, never verified.** You ship a new feature, test it with OpenAI, and assume the other 6 providers work the same way. They usually do. Until they don't, and a user files a bug report. - -TanStack AI's previous smoke-test setup hit all three of these walls. Tests were fragmented across multiple directories, coverage was inconsistent, and there was no way to run the full matrix quickly or deterministically. We needed something better. - -## What We Built - -The new E2E infrastructure has three components: - -1. **A TanStack Start test app** that serves as the harness, with routes for every provider/feature combination -2. **aimock** as a drop-in replacement for real LLM APIs, serving deterministic fixture responses -3. **Playwright** driving the browser with per-test isolation - -The flow is straightforward: - -```mermaid -graph LR - A[Playwright] --> B[TanStack Start App] - B --> C[Provider Adapter] - C --> D[aimock] - D --> E[Fixture JSON] -``` - -Playwright opens the test app, navigates to a route like `/openai/chat`, and interacts with the UI. The app's provider adapter thinks it's talking to OpenAI, but the `baseURL` points at aimock. aimock matches the request against a fixture file and returns a deterministic response. - -Every test gets a unique `X-Test-Id` header. This is what makes parallel execution work: aimock uses the header to route each test to its own fixture sequence, so 137 tests can run simultaneously without stepping on each other. - -## The Numbers - -137 tests cover 17 features across 7 providers. Here's the matrix: - -| Category | Tests | Features | -| --------------- | ----- | ----------------------------------------------------------------- | -| Chat and text | 28 | chat, one-shot-text, multi-turn, structured-output | -| Tool calling | 38 | single, parallel, approval, text-tool-text, agentic-structured | -| Multimodal | 10 | image input, structured multimodal | -| Generation | 20 | summarize, summarize-stream, image-gen, TTS, transcription | -| Reasoning | 3 | OpenAI, Anthropic, Gemini thinking blocks | -| Tools-test page | 29 | client tools, approvals, race conditions, server-client sequences | -| Advanced | 6 | abort, lazy tools, custom events, middleware, error handling | -| Middleware | 3 | onChunk transform, onBeforeToolCall skip, passthrough | - -**Providers:** OpenAI, Anthropic, Gemini, Ollama, Groq, Grok, OpenRouter. - -Not every provider supports every feature (Ollama doesn't do image generation, for example). The test matrix encodes this: each test checks whether the provider supports the feature and skips gracefully if not. When a provider adds support, you flip a flag in the support matrix and the tests start running automatically. - -The full suite completes in about 2 minutes with parallel execution. - -## Why This Matters Long-Term - -Testing infrastructure isn't exciting. But it's the difference between an SDK you can trust for years and one that breaks in subtle ways every few releases. - -**Regressions are caught before they merge.** Every PR runs the full E2E suite. If a change to the streaming parser breaks Anthropic's tool calling, CI goes red before it hits main. No user reports. No hotfix releases. - -**New providers slot into the matrix.** Adding a provider means implementing the adapter, adding it to the support matrix, and pointing it at aimock. The existing 17 feature tests run against it automatically. The infrastructure scales with the project. - -**Contributors get fast feedback.** A 2-minute test suite that runs without API keys means anyone can fork the repo, make a change, and verify it works across all providers. No secrets to configure, no tokens to burn, no flaky CI to retry. - -**The test suite is documentation.** Every fixture file is a concrete example of what TanStack AI expects from a provider. The `chat/basic.json` fixture shows exactly what a chat response looks like. The `tool-calling/single.json` fixture shows the full tool call and response sequence. When behavior is ambiguous, the fixtures are the source of truth. - -This is how you build compound stability. Each test guards against a specific regression. Over time, the suite accumulates into a safety net that gets stronger with every contribution. - -## How aimock Makes It Work - -[aimock](https://github.com/CopilotKit/aimock) is an open-source mock server built by the CopilotKit team. It serves deterministic LLM responses from JSON fixture files. Here's what a fixture looks like: - -```json -{ - "fixtures": [ - { - "match": { "userMessage": "[chat] recommend a guitar" }, - "response": { - "content": "I'd recommend the Fender Stratocaster for its versatile tone..." - } - } - ] -} -``` - -For tool calling, fixtures support multi-step sequences: - -```json -{ - "fixtures": [ - { - "match": { - "userMessage": "[toolcall] what guitars do you have in stock", - "sequenceIndex": 0 - }, - "response": { - "toolCalls": [{ "name": "getGuitars", "arguments": "{}" }] - } - }, - { - "match": { - "userMessage": "[toolcall] what guitars do you have in stock", - "sequenceIndex": 1 - }, - "response": { - "content": "Here's what we have in stock: ..." - } - } - ] -} -``` - -The `sequenceIndex` field lets you model multi-turn tool flows: the LLM calls a tool, gets a result, then responds with text. aimock plays back each step in order. - -A single aimock instance starts in `globalSetup` and is shared across all tests. Provider adapters are configured to point at aimock's URL instead of the real API endpoint. The `X-Test-Id` header isolates parallel tests so they don't share fixture state. - -aimock also has a recording mode: set a real API key, run the tests, and it captures live responses as fixture files. This makes it easy to add new tests or update existing fixtures when provider behavior changes. - -This testing infrastructure is one of the things that makes [TanStack AI](https://tanstack.com/ai/latest) a reliable foundation for building AI-powered applications. If you're looking for an AI SDK that takes provider parity seriously, give it a try. - ---- - -If you're interested in using aimock for your own project's LLM testing, check out the repo at [github.com/CopilotKit/aimock](https://github.com/CopilotKit/aimock). It's open source and works with any AI SDK, not just TanStack AI. diff --git a/src/blog/incident-followup.md b/src/blog/incident-followup.md deleted file mode 100644 index 013c5dd39..000000000 --- a/src/blog/incident-followup.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: 'Hardening TanStack After the npm Compromise' -published: 2026-05-12 -draft: false -excerpt: "A companion to our incident postmortem: what we're changing across the org so the May 11 supply-chain attack can't happen the same way again." -authors: - - Sarah Gerrard - - Corbin Crutchley - - Jack Herrington - - Tanner Linsley - - Florian Pellet - - Harry Whorlow ---- - -> **Status (2026-05-15): All clear ✅** -> -> After a three-day full security sweep and hardening pass, we're issuing an official all-clear on TanStack repo and package security. -> -> - Only the Router/Start repo was affected — 42 monorepo packages, 2 versions each. All were deprecated within the hour and removed by npm shortly after. -> - All other TanStack repos and packages were unaffected and remain secure: Query, DB, Store, AI, Table, Form, HotKeys, Virtual, Pacer, Config, Devtools, CLI, Intent, etc. -> - Every currently-available published version of every TanStack package — Router and Start included — is safe to install. - -_Last updated 2026-05-15 — see [Changelog](#changelog)._ - -This week, 42 of our packages were republished to npm with malware baked into the published artifacts. The releases were triggered by our normal release pipeline after changes landed on main, but the malicious code was not authored, reviewed, or approved by us. By the time the first report reached our issue tracker, those compromised versions had already been available on the registry for about 20 to 26 minutes. - -We've already published [the full incident postmortem](/blog/npm-supply-chain-compromise-postmortem), and if you want the timeline, the attack chain, the exact package list, the IOCs, and the "what to do if you installed a bad version" guidance, that's the source of truth. Read that first. - -This post is the companion piece. The postmortem covered what happened. This one is about what we're changing because of it. - -## The shortest possible recap - -Just enough context so this post makes sense on its own: - -Someone opened a pull request from a throwaway fork. While we never got a chance to see the PR since it was immediately closed, it had still triggered a workflow that checked out the contributor's code and ran it in a job that had write access to our shared CI cache. - -The attacker's code poisoned the cache and later, when an entirely legitimate merge to main, from a different PR, ran our release workflow, it restored that poisoned cache, extracted our short-lived publish token out of the runner's memory, and used it to push malicious versions across our router-family packages. - -Just to be clear, no maintainer was phished, had a password leak, or a token stolen from their account. Since this attack rode in through a trusted cache, it didn't need to. The attacker managed to engineer a path where our own CI pipeline stole its own publish token for them, at the exact moment it was created, by way of a cache that everyone in the chain implicitly trusted. It is a sophisticated approach that we hadn't anticipated and that we're taking very seriously. - -## How our releases normally work - -In order to understand how this attack worked against us, it's important to understand how the release process for TanStack packages typically works. Every release that ships to npm under a `@tanstack/*` name goes through a staged process: - -1. Through a reviewed pull request, a change lands on `main` that includes a version bump and a changelog update. That PR is reviewed and merged by a maintainer, just like any other PR. -2. Releases are cut from `main` by a CI workflow (when it works), not by a person running `npm publish` from a laptop. -3. The workflow authenticates to npm through GitHub's OIDC trusted-publisher integration, which means there is no long-lived npm publish token sitting waiting to be stolen. -4. The publish credential is minted at release time, scoped to that workflow, and expires almost immediately. - -The distinction that there was no publish token to be stolen is important. A large class of npm supply-chain attacks work by stealing a maintainer's publish token, often from their local machine or a compromised dev environment, that are then used to publish malicious versions directly. - -Our setup was designed specifically to make that path a non-starter. With no long-lived npm publish token sitting on a maintainer machine, an attacker couldn't simply compromise a laptop, steal credentials from a local environment, and publish malicious packages at will. That matters especially in the context of the current wave of malware and infostealer campaigns targeting developer machines and browser-stored secrets. - -OIDC-based publishing also gave us tighter control over who could release packages and under what conditions. Releases were tied to specific GitHub workflows running from specific repositories and branches, rather than to a reusable credential that could be copied, shared, or quietly reused elsewhere. - -With publishes tied back to workflow runs instead of a token, we also had a much easier time understanding the scope of the attack once we had the first report. It showed us exactly what workflow run triggered the publish, which branch it ran from, and when it ran. That audit trail was a significant advantage during incident response and helped us scope the what had happened much more quickly. - -## The honest part - -The way we had our workflow structured was inevitably how this was attack was made possible. The attack vector was a workflow that ran on [`pull_request_target`](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target), which is a special event that runs in the context of the base repository instead of the fork. That means the fork, which was renamed to avoid being detected as a fork of the repo, had write access to the cache that our release workflow later read from. While this has been [documented as a known-bad pattern by GitHub's own security team](https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/) for over three years, we still had it in our workflow and it was the root cause of this attack. - -Knowing we had added in production is something we have to sit with now. While there are many things we had in place that worked as intended, not being on top of the fact that this pattern was in our workflow is a failure on our part. We had the information we needed to know that this was a potential problem, but we didn't connect the dots to our own setup. - -While we can say that the npm provenance, SLSA, OIDC, and 2FA all worked as advertised and still didn't stop this attack, that's not the whole story. The workflow shape itself was the hole, and that's what we're rebuilding now. - -Modern supply-chain defences are important, but they're not always enough on their own. We have to be more proactive about identifying and closing any holes in our workflows that could be exploited, rather than relying solely on the security features of the tools we use. - -Knowing that we had a known-bad pattern in our workflow, and it wasn't on our radar, means there was a breakdown in our internal processes and that's where we have to do better. We have to be more proactive about identifying and closing any holes in our workflows that could be exploited, rather than relying solely on the security features of the tools we use. - -## What we've already done - -These are the changes that have landed since the incident. They're not the whole plan — they're the things we could do quickly. - -- **Disabled the pnpm cache in our release pipeline.** We are still evaluating exactly what role the cache played here, whether it can be safely reintroduced (or if we even want it), and what additional hardening would be required if it is. -- **Removed all caches from GitHub Actions** across the affected workflows. Same reasoning. -- **Pinned every action in the org to a commit SHA.** No more `actions/checkout@v6.0.2`. A retargeted tag has the same blast radius as cache poisoning, and we'd just lived through cache poisoning, so we'd rather not open that wound again. -- **Enforced non-SMS 2FA across npm and GitHub.** 2FA was already required, but SMS was still allowed as a factor. It isn't anymore. -- **Removed every use of `pull_request_target` from our CI.** It was never in our CD pipeline but it shouldn't have been in CI either. If we need base-repo permissions to react to a PR, we'll do it through `workflow_run` against artefacts from a sandboxed `pull_request` job — the GitHub-recommended pattern for this exact scenario. -- **Upgraded every repo to pnpm 11**, so we inherit the ecosystem install-cooldown behaviour. It's not a fix on its own. It buys us a window. - -Most of those are blast-radius reductions rather than root-cause fixes; they shrink what a similar attack could reach, but the workflow shape itself is what we're rebuilding in the next round. - -## What we're working on next - -Some of the changes we're making take more than a hotfix to land properly, and a few of them are still open questions we're working through together. We're including the unresolved ones because how we're thinking about them feels as relevant as what we've already shipped. - -- **Adding [`zizmor`](https://github.com/woodruffw/zizmor) as a required PR check on every repo.** It's a static analyser for GitHub Actions workflows, and there's a chance if we had it running against the workflow this incident exploited, it could have flagged the `pull_request_target` + fork-checkout pattern before any of this happened. -- **Putting `CODEOWNERS` on `.github` folders**, restricting workflow changes to core maintainers. We're leaning toward "yes" on this one; workflow files are code that runs with our credentials, and that means they should be treated like the most privileged code in the repo, because that's effectively what they are. -- **Replacing the pnpm setup cache with `actions/cache/restore`**, which has more conservative defaults: explicit restore, no implicit write-on-exit. The auto-save behaviour of `actions/cache@v5` is what allowed the cache write to happen regardless of our `permissions:` block, and we want that path closed by design instead of by accident. - -And then the more difficult thing... - -### The PR question - -This is the change we're least sure about. - -We've been talking about closing the ability for external contributors to open pull requests against TanStack repos. Not closing source (to be very clear, we are absolutely not going closed source) but shifting from "PRs welcome from anyone" to something closer to "issues and discussions welcome from anyone, PRs from non-maintainers by invitation." Contributors who want to land code might do it by filing a detailed issue or patch suggestion, or by doing the kind of investigative and review work that earns a path into a committer role over time. - -We have complicated feelings about this, because open PRs are part of how a lot of us became maintainers in the first place. The path from "user with a fix" to "trusted committer" tends to run straight through opening a PR and having someone review it, and closing that path narrows what it means to participate in the project in ways that aren't only about us. There's also a version of this where it doesn't actually solve the underlying problem; the attack vector wasn't "a malicious PR got merged," it was "a malicious PR ran in CI," and you can keep PRs open and still close the CI side of that hole, which is what most of the changes earlier in this post are doing. - -So with that in mind... we haven't decided. The discussion is ongoing, and we're flagging it here because we'd rather you hear about it from us, with the context attached, than read about it sideways somewhere later. If we do go that route, it'll come with its own post about how the new contribution model actually works in practice since telling people to "send a patch via an issue" without the infrastructure to make that a real path is just a polite way of telling them to go away. - -## What we're taking from this - -It's tempting, after something like this, to end on a paragraph about how the ecosystem needs to change (and it does). Cache scoping in GitHub Actions shouldn't silently bridge fork PRs and base-repo branches. Provenance shouldn't be confused with innocence. The gap between "this is a known-bad pattern" and "this pattern is hard to write by accident" is a gap the platform itself should be closing, not one every project should have to notice on its own. All of that is true, and we'll be advocating for it where we can. - -But it's not what we're sitting with right now. What we're sitting with is that the workflow shape that got exploited had been quietly working for a long time, and the parts of our security posture we actively thought about — OIDC, lockfiles, 2FA, signed commits — were the parts we'd already invested in. CI was the part we hadn't, and that's the part we're rebuilding now. - -To everyone who reported, verified, and helped triage — thank you. The fact that an independent researcher caught the dead-man's switch in the payload and warned responders before anyone started revoking tokens is the single reason this incident isn't significantly worse (even though it _is_ bad). That kind of work doesn't tend to make the headline, but it makes the difference, and we noticed. - -We'll do better. - -## Changelog - -- **2026-05-15** — Corrected the package count from "fourteen" (the scope in the initial third-party report) to 42 (the full scope identified by our subsequent scan). Refined the registry-exposure window from "about 20 minutes" to "20 to 26 minutes" using verified publish and report timestamps. Added an All-clear status banner. diff --git a/src/blog/multi-turn-structured-output.md b/src/blog/multi-turn-structured-output.md deleted file mode 100644 index 8229356b0..000000000 --- a/src/blog/multi-turn-structured-output.md +++ /dev/null @@ -1,259 +0,0 @@ ---- -title: 'Structured Output That Remembers Across Turns' -published: 2026-05-19 -excerpt: 'useChat({ outputSchema }) used to keep one slot for partial/final, so multi-turn structured chats lost prior turns the moment a new one streamed in. Every assistant turn now carries its own typed StructuredOutputPart on its UIMessage. History is preserved by default, and the schema generic threads all the way down to messages[i].parts[j].data.' -library: ai -authors: - - Alem Tuzlak ---- - -![Structured Output That Remembers Across Turns](/blog-assets/multi-turn-structured-output/header.png) - -You ask the LLM for a recipe. It plates up _Spaghetti Pomodoro_ — title, cuisine, servings, ingredients, steps, all typed against your schema. Beautiful. You ask it to make the recipe vegan. A new recipe streams in. - -Then the first one vanishes. - -That used to be the deal with `useChat({ outputSchema })`: one hook-level `partial` / `final` slot. The instant a new turn started streaming, the previous turn's recipe was clobbered. Multi-turn anything (recipe refinement, ticket triage, iterative form filling) needed manual history plumbing — you'd intercept every chunk, snapshot `final` into your own state, juggle a `recipes[]` array yourself, and keep it in sync with `messages[]` to avoid drift. The schema's type safety also stopped at `partial` / `final`; once a structured payload landed in your local array, it was `unknown` again unless you cast. - -We just shipped a different shape. Every assistant turn now carries its own typed `structured-output` part on its `UIMessage`. History is preserved by default. The schema generic threads all the way down to `messages[i].parts.find(p => p.type === 'structured-output').data` — no casts, no manual tracking. Same `useChat` hook, same `outputSchema` option, less code in your component. If you missed the prior post on streaming a single typed object end-to-end, [start here](https://tanstack.com/blog/streaming-structured-output) — that piece is the antecedent to this one. - -This post walks through what changed, why it matters for any multi-turn UI, and how to build the recipe-builder pattern end-to-end in roughly 80 lines (split across a server route and a client component). - -## The old shape - -The previous `useChat({ outputSchema })` exposed two values that tracked the _current_ run: - -- `partial` — `DeepPartial`, the progressively-parsed object as JSON streamed in -- `final` — `T | null`, the validated object once `structured-output.complete` fired - -On a single-turn extractor (paste a paragraph → get a typed `Person`), this was perfect. Field-by-field reveal as the JSON streamed, validated payload on terminal event, one render to consume both. - -On a multi-turn chat it fell apart. `partial` and `final` were a _single slot_, scoped to whichever run was most recent. As soon as you called `sendMessage()` again, the previous turn's `final` was gone. The runtime had no place to keep it — the typed structured payload didn't live on the message itself, only in this transient hook state. - -The workaround we'd see in user code: - -```tsx -const [recipes, setRecipes] = useState>([]) - -const { sendMessage, final } = useChat({ - outputSchema: RecipeSchema, - connection: fetchServerSentEvents('/api/recipes'), - onFinish: () => { - if (final) setRecipes((prev) => [...prev, final]) - }, -}) -``` - -Three problems with that: - -1. **The data lives in two places now.** `messages[]` has the assistant turns; `recipes[]` has the typed objects. Keeping them in sync (across reloads, retries, edits, server snapshots, replays) is your problem. -2. **The model can't see the history.** Your `recipes[]` array is local to the component, the wire layer never sees it. When the user types "now make the vegan one cheaper," the LLM has no idea what "the vegan one" refers to; it only sees the raw text of each prior turn, with the structured response stripped to whatever it landed on the original `TextPart` as. Multi-turn refinement collapses. -3. **You lose the schema's type safety.** `recipes` is typed, but the moment you try to round-trip a prior recipe back into the conversation, you're stringifying a typed object into the wire payload by hand. The library can't help you because it doesn't know `recipes` exists. - -The right place for this state is _on the message it came from_. That's what we shipped. - -## The new shape: typed parts on every assistant message - -Every `UIMessage` has a `parts: MessagePart[]` array. Before, the variants were `text`, `image`, `audio`, `video`, `document`, `tool-call`, `tool-result`, `thinking`. We added one: - -```ts -type StructuredOutputPart = { - type: 'structured-output' - status: 'streaming' | 'complete' | 'error' - /** Progressive parse — populated while streaming and after complete. */ - partial?: DeepPartial - /** Validated final object — set when status === 'complete'. */ - data?: TData - /** Accumulating JSON buffer — source of truth for the wire round-trip. */ - raw: string - reasoning?: string - errorMessage?: string -} -``` - -The runtime routes `TEXT_MESSAGE_CONTENT` deltas (the streaming JSON bytes) into this part instead of building a `TextPart`. On the terminal `structured-output.complete` event, `status` flips to `'complete'` and `data` is populated with the validated object. Every assistant turn produces a _new_ assistant message, which carries its _own_ `structured-output` part. The previous turn's part is untouched. - -The hook-level `partial` and `final` still exist, they're derived from the _latest_ assistant message's part now, instead of being a sticky slot. That means they read `{}` and `null` between `sendMessage()` and the first chunk (because no new assistant message exists yet), and they snap to the freshest turn's payload as it streams. The migration is zero, the same code that read `partial` / `final` for a single-turn extractor reads identical values in the new shape. - -What changed is everything _else_. Walking `messages[]` now exposes the full history of typed objects: - -```tsx -type RecipePart = StructuredOutputPart - -messages.map((m) => { - if (m.role === 'assistant') { - const part = m.parts.find( - (p): p is RecipePart => p.type === 'structured-output', - ) - // part.data is Recipe (the schema-inferred type) — no cast. - // part.partial is DeepPartial. - if (part) return - } - return null -}) -``` - -The schema generic flows through `useChat` → `UIMessage` → `MessagePart` → `StructuredOutputPart`, so `part.data` resolves to `Recipe` with no cast. The `RecipePart` alias is for readability; you can inline `Extract` instead if you'd rather not name it. The same flow runs in Vue (`computed`), Solid (`createMemo`), and Svelte (`$derived.by`) — the parity work shipped in the same release. - -## Building a recipe refinement loop - -Here's the full recipe-builder pattern. Server endpoint first: - -```ts -// app/api/recipes/route.ts -import { chat, toServerSentEventsResponse } from '@tanstack/ai' -import { openaiText } from '@tanstack/ai-openai' -import { z } from 'zod' - -export const RecipeSchema = z.object({ - title: z.string(), - cuisine: z.string(), - servings: z.number(), - estimatedCostUsd: z.number(), - ingredients: z.array(z.object({ item: z.string(), amount: z.string() })), - steps: z.array(z.string()), - tips: z.array(z.string()), -}) - -export type Recipe = z.infer - -const SYSTEM = `You are a chef. Reply with a single recipe matching the JSON schema. When the user asks for modifications, produce a new recipe in the same shape that reflects the change.` - -export async function POST(request: Request) { - const { messages } = await request.json() - const stream = chat({ - adapter: openaiText('gpt-5.2'), - messages, - systemPrompts: [SYSTEM], - outputSchema: RecipeSchema, - stream: true, - }) - return toServerSentEventsResponse(stream) -} -``` - -That's the whole server side. The only structured-output-specific lines are `outputSchema: RecipeSchema` and `stream: true`. The runtime takes care of converting the schema to JSON Schema, hitting the provider's native structured-output API, validating the response, and emitting `structured-output.start` + `structured-output.complete` events to the client. - -Client: - -```tsx -import { useState } from 'react' -import { useChat, fetchServerSentEvents } from '@tanstack/ai-react' -import type { StructuredOutputPart } from '@tanstack/ai-client' -import { RecipeSchema, type Recipe } from './api/recipes' - -type RecipePart = StructuredOutputPart - -export function RecipeBuilder() { - const [input, setInput] = useState('') - - const { messages, sendMessage, isLoading } = useChat({ - outputSchema: RecipeSchema, - connection: fetchServerSentEvents('/api/recipes'), - }) - - return ( -
- {messages.map((m) => { - if (m.role === 'user') { - const text = m.parts - .filter((p) => p.type === 'text') - .map((p) => p.content) - .join('') - return - } - if (m.role === 'assistant') { - const part = m.parts.find( - (p): p is RecipePart => p.type === 'structured-output', - ) - if (!part) return null - return - } - return null - })} - - setInput(e.target.value)} /> - -
- ) -} - -function RecipeCard({ part }: { part: RecipePart }) { - // `data` is Recipe (typed by the schema). `partial` is DeepPartial. - // Both are typed — no cast, no `unknown`. - const recipe = part.data ?? part.partial ?? {} - return ( -
-

{recipe.title ?? 'Plating up…'}

- {recipe.cuisine &&

{recipe.cuisine}

} - {recipe.ingredients?.map((ing, i) => ( -
  • - {ing?.amount} {ing?.item} -
  • - ))} -
    - ) -} -``` - -That's the entire client side. There's no separate `recipes[]` state, no `onFinish` callback, no manual history sync. Each `sendMessage()` triggers a new run, which produces a new assistant message, which carries its own typed structured-output part. The render loop walks `messages` and the new card just lands. - -Try it: - -> "Pasta dinner for two, under $15." → recipe lands. -> -> "Now make it vegan." → second recipe lands. The first is still on screen. -> -> "Add a salad and make it gluten-free." → third recipe lands. Both previous turns are still there. - -## How the round-trip stays coherent - -Multi-turn structured chats only work if the model sees its own prior responses on each follow-up turn. Otherwise turn N+1 has no idea what "make it vegan" refers to. - -The wire layer handles this. When `useChat` sends turn N+1, it serializes the conversation back through `uiMessageToModelMessages`. For each assistant message with a completed `structured-output` part, the converter emits: - -```ts -{ role: 'assistant', content: part.raw } -``` - -`part.raw` is the original JSON the model produced, preserved byte-for-byte from the streaming bytes that built the part in the first place. The model sees its own prior recipe verbatim and can reason about modifications. There's a defensive fallback (`JSON.stringify(part.data)`) for terminal-only completes that arrived without streamed bytes, plus a "drop the turn if we can't serialize it" guard for unserializable `data` like `BigInt`s or circular refs. Streaming and errored parts are dropped from the round-trip; you don't want to feed an incomplete JSON fragment back to the LLM. - -You don't see any of this. You called `sendMessage('now make it vegan')` and the model knew what it was modifying. - -## The schema generic threads through every framework - -The same release shipped parity across the framework hook packages: `@tanstack/ai-react`, `@tanstack/ai-vue`, `@tanstack/ai-solid`, and `@tanstack/ai-svelte` (which uses `createChat` instead of `useChat`). Each one threads the `TSchema` generic the same way: - -- `useChat` (or `createChat` for Svelte) substitutes `TData = InferSchemaType` when a schema is supplied. -- `messages` becomes `Array>`. -- `MessagePart` substitutes `StructuredOutputPart` into the discriminated union. - -Net effect: in any of the four frameworks, with `outputSchema: RecipeSchema`, `messages[i].parts.find(p => p.type === 'structured-output').data` resolves to `Recipe | undefined`. Default `TData = unknown` keeps every existing consumer that doesn't pass a schema source-compatible. - -`@tanstack/ai-preact` doesn't support `outputSchema` yet; that's tracked separately. - -## When to use this - -The multi-turn pattern lights up any UI where: - -- Users iterate on a structured object across turns (recipe builder, design spec refinement, ticket triage, A/B copy variants). -- You want to render the history of typed objects, not just the latest. -- You want the model to remember its own structured responses on follow-ups without you serializing them yourself. - -For a single round-trip (one prompt, one typed object), use [`chat({ outputSchema })`](https://tanstack.com/ai/docs/structured-outputs/one-shot), the non-streaming activity is simpler. For a streaming UI that progressively fills in one object (the classic field-by-field form), use [`useChat({ outputSchema })`](https://tanstack.com/ai/docs/structured-outputs/streaming) and read `partial` / `final`, the new shape preserves that surface unchanged. Use [multi-turn](https://tanstack.com/ai/docs/structured-outputs/multi-turn) when history is a feature, not a chore. - -## Try it - -The full recipe-builder UI ships in the [`ts-react-chat` example](https://github.com/TanStack/ai/tree/main/examples/ts-react-chat) at `/generations/structured-chat`: cuisine-aware hero banners, streaming preview, multi-turn history, the lot. The docs walk through the pattern at [structured-outputs/multi-turn](https://tanstack.com/ai/docs/structured-outputs/multi-turn). - -If you already use `useChat({ outputSchema })`, you don't need to change anything to get the new types — `partial` and `final` keep working. You opt into multi-turn the moment you walk `messages` instead of just reading the hook-level sugar. - -[**Read the multi-turn docs →**](https://tanstack.com/ai/docs/structured-outputs/multi-turn) diff --git a/src/blog/netlify-partnership.md b/src/blog/netlify-partnership.md deleted file mode 100644 index 02e04020f..000000000 --- a/src/blog/netlify-partnership.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: TanStack + Netlify Partnership -published: 2025-03-18 -excerpt: Netlify is now the official deployment partner for TanStack Start. Their focus on speed, simplicity, and flexibility aligns perfectly with our vision for full-stack development. -authors: - - Tanner Linsley ---- - -![Netlify Header](/blog-assets/netlify-partnership/header.jpg) - -We’re excited to announce that **Netlify** is now the **Official Deployment Partner** for **TanStack Start**! Together, we’re making it easier than ever for developers to build and deploy modern, type-safe, and user-focused web applications. - -Netlify has earned its reputation as the ultimate deployment platform for modern web developers. Its focus on speed, simplicity, modularity, and flexibility aligns perfectly with TanStack Start’s vision for full-stack development. Here’s why Netlify stands out: - -- **No-config simplicity** – Deploy your TanStack Start apps in seconds, with zero setup hassle. -- **Serverless power** – Netlify Functions enable dynamic, real-time features effortlessly. -- **Global performance** – Fast, reliable apps served from Netlify’s global edge network. -- **Developer-first tools** – Instant previews, automated workflows, and seamless integrations make building a joy. - -## Why Netlify? - -Netlify is more than just a deployment provider. They’ve worked closely with us to ensure that deploying TanStack Start applications is not just fast, but optimized for the best possible developer experience. Whether you’re building interactive UIs, data-heavy dashboards, real-time tools, or AI-powered applications, Netlify’s platform makes the process seamless. - -As part of this partnership, Netlify has also launched a **full-stack AI chatbot starter template** that showcases TanStack Start’s powerful data management capabilities alongside Netlify Functions. This template provides: - -- **Real-time data handling** with TanStack Query -- **Efficient routing** with TanStack Router -- **Seamless server function integration** with Netlify -- **A production-ready deployment configuration** - -To get started with the template, simply run: - -``` -npx create-tsrouter-app@latest --template file-router --add-ons tanchat -``` - -This template is a great way to explore how TanStack Start and Netlify work together to simplify modern web development. - -## What’s Next? - -We’re just getting started. Expect more updates, new features, and deeper collaboration between TanStack Start and Netlify. Stay tuned for success stories, guides, and real-world examples showcasing what’s possible with this powerful combination. - -Additionally, join us March 31 for a **special TanStack Start episode on [Netlify’s Remote Desk series](https://www.netlify.com/webinars/netlify-remote-desk/)**. We’ll dive into live demos, developer tips, and an exclusive Q&A to show how to unlock the full potential of TanStack Start on Netlify. - -**Ready to dive in?** Check out the [TanStack Start docs](/start/latest/docs/framework/react/overview), explore the deployment guides, and start building with Netlify today. - -A huge thank you to Netlify for supporting and empowering developers. Let’s build something incredible together! 🚀 diff --git a/src/blog/npm-stats-the-right-way.md b/src/blog/npm-stats-the-right-way.md deleted file mode 100644 index e194ca711..000000000 --- a/src/blog/npm-stats-the-right-way.md +++ /dev/null @@ -1,328 +0,0 @@ ---- -title: 'How We Track Billions of Downloads: The NPM Stats API Deep Dive' -published: 2025-12-02 -excerpt: When you're tracking download stats for 200+ packages with over 4 billion total downloads, you learn a few things about NPM's API. Some lessons are documented. Others you discover the hard way. -authors: - - Tanner Linsley ---- - -When you're tracking download stats for an ecosystem of 200+ packages that have been downloaded over 4 billion times, you learn a few things about NPM's download counts API. Some of those lessons are documented. Others you discover the hard way. - -This post is about one of those hard-learned lessons: **why we can't just ask NPM for all-time download stats in a single request, and why the approach matters more than you'd think.** - ---- - -## The Two Faces of NPM Stats - -NPM offers two endpoints for download statistics: - -**The Point Endpoint** (`/downloads/point/{period}/{package}`) - -```json -{ - "downloads": 585291892, - "start": "2024-06-06", - "end": "2025-12-06", - "package": "@tanstack/react-query" -} -``` - -This gives you a single aggregate number. Clean, simple, exactly what you want. - -**The Range Endpoint** (`/downloads/range/{period}/{package}`) - -```json -{ - "downloads": [ - { "day": "2024-06-06", "downloads": 1904088 }, - { "day": "2024-06-07", "downloads": 1847293 }, - ... - ], - "start": "2024-06-06", - "end": "2025-12-06", - "package": "@tanstack/react-query" -} -``` - -This gives you day-by-day breakdowns. More data, but you have to sum it yourself if you want totals. - -On the surface, these should return the same numbers. The range endpoint just has more detail, right? - -Not quite. - ---- - -## The 18-Month Wall - -Here's what the docs say: **both endpoints are limited to 18 months of historical data for standard queries.** - -But here's what the docs don't emphasize: when you request more than 18 months, **the API silently truncates your results.** - -Let me show you what I mean. - ---- - -## The Experiment - -I built a script to test this. Simple premise: query the same packages with both endpoints across different time ranges, and see what happens. - -```javascript -// Query @tanstack/react-query for different time periods -const periods = [ - 'last-week', // 7 days - 'last-month', // 30 days - '2024-12-06:2025-12-06', // 12 months - '2024-06-06:2025-12-06', // 18 months - '2023-12-07:2025-12-06', // 24 months - '2015-01-10:2025-12-06', // All-time -] -``` - -For short periods (7 days, 30 days, 12 months), everything worked perfectly. Both endpoints returned identical data: - -``` -Last 7 days: - Point: 13,085,419 - Range: 13,085,419 (7 days) - ✅ Difference: 0.00% - -Last 30 days: - Point: 54,052,299 - Range: 54,052,299 (30 days) - ✅ Difference: 0.00% - -12 months: - Point: 479,463,656 - Range: 479,463,656 (366 days) - ✅ Difference: 0.00% -``` - -Great! The endpoints match. Time to query all-time stats. - ---- - -## The Plot Twist - -Here's where things get interesting: - -``` -18 months: - Point: 585,291,892 - Range: 585,291,892 (549 days) - ✅ Difference: 0.00% - -24 months (beyond limit): - Point: 585,291,892 - Range: 585,291,892 (549 days) - ✅ Difference: 0.00% - -All-time (from 2015): - Point: 585,291,892 - Range: 585,291,892 (549 days) - ✅ Difference: 0.00% -``` - -Notice something? **The numbers are identical for 18 months, 24 months, and all-time.** - -Same downloads. Same number of days (549). **Both endpoints are silently capped at roughly 18 months**, returning exactly 549 days of data no matter what date range you request. - -This means: - -- Requesting "all downloads since 2015" gives you 18 months of data -- The API doesn't tell you it truncated your request -- Both endpoints fail the same way - -TanStack Query has been around since 2019. React has been around since 2013. If we just asked NPM for "all-time" stats, we'd be missing **years** of download history. - ---- - -## Proof: The Real Numbers - -To validate this, I ran a second test comparing single requests vs properly chunked requests: - -**Single Request** (2019-10-25 to today): - -``` -Downloads: 585,291,892 -Days: 549 days -``` - -**Chunked Requests** (same period, 5 chunks of ~500 days each): - -``` -Chunk 1 (2019-10-25 → 2021-03-08): 0 downloads -Chunk 2 (2021-03-09 → 2022-07-22): 0 downloads -Chunk 3 (2022-07-23 → 2023-12-05): 86,135,448 downloads -Chunk 4 (2023-12-06 → 2025-04-19): 284,835,067 downloads -Chunk 5 (2025-04-20 → 2025-12-06): 373,366,977 downloads - -Total: 744,337,492 -Days: 2,235 days -``` - -**The difference? 159 million downloads.** That's **27% of the data** completely missing from the single request approach. - -You can verify this yourself using tools like [npm-stat.com](https://npm-stat.com), which properly implements chunking and shows ~744M downloads for TanStack Query - matching our chunked approach, not the naive single-request number. - ---- - -## Why This Matters - -When you're tracking growth for an open source ecosystem, accuracy matters. Not for vanity metrics, but because: - -1. **Sponsorship decisions** are made based on real adoption numbers -2. **Contributors want to see impact** from their work -3. **Companies evaluating libraries** look at download trends - -If we naively queried for all-time stats, we'd be reporting 585 million downloads for TanStack Query. The real number? **744 million**. That's 159 million downloads (27%) missing. - -For the entire TanStack ecosystem with 200+ packages? We'd be off by billions. - ---- - -## The Right Way: Chunked Requests - -The solution is to break time into chunks and request each period separately: - -```typescript -async function fetchAllTimeDownloads(packageName: string, createdDate: string) { - const chunks = [] - const maxChunkDays = 500 // Stay under 18-month limit - - let currentDate = new Date(createdDate) - const today = new Date() - - while (currentDate < today) { - const chunkEnd = new Date(currentDate) - chunkEnd.setDate(chunkEnd.getDate() + maxChunkDays) - - if (chunkEnd > today) { - chunkEnd = today - } - - const from = formatDate(currentDate) - const to = formatDate(chunkEnd) - - const url = `https://api.npmjs.org/downloads/range/${from}:${to}/${packageName}` - const data = await fetchWithRetry(url) - - chunks.push(data) - currentDate = chunkEnd - - // Small delay to avoid rate limiting - await sleep(200) - } - - // Sum all chunks - return chunks.reduce((total, chunk) => { - return total + chunk.downloads.reduce((sum, day) => sum + day.downloads, 0) - }, 0) -} -``` - -This approach: - -- Breaks the timeline into ~17-month chunks (staying safely under the limit) -- Fetches each chunk sequentially to avoid rate limiting -- Sums the results to get true all-time totals -- Includes retry logic for reliability - -It's more work, but it's the only way to get accurate historical data. - ---- - -## Point vs Range: Which One? - -After all this, you might wonder: should you use `/point/` or `/range/`? - -**For all-time stats, use `/range/` with chunking.** Here's why: - -1. **They return the same totals** (when you sum the daily breakdowns from range) -2. **Range gives you daily granularity** for trend analysis -3. **Range is what you need for chunking anyway** (you have to sum across chunks) -4. **Both are limited to 18 months**, so there's no advantage to point - -The point endpoint is useful for quick spot checks or when you only need recent data. But for building a real stats system, range is the way to go. - ---- - -## Our Implementation - -At TanStack, we've built a sophisticated stats system that handles this properly: - -- **Automatic chunking** for packages created before 18 months ago -- **Rate limit handling** with exponential backoff -- **Concurrent processing** of 8 packages at a time (to balance speed and API limits) -- **Database caching** with 24-hour TTL to avoid hammering NPM -- **Scheduled refreshes** every 6 hours via Netlify functions -- **Growth rate calculation** from the most recent 7 days for live animations - -The full implementation is in [`src/utils/stats.functions.ts`](https://github.com/TanStack/tanstack.com/blob/main/src/utils/stats.functions.ts) if you want to see how we handle the details. - -### Library-Level Aggregation - -One interesting aspect of our system is how we track stats at the library level. Each TanStack library (Query, Table, Router, etc.) maps to a GitHub repository, and we aggregate downloads for **all npm packages** published from that repo. - -This includes: - -1. **Scoped packages**: Like `@tanstack/react-query`, `@tanstack/query-core` -2. **Legacy packages**: Like `react-query` (the pre-rebrand name) -3. **Addon packages**: Like `@tanstack/react-query-devtools`, `@tanstack/react-query-persist-client` - -For example, TanStack Query's library stats include: - -```typescript -// From src/libraries/query.tsx -{ - repo: 'tanstack/query', - legacyPackages: ['react-query'] -} -``` - -This means our library metrics sum up all related packages, which can include addons and dependencies that might also depend on the core package. This **could inflate library-level numbers** since some downloads might be for packages that themselves depend on other packages in the same library. - -We know this. We're keeping it simple for now. - -The alternative would be dependency analysis and deduplication - figuring out which packages depend on each other and avoiding double-counting. That's a project for another day. For now, the simple aggregation gives us a reasonable approximation of ecosystem reach, even if it's not perfectly precise. - -What matters is consistency: we track the same way over time, so trends and growth rates remain meaningful. - ---- - -## The Results - -This approach lets us accurately track downloads across **203 packages** (199 scoped @tanstack/\* + 4 legacy packages), maintaining historical accuracy going back to 2015. - -The stats you see on [tanstack.com](/stats) aren't guesses or estimates. They're the sum of thousands of individual API calls, properly chunked, cached, and aggregated. - -When we say TanStack has been downloaded over 4 billion times, that number is real. And it's growing by millions every day. - ---- - -## Key Takeaways - -If you're building a system to track NPM download stats: - -1. **Never trust a single "all-time" request** - it's capped at 18 months -2. **Use the `/range/` endpoint with chunking** for historical accuracy -3. **Implement retry logic** - rate limiting will happen -4. **Cache aggressively** - NPM's data doesn't update instantly -5. **Test your assumptions** - build experiments to verify behavior - -The NPM download counts API is powerful, but it has sharp edges. Understanding these limitations is the difference between showing users vanity metrics and giving them real data. - ---- - -## Why This Matters to TanStack - -We care about this because **transparency matters**. When we show download stats, we want them to be accurate. When we talk about growth, we want it to be real. - -The same principle applies to our libraries. We don't hide complexity behind magic. We build tools that are powerful when you need them to be, and simple when you don't. - -That's the TanStack way. - ---- - -**Want to dive deeper into how we build TanStack?** [Join our Discord](https://tlinz.com/discord) where we talk about architecture, API design, and the technical decisions behind the ecosystem. - -**Using TanStack and want to support the work?** [Check out our sponsors and partners page](/partners). Every contribution helps us keep building open source the right way. diff --git a/src/blog/npm-supply-chain-compromise-postmortem.md b/src/blog/npm-supply-chain-compromise-postmortem.md deleted file mode 100644 index 15a25c6d7..000000000 --- a/src/blog/npm-supply-chain-compromise-postmortem.md +++ /dev/null @@ -1,261 +0,0 @@ ---- -title: 'Postmortem: TanStack npm supply-chain compromise' -published: 2026-05-11 -excerpt: On 2026-05-11, an attacker chained a pull_request_target Pwn Request, GitHub Actions cache poisoning across the fork↔base trust boundary, and OIDC token extraction from runner memory to publish 84 malicious versions across 42 @tanstack/* packages on npm. Full postmortem. -authors: - - Tanner Linsley ---- - -> **Status (2026-05-15): All clear ✅** -> -> After a three-day full security sweep and hardening pass, we're issuing an official all-clear on TanStack repo and package security. -> -> - Only the Router/Start repo was affected — 42 monorepo packages, 2 versions each. All were deprecated within the hour and removed by npm shortly after. -> - All other TanStack repos and packages were unaffected and remain secure: Query, DB, Store, AI, Table, Form, HotKeys, Virtual, Pacer, Config, Devtools, CLI, Intent, etc. -> - Every currently-available published version of every TanStack package — Router and Start included — is safe to install. -> -> See also: [Hardening TanStack After the npm Compromise](/blog/incident-followup) — the companion piece covering what we're changing because of this incident. - -_Last updated 2026-05-15 — see [Changelog](#changelog)._ - -## TL;DR - -On 2026-05-11 between 19:20 and 19:26 UTC, an attacker published 84 malicious versions across 42 `@tanstack/*` npm packages by combining: the `pull_request_target` "Pwn Request" pattern, GitHub Actions cache poisoning across the fork↔base trust boundary, and runtime memory extraction of an OIDC token from the GitHub Actions runner process. No npm tokens were stolen and the npm publish workflow itself was not compromised. - -The malicious versions were detected publicly within 20 to 26 minutes (depending on which of the two publish batches a given version came from) by an external researcher `ashishkurmi` working for `stepsecurity`. All affected versions have been deprecated; npm security has been engaged to pull tarballs from the registry. We have no evidence of npm credentials being stolen, but we strongly recommend that anyone who installed an affected version on 2026-05-11 rotate AWS, GCP, Kubernetes, Vault, GitHub, npm, and SSH credentials reachable from the install host. - -**Tracking issue:** [TanStack/router#7383](https://github.com/TanStack/router/issues/7383) -**GitHub Security Advisory:** [GHSA-g7cv-rxg3-hmpx](https://github.com/TanStack/router/security/advisories/GHSA-g7cv-rxg3-hmpx) - -## Impact - -### Packages affected - -42 packages, 84 versions (two per package, published roughly 6 minutes apart). See the tracking issue for the full table. Confirmed-clean families: `@tanstack/query*`, `@tanstack/table*`, `@tanstack/form*`, `@tanstack/virtual*`, `@tanstack/store`, `@tanstack/start` (the meta-package, not `@tanstack/start-*`). - -### What the malware does - -When a developer or CI environment runs `npm install`, `pnpm install`, or `yarn install` against any affected version, npm resolves the malicious `optionalDependencies` entry, fetches the orphan payload commit from the fork network, runs its `prepare` lifecycle script, and executes a ~2.3 MB obfuscated `router_init.js` smuggled into the affected tarball. The script: - -- Harvests credentials from common locations: AWS IMDS / Secrets Manager, GCP metadata, Kubernetes service-account tokens, Vault tokens, `~/.npmrc`, GitHub tokens (env, `gh` CLI, `.git-credentials`), SSH private keys -- Exfiltrates over the Session/Oxen messenger file-upload network (`filev2.getsession.org`, `seed{1,2,3}.getsession.org`) — end-to-end encrypted with no attacker-controlled C2, so blocking by IP/domain is the only network mitigation -- Self-propagates: enumerates other packages the victim maintains via `registry.npmjs.org/-/v1/search?text=maintainer:` and republishes them with the same injection - -Because the payload runs as part of npm install's lifecycle, anyone who installed an affected version on 2026-05-11 must treat the install host as potentially compromised. - ---- - -## Timeline - -All times UTC. Local timestamps from GitHub API and npm registry. - -### Pre-attack (cache poisoning phase) - -| Time | Event | -| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-05-10 17:16 | Attacker creates fork [github.com/zblgg/configuration](https://github.com/zblgg/configuration) (a fork of TanStack/router, deliberately renamed to evade fork-list searches) | -| 23:29 | Malicious commit `65bf499d16a5e8d25ba95d69ec9790a6dd4a1f14` authored on the fork by fabricated identity `claude `. Adds `packages/history/vite_setup.mjs` (a ~30,000-line bundled JS payload). Commit message prefixed with `[skip ci]` to suppress CI on the push event | -| 2026-05-11 ~10:49 | PR #7378 opened against `TanStack/router#main` titled "WIP: simplify history build" by `zblgg` | -| 10:49 onwards | `bundle-size.yml` and `labeler.yml` (both `pull_request_target`) auto-run for the PR — no first-time-contributor approval required because `pull_request_target` bypasses that gate. `pr.yml` (which uses `pull_request`) does NOT run, blocked pending approval that never came | -| 11:01–11:11 | Multiple force-pushes by `zblgg` to the PR head, each triggering more `pull_request_target` runs | -| 11:11 | Force-push lands `65bf499d` (the malicious commit) on the PR head. `bundle-size.yml`'s `benchmark-pr` job checks out `refs/pull/7378/merge`, runs `pnpm install` + `pnpm nx run @benchmarks/bundle-size:build` — this executes `vite_setup.mjs` | -| 11:29 | Cache entry `Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11` (1.1 GB) saved to GitHub Actions cache for `TanStack/router`, scope `refs/heads/main` — keyed to match what `release.yml` will look up on the next push to main | -| 11:31 | Attacker force-pushes the PR back to current main HEAD (`b1c061af`), making the visible PR a 0-file no-op. PR closed and branch deleted in the same minute. Cache poison persists. | - -### Detonation (publish phase) - -| Time | Event | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-05-11 19:15:44 | Workflow run `25613093674` (Release) is re-run as attempt #4 against main HEAD `b1c061af`. Original trigger was PR #7369 (Shkumbin's `CSS.supports` fix), merged 2026-05-09 22:11:51 UTC. Poisoned cache is restored on the runner. | -| 19:16:18 | Manuel merges PR #7382 (jiti tsconfig paths fix) → push to main triggers a fresh `release.yml` run | -| 19:16:22 | Workflow run `25691781302` starts (attempt #1). Same poisoned cache restored. | -| 19:20:39 | npm registry receives publish for `@tanstack/history@1.161.9` and 41 sibling packages from run `25613093674` (~half of the eventual 84 versions; the remainder come during run #2). Publish is authenticated via OIDC trusted-publisher binding for `TanStack/router release.yml@refs/heads/main` — but it does not come from the workflow's defined Publish Packages step, which was skipped because tests failed. It comes from the malware running during the test/cleanup phase, which mints an OIDC token via the workflow's `id-token: write` permission and POSTs directly to `registry.npmjs.org` | -| 19:20:48 | Run `25613093674` completes (status: failure) | -| 19:26:14 | npm registry receives publish for the second-version-per-package set (`@tanstack/history@1.161.12` etc.) from run `25691781302`. Same OIDC mechanism | -| 19:26:22 | Run `25691781302` completes (status: failure) | - -### Detection and response - -| Time | Event | -| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-05-11 19:46 | External researcher `ashishkurmi` working for StepSecurity opens issue #7383 with a complete writeup of the malicious `optionalDependencies` fingerprint and the package list (initially 14 of the 42) | -| ~19:50 | Researcher notifies npm security directly | -| ~20:00 | Manuel acknowledges in #7383 — incident response begins | -| ~20:10 | Manuel removes all other team push permissions on GitHub in case of user machines have been compromised | -| 20:19 | Tanner deprecates `@tanstack/history@1.161.9` and `@1.161.12` — the first two versions taken out of circulation | -| 20:41 | Batch deprecation runs across the initial 14-package / 28-version scope from issue #7383 | -| ~21:00 | Comprehensive scan of all 295 `@tanstack/*` packages confirms full scope: 42 packages, 84 versions (28 more than the StepSecurity report).

    Public Twitter/X/LinkedIn/Bluesky disclosure from @tan_stack and maintainers | -| 21:03 | Final batch deprecation runs across the remaining versions to cover the full 42-package / 84-version scope | -| 21:30 | Investigation identifies `bundle-size.yml` `pull_request_target` cache-poisoning vector and the `zblgg/configuration` fork.

    All cache entries for all `TanStack/*` GitHub repositories purged via API.

    Hardening PR merged: `bundle-size.yml` restructured, `repository_owner` guards added, third-party action refs pinned to SHAs.

    Official GitHub Security Advisory is published, CVE requested | -| 22:13–23:55 | npm removes the affected tarballs registry-side in response to the StepSecurity notification from earlier in the evening — first removal at 22:13:38 (`@tanstack/query-core`), last at 23:55:26 (`@tanstack/router-core`) | -| 2026-05-12 05:02 | Tanner emails security@npmjs.com and GitHub security with full IOC list and a formal request to pull tarballs registry-side.

    Formal malware reports are submitted via npm | - -### Response timing - -Elapsed time from two reference points: the first malicious **publish** (19:20 UTC) and the moment the incident became **known** (StepSecurity issue #7383, 19:46 UTC). - -| Milestone | From publish | From known | -| ----------------------------------- | ------------ | ---------- | -| Compromise publicly detected | ~26 min | — | -| First versions deprecated (2 of 84) | ~59 min | ~33 min | -| Initial scope deprecated (28 of 84) | ~1h 21m | ~55 min | -| Full scope deprecated (84 of 84) | ~1h 43m | ~1h 17m | -| First package removed by npm | ~2h 53m | ~2h 27m | -| Last package removed by npm | ~4h 35m | ~4h 9m | - ---- - -## Root cause - -Three vulnerabilities chained together. Each is necessary for the attack; none alone is sufficient. - -### 1. `pull_request_target` "Pwn Request" pattern in `bundle-size.yml` - -`bundle-size.yml` ran `pull_request_target` for fork PRs and, inside that trigger context, checked out the fork's PR-merge ref and ran a build: - -```yaml -on: - pull_request_target: - paths: ['packages/**', 'benchmarks/**'] - -jobs: - benchmark-pr: - steps: - - uses: actions/checkout@v6.0.2 - with: - ref: refs/pull/${{ github.event.pull_request.number }}/merge # fork's merged code - - - uses: TanStack/config/.github/setup@main # transitively calls actions/cache@v5 - - - run: pnpm nx run @benchmarks/bundle-size:build # executes fork-controlled code -``` - -The author of the workflow attempted a trust split (the `comment-pr` job is separate from `benchmark-pr`, with a comment in the YAML noting the intent to keep `benchmark-pr` "untrusted with read-only permissions"). The split is correct in spirit but missed two facts: - -- `actions/cache@v5`'s post-job save is not gated by `permissions:`. Cache writes use a runner-internal token, not the workflow `GITHUB_TOKEN`. Setting `permissions: contents: read` does not block cache mutation. -- Cache scope is per-repo, shared across `pull_request_target` runs (which use the base repo's cache scope) and pushes to main. A PR running in the base repo's cache scope can poison entries that production workflows on main will later restore. - -### 2. GitHub Actions cache poisoning across trust boundaries - -The malicious `vite_setup.mjs` was specifically designed to write data into the pnpm-store directory under a key the legit `release.yml` workflow would compute and look up: `Linux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')}`. When the `benchmark-pr` job ended, `actions/cache@v5`'s post-step saved the (now-poisoned) pnpm store to that exact key. When `release.yml` next ran on a push to main, its Setup Tools step restored the poisoned entry — entirely as designed. - -This is the class of attack documented by Adnan Khan in 2024. It's not a TanStack-specific bug; it's a known GitHub Actions design issue that requires conscious mitigation. - -### 3. OIDC token extraction from runner memory - -`release.yml` declares `id-token: write` (legitimately needed for npm OIDC trusted publishing). When the poisoned pnpm store is restored on the runner, attacker-controlled binaries are now on disk and get invoked during the build step. Those binaries: - -- Locate the GitHub Actions `Runner.Worker` process via `/proc/*/cmdline` -- Read `/proc//maps` and `/proc//mem` to dump the worker's memory -- Extract the OIDC token (which the runner mints lazily, in memory, when `id-token: write` is set) -- Use the token to authenticate POST requests directly to `registry.npmjs.org` — bypassing the workflow's Publish Packages step entirely - -This is the same memory-extraction technique (and verbatim Python script, with attribution comment) used in the `tj-actions/changed-files` compromise of March 2025. The attacker did not invent novel tradecraft; they recombined published research. - -### Why none alone is enough - -- `pull_request_target` alone is fine for trusted operations (labeling, comments) -- Cache poisoning alone (e.g., from inside an already-compromised dep) requires a separate publish vehicle -- OIDC token extraction alone requires existing code execution on the runner - -The chain only works because each vulnerability bridges the trust boundary the others assumed: PR fork code crossing into base-repo cache, base-repo cache crossing into release-workflow runtime, and release-workflow runtime crossing into npm registry write access. - ---- - -## Detection - -### How we found out - -Detection was external. External researcher `ashishkurmi` working for StepSecurity opened issue #7383 ~20 minutes after the publish, with full technical analysis. Tanner received a phone call from Socket.dev just moments after starting the war room confirming the situation. - -### IOC fingerprints (for downstream maintainers and security tools) - -In any `@tanstack/*` package's manifest: - -```json -"optionalDependencies": { - "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" -} -``` - -- File: `router_init.js` (~2.3 MB, package root, not in `"files"`) -- Cache key: `Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11` -- 2nd-stage payload URLs: `https://litter.catbox.moe/h8nc9u.js`, `https://litter.catbox.moe/7rrc6l.mjs` -- Exfiltration network: `filev2.getsession.org`, `seed{1,2,3}.getsession.org` -- Forged commit identity: `claude ` (note: not the real Anthropic Claude — fabricated GitHub no-reply email) -- Real attacker accounts: `zblgg` (id 127806521), `voicproducoes` (id 269549300) -- Attacker fork: [github.com/zblgg/configuration](https://github.com/zblgg/configuration) (fork of TanStack/router renamed to evade fork searches) -- Orphan payload commit (in fork network): `79ac49eedf774dd4b0cfa308722bc463cfe5885c` -- Workflow runs that performed the malicious publishes: - - [github.com/TanStack/router/actions/runs/25613093674](https://github.com/TanStack/router/actions/runs/25613093674) (attempt 4) - - [github.com/TanStack/router/actions/runs/25691781302](https://github.com/TanStack/router/actions/runs/25691781302) - ---- - -## Lessons learned - -### What went well - -- External researchers noticed and reported with full technical detail within ~20 min of the incident -- Maintainer team coordinated immediately and effectively across many timezones -- The detection community already had a clear public IOC pattern within hours - -### What could have been better - -- No internal alerting. We learned about the compromise from a third party. We need monitoring on our own publishes. We'll be working closely with security researcher firms in the ecosystem that have the ability to detect these issues very quickly, potentially even in-house, and making the feedback loop even tighter. -- `pull_request_target` workflows had not been audited despite being a long-known dangerous pattern -- Floating refs (`@v6.0.2`, `@main`) on third-party actions create standing supply-chain risk independent of this incident -- Unpublish was unavailable for nearly all affected packages because of npm's "no unpublish if dependents exist" policy. We have to rely on npm security to pull tarballs server-side, which adds hours of delay during which malicious tarballs remain installable -- The 7-maintainer list on the npm scope means seven separate credential-theft targets for the same blast radius -- OIDC trusted-publisher binding has no per-publish review. Once configured, any code path in the workflow can mint a publish-capable token. We need either (a) move to short-lived classic tokens with manual review, or (b) add provenance-source-verification to detect publishes from unexpected workflow steps - -### What we got lucky on - -- The attacker chose a payload that broke tests, which made the publish step (which would have produced cleaner-looking tarballs) skip — meaning the attack was loud enough to detect quickly. A more careful attacker who didn't break tests could have published silently for hours longer -- The attacker reused public tradecraft (verbatim memory-dump script with attribution comment) instead of writing novel code — making the IOC-matching faster - ---- - -## Open questions - -These need answers before we close the postmortem. - -- Did `bundle-size.yml`'s Setup Tools step actually call `actions/cache@v5`? Verify by reading the post-job logs from one of the `pull_request_target` runs against PR #7378 (e.g., run id `25666610798`). Tanner has access; needs to be done manually -- What was in the initial PR head commit (before the force-pushes wiped it)? GitHub's reflog may have it. Check via `gh api` or the GitHub support team -- How did the malicious commit get into the fork's git object store specifically — was it pushed directly via git, or was it created via the GitHub web UI (which would leave audit-log entries)? -- Was `voicproducoes` a real account or a sock puppet? Cross-reference its activity history -- Did the npm cache also get poisoned (the 6 duplicate `linux-npm-store-*` entries)? Were any actually used? -- Can we identify any other fork in the `TanStack/router` fork network that contains the orphan payload commit? (If yes, the cleanup is harder — every fork hosting it keeps it accessible via `github:tanstack/router#79ac49ee...`) -- Are any other TanStack repos (router, query, table, form, virtual, etc.) using the same `bundle-size.yml`-style pattern? Audit needed -- How many users actually downloaded the affected versions during the publish window? Get from npm support -- Did any of the seven listed maintainers' machines get compromised separately? (None of the malicious publishes used a maintainer's npm token, but maintainer machines could have been the secondary target via the self-propagation logic) - ---- - -## References - -- Tracking issue: [TanStack/router#7383](https://github.com/TanStack/router/issues/7383) -- GitHub Security Advisory: [GHSA-g7cv-rxg3-hmpx](https://github.com/TanStack/router/security/advisories/GHSA-g7cv-rxg3-hmpx) -- Related research: - - Adnan Khan, "The Monsters in Your Build Cache: Github Actions Cache Poisoning" (May 2024) — [adnanthekhan.com](https://adnanthekhan.com/2024/05/06/the-monsters-in-your-build-cache-github-actions-cache-poisoning/) - - GitHub Security Lab, "Keeping your GitHub Actions and workflows secure: Preventing pwn requests" — [securitylab.github.com](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/) - - StepSecurity, "Harden-Runner detection: tj-actions/changed-files action is compromised" (March 2025) — [stepsecurity.io](https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised) -- npm policies: - - Unpublish: [docs.npmjs.com/policies/unpublish](https://docs.npmjs.com/policies/unpublish) - - Provenance: [docs.npmjs.com/generating-provenance-statements](https://docs.npmjs.com/generating-provenance-statements) - ---- - -## Appendix A — Affected versions - -See the GitHub Security Advisory for the full list of affected versions: [GHSA-g7cv-rxg3-hmpx](https://github.com/TanStack/router/security/advisories/GHSA-g7cv-rxg3-hmpx) - -## Changelog - -- **2026-05-15** — Refined the timeline with verified timestamps from GitHub, npm registry, and internal shell history. Notable corrections: - - PR #7369 was merged 2026-05-09 (the 19:15 UTC May 11 event was a re-run of that workflow, not a fresh merge). - - The StepSecurity issue was opened at 19:46:46 UTC (not ~19:50). - - Deprecation actually ran in two phases at ~20:19 / ~20:41 / ~21:03 UTC (not a single ~21:00 batch). - - The npm-side tarball removal window of 22:13–23:55 UTC was reattributed to npm acting on the StepSecurity notification rather than to our later email. - - The formal IOC email to npm/GitHub Security went out at 05:02 UTC May 12 (not ~20:30 UTC May 11). - - Added a Response timing summary and an All-clear status banner. diff --git a/src/blog/openrouter-partnership.md b/src/blog/openrouter-partnership.md deleted file mode 100644 index f8b63798e..000000000 --- a/src/blog/openrouter-partnership.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: TanStack + OpenRouter Partnership -published: 2026-03-09 -excerpt: OpenRouter is now an official TanStack sponsor. The most concrete result is already shipped — a first-class TanStack AI adapter giving you access to 300+ models from 60+ providers through a single API. -authors: - - Tanner Linsley ---- - -![TanStack + OpenRouter](/blog-assets/openrouter-partnership/header.png) - -**OpenRouter is now an official TanStack sponsor.** And the most concrete expression of that is already shipped: [`@tanstack/ai-openrouter`](https://tanstack.com/ai/latest/docs/adapters/openrouter) — a first-class TanStack AI adapter that gives you access to 300+ models from 60+ providers through a single, unified API. - -## Why OpenRouter - -When we started building TanStack AI, one of our core beliefs was that you shouldn't have to bet your integration on a single provider. The AI model landscape is moving faster than anyone can predict. The model that wins this quarter might not be the one you want next quarter, and rewriting your AI layer every time a new frontier model drops is exactly the kind of undifferentiated toil we want to help you avoid. - -OpenRouter solves this cleanly. One API key. One integration. GPT-5, Claude, Gemini, Llama, Mistral, DeepSeek — and whatever ships next month. When you want to try a different model, you change a string. When a provider goes down, OpenRouter routes around it automatically. That's the kind of leverage I want TanStack developers to have. - -## The Adapter - -```bash -npm install @tanstack/ai-openrouter -``` - -```typescript -import { chat } from '@tanstack/ai' -import { openRouterText } from '@tanstack/ai-openrouter' - -const stream = chat({ - adapter: openRouterText('anthropic/claude-sonnet-4.5'), - messages: [{ role: 'user', content: 'Hello!' }], -}) -``` - -Swap the model string for any of the [300+ models on OpenRouter](https://openrouter.ai/models?utm_source=tanstack). Everything else stays the same. - -One feature I particularly love is the auto-router with fallbacks. It's dead simple to set up and gives your app real production resilience without any retry logic of your own: - -```typescript -const stream = chat({ - adapter: openRouterText('openrouter/auto'), - messages, - providerOptions: { - models: [ - 'openai/gpt-5', - 'anthropic/claude-sonnet-4.5', - 'google/gemini-3-pro-preview', - ], - route: 'fallback', - }, -}) -``` - -If the primary model fails or gets rate-limited, OpenRouter falls through to the next one. No outage pages, no extra infrastructure. - -## Jack's Image Generation Demo - -Our own Jack Herrington put together a demo showing off TanStack AI with the OpenRouter adapter to do image generation. It's a great look at how far this goes beyond just chat: - - - -## What This Means Going Forward - -OpenRouter's sponsorship of TanStack means the adapter is actively maintained, tested, and will stay in sync with both libraries as they evolve. More importantly, both teams are genuinely aligned on the same goal: give developers the most flexible AI integration possible without locking them into anything. - -If you're building AI features with TanStack, the OpenRouter adapter is the one I'd reach for first. - -- [TanStack AI + OpenRouter docs](https://tanstack.com/ai/latest/docs/adapters/openrouter) -- [Browse OpenRouter models](https://openrouter.ai/models?utm_source=tanstack) diff --git a/src/blog/power-in-pragmatism.md b/src/blog/power-in-pragmatism.md deleted file mode 100644 index 8ca88e364..000000000 --- a/src/blog/power-in-pragmatism.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: The Power in Pragmatism -published: 2025-05-22 -excerpt: The web ecosystem moves fast. I've chased purity before, but pragmatism usually wins in the long run — especially when you're building tools developers can trust and adopt incrementally. -authors: - - Tanner Linsley ---- - -![The Power in Pragmatism](/blog-assets/power-in-pragmatism/header.jpg) - -The web ecosystem moves fast. Frameworks evolve, paradigms shift, and tools get rewritten in pursuit of better ideas, or sometimes just cleaner abstractions. That kind of exploration can be valuable, but it can also come at the cost of leaving developers behind. - -I've chased purity before. Type-safety, for example, is a hill I've been more than willing to climb. And I still believe some of those pursuits are worth it. But I've also learned that pragmatism usually wins in the long run, especially when you're trying to build tools that developers can trust, adopt incrementally, and grow with. - -## Foundations That Don't Move - -One of the most exciting parts of building [**TanStack Start**](https://tanstack.com/start) and [**TanStack Router**](https://tanstack.com/router) is that they aren't tied to any one framework. They're framework-agnostic by design, built on the belief that UI frameworks are just rendering tools, not foundations your entire app should depend on. - -That decision wasn't made to hedge against future shifts. It was made to keep the core ideas of routing and app composition flexible, testable, and broadly useful, regardless of whether you're building with React, Solid, Vue, or whatever's next. - -Because of that, TanStack tools aren't in need of a pivot. They're already designed to adapt to where frameworks go. - -## The Spirit of TanStack Query - -This philosophy isn't new to us. [**TanStack Query**](https://tanstack.com/query) became what it is by solving real-world problems with data-fetching, caching, and invalidation, without trying to own your app architecture. It made working with async state better, no matter your stack. - -TanStack Start and Router carry that same spirit forward: composable, type-safe primitives that solve specific problems really well, while giving you complete control over how deep you go. - -## Composability Is the Strategy - -Some tools aim to provide the entire full-stack experience out of the box. That works for many developers, and there's nothing wrong with it. But TanStack Start is built differently: it's designed to scale _with_ your needs, not ahead of them. - -You can use just the router. Or add server functions. Or bring in the mini query cache. You can even replace pieces with your own. That's not just flexibility, it's composability by design. - -We believe abstractions should be optional and interchangeable. That's how you future-proof a toolset. - -## What Comes Next - -The ecosystem is shifting again, and that's okay. Some are exploring new rendering models. Some are doubling down on server-first patterns. Others are rethinking what a framework even is. - -We welcome that. In fact, we've already built for it. - -By focusing on primitives instead of platforms, TanStack tools grow with the web, not away from it. We're not chasing clean slates. We're building on what works, refining what doesn't, and staying focused on helping developers build great apps today and tomorrow. - -We're not here to convert you. We're here to support you, wherever you are, and wherever you're headed. diff --git a/src/blog/react-server-components.md b/src/blog/react-server-components.md deleted file mode 100644 index fc9a1b9e8..000000000 --- a/src/blog/react-server-components.md +++ /dev/null @@ -1,420 +0,0 @@ ---- -title: 'React Server Components Your Way' -published: 2026-04-13 -draft: false -excerpt: RSCs are genuinely exciting — smaller bundles, streaming UI, moving heavy work off the client — but existing implementations force you into a one-size-fits-all pattern. What if you could fetch, cache, and compose them on your own terms? -library: start -authors: - - Manuel Schiller - - Tanner Linsley - - Jack Herrington ---- - -![React Server Components](/blog-assets/react-server-components/header.jpg) - -At TanStack, we have always strived to build tools that cover the 90% use case with ease, but still give you the flexibility to break out of the box for advanced use cases. Why? Because we know that when things get serious, **you know what's best for your application and deserve the freedom to take control**. - -That's always been the TanStack philosophy, and we're happy that you've trusted us to take our time to deliver that same experience with React Server Components. - -## What are server components? - -Yeah... no. This is not a server component crash course. If you are not familiar with Server Components by this point, please stop by the [official React Server Components documentation](https://react.dev/reference/rsc/server-components) for a few minutes to get acquainted. - -## Why do RSCs matter? - -RSCs are a necessary primitive for moving heavy or expensive rendering logic off the client and onto the server. - -**Especially static or infrequently changing content** that can be cached consistently and granularly. - -Markdown parsers, syntax highlighters, date formatting libraries, search indexing, content transforms, etc. are all great use cases, but certainly not the only ones. - -They are a _very powerful primitive_. - -## The RSC Status Quo - -Most people now think of RSCs in a server-first way: the server owns the tree, `'use client'` marks the interactive parts, and the framework conventions decide how the whole thing fits together. - -That model can be compelling. It makes streaming, server rendering, and colocated server-side work feel built in from the start. - -But it also turns RSCs from a useful primitive into the thing your whole app has to orbit. The framework ends up owning how RSCs are created, where they render, how interactive boundaries are defined, and how UI gets recomposed when data or user actions change. - -That is the part we kept getting hung up on. We do not think you should have to buy into that whole model up front just to get value out of RSCs. - -## A Different RSC Model - -What if you could use RSCs as granularly as you could fetch JSON on the client? In fact, what if the client decided how server-rendered UI gets fetched, cached, and composed in the first place? - -In TanStack Start, the core idea is that RSCs are **just streams of data** that you can fetch, cache, and render on your terms at any time on the client instead of a server-owned component tree. That one shift makes them far more composable without changing anything fundamental about how they work. - -With TanStack Start, RSCs are just React Flight streams. That sounds almost too obvious to say out loud, but that's exactly the point. We did not want them wrapped in a black-box convention with special rules, APIs, and network effects that change everything about the framework. - -We wanted RSCs to behave more like any other piece of server data, which means **nothing special should need to happen in a framework to support them** outside of simply supporting streaming as a first-class citizen. - -What does this mean in practice? You can: - -- Create and return them from anywhere on the server (server functions, API routes) -- Decode them wherever you want (both SSR and on the client) -- Cache them however you want. They're just streams of bytes. Immediately compatible with all existing tools. - -## What do they look like? - -Here is an RSC in TanStack Start: - -> Naturally, to cut down on server sync logic, we'll use TanStack Query to manage it! - -```tsx -import { createServerFn } from '@tanstack/react-start' -import { - createFromReadableStream, - renderToReadableStream, -} from '@tanstack/react-start/rsc' - -// Create a server function -const getGreeting = createServerFn().handler(async () => { - // Create an RSC readable stream - return renderToReadableStream( - // Return JSX -

    Hello from the server

    , - ) -}) - -function Greeting() { - const query = useQuery({ - queryKey: ['greeting'], - queryFn: async () => - // Create a renderable element from the stream - createFromReadableStream( - // Call our server function to get the stream - await getGreeting(), - ), - }) - - // Render! - return <>{query.data} -} -``` - -## The Primitive APIs - -At the primitive level, the API surface is intentionally small: - -- **`renderToReadableStream`** renders React elements to a Flight stream on the server. -- **`createFromReadableStream`** decodes a Flight stream on the client or during SSR. -- **`createFromFetch`** decodes directly from a fetch response when that shape is more convenient. - -Under the hood, the story is straightforward: React renders to a Flight stream on the server, and the client decodes that stream back into a React element tree. - -There is no secret extra protocol here. That is the primitive. Standard Flight streams in, standard React elements out. - -That is enough to build a lot. You can treat RSC output like any other async resource in your app instead of a special-case framework-owned convention that you have to route every decision through. - -## Caching - -Speaking of unnecessary framework convention: caching is not something new to reinvent, and **RSCs are no exception**. - -When RSCs become "just data", the caching story gets a lot simpler. And because these are just granular streams delivered plainly over HTTP and handled transparently during rendering, they are not only easy to cache on the client, but also anywhere along the way on the server: in memory, in a database, behind a CDN, or wherever else your architecture already caches bytes, responses, or data. - -This equally applies to **caching layers you likely already know on the client** as well, instead of requiring novel approaches and mental model shifts. - -Let me explain. - -### Query: Fine-Grained Control - -TanStack Query illustrates this so well. It does not need a special "RSC mode". Once the RSC payload is part of an async query, you still get explicit cache keys, `staleTime`, background refetching, and the rest of Query's toolbox. For static content, just set `staleTime: Infinity` and you are done. - -```tsx -import { createServerFn } from '@tanstack/react-start' -import { - createFromReadableStream, - renderToReadableStream, -} from '@tanstack/react-start/rsc' - -const getGreeting = createServerFn().handler(async () => { - return renderToReadableStream(

    Hello from the server

    ) -}) - -function PostPage({ postId }: { postId: string }) { - const { data } = useSuspenseQuery({ - queryKey: ['greeting-rsc', postId], - queryFn: async () => ({ - Greeting: await createFromReadableStream(await getGreeting()), - }), - staleTime: 5 * 60 * 1000, - }) - - return <>{data.Greeting} -} -``` - -### Router: Automatic Route-Based Caching - -TanStack Router is even cooler. Because it natively supports streams, RSC payloads in route loaders are, again, "just data". - -You can await them, stream them (yes, stream a stream), and naturally cache the result in the router cache just like any other loader output. - -```tsx -const getGreeting = createServerFn().handler(async () => { - return renderToReadableStream(

    Hello from the server

    ) -}) - -export const Route = createFileRoute('/hello')({ - loader: async () => ({ - greeting: getGreeting(), - }), - component: function HelloPage() { - const { greeting } = Route.useLoaderData() - return <>{greeting} - }, -}) -``` - -> Navigate from `/posts/abc` to `/posts/xyz` and the loader runs again. Navigate back to `/posts/abc` and Router can serve the cached result instantly. That snappy back-button experience falls out of the same loader caching model you are already using. - -### CDN: Cache The GET Response Itself - -Because GET server functions are still just HTTP under the hood, you can also cache the response itself at the CDN layer. - -```tsx -import { createServerFn } from '@tanstack/react-start' -import { renderToReadableStream } from '@tanstack/react-start/rsc' -import { setResponseHeaders } from '@tanstack/react-start/server' - -const getGreeting = createServerFn({ method: 'GET' }).handler(async () => { - setResponseHeaders( - new Headers({ - 'Cache-Control': 'public, max-age=0, must-revalidate', - 'Netlify-CDN-Cache-Control': - 'public, max-age=300, durable, stale-while-revalidate=300', - }), - ) - - return renderToReadableStream(

    Hello from the server

    ) -}) -``` - -That is the same pattern we use here for blog and docs content. Browser cache rules can stay conservative while the CDN caches the server function response much more aggressively. - -With Start, RSCs fit into the same data workflows you already use. - -## Security: One-Way Data Flow - -You've probably seen recent CVEs around RSC stacks. - -We intentionally do not support `'use server'` actions, both because of existing attack vectors and because they can create highly implicit network boundaries. - -TanStack Start requires explicit RPCs via `createServerFn`. The client-server boundary is deliberate, with hardened serialization, validation, and middleware semantics that encourage treating all user input as untrusted by default. - -This keeps the attack surface smaller because the communication pattern is explicit. Still treat server functions like any API surface: authenticate, validate, and keep dependencies patched. - -## The Full Spectrum - -With RSCs as primitives, TanStack Start covers every frontend use case. And we mean _every_ one: - -- **Fully Interactive** - No server components at all. Client-first, SPA-style. RSCs are an optimization you add when helpful, not a paradigm you're forced to build around. This is where most "apps" already live today. - -- **Hybrid** - Server components for static shells, data-heavy regions, or SEO-critical content, with client components where interactivity matters. Mixed "app" and "site" projects (product + marketing) fit this especially well. In practice, this tends to help the "site" side more often than the "app" side. - -- **Mostly Static** - Predominantly static content, parsed and rendered server-side as RSCs, but still a powerful and hydrated SPA with bits of client interactivity sprinkled in where needed (e.g. comments, search, dynamic widgets). Think blogs, docs, marketing pages. - -- **Fully Static** - Pre-render everything at build time. Just ship HTML. You might not even need RSCs here if you never need to hydrate! - -**One framework. One mental model. The entire spectrum.** -You don't have to choose "interactive framework" or "static framework" or "RSC framework." -You choose patterns **per-route, per-component, per-use-case**. The architecture supports all of it. Because, again, you know what's best for your app. - -## What We Saw On TanStack.com - -We did not want to make the case with vibes, so we migrated the content-heavy parts of tanstack.com and measured it. - -The result was exactly what we hoped for, and also more limited than the hype would suggest. - -The best pages got meaningfully smaller: - -- **Blog post pages** dropped about **153 KB gzipped** from the client JS graph. -- **Docs pages** dropped about **153 KB gzipped**. -- **Docs example pages** dropped about **40 KB gzipped**. - -And the real-world numbers moved with them: - -- **`/blog/react-server-components`** went from **52 -> 74** in Lighthouse. - - Total Blocking Time dropped from **1,200ms -> 260ms**. - - Transfer size dropped from **1,101 KiB -> 785 KiB**. -- **`/router/latest/docs/overview`** went from **78 -> 81**. - - Total Blocking Time dropped from **280ms -> 200ms**. - - Transfer size dropped from **917 KiB -> 777 KiB**. - -That is the point. **Heavy client work stopped shipping to the client.** Markdown parsing went away. Syntax highlighting went away. The browser got less JavaScript and did less work. As a side effect, we also got to delete the old client markdown and highlighting path instead of carrying two versions of the same rendering logic around. - -But RSCs are not a universal coupon code for performance. Some landing pages were basically flat, and a few were slightly worse. Pages that are already dominated by interactive UI shell do not automatically get faster just because you threaded a server component into the tree somewhere. - -That is the tradeoff: - -- **RSCs are great when the page is content-heavy, dependency-heavy, or both.** Docs, blogs, markdown pipelines, syntax highlighting, slow-changing content, SEO-heavy pages. That is the sweet spot. -- **RSCs are less obviously useful when the page is already mostly client state and interaction.** Dashboards, builders, long-lived app sessions, and some landing pages can be flat or mixed unless you are removing real client-side work. - -That is why we think they matter. Not because every route should become a server component. Because when you use them where they fit, the payoff is measurable and not subtle. - -## Introducing Composite Components - -Everything above stands on its own. If all TanStack Start did was treat RSCs as fetchable, cacheable, renderable data, we would already think that was a better foundation for RSCs. - -But we kept pulling on one question: what if the server did not need to decide every client-shaped part of the UI at all? - -That led us to create something entirely new: **Composite Components**. - -`use client` still works the same way in TanStack Start when the server intentionally wants to render a client component. `use server` does not. Start uses explicit [Server Functions](/start/latest/docs/framework/react/guide/server-functions) instead. - -Composite Components are not a replacement for `use client`. They solve a similar composition problem from the opposite direction. Instead of the server deciding which client component renders where, the server can leave join points open and let the client own the tree and decide what fills them. - -That is the part that feels genuinely new to us. - -### Slots Inside One Component - -A Composite Component can render server UI while exposing **slots** for client content. Slots use plain React patterns you already know: - -- `children` -- render props (like `renderPostActions`) - -Because the client owns the component tree, the components you pass into slots are regular client components. No `'use client'` directive required. The server positions them as opaque placeholders but can't inspect, clone, or transform them. That is the point: the server can ask for "something goes here" without needing to know what that something is. - -#### Server - -```tsx -import { createCompositeComponent } from '@tanstack/react-start/rsc' - -const getPost = createServerFn().handler(async ({ data }) => { - const post = await db.posts.get(data.postId) - - const src = await createCompositeComponent( - (props: { - children?: React.ReactNode - renderPostActions?: (data: { - postId: string - authorId: string - }) => React.ReactNode - }) => ( -
    -

    {post.title}

    -

    {post.body}

    - - {/* Server renders this link directly */} - - Next Post - - - {/* Slot: server requests client UI here */} -
    - {props.renderPostActions?.({ - postId: post.id, - authorId: post.authorId, - })} -
    - - {/* Slot: client fills this with children */} - {props.children} -
    - ), - ) - - return { src } -}) -``` - -#### Client - -```tsx -import { CompositeComponent } from '@tanstack/react-start/rsc' - -function PostPage({ postId }) { - const { data } = useSuspenseQuery({ - queryKey: ['post', postId], - queryFn: () => getPost({ data: { postId } }), - }) - - return ( - ( - // Full client interactivity: hooks, state, context - - )} - > - - - ) -} -``` - -The server renders the `` directly and leaves join points for the client: - -- A `renderPostActions` slot for `` with server-provided arguments -- A `children` slot for `` - -Slot names are just props. `renderPostActions` is example code, not special API syntax. - -Since a Composite Component is still data, the client can also treat it as a building block: - -- Interleave multiple fragments in a new tree -- Wrap them in client providers or layouts -- Nest them through slots -- Reorder or swap them based on client state - -Same mental model as normal React composition. The difference is that the server no longer has to decide every interesting part of the tree ahead of time. - -## Current Status: Experimental - -RSC support is experimental in TanStack Start RC and will remain experimental into early v1. - -**Serialization:** This release uses React's native Flight protocol. TanStack Start's usual serialization features aren't available within server components for now. - -**API surface:** The current helpers are stable enough to use, but expect refinements while the feature is experimental. The docs will stay current as the APIs evolve. - -If you hit rough edges, [open an issue](https://github.com/tanstack/router/issues) or join the [Discord](https://tlinz.com/discord). - -## FAQ - -We get questions. Here are answers. - -### How does this compare to Next.js App Router? - -Next.js App Router is server-first: your component tree lives on the server by default, and you opt into client interactivity with `'use client'`. - -TanStack Start is **isomorphic-first**: your tree lives wherever makes sense. At the base level, RSC output can be fetched, cached, and rendered where it makes sense instead of owning the whole tree. When you want to go further, Composite Components let the client assemble the final tree instead of just accepting a server-owned one. - -### Can I use this with Next.js or Remix? - -Not directly—TanStack Start is its own framework. But if you use TanStack Query or Router already, the mental model transfers. - -### Do I have to use RSCs? - -Nope. RSCs are completely opt-in. You can build fully client-side routes (including `ssr: false`), use traditional SSR without server components, or go fully static. - -They are another tool in the box, not the new mandatory center of gravity. - -### Where should I look for the full technical docs? - -The [Server Components docs](/start/latest/docs/framework/react/guide/server-components). They cover setup, helper APIs, examples, constraints, and the low-level details this post intentionally skips. - -### What about security? - -See the [Security: One-Way Data Flow](#security-one-way-data-flow) section above. The short version: TanStack Start's architecture doesn't parse Flight data from the client, so recent CVEs affecting other RSC frameworks don't apply here. - ---- - -## Your RSCs, Your Way - -We started this post with a simple idea: you know what's best for your application architecture. That is why we built TanStack Start's RSC model to stay flexible instead of prescriptive. - -Too much of the ecosystem treats RSCs like they need to become your app architecture. We think they work better as a primitive. And when you want to go further, Composite Components open up a composition model that most RSC systems do not even try to offer. Want a fully interactive SPA? Go for it. Want to sprinkle in server components for heavy lifting? Easy. Want to go full static? That works too. The architecture supports all of it because _your_ app isn't one-size-fits-all, and your framework shouldn't be either. - -TanStack Start's RSC model is available now as an experimental feature. We're excited to see what you build with it. - -- [Server Components Docs](/start/latest/docs/framework/react/guide/server-components) -- [GitHub](https://github.com/tanstack/router) -- [Discord](https://tlinz.com/discord) - -Let's build something amazing together. diff --git a/src/blog/removing-third-party-ads.md b/src/blog/removing-third-party-ads.md deleted file mode 100644 index 5b8576993..000000000 --- a/src/blog/removing-third-party-ads.md +++ /dev/null @@ -1,166 +0,0 @@ ---- -title: 'We Removed 3rd Party Ads from TanStack.com' -published: 2026-03-24 -excerpt: Cutting ad-tech and tracking overhead turned a pathological request graph into something sane again, and made the remaining first-party performance work finally obvious. -authors: - - Tanner Linsley ---- - -![We Removed Third-Party Ads. tanstack.com Got Faster.](/blog-assets/removing-third-party-ads/header.jpg) - -For a long time, third-party ads on tanstack.com sat in an uncomfortable category for me: useful, but increasingly hard to justify. - -They helped fund the work. That part was real. When you are trying to run a serious open-source business without paywalls, VC, or licensing games, you do not casually throw away revenue. And ad revenue has a dangerous quality to it: it can feel free. Drop in the scripts, let the machinery run, and money shows up. - -What changed is that our partnership revenue finally reached the point where the ROI made sense to let the ad stack go. We had another engine for sustainability. That gave us room to stop optimizing for money that felt easy and start optimizing harder for product quality. - -But the cost was real too. More third-party JavaScript, more request fanout, more main-thread contention, more tracking surface area, and a worse experience for people who were just trying to read docs. The site was carrying around a small ad-tech zoo, and every optimization pass kept running into the same truth: a lot of the slowness was not ours. - -At some point you have to decide what business you are actually in. We're in the business of making the web feel faster, simpler, and more reliable. So we made the call to remove third-party ads from tanstack.com. Not reduce them. Not defer them. Remove them. - -We kept the ad real estate, but stopped letting Publift, Fuse, and the rest of the auction stack run the page. Then we measured the before and after. - -For the latest pass, I reran mobile Lighthouse against just three pages on the old site and the current site: - -- `/` -- `/query` -- `/query/latest/docs/framework/react/guides/queries` - -## The Before and After - -> I am not presenting these as triumphant final numbers. They are not. There is still a lot of work left to do, especially on the docs side. But this post is about the changes we made and what those changes bought us, not pretending the work is finished. - -Across the three production pages I care most about, the current site now lands between **51.5** and **80** on two-run mobile averages. That is still not amazing or perfect, but it is a very different place than the old site. - -The homepage is the cleanest place to start. - -| Metric | Old homepage | Current homepage | -| ---------------- | -----------: | ---------------: | -| Lighthouse score | **37** | **80** | -| FCP | 4.5s | 3.0s | -| LCP | 6.8s | 3.9s | -| TBT | 1,370ms | 171ms | -| TTI | 34.3s | 5.4s | -| Script transfer | 1.95 MiB | 547 KiB | -| Total transfer | 13.1 MiB | 830 KiB | -| Requests | 103 | 61 | -| Domains | 17 | 3 | - -That alone would have been a worthwhile cleanup. - -The biggest win after that was the Query landing page. The old `/query` run was a disaster. The current `/query/latest` page is not perfect, but it is finally behaving like a real product page instead of an out-of-control dependency graph. - -| Metric | Old `/query` | Current `/query/latest` | -| ---------------- | -----------: | ----------------------: | -| Lighthouse score | **31** | **78.5** | -| FCP | 3.9s | 2.9s | -| LCP | 7.1s | 4.5s | -| TBT | 2,110ms | 66ms | -| TTI | 47.6s | 5.3s | -| Script transfer | 5.04 MiB | 532 KiB | -| Total transfer | 93.5 MiB | 676 KiB | -| Requests | 718 | 60 | -| Domains | 37 | 5 | - -That is the kind of improvement that changes how a page feels, not just how a benchmark reads. - -But the docs page is the right place to look if you want to understand the work that is still left. - -| Metric | Old docs page | Current docs page | -| ---------------- | ------------: | ----------------: | -| Lighthouse score | **32** | **51.5** | -| FCP | 6.1s | 5.7s | -| LCP | 18.8s | 9.0s | -| TBT | 1,960ms | 374ms | -| TTI | 19.7s | 9.0s | -| Script transfer | 2.38 MiB | 1.1 MiB | -| Total transfer | 2.83 MiB | 1.29 MiB | -| Requests | 116 | 99 | -| Domains | 17 | 5 | - -That `80` on the homepage is not a promise that every Lighthouse run will land there forever. Lighthouse has variance. I do not care about pretending otherwise. But the direction here is not subtle. - -It is also worth saying plainly: these numbers are not the ceiling of what [TanStack Start](/start) can do. TanStack Start is extremely fast and lightweight. If anything, this post is about us getting tanstack.com closer to being the kind of showcase it should already be. This is a stepping stone, not the limit. - -## What Removing the Ad Stack Actually Bought Us - -This was not just a Lighthouse-number cleanup. It simplified the page. The homepage now loads less JavaScript, talks to fewer systems, and does less work before it becomes useful. - -And yes, the ad-heavy version could get absurd. In the latest rerun, the old homepage still pushed more than **13 MiB**. The old `/query` result was much worse: **93.5 MiB** total transfer and **718** requests. That is not a healthy page. That is a page losing control of its own dependency graph. - -And more importantly, the page stopped pulling in a bunch of third-party ad infrastructure: - -- `cdn.fuseplatform.net` -- `securepubads.g.doubleclick.net` -- `c.amazon-adsystem.com` -- `config.aps.amazon-adsystem.com` -- `cdn.id5-sync.com` -- `cdn.confiant-integrations.net` -- `metrics.rapidedge.io` - -Those are not harmless little tags. They bring auctions, identity sync, dependency chains, and whatever surprises their vendors decide to ship next. - -After the change, the remaining third-party footprint on the homepage is much narrower. We removed GTM and Funding Choices too, so at this point it is mostly direct analytics and monitoring: - -- Google Analytics -- Sentry - -That is still not free, but it is a much smaller and more understandable trade. - -## Bundle Size, JavaScript, and Cookies - -Removing the ad stack did not fix everything by itself. We also kept doing normal performance work: trimming homepage code, deferring non-critical work, and cutting font payload. But removing ads was one of the cleanest wins because it improved multiple things at once: - -- less JavaScript loaded -- fewer third-party origins -- fewer chained requests -- less main-thread contention -- smaller cookie and privacy surface area - -That last one matters more than people admit. Even when consent tooling is present, an ad-tech stack tends to expand the number of systems touching identity, storage, and page lifecycle. Killing that whole class of dependencies is cleaner than trying to politely optimize it. - -## What We Do Instead Now - -We still have promotional real estate on the site, but now it is ours. That means TanStack products, our own announcements, and carefully chosen partnerships that we can serve directly without dragging in an auction waterfall. If something shows up on tanstack.com, it should earn its place. - -No mystery JavaScript. No third-party ad network stack. No performance tax disguised as monetization. - -## What Still Needs Work - -This is not the end of the performance story. It is the point where the remaining work becomes more honest. - -The homepage is now in good shape. The Query landing page is much healthier too. The docs pages are where the remaining weight is concentrated. - -In our latest production mobile checks, the averages looked like this: - -- homepage: **80** -- Query landing page: **78.5** -- Query docs page: **51.5** - -That gap matters. And the reason is not mysterious. - -Right now, too much of our docs rendering pipeline still ends up in the client. On the docs page we measured about **1.1 MiB** of script transfer, and about **358 KiB** of that is clearly tied to syntax highlighting alone: Shiki, its WebAssembly payload, themes, and language chunks. - -The markdown side is not fully out of the client either. Our docs component still renders markdown in React on the client path, and the markdown processor stack also shows up in client chunks. That means the browser is still paying for work that should mostly be done before hydration ever starts. - -That is exactly why I am excited about React Server Components here. And this is where a lot of people talk past each other. - -RSCs are not magic. They are just a good fit when you are optimizing for the right thing. - -In our case, the docs are mostly content at the end of a markdown pipeline that rarely changes. We care a lot about fast page loads, content retrieval, mobile devices, and shorter sessions. In that world, it makes perfect sense to do the markdown rendering and syntax highlighting on the server. The browser should get HTML, not a markdown pipeline. It should get highlighted code, not a syntax highlighter and a wasm runtime. - -If this were a client-heavy SPA, a dashboard, or an application where users stay in one session for a long time, the tradeoff could look different. In that case, shipping a markdown or highlighting pipeline once, paying the bundle cost, and amortizing it over the whole session can be completely reasonable, especially if it is lazy loaded. - -But that is not the optimization target here. Docs are not a dashboard. They are content. And for content like this, RSCs are a very good fit because they let us push the expensive pipeline work back to the server where it belongs. - -There will still be some client-side UI left for truly interactive bits. That is fine. But the heavy docs rendering path should not live there forever. - -So this ad-removal work is not the finish line. It is the cleanup pass that made the next layer of work obvious. We removed the third-party chaos first. Now we can go after the first-party rendering costs with much clearer visibility. - -## The Tradeoff - -I do not regret using ads when we needed them. They helped fund the work during an earlier stage, and I am grateful for that. But once partnership revenue crossed the right threshold, the calculus changed. Keeping the ad stack around meant saying goodbye to product quality in exchange for money that felt free. - -Performance is not a vanity metric for us. It is part of the product. So we made the call. The site is faster. The dependency graph is smaller. The privacy surface is smaller. The homepage feels more like ours. - -That is a trade I would make again. diff --git a/src/blog/run-coding-agents-in-a-sandbox.md b/src/blog/run-coding-agents-in-a-sandbox.md deleted file mode 100644 index 115237be5..000000000 --- a/src/blog/run-coding-agents-in-a-sandbox.md +++ /dev/null @@ -1,162 +0,0 @@ ---- -title: 'Run Any Coding Agent in a Sandbox, With One chat() Call' -published: 2026-06-30 -excerpt: "Wiring a coding agent into your app means gluing each CLI's protocol and deciding where it safely runs. TanStack AI's new sandbox layer collapses both into one pattern, and lets you plug in any Agent Client Protocol agent with no dedicated package." -library: ai -authors: - - Alem Tuzlak ---- - -![Run coding agents in a sandbox](/blog-assets/run-coding-agents-in-a-sandbox/header.png) - -Wiring a coding agent into your own app sounds simple until you actually do it. Then you hit two walls at once: every agent CLI speaks its own protocol, and every one of them wants to run somewhere it can touch a real filesystem and a shell. - -So you write a bespoke adapter for Claude Code. A different one for Codex. A third for whatever ships next quarter. And then you still have to answer the scary question: where does this thing run, and what can it reach while it runs? - -TanStack AI's new sandbox layer collapses both walls into one pattern. A coding agent runs **inside an isolated sandbox**, and its work streams straight back through the same `chat()` you already use. This post walks through how it fits together, and the part that has no equivalent anywhere else: plugging in _any_ agent that speaks the Agent Client Protocol, with no dedicated package. - -## The real problem: two moving parts, glued by hand - -A "coding agent in your app" is really two independent decisions: - -- **Which agent runs** - Claude Code, Codex, OpenCode, Grok Build, or something newer. -- **Where it runs** - your laptop for dev, a container for isolation, a cloud VM or the edge for production. - -Most setups hard-couple those two decisions. The agent's SDK assumes a runtime, the runtime assumes an agent, and swapping either one means rewriting the integration. You end up maintaining a matrix of glue you never wanted to own. - -The fix is to make both decisions **independent and swappable**, behind one interface. That is exactly what the sandbox layer does. - -## One pattern: `chat()` + `withSandbox()` - -Here is a complete, runnable shape. An agent clones a repo into a Docker sandbox, fixes a bug, and streams its work back: - -```ts -import { chat } from '@tanstack/ai' -import { grokBuildText } from '@tanstack/ai-grok-build' -import { - createSecrets, - defineSandbox, - defineWorkspace, - githubRepo, - withSandbox, -} from '@tanstack/ai-sandbox' -import { dockerSandbox } from '@tanstack/ai-sandbox-docker' - -const sandbox = defineSandbox({ - id: 'fix-the-bug', - provider: dockerSandbox({ image: 'node:22' }), - workspace: defineWorkspace({ - source: githubRepo({ repo: 'owner/app' }), - setup: ['corepack enable', 'pnpm install'], - secrets: createSecrets({ XAI_API_KEY: process.env.XAI_API_KEY ?? '' }), - }), -}) - -const stream = chat({ - adapter: grokBuildText('grok-build'), - messages, - middleware: [withSandbox(sandbox)], -}) -``` - -The harness adapter (`grokBuildText`) declares it needs a sandbox, `withSandbox()` provides one, and the agent's tool calls, file edits, and reasoning come back as the same AG-UI stream chunks any other adapter produces. Your UI does not need to know a coding agent is on the other end. - -Everything below is a variation on this one snippet. - -## Pick where it runs: the provider axis - -The provider owns isolation - _where_ the agent actually runs. Every provider implements the same `SandboxHandle` contract, so swapping one is a one-line change to your sandbox definition: - -```ts -import { localProcessSandbox } from '@tanstack/ai-sandbox-local-process' -import { dockerSandbox } from '@tanstack/ai-sandbox-docker' -import { daytonaSandbox } from '@tanstack/ai-sandbox-daytona' -import { vercelSandbox } from '@tanstack/ai-sandbox-vercel' - -const dev = localProcessSandbox() // your machine, fast dev loop -const isolated = dockerSandbox({ image: 'node:22' }) // a real container boundary -const cloud = daytonaSandbox() // managed cloud sandbox -const microvm = vercelSandbox({ runtime: 'node24' }) // managed microVM -``` - -There is also a Cloudflare provider for running the agent and a live preview at the edge. The workspace, secrets, and policy you attach do not change when you swap providers - only the isolation, auth, and snapshot behavior do. - -## Pick which agent runs: the harness axis - -The harness decides _what_ runs. The first-class adapters cover the agents most people reach for: - -- `@tanstack/ai-grok-build` - `grokBuildText` -- `@tanstack/ai-claude-code` - `claudeCodeText` -- `@tanstack/ai-codex` - `codexText` -- `@tanstack/ai-opencode` - `opencodeText` - -Switching agents is the other one-line change: - -```ts -import { claudeCodeText } from '@tanstack/ai-claude-code' - -const stream = chat({ - adapter: claudeCodeText('sonnet'), - messages, - middleware: [withSandbox(sandbox)], -}) -``` - -Same sandbox, same wiring, different agent. The two axes really are independent. - -## The part nobody else has: `acpCompatible` - -First-class adapters are great until the agent you want does not have one. This is where most stacks stop and you go back to writing glue. - -TanStack AI does not. The [Agent Client Protocol](https://agentclientprotocol.com) (ACP) is a shared JSON-RPC protocol that a growing list of coding agents already speak. `acpCompatible` turns _any_ of them into a `chat()` adapter, with no dedicated package. Think of it as the `openaiCompatible` of coding agents. - -```ts -import { acpCompatible } from '@tanstack/ai-acp' - -const pi = acpCompatible({ - name: 'pi', - command: ({ model, harnessCwd }) => - `pi --acp -m ${model} --cwd ${harnessCwd}`, -}) - -const stream = chat({ - adapter: pi('pi-fast'), - messages, - middleware: [withSandbox(sandbox)], -}) -``` - -That is the whole integration. You describe how to launch the agent's ACP server, and `acpCompatible` handles the session, permissions, tool bridging, and translation into the same AG-UI stream as every other adapter. - -The payoff is breadth. The [ACP agents list](https://agentclientprotocol.com/get-started/agents) already includes `gemini --acp`, Cursor, Goose, Cline, OpenHands, Qwen Code, Kimi CLI, Pi, and dozens more. Any of them drops into a sandbox with the snippet above. Like `openaiCompatible`, you can declare typed `models` and per-call `modelOptions` so the whole thing stays type-checked. - -## What you get for free - -Because every harness runs through the same sandbox layer, capabilities are shared rather than re-implemented per agent: - -- **Secrets** - `createSecrets()` injects values into the agent's environment at create time. They are never written to snapshots or the event log. -- **Provisioning** - clone a repo, run setup commands, and project workspace skills (files, instructions, MCP servers) into the harness's native conventions. -- **Streaming** - text, reasoning, and tool activity come back as standard AG-UI chunks, so existing chat UIs render an in-sandbox agent with no changes. -- **Session resume** - thread a session id back through `modelOptions.sessionId` to continue where the agent left off. -- **Guardrails** - permission modes (and `defineSandboxPolicy`) decide what the agent may do without prompting. - -## How it fits together - -```mermaid -flowchart LR - UI["chat() in your app"] --> MW["withSandbox() middleware"] - MW --> SBX["Sandbox: real fs + shell"] - SBX --> CLI["Harness CLI: grok / claude / codex / any ACP agent"] - CLI -->|"ACP or stream"| TR["Translate to AG-UI chunks"] - TR --> UI -``` - -The middleware ensures the sandbox exists, the adapter spawns the agent inside it, and the agent's protocol (ACP for the compatible path, native streams for the first-class ones) is translated back into the chunks your UI already speaks. - -## Try it - -If you have ever wanted an agent to actually act on a real codebase - clone it, run the tests, edit files, hand back a diff - this is the shortest path there. Pick a provider, pick an agent (or bring your own ACP agent), and drive it with the `chat()` you already know. - -Read the [sandbox docs](https://tanstack.com/ai/latest/docs/sandbox/overview) to get an agent fixing a bug in a sandbox on your laptop in a few minutes, then swap the provider when you are ready to ship. - -One import. Any agent. Any sandbox. diff --git a/src/blog/search-params-are-state.md b/src/blog/search-params-are-state.md deleted file mode 100644 index 176b77c1c..000000000 --- a/src/blog/search-params-are-state.md +++ /dev/null @@ -1,187 +0,0 @@ ---- -title: Search Params Are State -published: 2025-06-03 -excerpt: Search params are global, serializable, and shareable — but in most apps they're hacked together with string parsing and brittle utils. They deserve to be treated as first-class state. -library: router -authors: - - Tanner Linsley ---- - -![Search Params Are State Header](/blog-assets/search-params-are-state/search-params-are-state-header.jpg) - -## Search Params Are State . Treat Them That Way - -Search params have been historically treated like second-class state. They're global, serializable, and shareable . but in most apps, they’re still hacked together with string parsing, loose conventions, and brittle utils. - -Even something simple, like validating a `sort` param, quickly turns verbose: - -```ts -const schema = z.object({ - sort: z.enum(['asc', 'desc']), -}) - -const raw = Object.fromEntries(new URLSearchParams(location.href)) -const result = schema.safeParse(raw) - -if (!result.success) { - // fallback, redirect, or show error -} -``` - -This works, but it’s manual and repetitive. There’s no inference, no connection to the route itself, and it falls apart the moment you want to add more types, defaults, transformations, or structure. - -Even worse, `URLSearchParams` is string-only. It doesn’t support nested JSON, arrays (beyond naive comma-splitting), or type coercion. So unless your state is flat and simple, you’re going to hit walls fast. - -That’s why we’re starting to see a rise in tools and proposals . things like Nuqs, Next.js RFCs, and userland patterns . aimed at making search params more type-safe and ergonomic. Most of these focus on improving _reading_ from the URL. - -But almost none of them solve the deeper, harder problem: **writing** search params, safely and atomically, with full awareness of routing context. - ---- - -### Writing Search Params Is Where It Falls Apart - -It’s one thing to read from the URL. It’s another to construct a valid, intentional URL from code. - -The moment you try to do this: - -```tsx - -``` - -You realize you have no idea what search params are valid for this route, or if you’re formatting them correctly. Even with a helper to stringify them, nothing is enforcing contracts between the caller and the route. There’s no type inference, no validation, and no guardrails. - -This is where **constraint becomes a feature**. - -Without explicitly declaring search param schemas in the route itself, you’re stuck guessing. You might validate in one place, but there’s nothing stopping another component from navigating with invalid, partial, or conflicting state. - -Constraint is what makes coordination possible. It’s what allows **non-local callers** to participate safely. - ---- - -### Local Abstractions Can Help . But They Don’t Coordinate - -Tools like **Nuqs** are a great example of how local abstractions can improve the _ergonomics_ of search param handling. You get Zod-powered parsing, type inference, even writable APIs . all scoped to a specific component or hook. - -They make it easier to read and write search params **in isolation** . and that’s valuable. - -But they don’t solve the broader issue of **coordination**. You still end up with duplicated schemas, disjointed expectations, and no way to enforce consistency between routes or components. Defaults can conflict. Types can drift. And when routes evolve, nothing guarantees all the callers update with them. - -That’s the real fragmentation problem . and fixing it requires bringing search param schemas into the routing layer itself. - ---- - -### How TanStack Router Solves It - -TanStack Router solves this holistically. - -Instead of spreading schema logic across your app, you define it **inside the route itself**: - -```ts -export const Route = createFileRoute('/dashboards/overview')({ - validateSearch: z.object({ - sort: z.enum(['asc', 'desc']), - filter: z.string().optional(), - }), -}) -``` - -This schema becomes the single source of truth. You get full inference, validation, and autocomplete everywhere: - -```tsx - -``` - -Want to update just part of the search state? No problem: - -```ts -navigate({ - search: (prev) => ({ ...prev, page: prev.page + 1 }), -}) -``` - -It’s reducer-style, transactional, and integrates directly with the router’s reactivity model. Components only re-render when the specific search param they use changes . not every time the URL mutates. - ---- - -### How TanStack Router Prevents Schema Fragmentation - -When your search param logic lives in userland . scattered across hooks, utils, and helpers . it’s only a matter of time before you end up with **conflicting schemas**. - -Maybe one component expects \`sort: 'asc' | 'desc'\`. Another adds a \`filter\`. A third assumes \`sort: 'desc'\` by default. None of them share a source of truth. - -This leads to: - -- Inconsistent defaults -- Colliding formats -- Navigation that sets values others can’t parse -- Broken deep linking and bugs you can’t trace - -TanStack Router prevents this by tying schemas directly to your route definitions . **hierarchically**. - -Parent routes can define shared search param validation. Child routes inherit that context, add to it, or extend it in type-safe ways. This makes it _impossible_ to accidentally create overlapping, incompatible schemas in different parts of your app. - ---- - -### Example: Safe Hierarchical Search Param Validation - -Here’s how this works in practice: - -```ts -// routes/dashboard.tsx -export const Route = createFileRoute('/dashboard')({ - validateSearch: z.object({ - sort: z.enum(['asc', 'desc']).default('asc'), - }), -}) -``` - -Then a child route can extend the schema safely: - -```ts -// routes/dashboard/$dashboardId.tsx -export const Route = createFileRoute('/dashboard/$dashboardId')({ - validateSearch: z.object({ - filter: z.string().optional(), - // ✅ \`sort\` is inherited automatically from the parent - }), -}) -``` - -When you match \`/dashboard/123?sort=desc&filter=active\`, the parent validates \`sort\`, the child validates \`filter\`, and everything works together seamlessly. - -Try to redefine the required parent param in the child route to something entirely different? Type error. - -```ts -validateSearch: z.object({ - // ❌ Type error: boolean does not extend 'asc' | 'desc' from parent - sort: z.boolean(), - filter: z.string().optional(), -}) -``` - -This kind of enforcement makes nested routes composable _and_ safe . a rare combo. - ---- - -### Built-In Discipline - -The magic here is that you don’t need to teach your team to follow conventions. The route _owns_ the schema. Everyone just uses it. There’s no duplication. No drift. No silent bugs. No guessing. - -When you bring validation, typing, and ownership into the router itself, you stop treating URLs like strings and start treating them like real state . because that’s what they are. - ---- - -### Search Params Are State - -Most routing systems treat search params like an afterthought. Something you _can_ read, maybe parse, maybe stringify, but rarely something you can actually **trust**. - -TanStack Router flips that on its head. It makes search params a core part of the routing contract . validated, inferable, writable, and reactive. - -Because if you’re not treating search params like state, you’re going to keep leaking it, breaking it, and working around it. - -Better to treat it right from the start. - -If you're intrigued by the possibilities of treating search params as first-class state, we invite you to try out [TanStack Router](https://tanstack.com/router). Experience the power of validated, inferable, and reactive search params in your routing logic. diff --git a/src/blog/start-adds-rsbuild-support.md b/src/blog/start-adds-rsbuild-support.md deleted file mode 100644 index 791c72f0b..000000000 --- a/src/blog/start-adds-rsbuild-support.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -published: 2026-06-02 -authors: - - Manuel Schiller -title: 'TanStack Start Adds First-Class Rsbuild Support' -excerpt: 'TanStack Start now supports Rsbuild 2 alongside Vite, so teams can choose the build tool that best fits their stack, preferences, and infrastructure.' -library: start ---- - -![React Server Components](/blog-assets/start-adds-rsbuild-support/header.jpg) - -TanStack Start now supports Rsbuild 2 alongside Vite, giving teams two supported build-tool paths for Start. - -Some teams prefer Vite. Some prefer Rsbuild because it is built on Rspack, feels familiar to teams with Webpack experience, or matches how the rest of their frontend infrastructure is organized. The new adapter makes that choice explicit: Start fits into either build tool through its plugin system. - -We built the adapter together with the Rsbuild and Rspack team, and some of that work fed back into Rsbuild and Rspack along the way. - -For a React app, the config looks like this: - -```ts title='rsbuild.config.ts' -import { defineConfig } from '@rsbuild/core' -import { pluginReact } from '@rsbuild/plugin-react' -import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild' - -export default defineConfig({ - plugins: [pluginReact(), tanstackStart()], -}) -``` - -React and Solid are supported today, and because the Rsbuild work lives at Start's shared build layer, future framework adapters have the same path available to them. - -Rsbuild owns the build, while the framework plugin handles React or Solid and `tanstackStart()` wires Start into both the client and server builds. If you have used Start with Vite, the pattern is intentionally familiar because the Start build-tool plugins are designed to fit naturally into the toolchain you choose. - -## A Start path for teams using Rsbuild - -Some teams want to use Start with Rsbuild because they want Rspack under the hood, a Webpack-familiar model, or a build tool that matches the rest of their stack. - -That also matters for existing apps. A team moving to Start can keep more of its build setup in place instead of changing framework and build tool at once. One large company is already using the adapter while moving several apps from a Webpack setup to Start on Rsbuild. - -That path includes the full Start feature set, including React and Solid apps, Server Functions, SSR and streaming SSR, HMR, import protection, and React Server Components for React apps. - -## The two-adapter rule - -There is a pattern we keep running into at TanStack: the first adapter proves something can work, and the second adapter shows which parts actually belong in the core. - -TanStack Router and TanStack Start already went through this once at the UI framework layer, where adding Solid made the React assumptions easier to see and improved the shared core. Adding Rsbuild brought the same clarity to the build layer. - -Because the first Start build adapter was Vite, some build logic naturally grew around Vite's plugin lifecycle, and adding Rsbuild made the boundary sharper: shared Start build behavior in one place, build-tool-specific behavior in the Vite and Rsbuild adapters. - -## Your build tool, your choice - -Choosing Start should let a team keep its preferred build tool. With Vite and Rsbuild both supported, teams can use the toolchain that fits their stack while building the same kind of Start app. - -Teams that prefer Rsbuild, or already work in the Webpack and Rspack world, now get a native Start integration through Rsbuild's plugin system. - -The Rsbuild adapter is available now. Try it in a new Rsbuild project or in an existing codebase moving to Start. diff --git a/src/blog/streaming-structured-output.md b/src/blog/streaming-structured-output.md deleted file mode 100644 index 09cc76cf9..000000000 --- a/src/blog/streaming-structured-output.md +++ /dev/null @@ -1,162 +0,0 @@ ---- -title: 'Stop Waiting on JSON: Stream Structured Output with One Schema' -published: 2026-05-14 -excerpt: 'Pass a Zod schema to useChat and get a typed `partial` and `final` for free. No more parsePartialJSON glue, no more onChunk wiring. TanStack AI now streams structured output end-to-end across OpenAI, OpenRouter, Grok, Groq, and Ollama.' -library: ai -authors: - - Alem Tuzlak ---- - -![Stop Waiting on JSON](/blog-assets/streaming-structured-output/header.png) - -You ask an LLM for a `Person` object. You hit send. The spinner spins. Five seconds. Ten. Twenty. Somewhere on a server in Oregon, the model is happily generating tokens, and your user is staring at a loading state until the very last `}` of the JSON arrives. - -That UX is bad and you already know it. The fix, in theory, is "just stream it." The fix, in practice, has been writing 15 lines of glue: an `onChunk` handler, a `useState`, a `parsePartialJSON` call, a manual cast to your `Person` type. Repeat in every project. Hope you got the types right. - -This release kills the glue. `useChat({ outputSchema })` now gives you a typed `partial` and `final` straight from the hook. One schema. End to end. - -## The old shape - -Until now, mixing streaming with `outputSchema` in `@tanstack/ai` looked something like this on the client: - -```tsx -const [partial, setPartial] = useState>({}) -const [final, setFinal] = useState(null) -let raw = '' - -useChat({ - connection: fetchServerSentEvents('/api/extract'), - onChunk: (chunk) => { - if (chunk.type === 'TEXT_MESSAGE_CONTENT') { - raw += chunk.delta - setPartial(parsePartialJSON(raw)) - } else if (chunk.name === 'structured-output.complete') { - setFinal(chunk.value.object as Person) - } - }, -}) -``` - -This works, but every byte of it is something you didn't want to write: - -- You're manually accumulating a string buffer. -- You're calling `parsePartialJSON` yourself and hoping it tolerates whatever half-JSON the model just emitted. -- You're casting `chunk.value.object` to `Person` with no real proof it actually matches your schema. -- You're doing this in every component that wants a typed live preview. - -The schema lives on the server. The same schema would happily describe `partial` and `final` on the client. There was no reason for the client to be guessing. - -## The new shape - -Pass the schema. Get the types back. - -```tsx -import { useChat } from '@tanstack/ai-react' -import { fetchServerSentEvents } from '@tanstack/ai-client/event-client' -import { PersonSchema } from './schemas' - -const { partial, final, status } = useChat({ - connection: fetchServerSentEvents('/api/extract'), - outputSchema: PersonSchema, -}) -``` - -That's the whole thing. - -- `partial` is `DeepPartial` and updates on every streamed delta. The framework parses the partial JSON for you and narrows the type to whatever fields have arrived so far. -- `final` is `Person | null`. It flips to a fully-typed `Person` the moment the model emits the structured-output completion event. -- No `parsePartialJSON` import. No `onChunk`. No `useState`. No casts. - -The same hook returns the message stream you already use, so partial UI previews and chat transcripts live side-by-side without conflict. - -## What changed under the hood - -The headline is the hook, but the work that made it possible touches the whole stack. - -**A real type for the structured-output stream.** `chat({ outputSchema, stream: true })` now returns a `StructuredOutputStream` that's a proper discriminated union: every regular `StreamChunk` plus a single tagged `StructuredOutputCompleteEvent` carrying a strongly-typed `value.object`. You no longer fight `any` when you destructure or `switch` on the event. - -**Tagged variants for the other custom events too.** While we were in there, `ApprovalRequestedEvent` and `ToolInputAvailableEvent` became their own tagged shapes. Tool-calling flows narrow cleanly without a helper. - -**Per-chunk debug logging in the structured path.** The streaming structured output path now calls `logger.provider` on every chunk, matching the behavior of plain `chatStream`. Provider-level debugging is no longer a black box just because you opted into a schema. - -**Provider coverage.** OpenAI, OpenRouter, Grok, Groq, and Ollama (anything riding the `openai-base`) all go through the same streaming structured output pipeline. The summarize adapter got the same treatment, so structured summaries stream end-to-end too. - -**Framework parity.** `useChat({ outputSchema })` works in `@tanstack/ai-react`, `@tanstack/ai-vue`, `@tanstack/ai-solid`, and `@tanstack/ai-svelte`. Same return shape, same types, same behavior. - -## Why this is the right shape - -There's a temptation, when designing this kind of API, to invent a separate hook: `useStructuredChat`, `useTypedChat`, something parallel. That would have been a mistake. A schema isn't a different mode of chatting, it's just extra information the hook can use. - -By folding `outputSchema` into the existing `useChat`, the upgrade path from "I'm using a chat hook" to "I'm using a chat hook with typed streaming output" is _literally one prop_. Your tool wiring, your approval prompts, your transcript state, everything that was already on `useChat` still works exactly the same way. The new behavior only exists for the keys that depend on the schema. - -The cost of "one more hook for a slightly different case" is paid in docs, in user confusion, and in the long tail of `useFooButSometimesBar` hooks people inevitably accumulate. We'd rather not. - -## Try it - -Install the latest: - -```bash -pnpm add @tanstack/ai @tanstack/ai-react @tanstack/ai-openai zod -``` - -Define the schema once. Use it on both sides. - -```ts -// schemas.ts -import { z } from 'zod' - -export const PersonSchema = z.object({ - name: z.string(), - age: z.number(), - email: z.string().email(), -}) -``` - -On the server, hand the schema to `chat` and stream the response as Server-Sent Events: - -```ts -// app/api/extract/route.ts -import { chat, toServerSentEventsResponse } from '@tanstack/ai' -import { openaiText } from '@tanstack/ai-openai/adapters' -import { PersonSchema } from './schemas' - -export async function POST(req: Request) { - const { messages } = await req.json() - - const stream = chat({ - adapter: openaiText('gpt-5.2'), - messages, - outputSchema: PersonSchema, - stream: true, - }) - - return toServerSentEventsResponse(stream) -} -``` - -On the client, point `useChat` at that endpoint and pass the same schema: - -```tsx -// app/extract/page.tsx -import { useChat } from '@tanstack/ai-react' -import { fetchServerSentEvents } from '@tanstack/ai-client/event-client' -import { PersonSchema } from './schemas' - -const { partial, final } = useChat({ - connection: fetchServerSentEvents('/api/extract'), - outputSchema: PersonSchema, -}) - -return ( -
    - - - - {final &&

    Done. Extracted a fully-validated Person.

    } -
    -) -``` - -Read the full guide: [tanstack.com/ai/docs/chat/structured-outputs](https://tanstack.com/ai/docs/chat/structured-outputs). - -Drop the glue. Pass the schema. Ship the feature. diff --git a/src/blog/tanstack-2-years.md b/src/blog/tanstack-2-years.md deleted file mode 100644 index f344f4dbd..000000000 --- a/src/blog/tanstack-2-years.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -title: The State of TanStack, Two Years of Full-Time OSS -published: 2025-11-24 -excerpt: Two years ago I went all in on TanStack. No consulting, no safety nets. What started as a handful of libraries has grown into a real ecosystem powering millions of developers. -authors: - - Tanner Linsley ---- - -![TanStack Form v1](/blog-assets/tanstack-2-years/tanstack-2-years-header.jpg) - -Two years ago I went all in on TanStack. No consulting, no safety nets, just a commitment to build open source at a professional, sustainable level. What started as a handful of libraries I built at Nozzle has grown into a real ecosystem powering millions of developers and many of the largest companies in the world. - -I can share plenty of numbers in this post, but this is really about what it feels like to run a modern open source organization: the highs, the lows, the cost, the growth, and the people who have made it possible. - ---- - -## The Big Challenge: TanStack Start - -Building a full stack framework is hard. I knew that from watching other teams do it. Most of them had something I didn't: capital. Next, Gatsby, Redwood, Remix... they all had funding, companies, or acquisition paths that helped them move fast. - -I didn't. TanStack had a ground swell behind it, but financially it was just me and whatever I had saved to survive another startup winter. I knew [Start](/start) would take a level of focus and sacrifice that most projects never require. It was an ecosystem-level rethink of how modern frontend applications should be built, starting with React but designed from day one to support Solid and other runtimes through adapters. - -I could get far alone. I couldn’t finish it alone. - ---- - -## The Human Cost - -Money buys time, and time has to be protected. Before I started this journey, I knew my personal, financial, and family life needed to be solid. When you’re trying to build something this big, your foundation matters more than your code. - -My family has kept me grounded. They are the thing I run back to every afternoon and weekend. They’ve supported the late nights and the intense sprints, and in return I try to be fully present when I’m not at my desk. Without my wife, kids, and extended family, I would have burned out months in. - -And yes, stepping away is hard. When you’re deep in flow, taking a vacation feels like losing momentum. But I love being with my family too much to trade that away. Even if I secretly worry I’ll forget how to code after a week on the beach. - ---- - -## The Team - -The other key ingredient was people. If I was going to take breaks and keep my sanity, I needed a team that could carry the torch with pride and consistency. Great software is always built by great people. - -I wanted contributors who could give their best work without feeling exploited or drained. That meant finding enough funding to pay them fairly and keep the lights on. Some of our contributors would tell you that money isn’t why they’re here, and they’re right. Their loyalty and pride in what we’re building is unshakeable. Still, I sleep better knowing they’re compensated for the value they bring. - ---- - -## Some Numbers - -At the time of writing, TanStack has **[16 partners](/partners)** funding a model that actually feels sustainable. Their support covers a reasonable salary for me, a growing rainy-day fund for the organization, monthly sponsorships for around **12 core contributors**, and short-term contracts for another **3 to 5 people**. - -Two years ago I had no idea if this approach to open source would work. So far it has. The real test will come in 2026, but we’ll get to that. - ---- - -## The Growth - -Here’s where the work shows. - -TanStack now includes **[13 active projects](/)** maintained by **36 core contributors** and supported by a community of **[6,300+ on Discord](https://tlinz.com/discord)**. Our libraries have been downloaded over **4 billion times**, have **[112,660 GitHub stars](https://github.com/tanstack)**, **[2,790 contributors](https://github.com/tanstack)**, and more than **1.3 million dependent repositories**. - -Even the website has become a real destination. In the last year, TanStack.com saw **3 million users**, **40 million page views**, and an average **session duration of 15 minutes**. - -More importantly, real companies are building real things on [TanStack Start](/start). Behind those numbers are teams from some of the largest and most well-known companies in the world, spanning tech, finance, e-commerce, entertainment, hardware, and healthcare. Seeing that level of adoption from both startups and global enterprises has been one of the most rewarding parts of this journey. - -Today, over **9,000 companies** are in our ongoing usage funnel, with another **33,000** in evaluation or experimentation. - -Every active TanStack library continues to grow month over month. That kind of growth isn’t hype. It’s teams building long-term bets. - -These numbers don’t define us, but they prove that principled open source can scale without compromising what makes it good. - ---- - -## Two Years of Full-Time OSS - -Going full time on open source felt risky. I had conviction, not guarantees. But the shift to sustainability changed everything. I could finally think long term. Not “next sprint” long term. Next decade long term. - -The experience has also taught me a lot about leadership. I’ve had to say no more often, slow things down at times, and balance my drive to create with the responsibility to maintain. Every decision has both a technical and a human cost. - -Success used to mean numbers. Now it means letting the people around me thrive too. - -Looking back, going full time wasn’t a bet on myself. It was a bet on us. A bet that an independent, principled community could build professional-grade software and still remain free, open, and sustainable. - ---- - -## Reflections - -TanStack has always been built on kindness. Inclusive, patient, and fiercely protective of quality. I’ve even had contributors turn down money purely out of principle, which still blows my mind. I usually convince them to take it anyway, but it says a lot about the kind of people involved. - -I still wrestle with polish and focus. That last ten percent takes me forever. I lean on my team for that. And I’m forgetful enough that I’m probably one missed calendar reminder away from hiring a personal assistant. - -If TanStack vanished tomorrow, I’d want people to remember the care we put into it. Not just in the code, but in how we treated each other. - ---- - -## What’s Next - -The next two years are about scale. - -[TanStack Start](/start) is closing in on 1.0. We're finalizing React Server Component support in a way that feels uniquely TanStack: pragmatic, cache-aware, and treating RSCs as another stream of server-side state rather than a whole new worldview. - -On the [Router](/router) side, some long-planned features may finally get their moment in 2026. - -And yes, we’ve already started work on a massive new library that will take most of next year to get off the ground. It’s one of the biggest things we’ve ever attempted. I can’t share details yet, but it will open a new chapter for the entire ecosystem. - -If the last two years were roots, the next two will be growth. - ---- - -## Gratitude - -None of this happens alone. - -To the contributors: thank you. You’ve turned ideas into software people can rely on. - -To our sponsors and partners: thank you for believing that open source can be sustainable, not just inspirational. - -And to the community: thank you for building your products, companies, and careers on our work. We don’t take that for granted for a second. - -These past two years have been the most demanding and fulfilling of my career. We’ve shown that open source can be principled, independent, and sustainable. And we’re still just getting started. - ---- - -## How to Support TanStack - -If TanStack has helped you or your team, here's how to help us keep going: - -- ⭐ [Star our repos on GitHub](https://github.com/tanstack) -- 💬 [Join the TanStack Discord](https://tlinz.com/discord) -- 💼 [Become a sponsor or partner](/partners) - -Every bit of support helps more than you realize. diff --git a/src/blog/tanstack-ai-alpha-2.md b/src/blog/tanstack-ai-alpha-2.md deleted file mode 100644 index f779deb1c..000000000 --- a/src/blog/tanstack-ai-alpha-2.md +++ /dev/null @@ -1,173 +0,0 @@ ---- -title: 'TanStack AI Alpha 2: Every Modality, Better APIs, Smaller Bundles' -published: 2025-12-19 -excerpt: Two weeks since the first alpha and we've prototyped through 5-6 internal architectures. Alpha 2 brings every modality, better APIs, and smaller bundles. -library: ai -authors: - - Alem Tuzlak - - Jack Herrington - - Tanner Linsley ---- - -![TanStack AI Alpha 2](/blog-assets/tanstack-ai-alpha-2/header.jpeg) - -It's been two weeks since we released the first alpha of TanStack AI. To us, it feels like decades ago. We've prototyped through 5-6 different internal architectures to bring you the best experience possible. - -Our goals were simple: move away from monolithic adapters and their complexity, while expanding the flexibility and power of our public APIs. This release delivers on both. - -## New Adapter Architecture - -We wanted to support everything AI providers offer. Image generation, video, audio, text-to-speech, transcription. Without updating every adapter simultaneously. - -We're a small team. Adding image support shouldn't mean extending `BaseAdapter`, updating 5+ provider implementations, ensuring per-model type safety for each, and combing through docs manually. That's a week per provider. Multiply that by 20 providers and 6 modalities. - -So we split the monolith. - -Instead of: - -```ts -import { openai } from '@tanstack/ai-openai' -``` - -You now have: - -```ts -import { openaiText, openaiImage, openaiVideo } from '@tanstack/ai-openai' -``` - -### Why This Matters - -**Incremental feature support.** Add image generation to OpenAI this week, Gemini next week, video for a third provider the week after. Smaller releases, same pace. - -**Easier maintenance.** Our adapter abstraction had grown to 7 type generics with only text, summarization, and embeddings. Adding 6 more modalities would have exploded complexity. Now each adapter is focused. 3 generics max. - -**Better bundle size.** You control what you pull in. Want only text? Import `openaiText`. Want text and images? Import both. Your bundle, your choice. - -**Faster contributions.** Add support for your favorite provider with a few hundred lines. We can review and merge it quickly. - -## New Modalities - -What do we support now? - -- Structured outputs -- Image generation -- Video generation -- Audio generation -- Transcription -- Text-to-speech - -You have a use-case with AI? We support it. - -## API Changes - -We made breaking changes. Here's what and why. - -### Model Moved Into the Adapter - -Before: - -```ts -chat({ - adapter: openai(), - model: 'gpt-4', - // now you get typesafety... -}) -``` - -After: - -```ts -chat({ - adapter: openaiText('gpt-4'), - // immediately get typesafety -}) -``` - -Fewer steps to autocomplete. No more type errors from forgetting to define the model. - -### providerOptions → modelOptions - -Quick terminology: - -- **Provider**: Your LLM provider (OpenAI, Anthropic, Gemini) -- **Adapter**: TanStack AI's interface to that provider -- **Model**: The specific model (GPT-4, Claude, etc.) - -The old `providerOptions` were tied to the _model_, not the provider. Changing from `gpt-4` to `gpt-3.5-turbo` changes those options. So we renamed them: - -```ts -chat({ - adapter: openaiText('gpt-4'), - modelOptions: { - text: {}, - }, -}) -``` - -### Options Flattened to Root - -Settings like `temperature` work across providers. Our other modalities already put config at the root: - -```ts -generateImage({ - adapter, - numberOfImages: 3, -}) -``` - -So we brought chat in line: - -```ts -chat({ - adapter: openaiText('gpt-4'), - modelOptions: { - text: {}, - }, - temperature: 0.6, -}) -``` - -Start typing to see what's available. - -### The Full Diff - -```diff -chat({ -- adapter: openai(), -+ adapter: openaiText("gpt-4"), -- model: "gpt-4", -- providerOptions: { -+ modelOptions: { - text: {} - }, -- options: { -- temperature: 0.6 -- }, -+ temperature: 0.6 -}) -``` - -## What's Next - -**Standard Schema support.** We're dropping the Zod constraint for tools and structured outputs. Bring your own schema validation library. - -**On the roadmap:** - -- Middleware -- Tool hardening -- Headless UI library for AI components -- Context-aware tools -- Better devtools and usage reporting -- More adapters: AWS Bedrock, OpenRouter, and more - -Community contributions welcome. - -## Wrapping Up - -We've shipped a major architectural overhaul, new modalities across the board, and a cleaner API. The adapters are easy to make, easy to maintain, and easy to reason about. Your bundle stays minimal. - -We're confident in this direction. We think you'll like it too. - - diff --git a/src/blog/tanstack-ai-alpha-your-ai-your-way.md b/src/blog/tanstack-ai-alpha-your-ai-your-way.md deleted file mode 100644 index 7b75d45b4..000000000 --- a/src/blog/tanstack-ai-alpha-your-ai-your-way.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: 'TanStack AI Alpha: Your AI, Your Way' -published: 2025-12-04 -excerpt: The current AI landscape has a problem — pick a framework, pick a cloud provider, and suddenly you're locked in. TanStack AI is a framework-agnostic toolkit built for developers who want control. -library: ai -authors: - - Jack Herrington - - Alem Tuzlak - - Tanner Linsley ---- - -![TanStack AI Alpha](/blog-assets/tanstack-ai-alpha-your-ai-your-way/header.jpg) - -**The TanStack team is excited to announce the alpha release of [TanStack AI](/ai), a framework-agnostic AI toolkit built for developers who want control over their stack.** - -Let's be honest. The current AI landscape has a problem. You pick a framework, you pick a cloud provider, and suddenly you're locked into an ecosystem that dictates how you build. We think that's backwards. - -TanStack AI takes a different approach. We're building the Switzerland of AI tooling. An honest, open source set of libraries (across multiple languages) that works with your existing stack instead of replacing it. - -## What's in the Alpha - -**Server support across multiple languages.** We're shipping with JavaScript/TypeScript, PHP, and Python support out of the gate. All three support full agentic flows with tools. (Python and PHP have not yet been released to the appropriate package systems.) - -**Adapters for the providers you actually use.** TypeScript adapters for OpenAI, Anthropic, Gemini, and Ollama. The TypeScript server library also handles summarizations and embeddings. - -**An open, published protocol.** We've documented exactly how the server and client communicate. Use whatever language you want. Use whatever transport layer you want. HTTP, websockets, smoke signals. As long as you speak the protocol through a connection adapter, our client will work with your backend. - -**Isomorphic Tool Support.** Define your tools once with meta definitions, then provide isolated server and client implementations. This architecture gives you type safety that actually works across your entire application. - -**Client libraries that meet you where you are.** Vanilla JS, React, and Solid are ready now. Svelte and more are on the way. - -**Real examples that actually ship.** We're not just giving you docs, we're giving you working code: - -- [TanStack Start](/start) with React - -- [TanStack Start](/start) with Solid - -- PHP with Slim running a Vanilla client - -- Laravel with React (coming soon) - -- Python FastAPI with Vanilla frontend - -- A multi-user group chat built on [TanStack Start](/start) using Cap'n'Web RPC and websockets - -**Per-model type safety that actually matters.** Every provider has different options. Every model supports different modalities. Text, audio, video, tools. We give you full typing for providerOptions on a per-model basis, so your IDE knows exactly what each model can do. No more guessing. No more runtime surprises. - -**Isomorphic devtools.** A full AI devtools panel that gives you unparalleled insight into what the LLM is doing on both sides of the connection. See what's happening on the server. See what's happening on the client. Debug your AI workflows the way you debug everything else. Built on [TanStack Devtools](/devtools). - -## Coming Soon - -**Headless chatbot UI components.** Think Radix, but for AI chat interfaces. Fully functional, completely unstyled components for React and Solid that you can skin to match your application. You handle the look and feel, we handle the complexity underneath. Similar to how [TanStack Table](/table) and [TanStack Form](/form) work. - -## The Catch - -The only catch is that we're still in alpha. There will be bugs. There will be rough edges. There will be things that don't work as expected. We're not perfect. But we're honest. We're transparent. We're here to help. We're here to make building AI applications easier. What we are looking for is your feedback. Your suggestions. Your ideas. We're not done yet. But we're here to build the best AI toolkit possible. - -We are also taking a lot on here. All the TanStack teams are small and totally volunteer. So if you want to step up to help us build adapters, or help us work on the Python or PHP support, or really anything, we are here for it. - -## Why We Built This - -TanStack AI exists because developers deserve better. We're not selling a service. There's no platform to migrate to. No vendor lock-in waiting around the corner. Nor will there ever be. - -Just real honest open source tooling from the team you trust that has been shipping framework-agnostic developer tools for years. We've always had your back, and that's not changing now. diff --git a/src/blog/tanstack-ai-audio-generation.md b/src/blog/tanstack-ai-audio-generation.md deleted file mode 100644 index 78c84becf..000000000 --- a/src/blog/tanstack-ai-audio-generation.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: 'TanStack AI Just Learned to Compose Music' -published: 2026-04-24 -excerpt: TanStack AI adds a new generateAudio activity with streaming, plus fal and Gemini Lyria adapters for music, sound effects, text-to-speech, and transcription. One typed API, any provider. -library: ai -authors: - - Alem Tuzlak ---- - -![TanStack AI Just Learned to Compose Music](/blog-assets/tanstack-ai-audio-generation/header.png) - -The AI audio ecosystem is a mess. Gemini's Lyria wants a natural-language prompt and returns raw PCM you have to wrap in a RIFF header yourself. Fal hosts dozens of audio models where one wants `music_length_ms` in milliseconds, the next wants `seconds_total`, and most want plain `duration`. ElevenLabs has its own shape. Whisper has another. Every provider disagrees on whether you get a URL, a base64 blob, or a raw buffer. - -If you are shipping an AI product that needs music, sound effects, speech, or transcription, you end up writing the same boring glue code five times. - -**TanStack AI just removed that glue.** The latest release lands a full audio stack: a new `generateAudio` activity, streaming support, fal and Gemini Lyria adapters, and framework hooks for React, Solid, Vue, and Svelte. One typed API, any provider. - -Here is what shipped and why you should care. - -## One activity, any audio model - -The new `generateAudio()` activity sits alongside `generateImage`, `generateSpeech`, `generateVideo`, and `generateTranscription` in `@tanstack/ai`. It takes a text prompt, dispatches to whatever adapter you hand it, and returns a `GeneratedAudio` object with exactly one of `url` or `b64Json`. - -```typescript -import { generateAudio } from '@tanstack/ai' -import { geminiAudio } from '@tanstack/ai-gemini/adapters' - -const adapter = geminiAudio('lyria-3-pro-preview') - -const result = await generateAudio({ - adapter, - prompt: 'A cinematic orchestral piece with a rising string motif', -}) - -// result.audio is { url: string } | { b64Json: string } — exactly one, enforced by the type -``` - -Swap `geminiAudio` for `falAudio` and the exact same call generates music through MiniMax, DiffRhythm, Stable Audio 2.5, or any of the other models in fal's catalog. The adapter translates per-model details (like fal's `music_length_ms` vs `seconds_total` vs `duration` naming) so your app code never sees them. - -## Streaming, because audio generation takes seconds - -Music and SFX generation is slow. Lyria 3 Pro takes several seconds. Stable Audio takes longer. If you are building a UI, blocking the request the whole time is a bad experience. - -`generateAudio` now supports `stream: true`, returning an `AsyncIterable` you can pipe straight through `toServerSentEventsResponse()`: - -```typescript -export async function POST(req: Request) { - const { prompt } = await req.json() - - const stream = await generateAudio({ - adapter: falAudio('fal-ai/minimax-music/v2.6'), - prompt, - stream: true, - }) - - return toServerSentEventsResponse(stream) -} -``` - -The client receives progress events and the final audio over a single SSE connection, the same transport model already used by `generateImage` and `generateVideo`. No new infrastructure, no special-case code paths. - -## Framework hooks that feel like the others - -Every framework integration gets a new hook matching the existing media-hook shape: - -- `@tanstack/ai-react`: `useGenerateAudio` -- `@tanstack/ai-solid`: `useGenerateAudio` -- `@tanstack/ai-vue`: `useGenerateAudio` -- `@tanstack/ai-svelte`: `createGenerateAudio` - -The API is identical to `useGenerateImage` and friends: - -```tsx -import { useGenerateAudio } from '@tanstack/ai-react' - -function MusicGen() { - const { generate, result, isLoading, error, stop, reset } = useGenerateAudio({ - connection, - }) - - return ( - <> - - {isLoading && } - {result?.audio.url &&