From 664744cef41f5f7f1bb58f8c1ef0b5f9f8549ed0 Mon Sep 17 00:00:00 2001
From: Urata Daiki <7nohe@users.noreply.github.com>
Date: Tue, 16 Dec 2025 21:18:52 +0900
Subject: [PATCH 1/2] feat: add TanStack Router integration with loader
factories
---
README.md | 70 +++++++
.../content/docs/examples/tanstack-router.md | 172 ++++++++++++++++-
docs/src/content/docs/guides/introduction.mdx | 5 +-
docs/src/content/docs/index.mdx | 2 +-
src/constants.mts | 1 +
src/tsmorph/buildRouter.mts | 130 +++++++++++++
src/tsmorph/generateFiles.mts | 90 +++++++++
src/tsmorph/index.mts | 1 +
tests/__snapshots__/createSource.test.ts.snap | 64 +++++++
tests/createSource.test.ts | 9 +-
tests/tsmorph/buildRouter.test.ts | 180 ++++++++++++++++++
11 files changed, 719 insertions(+), 5 deletions(-)
create mode 100644 src/tsmorph/buildRouter.mts
create mode 100644 tests/tsmorph/buildRouter.test.ts
diff --git a/README.md b/README.md
index 652c5ea..bbd3558 100644
--- a/README.md
+++ b/README.md
@@ -10,3 +10,73 @@
- Generates custom functions that use React Query's `ensureQueryData` and `prefetchQuery` functions
- Generates query keys and functions for query caching
- Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts)
+- Generates loader factories and helpers for [TanStack Router](https://tanstack.com/router) integration
+
+## TanStack Router Integration
+
+The generated `router.ts` file provides loader factories and helpers for seamless integration with TanStack Router.
+
+### Using Loader Factories
+
+Use `loaderUse*` functions in your route definitions to prefetch data:
+
+```tsx
+// routes/pets.$petId.tsx
+import { createFileRoute } from "@tanstack/react-router";
+import { loaderUseFindPetById } from "../openapi/queries/router";
+import { queryClient } from "../queryClient";
+
+export const Route = createFileRoute("/pets/$petId")({
+ loader: ({ params }) =>
+ loaderUseFindPetById({ queryClient })({
+ params: { petId: Number(params.petId) },
+ }),
+ component: PetDetail,
+});
+```
+
+For SSR/TanStack Start, pass `queryClient` from the router context:
+
+```tsx
+loader: ({ context, params }) =>
+ loaderUseFindPetById({ queryClient: context.queryClient })({
+ params: { petId: Number(params.petId) },
+ }),
+```
+
+### Using withQueryPrefetch for Hover/Touch Prefetching
+
+The `withQueryPrefetch` helper enables prefetching on hover or touch:
+
+```tsx
+import { withQueryPrefetch } from "../openapi/queries/router";
+import { prefetchUseFindPetById } from "../openapi/queries/prefetch";
+import { queryClient } from "../queryClient";
+
+function PetLink({ petId }: { petId: number }) {
+ return (
+
+ prefetchUseFindPetById(queryClient, { path: { petId } })
+ )}
+ >
+ View Pet
+
+ );
+}
+```
+
+### Important Notes
+
+- **Router params are strings**: TanStack Router params are always strings. You must parse them to the correct type (e.g., `Number(params.petId)`) before passing to the loader.
+- **External cache configuration**: When using TanStack Query as the cache, set `defaultPreloadStaleTime: 0` in your router configuration to let React Query handle cache freshness:
+
+```tsx
+const router = createRouter({
+ routeTree,
+ defaultPreloadStaleTime: 0,
+});
+```
+
+- **Link preloading**: When using `` or `defaultPreload: "intent"`, TanStack Router will automatically call the route's `loader` on hover/touch. If your loader uses `ensureUse*Data`, prefetching happens automatically without needing `withQueryPrefetch`.
diff --git a/docs/src/content/docs/examples/tanstack-router.md b/docs/src/content/docs/examples/tanstack-router.md
index 4bbc0c1..17f0577 100644
--- a/docs/src/content/docs/examples/tanstack-router.md
+++ b/docs/src/content/docs/examples/tanstack-router.md
@@ -1,6 +1,174 @@
---
title: TanStack Router Example
-description: A simple example of using TanStack Router with OpenAPI React Query Codegen.
+description: Using TanStack Router with OpenAPI React Query Codegen for data loading and prefetching.
---
-Example of using Next.js can be found in the [`examples/tanstack-router-app`](https://github.com/7nohe/openapi-react-query-codegen/tree/main/examples/tanstack-router-app) directory of the repository.
+Example of using TanStack Router can be found in the [`examples/tanstack-router-app`](https://github.com/7nohe/openapi-react-query-codegen/tree/main/examples/tanstack-router-app) directory of the repository.
+
+## Generated Files
+
+The codegen generates a `router.ts` file that provides:
+
+- **Loader factories** (`loaderUse*`) for route data loading
+- **`withQueryPrefetch`** helper for hover/touch prefetching
+
+## Using Loader Factories
+
+Use `loaderUse*` functions in your route definitions to prefetch data before the route renders:
+
+```tsx
+// routes/pets.$petId.tsx
+import { createFileRoute } from "@tanstack/react-router";
+import { loaderUseFindPetById } from "../openapi/queries/router";
+import { queryClient } from "../queryClient";
+
+export const Route = createFileRoute("/pets/$petId")({
+ loader: ({ params }) =>
+ loaderUseFindPetById({ queryClient })({
+ params: { petId: Number(params.petId) },
+ }),
+ component: PetDetail,
+});
+
+function PetDetail() {
+ const { petId } = Route.useParams();
+ const { data } = useFindPetById({ path: { petId: Number(petId) } });
+
+ return
{data?.name}
;
+}
+```
+
+### For SSR / TanStack Start
+
+When using SSR or TanStack Start, pass `queryClient` from the router context instead of importing it directly:
+
+```tsx
+export const Route = createFileRoute("/pets/$petId")({
+ loader: ({ context, params }) =>
+ loaderUseFindPetById({ queryClient: context.queryClient })({
+ params: { petId: Number(params.petId) },
+ }),
+ component: PetDetail,
+});
+```
+
+### Operations Without Path Parameters
+
+For operations without path parameters, the loader is simpler:
+
+```tsx
+import { loaderUseFindPets } from "../openapi/queries/router";
+
+export const Route = createFileRoute("/pets")({
+ loader: () => loaderUseFindPets({ queryClient })(),
+ component: PetList,
+});
+```
+
+### Passing Additional Options
+
+You can pass additional client options through the `clientOptions` parameter:
+
+```tsx
+loader: ({ params }) =>
+ loaderUseFindPetById({
+ queryClient,
+ clientOptions: {
+ headers: { "X-Custom-Header": "value" },
+ },
+ })({
+ params: { petId: Number(params.petId) },
+ }),
+```
+
+## Using withQueryPrefetch
+
+The `withQueryPrefetch` helper enables prefetching on hover or touch events. This is useful for custom prefetch triggers outside of TanStack Router's built-in `` preloading:
+
+```tsx
+import { withQueryPrefetch } from "../openapi/queries/router";
+import { prefetchUseFindPetById } from "../openapi/queries/prefetch";
+import { queryClient } from "../queryClient";
+
+function PetLink({ petId }: { petId: number }) {
+ return (
+
+ prefetchUseFindPetById(queryClient, { path: { petId } })
+ )}
+ >
+ View Pet
+
+ );
+}
+```
+
+This spreads `onMouseEnter` and `onTouchStart` handlers that trigger the prefetch.
+
+## Router Configuration
+
+### External Cache Settings
+
+When using TanStack Query as an external cache, configure the router to delegate cache freshness to React Query:
+
+```tsx
+import { createRouter } from "@tanstack/react-router";
+import { routeTree } from "./routeTree.gen";
+
+const router = createRouter({
+ routeTree,
+ defaultPreloadStaleTime: 0, // Let React Query handle cache freshness
+});
+```
+
+### Link Preloading
+
+TanStack Router's `` component supports intent-based preloading:
+
+```tsx
+
+ View Pet
+
+```
+
+Or set it globally:
+
+```tsx
+const router = createRouter({
+ routeTree,
+ defaultPreload: "intent",
+ defaultPreloadStaleTime: 0,
+});
+```
+
+When using `preload="intent"`, the router automatically calls the route's `loader` on hover/touch. If your loader uses `ensureUse*Data` (which the generated loaders do), prefetching happens automatically.
+
+## Important Notes
+
+### Router Params Are Strings
+
+TanStack Router params are always strings. You must parse them to the correct type before passing to the loader:
+
+```tsx
+// Router params: { petId: string }
+// API expects: { petId: number }
+loader: ({ params }) =>
+ loaderUseFindPetById({ queryClient })({
+ params: { petId: Number(params.petId) }, // Convert string to number
+ }),
+```
+
+For type-safe parsing, consider using TanStack Router's `parseParams`:
+
+```tsx
+export const Route = createFileRoute("/pets/$petId")({
+ parseParams: (params) => ({
+ petId: Number(params.petId),
+ }),
+ loader: ({ params }) =>
+ loaderUseFindPetById({ queryClient })({
+ params: { petId: params.petId }, // Already a number
+ }),
+});
+```
diff --git a/docs/src/content/docs/guides/introduction.mdx b/docs/src/content/docs/guides/introduction.mdx
index 283df3b..860d757 100644
--- a/docs/src/content/docs/guides/introduction.mdx
+++ b/docs/src/content/docs/guides/introduction.mdx
@@ -12,6 +12,7 @@ OpenAPI React Query Codegen is a code generator for creating React Query (also k
- Generates custom functions that use React Query's `ensureQueryData` and `prefetchQuery` functions
- Generates query keys and functions for query caching
- Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts)
+- Generates loader factories and helpers for [TanStack Router](https://tanstack.com/router) integration
## Installation
@@ -75,6 +76,7 @@ openapi/
│ ├── infiniteQueries.ts
│ ├── prefetch.ts
│ ├── queries.ts
+│ ├── router.ts
│ └── suspense.ts
└── requests
├── index.ts
@@ -177,8 +179,9 @@ export default App;
- ensureQueryData.ts Generated ensureQueryData functions
- queries.ts Generated query/mutation hooks
- infiniteQueries.ts Generated infinite query hooks
- - suspenses.ts Generated suspense hooks
+ - suspense.ts Generated suspense hooks
- prefetch.ts Generated prefetch functions
+ - router.ts Generated loader factories and helpers for TanStack Router
- requests Output code generated by `@hey-api/openapi-ts`
diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx
index 40ce37e..76329bc 100644
--- a/docs/src/content/docs/index.mdx
+++ b/docs/src/content/docs/index.mdx
@@ -24,7 +24,7 @@ import { Card, CardGrid } from '@astrojs/starlight/components';
Generates custom react hooks that use React(TanStack) Query's useQuery, useSuspenseQuery, useMutation and useInfiniteQuery hooks.
- Generates custom functions that use React Query's `ensureQueryData` and `prefetchQuery` functions to integrate into frameworks like Next.js and Remix.
+ Generates custom functions that use React Query's `ensureQueryData` and `prefetchQuery` functions, plus loader factories for TanStack Router, to integrate into frameworks like Next.js, Remix, and TanStack Start.
Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) in case you still want to do type-safe API calls without React Query.
diff --git a/src/constants.mts b/src/constants.mts
index 651e6e8..8a14590 100644
--- a/src/constants.mts
+++ b/src/constants.mts
@@ -13,4 +13,5 @@ export const OpenApiRqFiles = {
index: "index",
prefetch: "prefetch",
ensureQueryData: "ensureQueryData",
+ router: "router",
} as const;
diff --git a/src/tsmorph/buildRouter.mts b/src/tsmorph/buildRouter.mts
new file mode 100644
index 0000000..60c621b
--- /dev/null
+++ b/src/tsmorph/buildRouter.mts
@@ -0,0 +1,130 @@
+import {
+ StructureKind,
+ VariableDeclarationKind,
+ type VariableStatementStructure,
+} from "ts-morph";
+import type { GenerationContext, OperationInfo } from "../types.mjs";
+
+/**
+ * Build the withQueryPrefetch helper.
+ * This helper provides event handlers for hover/touch prefetching.
+ *
+ * Example output:
+ * export const withQueryPrefetch = (prefetch: () => Promise) => ({
+ * onMouseEnter: () => void prefetch(),
+ * onTouchStart: () => void prefetch(),
+ * });
+ */
+export function buildWithQueryPrefetch(): VariableStatementStructure {
+ const initializer = `(prefetch: () => Promise) => ({
+ onMouseEnter: () => void prefetch(),
+ onTouchStart: () => void prefetch(),
+})`;
+
+ return {
+ kind: StructureKind.VariableStatement,
+ isExported: true,
+ declarationKind: VariableDeclarationKind.Const,
+ declarations: [
+ {
+ name: "withQueryPrefetch",
+ initializer,
+ },
+ ],
+ };
+}
+
+/**
+ * Check if an operation has a path parameter that is not `never`.
+ */
+function hasPathParam(op: OperationInfo): boolean {
+ const pathParam = op.parameters.find((p) => p.name === "path");
+ return Boolean(pathParam && pathParam.typeName !== "never");
+}
+
+/**
+ * Get required top-level keys (non-optional, non-never).
+ */
+function getRequiredTopLevelKeys(op: OperationInfo): string[] {
+ return op.parameters
+ .filter((p) => !p.optional && p.typeName !== "never")
+ .map((p) => p.name);
+}
+
+/**
+ * Build loader factory for a GET operation.
+ *
+ * For operations without path parameter:
+ * export const loaderUseFindPets = (deps: {
+ * queryClient: QueryClient;
+ * clientOptions?: Options;
+ * }) => async () => {
+ * await ensureUseFindPetsData(deps.queryClient, deps.clientOptions);
+ * return null;
+ * };
+ *
+ * For operations with path parameter:
+ * export const loaderUseFindPetById = (deps: {
+ * queryClient: QueryClient;
+ * clientOptions?: Omit, "path">;
+ * }) => async ({ params }: { params: FindPetByIdData["path"] }) => {
+ * const options: Options = { ...(deps.clientOptions ?? {}), path: params };
+ * await ensureUseFindPetByIdData(deps.queryClient, options);
+ * return null;
+ * };
+ */
+export function buildLoaderFactory(
+ op: OperationInfo,
+ ctx: GenerationContext,
+): VariableStatementStructure {
+ const loaderName = `loaderUse${op.capitalizedMethodName}`;
+ const ensureFnName = `ensureUse${op.capitalizedMethodName}Data`;
+
+ const dataTypeName = ctx.modelNames.includes(
+ `${op.capitalizedMethodName}Data`,
+ )
+ ? `${op.capitalizedMethodName}Data`
+ : "unknown";
+
+ const hasPath = hasPathParam(op);
+ const requiredTopLevelKeys = getRequiredTopLevelKeys(op);
+ const requiredNonPathKeys = requiredTopLevelKeys.filter((k) => k !== "path");
+
+ // Determine if clientOptions should be optional
+ const clientOptionsOptional = hasPath
+ ? requiredNonPathKeys.length === 0
+ : requiredTopLevelKeys.length === 0;
+
+ const optionalMark = clientOptionsOptional ? "?" : "";
+
+ let initializer: string;
+
+ if (hasPath) {
+ // Has path parameter - needs params in returned function
+ const clientOptionsType = `Omit, "path">`;
+ initializer = `(deps: { queryClient: QueryClient; clientOptions${optionalMark}: ${clientOptionsType} }) => async ({ params }: { params: ${dataTypeName}["path"] }) => {
+ const options: Options<${dataTypeName}, true> = { ...(deps.clientOptions ?? {}), path: params };
+ await ${ensureFnName}(deps.queryClient, options);
+ return null;
+}`;
+ } else {
+ // No path parameter - simpler factory
+ const clientOptionsType = `Options<${dataTypeName}, true>`;
+ initializer = `(deps: { queryClient: QueryClient; clientOptions${optionalMark}: ${clientOptionsType} }) => async () => {
+ await ${ensureFnName}(deps.queryClient, deps.clientOptions);
+ return null;
+}`;
+ }
+
+ return {
+ kind: StructureKind.VariableStatement,
+ isExported: true,
+ declarationKind: VariableDeclarationKind.Const,
+ declarations: [
+ {
+ name: loaderName,
+ initializer,
+ },
+ ],
+ };
+}
diff --git a/src/tsmorph/generateFiles.mts b/src/tsmorph/generateFiles.mts
index b389aa2..aeb57b1 100644
--- a/src/tsmorph/generateFiles.mts
+++ b/src/tsmorph/generateFiles.mts
@@ -29,6 +29,7 @@ import {
buildUseQueryHook,
buildUseSuspenseQueryHook,
} from "./buildQueryHooks.mjs";
+import { buildLoaderFactory, buildWithQueryPrefetch } from "./buildRouter.mjs";
import {
buildAxiosErrorImport,
buildClientImport,
@@ -293,6 +294,88 @@ function generateEnsureQueryDataFile(
return sourceFile.getFullText();
}
+/**
+ * Build imports for router.ts file.
+ */
+function buildRouterFileImports(
+ operations: OperationInfo[],
+ ctx: GenerationContext,
+): ImportDeclarationStructure[] {
+ const getOperations = operations.filter((op) => op.httpMethod === "GET");
+
+ // Get Data type names needed for GET operations
+ const dataTypeNames = getOperations
+ .map((op) => `${op.capitalizedMethodName}Data`)
+ .filter((name) => ctx.modelNames.includes(name));
+
+ // Get ensure function names
+ const ensureFnNames = getOperations.map(
+ (op) => `ensureUse${op.capitalizedMethodName}Data`,
+ );
+
+ const imports: ImportDeclarationStructure[] = [
+ // Options import from client
+ buildClientImport(ctx),
+ // QueryClient import
+ {
+ kind: StructureKind.ImportDeclaration,
+ moduleSpecifier: "@tanstack/react-query",
+ namedImports: [{ name: "QueryClient", isTypeOnly: true }],
+ },
+ ];
+
+ // Add Data types import if needed
+ if (dataTypeNames.length > 0) {
+ imports.push({
+ kind: StructureKind.ImportDeclaration,
+ moduleSpecifier: "../requests/types.gen",
+ namedImports: dataTypeNames.map((name) => ({ name })),
+ });
+ }
+
+ // Add ensureQueryData imports
+ if (ensureFnNames.length > 0) {
+ imports.push({
+ kind: StructureKind.ImportDeclaration,
+ moduleSpecifier: "./ensureQueryData",
+ namedImports: ensureFnNames.map((name) => ({ name })),
+ });
+ }
+
+ return imports;
+}
+
+/**
+ * Generate the router.ts file content.
+ */
+function generateRouterFile(
+ operations: OperationInfo[],
+ ctx: GenerationContext,
+): string {
+ const project = createGenerationProject();
+ const sourceFile = project.createSourceFile(
+ `${OpenApiRqFiles.router}.ts`,
+ undefined,
+ { overwrite: true },
+ );
+
+ // Add imports
+ sourceFile.addImportDeclarations(buildRouterFileImports(operations, ctx));
+
+ // Add withQueryPrefetch helper
+ sourceFile.addVariableStatement(buildWithQueryPrefetch());
+
+ // Only GET operations for loader factories
+ const getOperations = operations.filter((op) => op.httpMethod === "GET");
+
+ // Add loader factories
+ for (const op of getOperations) {
+ sourceFile.addVariableStatement(buildLoaderFactory(op, ctx));
+ }
+
+ return sourceFile.getFullText();
+}
+
/**
* Add the generated header comment to file content.
*/
@@ -355,5 +438,12 @@ export function generateAllFiles(
ctx.version,
),
},
+ {
+ name: `${OpenApiRqFiles.router}.ts`,
+ content: addHeaderComment(
+ generateRouterFile(operations, ctx),
+ ctx.version,
+ ),
+ },
];
}
diff --git a/src/tsmorph/index.mts b/src/tsmorph/index.mts
index 59e3c0a..6422308 100644
--- a/src/tsmorph/index.mts
+++ b/src/tsmorph/index.mts
@@ -3,3 +3,4 @@ export { createGenerationProject } from "./projectFactory.mjs";
export * from "./buildCommon.mjs";
export * from "./buildQueryHooks.mjs";
export * from "./buildMutationHooks.mjs";
+export * from "./buildRouter.mjs";
diff --git a/tests/__snapshots__/createSource.test.ts.snap b/tests/__snapshots__/createSource.test.ts.snap
index d759777..bded83c 100644
--- a/tests/__snapshots__/createSource.test.ts.snap
+++ b/tests/__snapshots__/createSource.test.ts.snap
@@ -112,6 +112,38 @@ export const prefetchUseFindPaginatedPets = (queryClient: QueryClient, clientOpt
"
`;
+exports[`createSource > createSource - @hey-api/client-axios 6`] = `
+"// generated with @7nohe/openapi-react-query-codegen@1.0.0
+
+import { type Options } from "@hey-api/client-axios";
+import { type QueryClient } from "@tanstack/react-query";
+import { FindPetsData, FindPetByIdData, FindPaginatedPetsData } from "../requests/types.gen";
+import { ensureUseFindPetsData, ensureUseGetNotDefinedData, ensureUseFindPetByIdData, ensureUseFindPaginatedPetsData } from "./ensureQueryData";
+
+export const withQueryPrefetch = (prefetch: () => Promise) => ({
+ onMouseEnter: () => void prefetch(),
+ onTouchStart: () => void prefetch(),
+ });
+export const loaderUseFindPets = (deps: { queryClient: QueryClient; clientOptions?: Options }) => async () => {
+ await ensureUseFindPetsData(deps.queryClient, deps.clientOptions);
+ return null;
+ };
+export const loaderUseGetNotDefined = (deps: { queryClient: QueryClient; clientOptions?: Options }) => async () => {
+ await ensureUseGetNotDefinedData(deps.queryClient, deps.clientOptions);
+ return null;
+ };
+export const loaderUseFindPetById = (deps: { queryClient: QueryClient; clientOptions?: Omit, "path"> }) => async ({ params }: { params: FindPetByIdData["path"] }) => {
+ const options: Options = { ...(deps.clientOptions ?? {}), path: params };
+ await ensureUseFindPetByIdData(deps.queryClient, options);
+ return null;
+ };
+export const loaderUseFindPaginatedPets = (deps: { queryClient: QueryClient; clientOptions?: Options }) => async () => {
+ await ensureUseFindPaginatedPetsData(deps.queryClient, deps.clientOptions);
+ return null;
+ };
+"
+`;
+
exports[`createSource > createSource - @hey-api/client-fetch 1`] = `
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
@@ -219,3 +251,35 @@ export const prefetchUseFindPetById = (queryClient: QueryClient, clientOptions:
export const prefetchUseFindPaginatedPets = (queryClient: QueryClient, clientOptions: Options = {}) => queryClient.prefetchQuery({ queryKey: Common.UseFindPaginatedPetsKeyFn(clientOptions), queryFn: () => findPaginatedPets({ ...clientOptions }).then(response => response.data) });
"
`;
+
+exports[`createSource > createSource - @hey-api/client-fetch 6`] = `
+"// generated with @7nohe/openapi-react-query-codegen@1.0.0
+
+import { type Options } from "@hey-api/client-fetch";
+import { type QueryClient } from "@tanstack/react-query";
+import { FindPetsData, FindPetByIdData, FindPaginatedPetsData } from "../requests/types.gen";
+import { ensureUseFindPetsData, ensureUseGetNotDefinedData, ensureUseFindPetByIdData, ensureUseFindPaginatedPetsData } from "./ensureQueryData";
+
+export const withQueryPrefetch = (prefetch: () => Promise) => ({
+ onMouseEnter: () => void prefetch(),
+ onTouchStart: () => void prefetch(),
+ });
+export const loaderUseFindPets = (deps: { queryClient: QueryClient; clientOptions?: Options }) => async () => {
+ await ensureUseFindPetsData(deps.queryClient, deps.clientOptions);
+ return null;
+ };
+export const loaderUseGetNotDefined = (deps: { queryClient: QueryClient; clientOptions?: Options }) => async () => {
+ await ensureUseGetNotDefinedData(deps.queryClient, deps.clientOptions);
+ return null;
+ };
+export const loaderUseFindPetById = (deps: { queryClient: QueryClient; clientOptions?: Omit, "path"> }) => async ({ params }: { params: FindPetByIdData["path"] }) => {
+ const options: Options = { ...(deps.clientOptions ?? {}), path: params };
+ await ensureUseFindPetByIdData(deps.queryClient, options);
+ return null;
+ };
+export const loaderUseFindPaginatedPets = (deps: { queryClient: QueryClient; clientOptions?: Options }) => async () => {
+ await ensureUseFindPaginatedPetsData(deps.queryClient, deps.clientOptions);
+ return null;
+ };
+"
+`;
diff --git a/tests/createSource.test.ts b/tests/createSource.test.ts
index 8b6eba4..0ceea77 100644
--- a/tests/createSource.test.ts
+++ b/tests/createSource.test.ts
@@ -18,7 +18,7 @@ describe(fileName, () => {
client: "@hey-api/client-fetch",
});
- expect(source).toHaveLength(7);
+ expect(source).toHaveLength(8);
expect(source.map((s) => s.name)).toEqual([
"index.ts",
"common.ts",
@@ -27,6 +27,7 @@ describe(fileName, () => {
"infiniteQueries.ts",
"prefetch.ts",
"ensureQueryData.ts",
+ "router.ts",
]);
const indexTs = source.find((s) => s.name === "index.ts");
@@ -43,6 +44,9 @@ describe(fileName, () => {
const prefetchTs = source.find((s) => s.name === "prefetch.ts");
expect(prefetchTs?.content).toMatchSnapshot();
+
+ const routerTs = source.find((s) => s.name === "router.ts");
+ expect(routerTs?.content).toMatchSnapshot();
});
test("createSource - @hey-api/client-axios", async () => {
@@ -69,5 +73,8 @@ describe(fileName, () => {
const prefetchTs = source.find((s) => s.name === "prefetch.ts");
expect(prefetchTs?.content).toMatchSnapshot();
+
+ const routerTs = source.find((s) => s.name === "router.ts");
+ expect(routerTs?.content).toMatchSnapshot();
});
});
diff --git a/tests/tsmorph/buildRouter.test.ts b/tests/tsmorph/buildRouter.test.ts
new file mode 100644
index 0000000..ea3a081
--- /dev/null
+++ b/tests/tsmorph/buildRouter.test.ts
@@ -0,0 +1,180 @@
+import { StructureKind, VariableDeclarationKind } from "ts-morph";
+import { describe, expect, it } from "vitest";
+import {
+ buildLoaderFactory,
+ buildWithQueryPrefetch,
+} from "../../src/tsmorph/buildRouter.mjs";
+import type { GenerationContext, OperationInfo } from "../../src/types.mjs";
+
+// Operation without path parameter (all optional)
+const mockOperationNoPath: OperationInfo = {
+ methodName: "findPets",
+ capitalizedMethodName: "FindPets",
+ httpMethod: "GET",
+ isDeprecated: false,
+ parameters: [{ name: "limit", typeName: "number", optional: true }],
+ allParamsOptional: true,
+ isPaginatable: false,
+};
+
+// Operation with path parameter
+const mockOperationWithPath: OperationInfo = {
+ methodName: "findPetById",
+ capitalizedMethodName: "FindPetById",
+ httpMethod: "GET",
+ isDeprecated: false,
+ parameters: [
+ { name: "path", typeName: "{ petId: number }", optional: false },
+ ],
+ allParamsOptional: false,
+ isPaginatable: false,
+};
+
+// Operation with path and other required params
+const mockOperationWithPathAndRequired: OperationInfo = {
+ methodName: "getPetDetails",
+ capitalizedMethodName: "GetPetDetails",
+ httpMethod: "GET",
+ isDeprecated: false,
+ parameters: [
+ { name: "path", typeName: "{ petId: number }", optional: false },
+ { name: "query", typeName: "{ include: string }", optional: false },
+ ],
+ allParamsOptional: false,
+ isPaginatable: false,
+};
+
+// Operation with path: never (no actual path params)
+const mockOperationWithPathNever: OperationInfo = {
+ methodName: "listPets",
+ capitalizedMethodName: "ListPets",
+ httpMethod: "GET",
+ isDeprecated: false,
+ parameters: [
+ { name: "path", typeName: "never", optional: true },
+ { name: "query", typeName: "{ limit: number }", optional: true },
+ ],
+ allParamsOptional: true,
+ isPaginatable: false,
+};
+
+const mockFetchContext: GenerationContext = {
+ client: "@hey-api/client-fetch",
+ modelNames: [
+ "Pet",
+ "FindPetsData",
+ "FindPetByIdData",
+ "GetPetDetailsData",
+ "ListPetsData",
+ ],
+ serviceNames: ["findPets", "findPetById", "getPetDetails", "listPets"],
+ pageParam: "page",
+ nextPageParam: "nextPage",
+ initialPageParam: "1",
+ version: "1.0.0",
+};
+
+describe("buildRouter", () => {
+ describe("buildWithQueryPrefetch", () => {
+ it("should build withQueryPrefetch helper", () => {
+ const result = buildWithQueryPrefetch();
+
+ expect(result.kind).toBe(StructureKind.VariableStatement);
+ expect(result.isExported).toBe(true);
+ expect(result.declarationKind).toBe(VariableDeclarationKind.Const);
+ expect(result.declarations[0].name).toBe("withQueryPrefetch");
+
+ const initializer = result.declarations[0].initializer as string;
+ expect(initializer).toContain("prefetch: () => Promise");
+ expect(initializer).toContain("onMouseEnter: () => void prefetch()");
+ expect(initializer).toContain("onTouchStart: () => void prefetch()");
+ });
+ });
+
+ describe("buildLoaderFactory", () => {
+ it("should build loader factory for operation without path param", () => {
+ const result = buildLoaderFactory(mockOperationNoPath, mockFetchContext);
+
+ expect(result.kind).toBe(StructureKind.VariableStatement);
+ expect(result.isExported).toBe(true);
+ expect(result.declarationKind).toBe(VariableDeclarationKind.Const);
+ expect(result.declarations[0].name).toBe("loaderUseFindPets");
+
+ const initializer = result.declarations[0].initializer as string;
+ expect(initializer).toContain("queryClient: QueryClient");
+ expect(initializer).toContain("clientOptions?:");
+ expect(initializer).toContain("Options");
+ expect(initializer).toContain("async () =>");
+ expect(initializer).toContain(
+ "ensureUseFindPetsData(deps.queryClient, deps.clientOptions)",
+ );
+ expect(initializer).toContain("return null");
+ });
+
+ it("should build loader factory for operation with path param", () => {
+ const result = buildLoaderFactory(
+ mockOperationWithPath,
+ mockFetchContext,
+ );
+
+ expect(result.declarations[0].name).toBe("loaderUseFindPetById");
+
+ const initializer = result.declarations[0].initializer as string;
+ expect(initializer).toContain("queryClient: QueryClient");
+ expect(initializer).toContain("clientOptions?:");
+ expect(initializer).toContain(
+ 'Omit, "path">',
+ );
+ expect(initializer).toContain("async ({ params }:");
+ expect(initializer).toContain('params: FindPetByIdData["path"]');
+ expect(initializer).toContain("...(deps.clientOptions ?? {})");
+ expect(initializer).toContain("path: params");
+ expect(initializer).toContain(
+ "ensureUseFindPetByIdData(deps.queryClient, options)",
+ );
+ });
+
+ it("should make clientOptions required when non-path required params exist", () => {
+ const result = buildLoaderFactory(
+ mockOperationWithPathAndRequired,
+ mockFetchContext,
+ );
+
+ const initializer = result.declarations[0].initializer as string;
+ // clientOptions should NOT have ? (required)
+ expect(initializer).toContain("clientOptions:");
+ expect(initializer).not.toMatch(/clientOptions\?:/);
+ });
+
+ it("should treat path: never as no path param", () => {
+ const result = buildLoaderFactory(
+ mockOperationWithPathNever,
+ mockFetchContext,
+ );
+
+ expect(result.declarations[0].name).toBe("loaderUseListPets");
+
+ const initializer = result.declarations[0].initializer as string;
+ // Should NOT have params in the inner function
+ expect(initializer).toContain("async () =>");
+ expect(initializer).not.toContain("{ params }");
+ // clientOptions should be optional since all params are optional
+ expect(initializer).toContain("clientOptions?:");
+ });
+
+ it("should use unknown for missing Data type", () => {
+ const contextWithoutDataType: GenerationContext = {
+ ...mockFetchContext,
+ modelNames: [],
+ };
+
+ const result = buildLoaderFactory(
+ mockOperationNoPath,
+ contextWithoutDataType,
+ );
+
+ const initializer = result.declarations[0].initializer as string;
+ expect(initializer).toContain("Options");
+ });
+ });
+});
From 8f7bfdd2ec95925a86f4a92d2d4036e5afed055c Mon Sep 17 00:00:00 2001
From: Urata Daiki <7nohe@users.noreply.github.com>
Date: Tue, 16 Dec 2025 22:05:13 +0900
Subject: [PATCH 2/2] docs: add TanStack Router integration guide using
ensureQueryData
---
README.md | 70 -------
.../content/docs/examples/tanstack-router.md | 76 +++-----
docs/src/content/docs/guides/introduction.mdx | 3 -
docs/src/content/docs/index.mdx | 2 +-
src/constants.mts | 1 -
src/tsmorph/buildRouter.mts | 130 -------------
src/tsmorph/generateFiles.mts | 90 ---------
src/tsmorph/index.mts | 1 -
tests/__snapshots__/createSource.test.ts.snap | 64 -------
tests/createSource.test.ts | 9 +-
tests/tsmorph/buildRouter.test.ts | 180 ------------------
11 files changed, 27 insertions(+), 599 deletions(-)
delete mode 100644 src/tsmorph/buildRouter.mts
delete mode 100644 tests/tsmorph/buildRouter.test.ts
diff --git a/README.md b/README.md
index bbd3558..652c5ea 100644
--- a/README.md
+++ b/README.md
@@ -10,73 +10,3 @@
- Generates custom functions that use React Query's `ensureQueryData` and `prefetchQuery` functions
- Generates query keys and functions for query caching
- Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts)
-- Generates loader factories and helpers for [TanStack Router](https://tanstack.com/router) integration
-
-## TanStack Router Integration
-
-The generated `router.ts` file provides loader factories and helpers for seamless integration with TanStack Router.
-
-### Using Loader Factories
-
-Use `loaderUse*` functions in your route definitions to prefetch data:
-
-```tsx
-// routes/pets.$petId.tsx
-import { createFileRoute } from "@tanstack/react-router";
-import { loaderUseFindPetById } from "../openapi/queries/router";
-import { queryClient } from "../queryClient";
-
-export const Route = createFileRoute("/pets/$petId")({
- loader: ({ params }) =>
- loaderUseFindPetById({ queryClient })({
- params: { petId: Number(params.petId) },
- }),
- component: PetDetail,
-});
-```
-
-For SSR/TanStack Start, pass `queryClient` from the router context:
-
-```tsx
-loader: ({ context, params }) =>
- loaderUseFindPetById({ queryClient: context.queryClient })({
- params: { petId: Number(params.petId) },
- }),
-```
-
-### Using withQueryPrefetch for Hover/Touch Prefetching
-
-The `withQueryPrefetch` helper enables prefetching on hover or touch:
-
-```tsx
-import { withQueryPrefetch } from "../openapi/queries/router";
-import { prefetchUseFindPetById } from "../openapi/queries/prefetch";
-import { queryClient } from "../queryClient";
-
-function PetLink({ petId }: { petId: number }) {
- return (
-
- prefetchUseFindPetById(queryClient, { path: { petId } })
- )}
- >
- View Pet
-
- );
-}
-```
-
-### Important Notes
-
-- **Router params are strings**: TanStack Router params are always strings. You must parse them to the correct type (e.g., `Number(params.petId)`) before passing to the loader.
-- **External cache configuration**: When using TanStack Query as the cache, set `defaultPreloadStaleTime: 0` in your router configuration to let React Query handle cache freshness:
-
-```tsx
-const router = createRouter({
- routeTree,
- defaultPreloadStaleTime: 0,
-});
-```
-
-- **Link preloading**: When using `` or `defaultPreload: "intent"`, TanStack Router will automatically call the route's `loader` on hover/touch. If your loader uses `ensureUse*Data`, prefetching happens automatically without needing `withQueryPrefetch`.
diff --git a/docs/src/content/docs/examples/tanstack-router.md b/docs/src/content/docs/examples/tanstack-router.md
index 17f0577..7b09dbb 100644
--- a/docs/src/content/docs/examples/tanstack-router.md
+++ b/docs/src/content/docs/examples/tanstack-router.md
@@ -5,27 +5,21 @@ description: Using TanStack Router with OpenAPI React Query Codegen for data loa
Example of using TanStack Router can be found in the [`examples/tanstack-router-app`](https://github.com/7nohe/openapi-react-query-codegen/tree/main/examples/tanstack-router-app) directory of the repository.
-## Generated Files
+## Route Data Loading
-The codegen generates a `router.ts` file that provides:
-
-- **Loader factories** (`loaderUse*`) for route data loading
-- **`withQueryPrefetch`** helper for hover/touch prefetching
-
-## Using Loader Factories
-
-Use `loaderUse*` functions in your route definitions to prefetch data before the route renders:
+Use the generated `ensureQueryData` functions in your route loaders to prefetch data before the route renders:
```tsx
// routes/pets.$petId.tsx
import { createFileRoute } from "@tanstack/react-router";
-import { loaderUseFindPetById } from "../openapi/queries/router";
+import { ensureUseFindPetByIdData } from "../openapi/queries/ensureQueryData";
+import { useFindPetById } from "../openapi/queries";
import { queryClient } from "../queryClient";
export const Route = createFileRoute("/pets/$petId")({
loader: ({ params }) =>
- loaderUseFindPetById({ queryClient })({
- params: { petId: Number(params.petId) },
+ ensureUseFindPetByIdData(queryClient, {
+ path: { petId: Number(params.petId) },
}),
component: PetDetail,
});
@@ -40,13 +34,13 @@ function PetDetail() {
### For SSR / TanStack Start
-When using SSR or TanStack Start, pass `queryClient` from the router context instead of importing it directly:
+When using SSR or TanStack Start, pass `queryClient` from the router context:
```tsx
export const Route = createFileRoute("/pets/$petId")({
loader: ({ context, params }) =>
- loaderUseFindPetById({ queryClient: context.queryClient })({
- params: { petId: Number(params.petId) },
+ ensureUseFindPetByIdData(context.queryClient, {
+ path: { petId: Number(params.petId) },
}),
component: PetDetail,
});
@@ -54,49 +48,33 @@ export const Route = createFileRoute("/pets/$petId")({
### Operations Without Path Parameters
-For operations without path parameters, the loader is simpler:
-
```tsx
-import { loaderUseFindPets } from "../openapi/queries/router";
+import { ensureUseFindPetsData } from "../openapi/queries/ensureQueryData";
export const Route = createFileRoute("/pets")({
- loader: () => loaderUseFindPets({ queryClient })(),
+ loader: () => ensureUseFindPetsData(queryClient),
component: PetList,
});
```
-### Passing Additional Options
+## Prefetching on Hover/Touch
-You can pass additional client options through the `clientOptions` parameter:
+Use `prefetchQuery` functions for custom prefetch triggers:
```tsx
-loader: ({ params }) =>
- loaderUseFindPetById({
- queryClient,
- clientOptions: {
- headers: { "X-Custom-Header": "value" },
- },
- })({
- params: { petId: Number(params.petId) },
- }),
-```
-
-## Using withQueryPrefetch
-
-The `withQueryPrefetch` helper enables prefetching on hover or touch events. This is useful for custom prefetch triggers outside of TanStack Router's built-in `` preloading:
-
-```tsx
-import { withQueryPrefetch } from "../openapi/queries/router";
import { prefetchUseFindPetById } from "../openapi/queries/prefetch";
import { queryClient } from "../queryClient";
function PetLink({ petId }: { petId: number }) {
+ const handlePrefetch = () => {
+ prefetchUseFindPetById(queryClient, { path: { petId } });
+ };
+
return (
- prefetchUseFindPetById(queryClient, { path: { petId } })
- )}
+ onMouseEnter={handlePrefetch}
+ onTouchStart={handlePrefetch}
>
View Pet
@@ -104,8 +82,6 @@ function PetLink({ petId }: { petId: number }) {
}
```
-This spreads `onMouseEnter` and `onTouchStart` handlers that trigger the prefetch.
-
## Router Configuration
### External Cache Settings
@@ -142,20 +118,18 @@ const router = createRouter({
});
```
-When using `preload="intent"`, the router automatically calls the route's `loader` on hover/touch. If your loader uses `ensureUse*Data` (which the generated loaders do), prefetching happens automatically.
+When using `preload="intent"`, the router automatically calls the route's `loader` on hover/touch.
## Important Notes
### Router Params Are Strings
-TanStack Router params are always strings. You must parse them to the correct type before passing to the loader:
+TanStack Router params are always strings. You must parse them to the correct type:
```tsx
-// Router params: { petId: string }
-// API expects: { petId: number }
loader: ({ params }) =>
- loaderUseFindPetById({ queryClient })({
- params: { petId: Number(params.petId) }, // Convert string to number
+ ensureUseFindPetByIdData(queryClient, {
+ path: { petId: Number(params.petId) }, // Convert string to number
}),
```
@@ -167,8 +141,8 @@ export const Route = createFileRoute("/pets/$petId")({
petId: Number(params.petId),
}),
loader: ({ params }) =>
- loaderUseFindPetById({ queryClient })({
- params: { petId: params.petId }, // Already a number
+ ensureUseFindPetByIdData(queryClient, {
+ path: { petId: params.petId }, // Already a number
}),
});
```
diff --git a/docs/src/content/docs/guides/introduction.mdx b/docs/src/content/docs/guides/introduction.mdx
index 860d757..636fc49 100644
--- a/docs/src/content/docs/guides/introduction.mdx
+++ b/docs/src/content/docs/guides/introduction.mdx
@@ -12,7 +12,6 @@ OpenAPI React Query Codegen is a code generator for creating React Query (also k
- Generates custom functions that use React Query's `ensureQueryData` and `prefetchQuery` functions
- Generates query keys and functions for query caching
- Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts)
-- Generates loader factories and helpers for [TanStack Router](https://tanstack.com/router) integration
## Installation
@@ -76,7 +75,6 @@ openapi/
│ ├── infiniteQueries.ts
│ ├── prefetch.ts
│ ├── queries.ts
-│ ├── router.ts
│ └── suspense.ts
└── requests
├── index.ts
@@ -181,7 +179,6 @@ export default App;
- infiniteQueries.ts Generated infinite query hooks
- suspense.ts Generated suspense hooks
- prefetch.ts Generated prefetch functions
- - router.ts Generated loader factories and helpers for TanStack Router
- requests Output code generated by `@hey-api/openapi-ts`
diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx
index 76329bc..b8593af 100644
--- a/docs/src/content/docs/index.mdx
+++ b/docs/src/content/docs/index.mdx
@@ -24,7 +24,7 @@ import { Card, CardGrid } from '@astrojs/starlight/components';
Generates custom react hooks that use React(TanStack) Query's useQuery, useSuspenseQuery, useMutation and useInfiniteQuery hooks.
- Generates custom functions that use React Query's `ensureQueryData` and `prefetchQuery` functions, plus loader factories for TanStack Router, to integrate into frameworks like Next.js, Remix, and TanStack Start.
+ Generates custom functions that use React Query's `ensureQueryData` and `prefetchQuery` functions to integrate into frameworks like Next.js, Remix, and TanStack Router.
Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) in case you still want to do type-safe API calls without React Query.
diff --git a/src/constants.mts b/src/constants.mts
index 8a14590..651e6e8 100644
--- a/src/constants.mts
+++ b/src/constants.mts
@@ -13,5 +13,4 @@ export const OpenApiRqFiles = {
index: "index",
prefetch: "prefetch",
ensureQueryData: "ensureQueryData",
- router: "router",
} as const;
diff --git a/src/tsmorph/buildRouter.mts b/src/tsmorph/buildRouter.mts
deleted file mode 100644
index 60c621b..0000000
--- a/src/tsmorph/buildRouter.mts
+++ /dev/null
@@ -1,130 +0,0 @@
-import {
- StructureKind,
- VariableDeclarationKind,
- type VariableStatementStructure,
-} from "ts-morph";
-import type { GenerationContext, OperationInfo } from "../types.mjs";
-
-/**
- * Build the withQueryPrefetch helper.
- * This helper provides event handlers for hover/touch prefetching.
- *
- * Example output:
- * export const withQueryPrefetch = (prefetch: () => Promise) => ({
- * onMouseEnter: () => void prefetch(),
- * onTouchStart: () => void prefetch(),
- * });
- */
-export function buildWithQueryPrefetch(): VariableStatementStructure {
- const initializer = `(prefetch: () => Promise) => ({
- onMouseEnter: () => void prefetch(),
- onTouchStart: () => void prefetch(),
-})`;
-
- return {
- kind: StructureKind.VariableStatement,
- isExported: true,
- declarationKind: VariableDeclarationKind.Const,
- declarations: [
- {
- name: "withQueryPrefetch",
- initializer,
- },
- ],
- };
-}
-
-/**
- * Check if an operation has a path parameter that is not `never`.
- */
-function hasPathParam(op: OperationInfo): boolean {
- const pathParam = op.parameters.find((p) => p.name === "path");
- return Boolean(pathParam && pathParam.typeName !== "never");
-}
-
-/**
- * Get required top-level keys (non-optional, non-never).
- */
-function getRequiredTopLevelKeys(op: OperationInfo): string[] {
- return op.parameters
- .filter((p) => !p.optional && p.typeName !== "never")
- .map((p) => p.name);
-}
-
-/**
- * Build loader factory for a GET operation.
- *
- * For operations without path parameter:
- * export const loaderUseFindPets = (deps: {
- * queryClient: QueryClient;
- * clientOptions?: Options;
- * }) => async () => {
- * await ensureUseFindPetsData(deps.queryClient, deps.clientOptions);
- * return null;
- * };
- *
- * For operations with path parameter:
- * export const loaderUseFindPetById = (deps: {
- * queryClient: QueryClient;
- * clientOptions?: Omit, "path">;
- * }) => async ({ params }: { params: FindPetByIdData["path"] }) => {
- * const options: Options = { ...(deps.clientOptions ?? {}), path: params };
- * await ensureUseFindPetByIdData(deps.queryClient, options);
- * return null;
- * };
- */
-export function buildLoaderFactory(
- op: OperationInfo,
- ctx: GenerationContext,
-): VariableStatementStructure {
- const loaderName = `loaderUse${op.capitalizedMethodName}`;
- const ensureFnName = `ensureUse${op.capitalizedMethodName}Data`;
-
- const dataTypeName = ctx.modelNames.includes(
- `${op.capitalizedMethodName}Data`,
- )
- ? `${op.capitalizedMethodName}Data`
- : "unknown";
-
- const hasPath = hasPathParam(op);
- const requiredTopLevelKeys = getRequiredTopLevelKeys(op);
- const requiredNonPathKeys = requiredTopLevelKeys.filter((k) => k !== "path");
-
- // Determine if clientOptions should be optional
- const clientOptionsOptional = hasPath
- ? requiredNonPathKeys.length === 0
- : requiredTopLevelKeys.length === 0;
-
- const optionalMark = clientOptionsOptional ? "?" : "";
-
- let initializer: string;
-
- if (hasPath) {
- // Has path parameter - needs params in returned function
- const clientOptionsType = `Omit, "path">`;
- initializer = `(deps: { queryClient: QueryClient; clientOptions${optionalMark}: ${clientOptionsType} }) => async ({ params }: { params: ${dataTypeName}["path"] }) => {
- const options: Options<${dataTypeName}, true> = { ...(deps.clientOptions ?? {}), path: params };
- await ${ensureFnName}(deps.queryClient, options);
- return null;
-}`;
- } else {
- // No path parameter - simpler factory
- const clientOptionsType = `Options<${dataTypeName}, true>`;
- initializer = `(deps: { queryClient: QueryClient; clientOptions${optionalMark}: ${clientOptionsType} }) => async () => {
- await ${ensureFnName}(deps.queryClient, deps.clientOptions);
- return null;
-}`;
- }
-
- return {
- kind: StructureKind.VariableStatement,
- isExported: true,
- declarationKind: VariableDeclarationKind.Const,
- declarations: [
- {
- name: loaderName,
- initializer,
- },
- ],
- };
-}
diff --git a/src/tsmorph/generateFiles.mts b/src/tsmorph/generateFiles.mts
index aeb57b1..b389aa2 100644
--- a/src/tsmorph/generateFiles.mts
+++ b/src/tsmorph/generateFiles.mts
@@ -29,7 +29,6 @@ import {
buildUseQueryHook,
buildUseSuspenseQueryHook,
} from "./buildQueryHooks.mjs";
-import { buildLoaderFactory, buildWithQueryPrefetch } from "./buildRouter.mjs";
import {
buildAxiosErrorImport,
buildClientImport,
@@ -294,88 +293,6 @@ function generateEnsureQueryDataFile(
return sourceFile.getFullText();
}
-/**
- * Build imports for router.ts file.
- */
-function buildRouterFileImports(
- operations: OperationInfo[],
- ctx: GenerationContext,
-): ImportDeclarationStructure[] {
- const getOperations = operations.filter((op) => op.httpMethod === "GET");
-
- // Get Data type names needed for GET operations
- const dataTypeNames = getOperations
- .map((op) => `${op.capitalizedMethodName}Data`)
- .filter((name) => ctx.modelNames.includes(name));
-
- // Get ensure function names
- const ensureFnNames = getOperations.map(
- (op) => `ensureUse${op.capitalizedMethodName}Data`,
- );
-
- const imports: ImportDeclarationStructure[] = [
- // Options import from client
- buildClientImport(ctx),
- // QueryClient import
- {
- kind: StructureKind.ImportDeclaration,
- moduleSpecifier: "@tanstack/react-query",
- namedImports: [{ name: "QueryClient", isTypeOnly: true }],
- },
- ];
-
- // Add Data types import if needed
- if (dataTypeNames.length > 0) {
- imports.push({
- kind: StructureKind.ImportDeclaration,
- moduleSpecifier: "../requests/types.gen",
- namedImports: dataTypeNames.map((name) => ({ name })),
- });
- }
-
- // Add ensureQueryData imports
- if (ensureFnNames.length > 0) {
- imports.push({
- kind: StructureKind.ImportDeclaration,
- moduleSpecifier: "./ensureQueryData",
- namedImports: ensureFnNames.map((name) => ({ name })),
- });
- }
-
- return imports;
-}
-
-/**
- * Generate the router.ts file content.
- */
-function generateRouterFile(
- operations: OperationInfo[],
- ctx: GenerationContext,
-): string {
- const project = createGenerationProject();
- const sourceFile = project.createSourceFile(
- `${OpenApiRqFiles.router}.ts`,
- undefined,
- { overwrite: true },
- );
-
- // Add imports
- sourceFile.addImportDeclarations(buildRouterFileImports(operations, ctx));
-
- // Add withQueryPrefetch helper
- sourceFile.addVariableStatement(buildWithQueryPrefetch());
-
- // Only GET operations for loader factories
- const getOperations = operations.filter((op) => op.httpMethod === "GET");
-
- // Add loader factories
- for (const op of getOperations) {
- sourceFile.addVariableStatement(buildLoaderFactory(op, ctx));
- }
-
- return sourceFile.getFullText();
-}
-
/**
* Add the generated header comment to file content.
*/
@@ -438,12 +355,5 @@ export function generateAllFiles(
ctx.version,
),
},
- {
- name: `${OpenApiRqFiles.router}.ts`,
- content: addHeaderComment(
- generateRouterFile(operations, ctx),
- ctx.version,
- ),
- },
];
}
diff --git a/src/tsmorph/index.mts b/src/tsmorph/index.mts
index 6422308..59e3c0a 100644
--- a/src/tsmorph/index.mts
+++ b/src/tsmorph/index.mts
@@ -3,4 +3,3 @@ export { createGenerationProject } from "./projectFactory.mjs";
export * from "./buildCommon.mjs";
export * from "./buildQueryHooks.mjs";
export * from "./buildMutationHooks.mjs";
-export * from "./buildRouter.mjs";
diff --git a/tests/__snapshots__/createSource.test.ts.snap b/tests/__snapshots__/createSource.test.ts.snap
index bded83c..d759777 100644
--- a/tests/__snapshots__/createSource.test.ts.snap
+++ b/tests/__snapshots__/createSource.test.ts.snap
@@ -112,38 +112,6 @@ export const prefetchUseFindPaginatedPets = (queryClient: QueryClient, clientOpt
"
`;
-exports[`createSource > createSource - @hey-api/client-axios 6`] = `
-"// generated with @7nohe/openapi-react-query-codegen@1.0.0
-
-import { type Options } from "@hey-api/client-axios";
-import { type QueryClient } from "@tanstack/react-query";
-import { FindPetsData, FindPetByIdData, FindPaginatedPetsData } from "../requests/types.gen";
-import { ensureUseFindPetsData, ensureUseGetNotDefinedData, ensureUseFindPetByIdData, ensureUseFindPaginatedPetsData } from "./ensureQueryData";
-
-export const withQueryPrefetch = (prefetch: () => Promise) => ({
- onMouseEnter: () => void prefetch(),
- onTouchStart: () => void prefetch(),
- });
-export const loaderUseFindPets = (deps: { queryClient: QueryClient; clientOptions?: Options }) => async () => {
- await ensureUseFindPetsData(deps.queryClient, deps.clientOptions);
- return null;
- };
-export const loaderUseGetNotDefined = (deps: { queryClient: QueryClient; clientOptions?: Options }) => async () => {
- await ensureUseGetNotDefinedData(deps.queryClient, deps.clientOptions);
- return null;
- };
-export const loaderUseFindPetById = (deps: { queryClient: QueryClient; clientOptions?: Omit, "path"> }) => async ({ params }: { params: FindPetByIdData["path"] }) => {
- const options: Options = { ...(deps.clientOptions ?? {}), path: params };
- await ensureUseFindPetByIdData(deps.queryClient, options);
- return null;
- };
-export const loaderUseFindPaginatedPets = (deps: { queryClient: QueryClient; clientOptions?: Options }) => async () => {
- await ensureUseFindPaginatedPetsData(deps.queryClient, deps.clientOptions);
- return null;
- };
-"
-`;
-
exports[`createSource > createSource - @hey-api/client-fetch 1`] = `
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
@@ -251,35 +219,3 @@ export const prefetchUseFindPetById = (queryClient: QueryClient, clientOptions:
export const prefetchUseFindPaginatedPets = (queryClient: QueryClient, clientOptions: Options = {}) => queryClient.prefetchQuery({ queryKey: Common.UseFindPaginatedPetsKeyFn(clientOptions), queryFn: () => findPaginatedPets({ ...clientOptions }).then(response => response.data) });
"
`;
-
-exports[`createSource > createSource - @hey-api/client-fetch 6`] = `
-"// generated with @7nohe/openapi-react-query-codegen@1.0.0
-
-import { type Options } from "@hey-api/client-fetch";
-import { type QueryClient } from "@tanstack/react-query";
-import { FindPetsData, FindPetByIdData, FindPaginatedPetsData } from "../requests/types.gen";
-import { ensureUseFindPetsData, ensureUseGetNotDefinedData, ensureUseFindPetByIdData, ensureUseFindPaginatedPetsData } from "./ensureQueryData";
-
-export const withQueryPrefetch = (prefetch: () => Promise) => ({
- onMouseEnter: () => void prefetch(),
- onTouchStart: () => void prefetch(),
- });
-export const loaderUseFindPets = (deps: { queryClient: QueryClient; clientOptions?: Options }) => async () => {
- await ensureUseFindPetsData(deps.queryClient, deps.clientOptions);
- return null;
- };
-export const loaderUseGetNotDefined = (deps: { queryClient: QueryClient; clientOptions?: Options }) => async () => {
- await ensureUseGetNotDefinedData(deps.queryClient, deps.clientOptions);
- return null;
- };
-export const loaderUseFindPetById = (deps: { queryClient: QueryClient; clientOptions?: Omit, "path"> }) => async ({ params }: { params: FindPetByIdData["path"] }) => {
- const options: Options = { ...(deps.clientOptions ?? {}), path: params };
- await ensureUseFindPetByIdData(deps.queryClient, options);
- return null;
- };
-export const loaderUseFindPaginatedPets = (deps: { queryClient: QueryClient; clientOptions?: Options }) => async () => {
- await ensureUseFindPaginatedPetsData(deps.queryClient, deps.clientOptions);
- return null;
- };
-"
-`;
diff --git a/tests/createSource.test.ts b/tests/createSource.test.ts
index 0ceea77..8b6eba4 100644
--- a/tests/createSource.test.ts
+++ b/tests/createSource.test.ts
@@ -18,7 +18,7 @@ describe(fileName, () => {
client: "@hey-api/client-fetch",
});
- expect(source).toHaveLength(8);
+ expect(source).toHaveLength(7);
expect(source.map((s) => s.name)).toEqual([
"index.ts",
"common.ts",
@@ -27,7 +27,6 @@ describe(fileName, () => {
"infiniteQueries.ts",
"prefetch.ts",
"ensureQueryData.ts",
- "router.ts",
]);
const indexTs = source.find((s) => s.name === "index.ts");
@@ -44,9 +43,6 @@ describe(fileName, () => {
const prefetchTs = source.find((s) => s.name === "prefetch.ts");
expect(prefetchTs?.content).toMatchSnapshot();
-
- const routerTs = source.find((s) => s.name === "router.ts");
- expect(routerTs?.content).toMatchSnapshot();
});
test("createSource - @hey-api/client-axios", async () => {
@@ -73,8 +69,5 @@ describe(fileName, () => {
const prefetchTs = source.find((s) => s.name === "prefetch.ts");
expect(prefetchTs?.content).toMatchSnapshot();
-
- const routerTs = source.find((s) => s.name === "router.ts");
- expect(routerTs?.content).toMatchSnapshot();
});
});
diff --git a/tests/tsmorph/buildRouter.test.ts b/tests/tsmorph/buildRouter.test.ts
deleted file mode 100644
index ea3a081..0000000
--- a/tests/tsmorph/buildRouter.test.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import { StructureKind, VariableDeclarationKind } from "ts-morph";
-import { describe, expect, it } from "vitest";
-import {
- buildLoaderFactory,
- buildWithQueryPrefetch,
-} from "../../src/tsmorph/buildRouter.mjs";
-import type { GenerationContext, OperationInfo } from "../../src/types.mjs";
-
-// Operation without path parameter (all optional)
-const mockOperationNoPath: OperationInfo = {
- methodName: "findPets",
- capitalizedMethodName: "FindPets",
- httpMethod: "GET",
- isDeprecated: false,
- parameters: [{ name: "limit", typeName: "number", optional: true }],
- allParamsOptional: true,
- isPaginatable: false,
-};
-
-// Operation with path parameter
-const mockOperationWithPath: OperationInfo = {
- methodName: "findPetById",
- capitalizedMethodName: "FindPetById",
- httpMethod: "GET",
- isDeprecated: false,
- parameters: [
- { name: "path", typeName: "{ petId: number }", optional: false },
- ],
- allParamsOptional: false,
- isPaginatable: false,
-};
-
-// Operation with path and other required params
-const mockOperationWithPathAndRequired: OperationInfo = {
- methodName: "getPetDetails",
- capitalizedMethodName: "GetPetDetails",
- httpMethod: "GET",
- isDeprecated: false,
- parameters: [
- { name: "path", typeName: "{ petId: number }", optional: false },
- { name: "query", typeName: "{ include: string }", optional: false },
- ],
- allParamsOptional: false,
- isPaginatable: false,
-};
-
-// Operation with path: never (no actual path params)
-const mockOperationWithPathNever: OperationInfo = {
- methodName: "listPets",
- capitalizedMethodName: "ListPets",
- httpMethod: "GET",
- isDeprecated: false,
- parameters: [
- { name: "path", typeName: "never", optional: true },
- { name: "query", typeName: "{ limit: number }", optional: true },
- ],
- allParamsOptional: true,
- isPaginatable: false,
-};
-
-const mockFetchContext: GenerationContext = {
- client: "@hey-api/client-fetch",
- modelNames: [
- "Pet",
- "FindPetsData",
- "FindPetByIdData",
- "GetPetDetailsData",
- "ListPetsData",
- ],
- serviceNames: ["findPets", "findPetById", "getPetDetails", "listPets"],
- pageParam: "page",
- nextPageParam: "nextPage",
- initialPageParam: "1",
- version: "1.0.0",
-};
-
-describe("buildRouter", () => {
- describe("buildWithQueryPrefetch", () => {
- it("should build withQueryPrefetch helper", () => {
- const result = buildWithQueryPrefetch();
-
- expect(result.kind).toBe(StructureKind.VariableStatement);
- expect(result.isExported).toBe(true);
- expect(result.declarationKind).toBe(VariableDeclarationKind.Const);
- expect(result.declarations[0].name).toBe("withQueryPrefetch");
-
- const initializer = result.declarations[0].initializer as string;
- expect(initializer).toContain("prefetch: () => Promise");
- expect(initializer).toContain("onMouseEnter: () => void prefetch()");
- expect(initializer).toContain("onTouchStart: () => void prefetch()");
- });
- });
-
- describe("buildLoaderFactory", () => {
- it("should build loader factory for operation without path param", () => {
- const result = buildLoaderFactory(mockOperationNoPath, mockFetchContext);
-
- expect(result.kind).toBe(StructureKind.VariableStatement);
- expect(result.isExported).toBe(true);
- expect(result.declarationKind).toBe(VariableDeclarationKind.Const);
- expect(result.declarations[0].name).toBe("loaderUseFindPets");
-
- const initializer = result.declarations[0].initializer as string;
- expect(initializer).toContain("queryClient: QueryClient");
- expect(initializer).toContain("clientOptions?:");
- expect(initializer).toContain("Options");
- expect(initializer).toContain("async () =>");
- expect(initializer).toContain(
- "ensureUseFindPetsData(deps.queryClient, deps.clientOptions)",
- );
- expect(initializer).toContain("return null");
- });
-
- it("should build loader factory for operation with path param", () => {
- const result = buildLoaderFactory(
- mockOperationWithPath,
- mockFetchContext,
- );
-
- expect(result.declarations[0].name).toBe("loaderUseFindPetById");
-
- const initializer = result.declarations[0].initializer as string;
- expect(initializer).toContain("queryClient: QueryClient");
- expect(initializer).toContain("clientOptions?:");
- expect(initializer).toContain(
- 'Omit, "path">',
- );
- expect(initializer).toContain("async ({ params }:");
- expect(initializer).toContain('params: FindPetByIdData["path"]');
- expect(initializer).toContain("...(deps.clientOptions ?? {})");
- expect(initializer).toContain("path: params");
- expect(initializer).toContain(
- "ensureUseFindPetByIdData(deps.queryClient, options)",
- );
- });
-
- it("should make clientOptions required when non-path required params exist", () => {
- const result = buildLoaderFactory(
- mockOperationWithPathAndRequired,
- mockFetchContext,
- );
-
- const initializer = result.declarations[0].initializer as string;
- // clientOptions should NOT have ? (required)
- expect(initializer).toContain("clientOptions:");
- expect(initializer).not.toMatch(/clientOptions\?:/);
- });
-
- it("should treat path: never as no path param", () => {
- const result = buildLoaderFactory(
- mockOperationWithPathNever,
- mockFetchContext,
- );
-
- expect(result.declarations[0].name).toBe("loaderUseListPets");
-
- const initializer = result.declarations[0].initializer as string;
- // Should NOT have params in the inner function
- expect(initializer).toContain("async () =>");
- expect(initializer).not.toContain("{ params }");
- // clientOptions should be optional since all params are optional
- expect(initializer).toContain("clientOptions?:");
- });
-
- it("should use unknown for missing Data type", () => {
- const contextWithoutDataType: GenerationContext = {
- ...mockFetchContext,
- modelNames: [],
- };
-
- const result = buildLoaderFactory(
- mockOperationNoPath,
- contextWithoutDataType,
- );
-
- const initializer = result.declarations[0].initializer as string;
- expect(initializer).toContain("Options");
- });
- });
-});