Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 36 additions & 77 deletions src/tsmorph/generateFiles.mts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)];
}

/**
Expand Down Expand Up @@ -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");
Expand All @@ -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();
}
Expand Down Expand Up @@ -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();
}
Expand All @@ -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();
}
Expand Down Expand Up @@ -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();
}
Expand Down
23 changes: 23 additions & 0 deletions src/tsmorph/projectFactory.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
33 changes: 33 additions & 0 deletions tests/tsmorph/projectFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
buildHookFileImports,
buildModelImport,
buildQueryImport,
buildQueryOptionsFileImports,
buildServiceImport,
createGenerationProject,
} from "../../src/tsmorph/projectFactory.mjs";
Expand Down Expand Up @@ -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);
});
});
});
Loading