Skip to content

v3: ts-morph pipeline, queryOptions factories, and hierarchical query keys#197

Open
7nohe wants to merge 28 commits into
mainfrom
v3
Open

v3: ts-morph pipeline, queryOptions factories, and hierarchical query keys#197
7nohe wants to merge 28 commits into
mainfrom
v3

Conversation

@7nohe

@7nohe 7nohe commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

v3 is a full rewrite of the code generator on top of a ts-morph pipeline. Generated output stays intentionally compatible with v2 except for infinite queries and error handling. See the migration guide for details.

Highlights

Breaking changes

  • Infinite query cache keys changed shape; useXxxInfiniteKey is now a readonly tuple instead of a string.
  • The page parameter is no longer accepted in clientOptions for infinite hooks.
  • Error responses reject instead of resolving undefined data (behavior fix, ensureQueryData incorrectly handles throwOnError #172).

All covered in the migration guide with mechanical fixes.

Test plan

  • 172 unit/snapshot tests pass (vitest run)
  • biome check and tsc build clean
  • react-app example regenerated; generated output type-checks against @tanstack/react-query 5, including partial options ({ enabled: false }), pagination overrides, prefetchInfiniteQuery, suspense infinite hooks, and prefetch/ensure options
  • CLI smoke-tested with commander 15

Changes since beta.3 (shipped in v3.0.0-beta.4, 2026-07-19)

Both verified end-to-end across all four infinite surfaces (hook, suspense hook, prefetch, infiniteQueryOptions factory); unit suite is now 180 tests.

7nohe and others added 15 commits December 7, 2025 22:51
…upport

Resolved by keeping the v3 ts-morph pipeline structure:
- src/service.mts and tests/service.test.ts taken from main (proven against 0.99 sdk.gen output)
- src/generate.mts taken from main (0.99 plugins config incl. operations.nesting migration)
- Old-architecture files remain deleted (createExports/createImports/createUseQuery/createUseMutation)
- Snapshots regenerated: hey-api 0.92+ exports the Error schema under its original name (was _Error in 0.73)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- TError defaults fall back to unknown (AxiosError<unknown> for axios) when
  the operation has no generated Error type (e.g. no error responses defined)
- allParamsOptional now uses the SDK function's parameter optionality as the
  authoritative check; in 0.92+ the options parameter is required when the
  operation has required path params, so hooks no longer emit an invalid
  `= {}` default
- Drop redundant client.setConfig from the react-app example; axios.ts
  already configures baseURL

Verified: 153 unit tests pass and all 5 example apps typecheck their
regenerated output (test:generated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ted hooks

- Import Options from ../requests/sdk.gen instead of ../requests/client;
  the sdk.gen Options is extended with `client` and `meta`, which v2 hooks
  accepted (per-request client override would fail to typecheck otherwise)
- Emit the operation's JSDoc (description and @deprecated) on every
  generated hook, matching v2 output

With this, regenerated output for all 5 example apps is semantically
identical to v2.2.0 — remaining diffs are import statement merging and
blank lines only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prerelease tags publish to the beta dist-tag so latest keeps pointing at
the current stable release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ted infinite query keys

queryOptions.ts (#173):
- Every GET operation gets a queryOptions() factory (e.g. findPetsOptions)
  usable with useQuery, useQueries, prefetchQuery, ensureQueryData,
  setQueryData and other TanStack Query utilities with full type safety
- Paginatable operations get an infiniteQueryOptions() factory with
  initialPageParam/getNextPageParam wired to the CLI options; numeric
  initialPageParam is emitted as a number literal so the inferred pageParam
  type matches getNextPageParam

Dedicated infinite query keys (#140):
- Infinite hooks and factories now use use<Op>InfiniteKey
  ("<Op>Infinite") and Use<Op>InfiniteKeyFn, so cached InfiniteData no
  longer collides with the plain query cache for the same operation
- The infinite client options type excludes the page parameter, which is
  supplied by TanStack Query's pageParam mechanism; specs with a required
  page parameter now compile

BREAKING CHANGE: useInfiniteQuery hooks changed their cache key from
Use<Op>KeyFn to Use<Op>InfiniteKeyFn and no longer accept the page
parameter in clientOptions.

Verified: 165 unit tests pass; all 5 example apps regenerate and
typecheck (test:generated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…af 6, lefthook 2)

- Migrate biome config to 2.x (tailwindDirectives, svg exclusion, example rule overrides)
- Remove dead code surfaced by biome 2 noUnusedVariables/noUnusedImports
- Keep TypeScript on 6.x (ts-morph 28 does not support the TS 7 native compiler yet)
…ite query hooks

The generated options parameter no longer requires initialPageParam and
getNextPageParam (they are provided by the generator), while still allowing
callers to override both for custom pagination schemes.

Closes #156
Refs #146, #139
Infinite query keys now share the plain query key as their first segment
(["FindPets", "infinite", params] instead of ["FindPetsInfinite", params]),
so invalidating [useFindPetsKey] matches both the plain and infinite cache
entries of an operation while cached InfiniteData still never collides with
plain query data.

BREAKING CHANGE: useXxxInfiniteKey is now a readonly tuple instead of a string.

Closes #174
Refs #140
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openapi-react-query-codegen Ready Ready Preview, Comment Jul 19, 2026 11:13am

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🟢 Lines 99.39% (🎯 95%) 495 / 498
🟢 Statements 98.64% (🎯 95%) 508 / 515
🟢 Functions 100% (🎯 95%) 133 / 133
🟢 Branches 91.66% (🎯 90%) 187 / 204
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/common.mts 100% 100% 100% 100%
src/constants.mts 100% 100% 100% 100%
src/createSource.mts 100% 100% 100% 100%
src/generate.mts 100% 100% 100% 100%
src/parseOperations.mts 89.65% 73.33% 100% 96.15% 27, 51, 62, 65, 74, 155
src/tsmorph/buildCommon.mts 100% 90% 100% 100%
src/tsmorph/buildKeys.mts 100% 100% 100% 100%
src/tsmorph/buildMutationHooks.mts 100% 100% 100% 100%
src/tsmorph/buildQueryHooks.mts 100% 97.72% 100% 100%
src/tsmorph/buildQueryOptions.mts 100% 100% 100% 100%
src/tsmorph/generateFiles.mts 100% 68.75% 100% 100%
src/tsmorph/index.mts 100% 100% 100% 100%
src/tsmorph/projectFactory.mts 100% 100% 100% 100%
Generated in workflow #414 for commit 5362ab5 by the Vitest Coverage Report Action

7nohe added 2 commits July 18, 2026 21:52
- Generate prefetchUseXxxInfinite via queryClient.prefetchInfiniteQuery for
  SSR hydration of paginated lists (#155)
- Generate useXxxSuspenseInfinite hooks sharing the infinite cache key
- Accept TanStack Query options in prefetch and ensure functions (#157)
- Call the SDK with throwOnError: true everywhere so error responses reject
  instead of resolving undefined data; fixes ensureQueryData caching
  undefined and error states never firing (#172)

Closes #155
Closes #157
Closes #172
…il, Renovate

- README: feature overview with queryOptions-first positioning, comparison
  with the hey-api tanstack-query plugin and Orval, stability policy,
  requirements, quick start
- Contributing guide and AGENTS.md: document the exact-pin + patch upgrade
  workflow for hey-api and the OperationInfo/GenerationContext IR boundary
  that keeps the generation layer backend-portable
- Add renovate.json: grouped hey-api update PRs (snapshot suite as the
  breaking-change detector) and grouped non-major dev tooling updates
…output

Review cleanup of the recent feature work:

- Extract SDK_CALL_ARGS, buildPagedQueryFn, formatInitialPageParam,
  buildGetNextPageParamExpr, buildInfiniteClientOptionsParam, and
  buildOverridableInfiniteOptionsType so the throwOnError call shape and the
  infinite-query templates live in one place instead of ten
- Merge buildUseInfiniteQueryHook / buildUseSuspenseInfiniteQueryHook into one
  parameterized implementation
- Emit initialPageParam as a numeric literal in hooks too, matching the
  factories and prefetch functions (server-prefetched page 1 now hydrates the
  same pageParam the client refetches with)
- Give prefetchUseXxxInfinite the same options passthrough as its siblings,
  which also makes the FetchInfiniteQueryOptions import real
- Reuse buildClientOptionsParam / getDataTypeName in prefetch, ensure, and
  queryOptions builders instead of inlined copies
- generateIndexFile returns its constant content without a ts-morph project;
  suspense/prefetch loops filter paginatable operations up front
- examples: use key={pet.id} instead of array-index keys and drop the
  noArrayIndexKey override
- Point the install command at ^3 instead of the beta tag
- Call out the throwOnError behavior change in the TL;DR (the one change
  the compiler cannot catch)
- Document the options parameter on prefetchUseXxxInfinite
Exercises the generated queryOptions factories, ensureQueryData,
prefetchInfiniteQuery, hierarchical key invalidation, and the throwOnError
behavior (#172) with real requests instead of type checks only. Runs in CI
after the example codegen step.
…wOnError workaround

- nextjs-app: prefetch the first infinite page on the server with
  prefetchUseFindPaginatedPetsInfinite + HydrationBoundary
- react-app / react-router-6-app: remove the client-level throwOnError
  workaround — v3 generated code sets it per call (#172)
7nohe added 3 commits July 19, 2026 17:21
`safeParseNumber` relied on `Number(value)`, and `Number("")` evaluates to
0. As a result `formatOptions` coerced `--initialPageParam ""` into a
numeric 0, so the generated infinite queries sent `page=0` instead of the
blank first page the API expected.

Treat blank strings as NaN so `formatOptions` falls through to the
passthrough branch and keeps the original value. All four infinite
surfaces (hook, suspense hook, prefetch, queryOptions factory) now emit
`initialPageParam: ""`.

Closes #177
`generateFiles.mts` imported `StructureKind` but never referenced it, so
Biome reported a noUnusedImports warning. The file only needs the
`ImportDeclarationStructure` type, so collapse the statement into a plain
type-only import.
Some APIs expect no page parameter (or an empty one) on the first request
of a paginated endpoint. Previously the only knob was --initialPageParam,
which always emits a concrete value.

With --omitInitialPageParam the generated infinite queries emit
`initialPageParam: undefined` and spread the page param into the request
only once TanStack Query supplies one, so the first request carries no
page param at all. The flag overrides --initialPageParam.

Verified end-to-end: the generated output typechecks against the example
app (TanStack Query infers TPageParam as `number | undefined`, so
`initialPageParam: undefined` is valid) across all four infinite surfaces
(hook, suspense, prefetch, queryOptions factory).

Refs #177
The react-app example targets the hey-api fetch client, whose Config
expects `baseUrl` (not axios-style `baseURL`) and which throws the
OpenAPI `Error` schema body (`{ code, message }`) rather than an axios
error with a `.response` property.

- src/axios.ts, verify-runtime.test.ts: pass `baseUrl` to
  `client.setConfig` — the `baseURL` typo silently left the client
  unconfigured, so runtime tests hit the default URL instead of the
  in-process test server
- src/App.tsx: read `error.message` directly instead of the
  nonexistent `error.response?.data.message`

`npx tsc -p ./tsconfig.json --noEmit` in examples/react-app is now
clean and verify-runtime.test.ts passes 6/6 locally.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant