diff --git a/src/tsmorph/generateFiles.mts b/src/tsmorph/generateFiles.mts index a0fbfab..fbaf85c 100644 --- a/src/tsmorph/generateFiles.mts +++ b/src/tsmorph/generateFiles.mts @@ -1,4 +1,4 @@ -import type { ImportDeclarationStructure } from "ts-morph"; +import type { SourceFile, VariableStatementStructure } from "ts-morph"; import { OpenApiRqFiles } from "../constants.mjs"; import type { GeneratedFile, @@ -32,47 +32,29 @@ import { buildQueryOptionsFn, } from "./buildQueryOptions.mjs"; import { - buildAxiosErrorImport, - buildClientImport, - buildCommonImport, - buildModelImport, - buildQueryImport, - buildQueryOptionsImport, - buildServiceImport, + buildCommonFileImports, + buildHookFileImports, + buildQueryOptionsFileImports, createGenerationProject, } from "./projectFactory.mjs"; /** - * Build imports for common.ts file. + * Add one variable statement per operation, skipping the ones the builder + * declines. A builder returns null when the operation is out of its scope — + * every infinite-query builder returns null for non-paginatable operations, + * which is how the paginatable subset gets selected. */ -function buildCommonFileImports( - ctx: GenerationContext, -): ImportDeclarationStructure[] { - const imports: ImportDeclarationStructure[] = [ - buildClientImport(ctx), - buildQueryImport(), - buildServiceImport(ctx), - ]; - - const modelImport = buildModelImport(ctx); - if (modelImport) { - imports.push(modelImport); - } - - if (ctx.client === "@hey-api/client-axios") { - imports.push(buildAxiosErrorImport()); +function addStatements( + sourceFile: SourceFile, + operations: OperationInfo[], + build: (op: OperationInfo) => VariableStatementStructure | null, +): void { + for (const op of operations) { + const statement = build(op); + if (statement) { + sourceFile.addVariableStatement(statement); + } } - - return imports; -} - -/** - * Build imports for hook files (queries, suspense, infinite, prefetch, ensure). - */ -function buildHookFileImports( - ctx: GenerationContext, -): ImportDeclarationStructure[] { - return [buildCommonImport(), ...buildCommonFileImports(ctx)]; } /** @@ -182,17 +164,7 @@ function generateQueryOptionsFile( ); // Add imports - const imports: ImportDeclarationStructure[] = [ - buildCommonImport(), - buildQueryOptionsImport(), - buildClientImport(ctx), - buildServiceImport(ctx), - ]; - const modelImport = buildModelImport(ctx); - if (modelImport) { - imports.push(modelImport); - } - sourceFile.addImportDeclarations(imports); + sourceFile.addImportDeclarations(buildQueryOptionsFileImports(ctx)); // Only GET operations have query options const getOperations = operations.filter((op) => op.httpMethod === "GET"); @@ -201,12 +173,10 @@ function generateQueryOptionsFile( sourceFile.addVariableStatement(buildQueryOptionsFn(op, ctx)); } - for (const op of getOperations) { - const infiniteOptions = buildInfiniteQueryOptionsFn(op, ctx); - if (infiniteOptions) { - sourceFile.addVariableStatement(infiniteOptions); - } - } + // Add infiniteQueryOptions factories + addStatements(sourceFile, getOperations, (op) => + buildInfiniteQueryOptionsFn(op, ctx), + ); return sourceFile.getFullText(); } @@ -236,13 +206,10 @@ function generateSuspenseFile( sourceFile.addVariableStatement(buildUseSuspenseQueryHook(op, ctx)); } - // Add useSuspenseInfiniteQuery hooks for paginatable operations - for (const op of getOperations.filter((o) => o.isPaginatable)) { - const hook = buildUseSuspenseInfiniteQueryHook(op, ctx); - if (hook) { - sourceFile.addVariableStatement(hook); - } - } + // Add useSuspenseInfiniteQuery hooks + addStatements(sourceFile, getOperations, (op) => + buildUseSuspenseInfiniteQueryHook(op, ctx), + ); return sourceFile.getFullText(); } @@ -264,18 +231,13 @@ function generateInfiniteQueriesFile( // Add imports sourceFile.addImportDeclarations(buildHookFileImports(ctx)); - // Only paginatable GET operations - const paginatableOperations = operations.filter( - (op) => op.httpMethod === "GET" && op.isPaginatable, - ); + // Only GET operations can be paginatable + const getOperations = operations.filter((op) => op.httpMethod === "GET"); // Add useInfiniteQuery hooks - for (const op of paginatableOperations) { - const hook = buildUseInfiniteQueryHook(op, ctx); - if (hook) { - sourceFile.addVariableStatement(hook); - } - } + addStatements(sourceFile, getOperations, (op) => + buildUseInfiniteQueryHook(op, ctx), + ); return sourceFile.getFullText(); } @@ -305,13 +267,10 @@ function generatePrefetchFile( sourceFile.addVariableStatement(buildPrefetchFn(op, ctx)); } - // Add prefetchInfiniteQuery functions for paginatable operations - for (const op of getOperations.filter((o) => o.isPaginatable)) { - const fn = buildPrefetchInfiniteQueryFn(op, ctx); - if (fn) { - sourceFile.addVariableStatement(fn); - } - } + // Add prefetchInfiniteQuery functions + addStatements(sourceFile, getOperations, (op) => + buildPrefetchInfiniteQueryFn(op, ctx), + ); return sourceFile.getFullText(); } diff --git a/src/tsmorph/projectFactory.mts b/src/tsmorph/projectFactory.mts index e3b74ba..eb996d9 100644 --- a/src/tsmorph/projectFactory.mts +++ b/src/tsmorph/projectFactory.mts @@ -174,3 +174,26 @@ export function buildHookFileImports( ): ImportDeclarationStructure[] { return [buildCommonImport(), ...buildCommonFileImports(ctx)]; } + +/** + * Build all imports needed for the queryOptions file. + * Narrower than the hook file imports: queryOptions only needs the + * queryOptions/infiniteQueryOptions helpers, not the TanStack hooks. + */ +export function buildQueryOptionsFileImports( + ctx: GenerationContext, +): ImportDeclarationStructure[] { + const imports: ImportDeclarationStructure[] = [ + buildCommonImport(), + buildQueryOptionsImport(), + buildClientImport(ctx), + buildServiceImport(ctx), + ]; + + const modelImport = buildModelImport(ctx); + if (modelImport) { + imports.push(modelImport); + } + + return imports; +} diff --git a/tests/tsmorph/projectFactory.test.ts b/tests/tsmorph/projectFactory.test.ts index d1a2cbe..150d713 100644 --- a/tests/tsmorph/projectFactory.test.ts +++ b/tests/tsmorph/projectFactory.test.ts @@ -8,6 +8,7 @@ import { buildHookFileImports, buildModelImport, buildQueryImport, + buildQueryOptionsFileImports, buildServiceImport, createGenerationProject, } from "../../src/tsmorph/projectFactory.mjs"; @@ -232,4 +233,36 @@ describe("projectFactory", () => { ).toBe(true); }); }); + + describe("buildQueryOptionsFileImports", () => { + it("should include Common, the queryOptions helpers, sdk and models", () => { + const result = buildQueryOptionsFileImports(mockFetchContext); + + // Import order is emitted verbatim, so it is part of the contract + expect(result.map((i) => i.moduleSpecifier)).toEqual([ + "./common", + "@tanstack/react-query", + "../requests/sdk.gen", + "../requests/sdk.gen", + "../requests/types.gen", + ]); + expect(result[0].namespaceImport).toBe("Common"); + // queryOptions never calls the TanStack hooks, so no hook import + expect( + result.some((i) => + i.namedImports?.some( + (n) => typeof n === "object" && n.name === "useQuery", + ), + ), + ).toBe(false); + }); + + it("should not include model import when no models", () => { + const result = buildQueryOptionsFileImports(mockEmptyModelsContext); + + expect( + result.some((i) => i.moduleSpecifier === "../requests/types.gen"), + ).toBe(false); + }); + }); });