Skip to content

Commit 51fc0cf

Browse files
committed
feat(prefetch): add prefetch query hook generation
closes: #91
1 parent 125c5b1 commit 51fc0cf

File tree

11 files changed

+327
-14
lines changed

11 files changed

+327
-14
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,16 @@ $ openapi-rq -i ./petstore.yaml
7171
```
7272
- openapi
7373
- queries
74-
- index.ts <- main file that exports common types, variables, and hooks
74+
- index.ts <- main file that exports common types, variables, and queries. Does not export suspense or prefetch hooks
7575
- common.ts <- common types
7676
- queries.ts <- generated query hooks
7777
- suspenses.ts <- generated suspense hooks
78+
- prefetch.ts <- generated prefetch hooks learn more about prefetching in in link below
7879
- requests <- output code generated by @hey-api/openapi-ts
7980
```
8081

82+
- [Prefetching docs](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#prefetching-and-dehydrating-data)
83+
8184
### In your app
8285

8386
```tsx

examples/react-app/src/main.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@ import App from "./App";
44
import "./index.css";
55
import { QueryClientProvider } from "@tanstack/react-query";
66
import { queryClient } from "./queryClient";
7+
import { useDefaultServiceFindPetsPrefetch } from "../openapi/queries/prefetch";
8+
9+
const PrefetchData = () => {
10+
useDefaultServiceFindPetsPrefetch(queryClient);
11+
return null;
12+
};
713

814
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
915
<React.StrictMode>
1016
<QueryClientProvider client={queryClient}>
17+
<PrefetchData />
1118
<App />
1219
</QueryClientProvider>
1320
</React.StrictMode>

src/constants.mts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@ export const requestsOutputPath = "requests";
44

55
export const serviceFileName = "services.gen";
66
export const modalsFileName = "types.gen";
7+
8+
export const OpenApiRqFiles = {
9+
queries: "queries",
10+
common: "common",
11+
suspense: "suspense",
12+
index: "index",
13+
prefetch: "prefetch",
14+
} as const;

src/createExports.mts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createUseQuery } from "./createUseQuery.mjs";
22
import { createUseMutation } from "./createUseMutation.mjs";
33
import { Service } from "./service.mjs";
4+
import { createPrefetch } from "./createPrefetch.mjs";
45

56
export const createExports = (service: Service) => {
67
const { klasses } = service;
@@ -23,6 +24,7 @@ export const createExports = (service: Service) => {
2324
);
2425

2526
const allGetQueries = allGet.map((m) => createUseQuery(m));
27+
const allPrefetchQueries = allGet.map((m) => createPrefetch(m));
2628

2729
const allPostMutations = allPost.map((m) => createUseMutation(m));
2830
const allPutMutations = allPut.map((m) => createUseMutation(m));
@@ -59,6 +61,12 @@ export const createExports = (service: Service) => {
5961

6062
const suspenseExports = [...suspenseQueries];
6163

64+
const allPrefetches = allPrefetchQueries
65+
.map(({ prefetchHook }) => [prefetchHook])
66+
.flat();
67+
68+
const allPrefetchExports = [...allPrefetches];
69+
6270
return {
6371
/**
6472
* Common types and variables between queries (regular and suspense) and mutations
@@ -72,5 +80,9 @@ export const createExports = (service: Service) => {
7280
* Suspense exports are the hooks that are used in the suspense components
7381
*/
7482
suspenseExports,
83+
/**
84+
* Prefetch exports are the hooks that are used in the prefetch components
85+
*/
86+
allPrefetchExports,
7587
};
7688
};

src/createPrefetch.mts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import ts from "typescript";
2+
import { MethodDeclaration } from "ts-morph";
3+
import {
4+
BuildCommonTypeName,
5+
extractPropertiesFromObjectParam,
6+
getNameFromMethod,
7+
} from "./common.mjs";
8+
import { type MethodDescription } from "./common.mjs";
9+
import {
10+
createQueryKeyFromMethod,
11+
getRequestParamFromMethod,
12+
hookNameFromMethod,
13+
} from "./createUseQuery.mjs";
14+
import { addJSDocToNode } from "./util.mjs";
15+
16+
/**
17+
* Creates a custom hook for a query
18+
* @param queryString The type of query to use from react-query
19+
* @param suffix The suffix to append to the hook name
20+
*/
21+
function createPrefetchHook({
22+
requestParams,
23+
method,
24+
className,
25+
}: {
26+
requestParams: ts.ParameterDeclaration[];
27+
method: MethodDeclaration;
28+
className: string;
29+
}) {
30+
const methodName = getNameFromMethod(method);
31+
const customHookName = hookNameFromMethod({ method, className });
32+
const queryKey = createQueryKeyFromMethod({ method, className });
33+
34+
// const
35+
const hookExport = ts.factory.createVariableStatement(
36+
// export
37+
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
38+
ts.factory.createVariableDeclarationList(
39+
[
40+
ts.factory.createVariableDeclaration(
41+
ts.factory.createIdentifier(`${customHookName}Prefetch`),
42+
undefined,
43+
undefined,
44+
ts.factory.createArrowFunction(
45+
undefined,
46+
undefined,
47+
[
48+
ts.factory.createParameterDeclaration(
49+
undefined,
50+
undefined,
51+
"queryClient",
52+
undefined,
53+
ts.factory.createTypeReferenceNode(
54+
ts.factory.createIdentifier("QueryClient")
55+
)
56+
),
57+
...requestParams,
58+
],
59+
undefined,
60+
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
61+
ts.factory.createCallExpression(
62+
ts.factory.createIdentifier("queryClient.prefetchQuery"),
63+
undefined,
64+
[
65+
ts.factory.createObjectLiteralExpression([
66+
ts.factory.createPropertyAssignment(
67+
ts.factory.createIdentifier("queryKey"),
68+
ts.factory.createArrayLiteralExpression(
69+
[
70+
BuildCommonTypeName(queryKey),
71+
method.getParameters().length
72+
? ts.factory.createArrayLiteralExpression([
73+
ts.factory.createObjectLiteralExpression(
74+
method
75+
.getParameters()
76+
.map((param) =>
77+
extractPropertiesFromObjectParam(param).map(
78+
(p) =>
79+
ts.factory.createShorthandPropertyAssignment(
80+
ts.factory.createIdentifier(p.name)
81+
)
82+
)
83+
)
84+
.flat()
85+
),
86+
])
87+
: ts.factory.createArrayLiteralExpression([]),
88+
],
89+
false
90+
)
91+
),
92+
ts.factory.createPropertyAssignment(
93+
ts.factory.createIdentifier("queryFn"),
94+
ts.factory.createArrowFunction(
95+
undefined,
96+
undefined,
97+
[],
98+
undefined,
99+
ts.factory.createToken(
100+
ts.SyntaxKind.EqualsGreaterThanToken
101+
),
102+
ts.factory.createCallExpression(
103+
ts.factory.createPropertyAccessExpression(
104+
ts.factory.createIdentifier(className),
105+
ts.factory.createIdentifier(methodName)
106+
),
107+
undefined,
108+
method.getParameters().length
109+
? [
110+
ts.factory.createObjectLiteralExpression(
111+
method
112+
.getParameters()
113+
.map((param) =>
114+
extractPropertiesFromObjectParam(param).map(
115+
(p) =>
116+
ts.factory.createShorthandPropertyAssignment(
117+
ts.factory.createIdentifier(p.name)
118+
)
119+
)
120+
)
121+
.flat()
122+
),
123+
]
124+
: undefined
125+
)
126+
)
127+
),
128+
]),
129+
]
130+
)
131+
)
132+
),
133+
],
134+
ts.NodeFlags.Const
135+
)
136+
);
137+
return hookExport;
138+
}
139+
140+
export const createPrefetch = ({
141+
className,
142+
method,
143+
jsDoc,
144+
}: MethodDescription) => {
145+
const requestParam = getRequestParamFromMethod(method);
146+
147+
const requestParams = requestParam ? [requestParam] : [];
148+
149+
const prefetchHook = createPrefetchHook({
150+
requestParams,
151+
method,
152+
className,
153+
});
154+
155+
const hookWithJsDoc = addJSDocToNode(prefetchHook, jsDoc);
156+
157+
return {
158+
prefetchHook: hookWithJsDoc,
159+
};
160+
};

src/createSource.mts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import ts from "typescript";
2+
import { Project } from "ts-morph";
3+
import { join } from "path";
4+
import { OpenApiRqFiles } from "./constants.mjs";
25
import { createImports } from "./createImports.mjs";
36
import { createExports } from "./createExports.mjs";
47
import { getServices } from "./service.mjs";
5-
import { Project } from "ts-morph";
6-
import { join } from "path";
78

89
const createSourceFile = async (outputPath: string, serviceEndName: string) => {
910
const project = new Project({
@@ -77,11 +78,18 @@ const createSourceFile = async (outputPath: string, serviceEndName: string) => {
7778
ts.NodeFlags.None
7879
);
7980

81+
const prefetchSource = ts.factory.createSourceFile(
82+
[commonImport, ...imports, ...exports.allPrefetchExports],
83+
ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
84+
ts.NodeFlags.None
85+
);
86+
8087
return {
8188
commonSource,
8289
mainSource,
8390
suspenseSource,
8491
indexSource,
92+
prefetchSource,
8593
};
8694
};
8795

@@ -95,29 +103,37 @@ export const createSource = async ({
95103
serviceEndName: string;
96104
}) => {
97105
const queriesFile = ts.createSourceFile(
98-
"queries.ts",
106+
`${OpenApiRqFiles.queries}.ts`,
99107
"",
100108
ts.ScriptTarget.Latest,
101109
false,
102110
ts.ScriptKind.TS
103111
);
104112
const commonFile = ts.createSourceFile(
105-
"common.ts",
113+
`${OpenApiRqFiles.common}.ts`,
106114
"",
107115
ts.ScriptTarget.Latest,
108116
false,
109117
ts.ScriptKind.TS
110118
);
111119
const suspenseFile = ts.createSourceFile(
112-
"suspense.ts",
120+
`${OpenApiRqFiles.suspense}.ts`,
113121
"",
114122
ts.ScriptTarget.Latest,
115123
false,
116124
ts.ScriptKind.TS
117125
);
118126

119127
const indexFile = ts.createSourceFile(
120-
"index.ts",
128+
`${OpenApiRqFiles.index}.ts`,
129+
"",
130+
ts.ScriptTarget.Latest,
131+
false,
132+
ts.ScriptKind.TS
133+
);
134+
135+
const prefetchFile = ts.createSourceFile(
136+
`${OpenApiRqFiles.prefetch}.ts`,
121137
"",
122138
ts.ScriptTarget.Latest,
123139
false,
@@ -129,8 +145,13 @@ export const createSource = async ({
129145
removeComments: false,
130146
});
131147

132-
const { commonSource, mainSource, suspenseSource, indexSource } =
133-
await createSourceFile(outputPath, serviceEndName);
148+
const {
149+
commonSource,
150+
mainSource,
151+
suspenseSource,
152+
indexSource,
153+
prefetchSource,
154+
} = await createSourceFile(outputPath, serviceEndName);
134155

135156
const comment = `// generated with @7nohe/openapi-react-query-codegen@${version} \n\n`;
136157

@@ -150,6 +171,10 @@ export const createSource = async ({
150171
comment +
151172
printer.printNode(ts.EmitHint.Unspecified, indexSource, indexFile);
152173

174+
const prefetchResult =
175+
comment +
176+
printer.printNode(ts.EmitHint.Unspecified, prefetchSource, prefetchFile);
177+
153178
return [
154179
{
155180
name: "index.ts",
@@ -167,5 +192,9 @@ export const createSource = async ({
167192
name: "suspense.ts",
168193
content: suspenseResult,
169194
},
195+
{
196+
name: "prefetch.ts",
197+
content: prefetchResult,
198+
},
170199
];
171200
};

src/createUseQuery.mts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export function createQueryKeyExport({
200200
);
201201
}
202202

203-
function hookNameFromMethod({
203+
export function hookNameFromMethod({
204204
method,
205205
className,
206206
}: {
@@ -211,7 +211,7 @@ function hookNameFromMethod({
211211
return `use${className}${capitalizeFirstLetter(methodName)}`;
212212
}
213213

214-
function createQueryKeyFromMethod({
214+
export function createQueryKeyFromMethod({
215215
method,
216216
className,
217217
}: {
@@ -228,7 +228,7 @@ function createQueryKeyFromMethod({
228228
* @param queryString The type of query to use from react-query
229229
* @param suffix The suffix to append to the hook name
230230
*/
231-
function createQueryHook({
231+
export function createQueryHook({
232232
queryString,
233233
suffix,
234234
responseDataType,

0 commit comments

Comments
 (0)