feat(react-doctor): add TanStack Start rules#124
Conversation
…middleware, data loading, SEO, navigation, linting, and project structure rules Researched from official TanStack docs, GitHub repos (tanstack/router), ESLint plugin source, and community patterns. Covers: - File-based routing conventions and naming patterns - createServerFn patterns with validation and file organization - Middleware composition, auth patterns, context management - Data loading with route property order (critical for TS inference) - Document head management and SEO meta tags - Type-safe navigation with Link and useNavigate - ESLint plugin-router rules (property order, param names) - Project structure, app config, deployment presets Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Add 9 TanStack Start-specific rules to react-doctor, following the same pattern as NEXTJS_RULES and REACT_NATIVE_RULES: - tanstack-start-route-property-order: enforces inference-sensitive property order (params/validateSearch → beforeLoad → loader → head) - tanstack-start-no-direct-fetch-in-loader: flags raw fetch() in loaders, recommends createServerFn() for type-safe RPC - tanstack-start-server-fn-validate-input: warns when .handler() accesses data without .inputValidator() — data crosses a network boundary - tanstack-start-no-useeffect-fetch: flags fetch() inside useEffect in route files — should use route loader or createServerFn() - tanstack-start-missing-head-content: warns when __root route is missing <HeadContent /> — without it, route head() meta tags are dropped - tanstack-start-no-anchor-element: flags <a href="/..."> in route files, recommends <Link> from @tanstack/react-router - tanstack-start-server-fn-method-order: enforces correct chain order (.middleware → .inputValidator → .client → .server → .handler) - tanstack-start-no-navigate-in-render: flags navigate() during render, recommends redirect() in beforeLoad/loader - tanstack-start-no-dynamic-server-fn-import: flags dynamic import() of .functions.ts files — bundler needs static imports for RPC stubs Also adds: - tanstack-start Framework type detection via @tanstack/react-start dep - TanStack Start category and help entries in run-oxlint.ts - Removes previous .agents/skills approach (wrong location) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…verage New rules: - tanstack-start-no-use-server-in-handler (error): flags 'use server' inside createServerFn handlers — TanStack Start handles this automatically, the directive causes compilation errors (GitHub issue #2849) - tanstack-start-no-secrets-in-loader (error): flags process.env.SECRET in loaders/beforeLoad — loaders are isomorphic and leak secrets to the client bundle (GitHub issue TanStack/cli#287) - tanstack-start-get-mutation (warn): flags GET server functions that perform mutations — should use POST to prevent CSRF and prefetch triggers - tanstack-start-redirect-in-try-catch (warn): flags throw redirect()/notFound() inside try-catch — the router catches these internally - tanstack-start-loader-parallel-fetch (warn): flags sequential awaits in loaders — should use Promise.all() to avoid waterfalls Total TanStack Start rules: 14 Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Bugs fixed: - tanstackStartRedirectInTryCatch: was using node.parent which oxlint JS plugins don't populate — rule never fired. Rewrote to use ThrowStatement visitor with tryCatchDepth tracking (matches existing nextjs pattern) - tanstackStartServerFnValidateInput: fragile chain-walking logic with potential infinite loop. Extracted shared walkServerFnChain() helper, removed unused serverFnChains variable - tanstackStartNoNavigateInRender: componentDepth never decremented for arrow components (no VariableDeclarator:exit), eventHandlerDepth was declared but never modified. Removed dead variable, simplified AGENTS.md violations fixed: - Removed unused hasDirective import - Moved SEQUENTIAL_AWAIT_THRESHOLD_FOR_LOADER to constants.ts - Moved TANSTACK_REDIRECT_FUNCTIONS to constants.ts - Moved TANSTACK_SERVER_FN_FILE_PATTERN to constants.ts - Replaced inline /^[A-Z]/ with existing UPPERCASE_PATTERN constant - Replaced duplicate MUTATION_CALLEE_NAMES with existing MUTATION_METHOD_NAMES - Removed 'as string' type cast, replaced with typeof guard Upgraded oxc: - oxlint: 1.59.0 → 1.60.0 - oxfmt: 0.44.0 → 0.45.0 Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
… cases Add tanstack-start-app fixture with: - __root.tsx missing HeadContent (triggers missing-head-content) - route-issues.tsx with 8 bad patterns (property order, direct fetch, useEffect fetch, anchor element, secrets in loader, redirect in try-catch, parallel fetch, navigate in render) - server-fn-issues.tsx with 4 bad patterns (missing validation, use-server in handler, GET mutation, dynamic import) Tests verify all rules fire with correct severity and category. Total tests: 165 → 178 (+13 new) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Add always-on TanStack Query rules (not framework-gated — useQuery is used across all React frameworks): - query-stable-query-client (error): flags new QueryClient() inside a function body — recreates the cache on every render. Should be at module scope or wrapped in useState() - query-no-rest-destructuring (warn): flags ...rest on useQuery() result — subscribes to all fields and causes unnecessary re-renders - query-no-void-query-fn (warn): flags empty queryFn body — must return a value for the cache. Use enabled option to disable instead - query-no-query-in-effect (warn): flags refetch() inside useEffect — React Query manages refetching via queryKey and enabled - query-mutation-missing-invalidation (warn): flags useMutation without invalidateQueries — stale data may remain cached - query-no-usequery-for-mutation (warn): flags useQuery with POST/PUT/DELETE fetch — use useMutation() for write operations All 6 rules have test coverage via query-issues.tsx fixture. Total tests: 178 → 184 (+6 new) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…update New rules sourced from vercel-labs/agent-skills react-best-practices: - js-flatmap-filter (warn): flags .map().filter(Boolean) chains — iterates the array twice and creates an intermediate array. Suggests .flatMap() for a single-pass transform+filter - rendering-script-defer-async (warn): flags <script src> without defer or async — blocks HTML parsing and delays First Contentful Paint Both rules have test fixtures and test cases. Total tests: 184 → 186 (+2 new) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…oute, remove duplicate helper
Bugbot findings:
1. getRouteOptionsObject used arguments[1] for direct calls like
createRootRoute({...}) and createRoute({...}), but these functions
take the options object as arguments[0]. This silently disabled
route-property-order, no-direct-fetch-in-loader, no-secrets-in-loader,
and loader-parallel-fetch for any route using createRootRoute or
createRoute. Fixed to use arguments[0] for both direct and curried
call patterns.
2. getChainCalleeName was an exact duplicate of the already-exported
getCalleeName in helpers.ts. Replaced all usages with the shared
helper per AGENTS.md DRY rule.
Added createRootRoute test case to verify the fix.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
| async ({ data }: any) => { | ||
| return data; | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Server function method order rule has no triggering test
Medium Severity
The wrongMethodOrder fixture only chains .handler() directly on createServerFn() — a single method with nothing to compare against. The tanstack-start-server-fn-method-order rule requires at least two order-sensitive methods (e.g., .handler().inputValidator()) to detect a violation. This fixture never triggers the rule, and the rule is entirely absent from the test suite, leaving it untested.
Reviewed by Cursor Bugbot for commit b2b5f30. Configure here.
Systematic analysis of all 22 rules found 3 bugs and 1 redundancy: 1. tanstackStartGetMutation only allowed 'POST' as a non-GET method, but PUT, PATCH, DELETE are also valid mutating methods. Now uses MUTATING_HTTP_METHODS constant to check all non-GET methods. 2. renderingScriptDeferAsync false-positived on non-executable script types like <script type="application/ld+json" src="...">. Now skips scripts with non-executable type attributes (matches the existing nextjsNoNativeScript pattern using EXECUTABLE_SCRIPT_TYPES). 3. queryStableQueryClient false-positived on non-component utility functions. Now restricts to uppercase-named functions (component convention) instead of all function scopes. Also: - Removed redundant condition in jsFlatmapFilter isFilterBoolean check - Replaced inline ["POST","PUT",...] array in queryNoUseQueryForMutation with existing MUTATING_HTTP_METHODS constant (DRY) - Removed unnecessary 'as string' type cast - Added edge-cases.tsx fixture testing false-positive freedom: correct property order, PUT/DELETE methods, validated server fns, JSON-LD scripts, deferred scripts - Added 4 false-positive-freedom tests Total tests: 186 → 190 (+4 edge case tests) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
- Remove dead empty ArrowFunctionExpression visitor in tanstackStartNoNavigateInRender (violated 'Remove unused code') - Move STABLE_HOOK_WRAPPERS to constants.ts (was inline in tanstack-query.ts, violated 'Put all magic numbers in constants.ts') - Move SCRIPT_LOADING_ATTRIBUTES to constants.ts (was inline in performance.ts, same violation) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Bugbot finding #3 — navigate-in-render ignores arrow components (Medium): Rewrote tanstackStartNoNavigateInRender to drop componentDepth tracking entirely. Since the rule is already scoped to /routes/ files, it now flags any navigate() call that isn't inside useEffect or a JSX event handler (onClick, onChange, etc.). This correctly catches arrow function components like const MyPage = () => { navigate(...) }. Bugbot finding #4 — server-fn-method-order has no triggering test (Medium): The wrongMethodOrder fixture only had .handler() (single method, nothing to compare). Changed to .handler().inputValidator() which has two order-sensitive methods in the wrong order. Added test case. Also simplified the createServerFn mock to use a Proxy for chainability. Total tests: 190 → 192 (+2 previously untested rules now covered) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…QueryClient
Bugbot finding: componentDepth was incremented for arrow function
components (const App = () => {...}) but never decremented — no
VariableDeclarator:exit handler. This caused componentDepth to leak,
so any new QueryClient() at module scope after an arrow component
would be falsely flagged.
Added the exit handler matching the enter condition, same pattern
used by architecture.ts for component tracking.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…der duplicates Bugbot finding: tanstackStartRedirectInTryCatch flagged throw redirect() inside catch blocks, but that's safe — a throw inside catch propagates normally and isn't re-caught. The rule's own message recommended 're-throw in the catch' which would trigger the same warning. Fixed by tracking CatchClause depth separately and skipping reports inside catch. Bugbot finding: tanstackStartServerFnMethodOrder fired on every CallExpression in a chain, producing duplicate diagnostics for 3+ method chains. Fixed by only reporting from the outermost call (where the last collected method matches the current node's method). Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…jsCombineIterations Bugbot: isFilterBoolean matched any single-param arrow whose body was any Identifier (e.g. .filter(x => someExternalVar)), not just identity functions like .filter(x => x). Fixed by verifying the body identifier name matches the parameter name. Bugbot: .map().filter(Boolean) triggered both jsFlatmapFilter and jsCombineIterations. Added early return in jsCombineIterations to skip .map().filter() pairs — the more specific jsFlatmapFilter handles those with a better suggestion (.flatMap instead of generic reduce). Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…dule scripts Bugbot: jsCombineIterations exclusion was too broad — skipped all .map().filter() chains, but jsFlatmapFilter only catches the .filter(Boolean) / .filter(x => x) subset. .map(fn).filter(predicate) lost lint coverage. Narrowed exclusion to only skip when the filter argument is Boolean or an identity arrow. Bugbot: renderingScriptDeferAsync false-positived on type="module" scripts. Per HTML spec, module scripts are deferred by default — the defer attribute has no effect on them. Added early return for type="module". Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…StartGetMutation Bugbot: the hasMutationCall walkAst block was dead code. findSideEffect already calls isMutatingDbCall internally, which checks the same MUTATION_METHOD_NAMES on member expressions. If findSideEffect finds a mutation it reports and returns; if not, hasMutationCall also finds nothing. Removed the redundant block and unused MUTATION_METHOD_NAMES import. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7e2b704. Configure here.
…ries, remove dead code Bugbot: walkAst descended into nested function bodies (arrow fns, callbacks, inner async functions), counting their awaits as loader-level sequential awaits. A loader defining an async helper plus one real await falsely triggered the rule. Replaced walkAst with hasTopLevelAwait that only checks direct await patterns in VariableDeclaration inits, ExpressionStatements, and ReturnStatements — same approach as the existing asyncParallel rule. Bugbot: the Property type check branch was dead code. property.value is always defined on Property nodes in ObjectExpression, so the nullish coalescing fallback never activated. Removed. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>


What
Adds 22 new rules to react-doctor: 14 TanStack Start, all with tests and edge case validation. Also upgrades oxc tooling and fixes 7 bugs found during review.
@tanstack/react-startis detectedOxc Upgrades
oxlint: 1.59.0 → 1.60.0oxfmt: 0.44.0 → 0.45.0All Bugs Fixed
tanstackStartRedirectInTryCatchnode.parentwhich oxlint JS plugins don't populate — rule never firedtanstackStartServerFnValidateInputgetRouteOptionsObject[1]instead of[0]) disabled 4 rules forcreateRootRoute/createRoutetanstackStartNoNavigateInRendercomponentDepthleaked for arrow components; deadeventHandlerDepthvariabletanstackStartGetMutationrenderingScriptDeferAsync<script type="application/ld+json">queryStableQueryClientValidation
Rules
See individual rule examples in previous PR description revision — all 22 rules are documented with bad/good code examples.