cushin-monorepo 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +14 -0
- package/.claude/settings.local.json +44 -0
- package/CHANGELOG.md +93 -0
- package/LICENSE +0 -0
- package/README.md +482 -0
- package/biome.json +34 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1552 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +84 -0
- package/dist/config/index.js +69 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/schema.d.ts +43 -0
- package/dist/config/schema.js +14 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +1666 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/client.d.ts +40 -0
- package/dist/runtime/client.js +260 -0
- package/dist/runtime/client.js.map +1 -0
- package/package.json +41 -0
- package/packages/api-codegen/CHANGELOG.md +86 -0
- package/packages/api-codegen/biome.json +34 -0
- package/packages/api-codegen/dist/cli.js +1038 -0
- package/packages/api-codegen/dist/cli.js.map +1 -0
- package/packages/api-codegen/dist/index.d.ts +103 -0
- package/packages/api-codegen/dist/index.js +1026 -0
- package/packages/api-codegen/dist/index.js.map +1 -0
- package/packages/api-codegen/node_modules/.bin/acorn +21 -0
- package/packages/api-codegen/node_modules/.bin/conventional-changelog +21 -0
- package/packages/api-codegen/node_modules/.bin/conventional-commits-parser +21 -0
- package/packages/api-codegen/node_modules/.bin/esbuild +21 -0
- package/packages/api-codegen/node_modules/.bin/eslint +21 -0
- package/packages/api-codegen/node_modules/.bin/jiti +21 -0
- package/packages/api-codegen/node_modules/.bin/next +21 -0
- package/packages/api-codegen/node_modules/.bin/tsc +21 -0
- package/packages/api-codegen/node_modules/.bin/tsserver +21 -0
- package/packages/api-codegen/node_modules/.bin/tsup +21 -0
- package/packages/api-codegen/node_modules/.bin/tsup-node +21 -0
- package/packages/api-codegen/node_modules/.bin/vitest +21 -0
- package/packages/api-codegen/package.json +88 -0
- package/packages/api-runtime/CHANGELOG.md +46 -0
- package/packages/api-runtime/README.md +95 -0
- package/packages/api-runtime/dist/chunk-3FFXWCVP.js +17 -0
- package/packages/api-runtime/dist/chunk-3FFXWCVP.js.map +1 -0
- package/packages/api-runtime/dist/chunk-EZ5P7OPH.js +267 -0
- package/packages/api-runtime/dist/chunk-EZ5P7OPH.js.map +1 -0
- package/packages/api-runtime/dist/client.d.ts +40 -0
- package/packages/api-runtime/dist/client.js +13 -0
- package/packages/api-runtime/dist/client.js.map +1 -0
- package/packages/api-runtime/dist/index.d.ts +3 -0
- package/packages/api-runtime/dist/index.js +21 -0
- package/packages/api-runtime/dist/index.js.map +1 -0
- package/packages/api-runtime/dist/schema.d.ts +45 -0
- package/packages/api-runtime/dist/schema.js +11 -0
- package/packages/api-runtime/dist/schema.js.map +1 -0
- package/packages/api-runtime/node_modules/.bin/esbuild +21 -0
- package/packages/api-runtime/node_modules/.bin/jiti +21 -0
- package/packages/api-runtime/node_modules/.bin/tsc +21 -0
- package/packages/api-runtime/node_modules/.bin/tsserver +21 -0
- package/packages/api-runtime/node_modules/.bin/tsup +21 -0
- package/packages/api-runtime/node_modules/.bin/tsup-node +21 -0
- package/packages/api-runtime/package.json +54 -0
- package/packages/cli/CHANGELOG.md +34 -0
- package/packages/cli/biome.json +34 -0
- package/packages/cli/dist/index.d.ts +27 -0
- package/packages/cli/dist/index.js +183 -0
- package/packages/cli/dist/index.js.map +1 -0
- package/packages/cli/node_modules/.bin/esbuild +21 -0
- package/packages/cli/node_modules/.bin/jiti +21 -0
- package/packages/cli/node_modules/.bin/tsc +21 -0
- package/packages/cli/node_modules/.bin/tsserver +21 -0
- package/packages/cli/node_modules/.bin/tsup +21 -0
- package/packages/cli/node_modules/.bin/tsup-node +21 -0
- package/packages/cli/package.json +47 -0
- package/pnpm-workspace.yaml +2 -0
- package/test-config.js +9 -0
- package/test-content-type-handling.mjs +100 -0
- package/test-endpoints-config.mjs +144 -0
- package/test-formdata-content-type-protection.mjs +127 -0
- package/test-formdata-runtime.mjs +127 -0
- package/test-full-integration.mjs +90 -0
- package/test-headers-formdata.mjs +97 -0
- package/test-headers-runtime.mjs +106 -0
- package/test-headers.mjs +79 -0
- package/test-internal-calls.mjs +57 -0
- package/test-ky-formdata.mjs +81 -0
- package/test-output/actions.ts +134 -0
- package/test-output/client.ts +81 -0
- package/test-output/hooks.ts +182 -0
- package/test-output/index.ts +9 -0
- package/test-output/prefetchs.ts +25 -0
- package/test-output/queries.ts +78 -0
- package/test-output/query-keys.ts +16 -0
- package/test-output/query-options.ts +38 -0
- package/test-output/server-client.ts +32 -0
- package/test-output/types.ts +61 -0
- package/test-real-endpoints.mjs +132 -0
- package/test-runtime-params.mjs +160 -0
- package/test-simple-config.mjs +71 -0
- package/tsconfig.base.json +29 -0
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
export * from "@cushin/api-runtime";
|
|
3
|
+
|
|
4
|
+
// src/config/index.ts
|
|
5
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
6
|
+
import path from "path";
|
|
7
|
+
var explorer = cosmiconfig("api-codegen", {
|
|
8
|
+
searchPlaces: [
|
|
9
|
+
"api-codegen.config.js",
|
|
10
|
+
"api-codegen.config.mjs",
|
|
11
|
+
"api-codegen.config.ts",
|
|
12
|
+
"api-codegen.config.json",
|
|
13
|
+
".api-codegenrc",
|
|
14
|
+
".api-codegenrc.json",
|
|
15
|
+
".api-codegenrc.js"
|
|
16
|
+
]
|
|
17
|
+
});
|
|
18
|
+
async function loadConfig(configPath) {
|
|
19
|
+
try {
|
|
20
|
+
const result = configPath ? await explorer.load(configPath) : await explorer.search();
|
|
21
|
+
if (!result || !result.config) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const userConfig = result.config;
|
|
25
|
+
const rootDir = path.dirname(result.filepath);
|
|
26
|
+
const endpointsPath = path.resolve(rootDir, userConfig.endpoints);
|
|
27
|
+
const outputDir = path.resolve(rootDir, userConfig.output);
|
|
28
|
+
const generateHooks = userConfig.generateHooks ?? true;
|
|
29
|
+
const generateServerActions = userConfig.generateServerActions ?? userConfig.provider === "nextjs";
|
|
30
|
+
const generateServerQueries = userConfig.generateServerQueries ?? userConfig.provider === "nextjs";
|
|
31
|
+
const generateClient = userConfig.generateClient ?? true;
|
|
32
|
+
const generatePrefetch = userConfig.generatePrefetch ?? true;
|
|
33
|
+
return {
|
|
34
|
+
...userConfig,
|
|
35
|
+
rootDir,
|
|
36
|
+
endpointsPath,
|
|
37
|
+
outputDir,
|
|
38
|
+
generateHooks,
|
|
39
|
+
generateServerActions,
|
|
40
|
+
generateServerQueries,
|
|
41
|
+
generateClient,
|
|
42
|
+
generatePrefetch
|
|
43
|
+
};
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Failed to load config: ${error instanceof Error ? error.message : String(error)}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function validateConfig(config) {
|
|
51
|
+
if (!config.endpoints) {
|
|
52
|
+
throw new Error('Config error: "endpoints" path is required');
|
|
53
|
+
}
|
|
54
|
+
if (!config.provider) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'Config error: "provider" must be specified (vite or nextjs)'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (!["vite", "nextjs"].includes(config.provider)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
'Config error: "provider" must be either "vite" or "nextjs"'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (!config.output) {
|
|
65
|
+
throw new Error('Config error: "output" directory is required');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/core/codegen.ts
|
|
70
|
+
import { createJiti } from "jiti";
|
|
71
|
+
|
|
72
|
+
// src/generators/hooks.ts
|
|
73
|
+
import fs from "fs/promises";
|
|
74
|
+
import path2 from "path";
|
|
75
|
+
|
|
76
|
+
// src/generators/base.ts
|
|
77
|
+
var BaseGenerator = class {
|
|
78
|
+
constructor(context) {
|
|
79
|
+
this.context = context;
|
|
80
|
+
}
|
|
81
|
+
isQueryEndpoint(endpoint) {
|
|
82
|
+
return endpoint.method === "GET";
|
|
83
|
+
}
|
|
84
|
+
isMutationEndpoint(endpoint) {
|
|
85
|
+
return !this.isQueryEndpoint(endpoint);
|
|
86
|
+
}
|
|
87
|
+
capitalize(str) {
|
|
88
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
89
|
+
}
|
|
90
|
+
getQueryTags(endpoint) {
|
|
91
|
+
return endpoint.tags || [];
|
|
92
|
+
}
|
|
93
|
+
getInvalidationTags(endpoint) {
|
|
94
|
+
const tags = endpoint.tags || [];
|
|
95
|
+
return tags.filter((tag) => tag !== "query" && tag !== "mutation");
|
|
96
|
+
}
|
|
97
|
+
hasParams(endpoint) {
|
|
98
|
+
return !!endpoint.params;
|
|
99
|
+
}
|
|
100
|
+
hasQuery(endpoint) {
|
|
101
|
+
return !!endpoint.query;
|
|
102
|
+
}
|
|
103
|
+
hasBody(endpoint) {
|
|
104
|
+
return !!endpoint.body;
|
|
105
|
+
}
|
|
106
|
+
getEndpointSignature(name, endpoint) {
|
|
107
|
+
const hasParams = this.hasParams(endpoint);
|
|
108
|
+
const hasQuery = this.hasQuery(endpoint);
|
|
109
|
+
const hasBody = this.hasBody(endpoint);
|
|
110
|
+
return {
|
|
111
|
+
hasParams,
|
|
112
|
+
hasQuery,
|
|
113
|
+
hasBody,
|
|
114
|
+
paramType: hasParams ? this.inferNonNull(`typeof apiConfig.endpoints.${name}.params`) : "never",
|
|
115
|
+
queryType: hasQuery ? this.inferNonNull(`typeof apiConfig.endpoints.${name}.query`) : "never",
|
|
116
|
+
bodyType: hasBody ? this.inferNonNull(`typeof apiConfig.endpoints.${name}.body`) : "never",
|
|
117
|
+
responseType: this.inferNonNull(`typeof apiConfig.endpoints.${name}.response`)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
generateMutationCall(name, hasParams, hasBody) {
|
|
121
|
+
if (hasParams && hasBody) {
|
|
122
|
+
return `return apiClient.${name}(input.params, input.body);`;
|
|
123
|
+
} else if (hasParams) {
|
|
124
|
+
return `return apiClient.${name}(input);`;
|
|
125
|
+
} else if (hasBody) {
|
|
126
|
+
return `return apiClient.${name}(input);`;
|
|
127
|
+
} else {
|
|
128
|
+
return `return apiClient.${name}();`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
inferNonNull(expr) {
|
|
132
|
+
return `z.infer<NonNullable<${expr}>>`;
|
|
133
|
+
}
|
|
134
|
+
toCamelCase(str) {
|
|
135
|
+
return str.toLowerCase().replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toLowerCase());
|
|
136
|
+
}
|
|
137
|
+
getResourceFromEndpoint(_name, endpoint) {
|
|
138
|
+
const tag = endpoint.tags?.find((t) => t !== "query" && t !== "mutation");
|
|
139
|
+
if (tag) return this.toCamelCase(tag);
|
|
140
|
+
const match = endpoint.path.match(/^\/([^/]+)/);
|
|
141
|
+
return match ? this.toCamelCase(match[1]) : "general";
|
|
142
|
+
}
|
|
143
|
+
groupEndpointsByResource() {
|
|
144
|
+
const groups = {};
|
|
145
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
146
|
+
([name, endpoint]) => {
|
|
147
|
+
const res = this.getResourceFromEndpoint(name, endpoint);
|
|
148
|
+
if (!groups[res]) groups[res] = [];
|
|
149
|
+
groups[res].push({ name, endpoint });
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
return groups;
|
|
153
|
+
}
|
|
154
|
+
resourceHasQueryEndpoints(resource) {
|
|
155
|
+
return this.groupEndpointsByResource()[resource]?.some(
|
|
156
|
+
({ endpoint }) => endpoint.method === "GET"
|
|
157
|
+
) ?? false;
|
|
158
|
+
}
|
|
159
|
+
getEndpointKeyName(name) {
|
|
160
|
+
return name.startsWith("get") ? name[3].toLowerCase() + name.slice(4) : name;
|
|
161
|
+
}
|
|
162
|
+
generateQueryKeyCall(resource, name, endpoint) {
|
|
163
|
+
const key = this.getEndpointKeyName(name);
|
|
164
|
+
const args = [];
|
|
165
|
+
if (endpoint.params) args.push("params");
|
|
166
|
+
if (endpoint.query) args.push("filters");
|
|
167
|
+
return args.length ? `queryKeys.${resource}.${key}(${args.join(", ")})` : `queryKeys.${resource}.${key}()`;
|
|
168
|
+
}
|
|
169
|
+
hasQueryOptions() {
|
|
170
|
+
return Object.values(this.context.apiConfig.endpoints).some(
|
|
171
|
+
(e) => e.method === "GET"
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/generators/hooks.ts
|
|
177
|
+
var HooksGenerator = class extends BaseGenerator {
|
|
178
|
+
async generate() {
|
|
179
|
+
const content = this.generateContent();
|
|
180
|
+
const outputPath = path2.join(this.context.config.outputDir, "hooks.ts");
|
|
181
|
+
await fs.mkdir(path2.dirname(outputPath), { recursive: true });
|
|
182
|
+
await fs.writeFile(outputPath, content, "utf-8");
|
|
183
|
+
}
|
|
184
|
+
generateContent() {
|
|
185
|
+
const useClientDirective = this.context.config.options?.useClientDirective ?? true;
|
|
186
|
+
const outputPath = path2.join(this.context.config.outputDir, "types.ts");
|
|
187
|
+
const endpointsPath = path2.join(this.context.config.endpointsPath);
|
|
188
|
+
const relativePath = path2.relative(path2.dirname(outputPath), endpointsPath).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
189
|
+
const content = `${useClientDirective ? "'use client';\n" : ""}
|
|
190
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
191
|
+
import { apiClient } from "./client";
|
|
192
|
+
import { queryKeys } from "./query-keys";
|
|
193
|
+
import { apiQueryOptions } from "./query-options";
|
|
194
|
+
import { z } from "zod";
|
|
195
|
+
import { apiConfig } from "${relativePath}";
|
|
196
|
+
|
|
197
|
+
${this.generateQueryHooks()}
|
|
198
|
+
${this.generateMutationHooks()}
|
|
199
|
+
`;
|
|
200
|
+
return content;
|
|
201
|
+
}
|
|
202
|
+
generateQueryHooks() {
|
|
203
|
+
const hooks = [];
|
|
204
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
205
|
+
([name, endpoint]) => {
|
|
206
|
+
if (endpoint.method === "GET")
|
|
207
|
+
hooks.push(this.generateQueryHook(name, endpoint));
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
return hooks.join("\n\n");
|
|
211
|
+
}
|
|
212
|
+
generateQueryHook(name, endpoint) {
|
|
213
|
+
const hookName = `use${this.capitalize(name)}`;
|
|
214
|
+
const resource = this.getResourceFromEndpoint(name, endpoint);
|
|
215
|
+
const optionName = this.getEndpointKeyName(name);
|
|
216
|
+
const inferParams = this.inferNonNull(
|
|
217
|
+
`typeof apiConfig.endpoints.${name}.params`
|
|
218
|
+
);
|
|
219
|
+
const inferQuery = this.inferNonNull(
|
|
220
|
+
`typeof apiConfig.endpoints.${name}.query`
|
|
221
|
+
);
|
|
222
|
+
const inferResponse = this.inferNonNull(
|
|
223
|
+
`typeof apiConfig.endpoints.${name}.response`
|
|
224
|
+
);
|
|
225
|
+
const params = [];
|
|
226
|
+
const optionParams = [];
|
|
227
|
+
const queryTags = this.getQueryTags(endpoint);
|
|
228
|
+
if (endpoint.params) {
|
|
229
|
+
params.push(`params: ${inferParams}`);
|
|
230
|
+
optionParams.push("params");
|
|
231
|
+
}
|
|
232
|
+
if (endpoint.query) {
|
|
233
|
+
params.push(`filters?: ${inferQuery}`);
|
|
234
|
+
optionParams.push("filters");
|
|
235
|
+
}
|
|
236
|
+
params.push(`options?: {
|
|
237
|
+
enabled?: boolean;
|
|
238
|
+
select?: <TData = ${inferResponse}>(data: ${inferResponse}) => TData;
|
|
239
|
+
}`);
|
|
240
|
+
return `/**
|
|
241
|
+
* ${endpoint.description || `Query hook for ${name}`}
|
|
242
|
+
* @tags ${queryTags.join(", ") || "none"}
|
|
243
|
+
*/
|
|
244
|
+
export function ${hookName}(${params.join(",\n ")}) {
|
|
245
|
+
return useQuery({
|
|
246
|
+
...apiQueryOptions.${resource}.${optionName}(${optionParams.join(", ")}),
|
|
247
|
+
...options,
|
|
248
|
+
});
|
|
249
|
+
}`;
|
|
250
|
+
}
|
|
251
|
+
generateMutationHooks() {
|
|
252
|
+
const hooks = [];
|
|
253
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
254
|
+
([name, endpoint]) => {
|
|
255
|
+
if (endpoint.method !== "GET")
|
|
256
|
+
hooks.push(this.generateMutationHook(name, endpoint));
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
return hooks.join("\n\n");
|
|
260
|
+
}
|
|
261
|
+
generateMutationHook(name, endpoint) {
|
|
262
|
+
const hookName = `use${this.capitalize(name)}`;
|
|
263
|
+
const resource = this.getResourceFromEndpoint(name, endpoint);
|
|
264
|
+
const inferParams = this.inferNonNull(
|
|
265
|
+
`typeof apiConfig.endpoints.${name}.params`
|
|
266
|
+
);
|
|
267
|
+
const inferBody = this.inferNonNull(
|
|
268
|
+
`typeof apiConfig.endpoints.${name}.body`
|
|
269
|
+
);
|
|
270
|
+
const inferResponse = this.inferNonNull(
|
|
271
|
+
`typeof apiConfig.endpoints.${name}.response`
|
|
272
|
+
);
|
|
273
|
+
const resourceHasQueries = this.resourceHasQueryEndpoints(resource);
|
|
274
|
+
let inputType;
|
|
275
|
+
let fnBody;
|
|
276
|
+
if (endpoint.params && endpoint.body) {
|
|
277
|
+
inputType = `{ params: ${inferParams}; body: ${inferBody}; }`;
|
|
278
|
+
fnBody = `({ params, body }: ${inputType}) => apiClient.${name}(params, body)`;
|
|
279
|
+
} else if (endpoint.params) {
|
|
280
|
+
inputType = `${inferParams}`;
|
|
281
|
+
fnBody = `(params: ${inputType}) => apiClient.${name}(params)`;
|
|
282
|
+
} else if (endpoint.body) {
|
|
283
|
+
inputType = `${inferBody}`;
|
|
284
|
+
fnBody = `(body: ${inputType}) => apiClient.${name}(body)`;
|
|
285
|
+
} else {
|
|
286
|
+
inputType = "void";
|
|
287
|
+
fnBody = `() => apiClient.${name}()`;
|
|
288
|
+
}
|
|
289
|
+
const invalidate = resourceHasQueries ? `queryClient.invalidateQueries({ queryKey: queryKeys.${resource}.all });` : "";
|
|
290
|
+
return `/**
|
|
291
|
+
* ${endpoint.description || `Mutation hook for ${name}`}
|
|
292
|
+
* @tags ${endpoint.tags?.join(", ") || "none"}
|
|
293
|
+
*/
|
|
294
|
+
export function ${hookName}(options?: {
|
|
295
|
+
onSuccess?: (data: ${inferResponse}, variables: ${inputType}, context: unknown) => void;
|
|
296
|
+
onError?: (error: Error, variables: ${inputType}, context: unknown) => void;
|
|
297
|
+
onSettled?: (data: ${inferResponse} | undefined, error: Error | null, variables: ${inputType}, context: unknown) => void;
|
|
298
|
+
onMutate?: (variables: ${inputType}) => Promise<unknown> | unknown;
|
|
299
|
+
}) {
|
|
300
|
+
${invalidate ? "const queryClient = useQueryClient();" : ""}
|
|
301
|
+
return useMutation({
|
|
302
|
+
mutationFn: ${fnBody},
|
|
303
|
+
onSuccess: (data, variables, context) => {
|
|
304
|
+
${invalidate}
|
|
305
|
+
options?.onSuccess?.(data, variables, context);
|
|
306
|
+
},
|
|
307
|
+
onError: options?.onError,
|
|
308
|
+
onSettled: options?.onSettled,
|
|
309
|
+
onMutate: options?.onMutate,
|
|
310
|
+
});
|
|
311
|
+
}`;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// src/generators/actions.ts
|
|
316
|
+
import fs2 from "fs/promises";
|
|
317
|
+
import path3 from "path";
|
|
318
|
+
var ServerActionsGenerator = class extends BaseGenerator {
|
|
319
|
+
async generate() {
|
|
320
|
+
const content = this.generateContent();
|
|
321
|
+
const outputPath = path3.join(this.context.config.outputDir, "actions.ts");
|
|
322
|
+
await fs2.mkdir(path3.dirname(outputPath), { recursive: true });
|
|
323
|
+
await fs2.writeFile(outputPath, content, "utf-8");
|
|
324
|
+
}
|
|
325
|
+
generateContent() {
|
|
326
|
+
const outputPath = path3.join(this.context.config.outputDir, "types.ts");
|
|
327
|
+
const endpointsPath = path3.join(this.context.config.endpointsPath);
|
|
328
|
+
const relativePath = path3.relative(path3.dirname(outputPath), endpointsPath).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
329
|
+
const imports = `'use server';
|
|
330
|
+
|
|
331
|
+
import { revalidateTag, revalidatePath } from 'next/cache';
|
|
332
|
+
import { serverClient } from './server-client';
|
|
333
|
+
import { z } from 'zod';
|
|
334
|
+
import { apiConfig } from '${relativePath}';
|
|
335
|
+
|
|
336
|
+
export type ActionResult<T> =
|
|
337
|
+
| { success: true; data: T }
|
|
338
|
+
| { success: false; error: string };
|
|
339
|
+
`;
|
|
340
|
+
const actions = [];
|
|
341
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
342
|
+
([name, endpoint]) => {
|
|
343
|
+
if (this.isMutationEndpoint(endpoint)) {
|
|
344
|
+
actions.push(this.generateServerAction(name, endpoint));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
return imports + "\n" + actions.join("\n\n");
|
|
349
|
+
}
|
|
350
|
+
generateServerAction(name, endpoint) {
|
|
351
|
+
const actionSuffix = this.context.config.options?.actionSuffix || "Action";
|
|
352
|
+
const actionName = `${name}${actionSuffix}`;
|
|
353
|
+
const signature = this.getEndpointSignature(name, endpoint);
|
|
354
|
+
const invalidationTags = this.getInvalidationTags(endpoint);
|
|
355
|
+
let inputType = "";
|
|
356
|
+
let callArgs = "";
|
|
357
|
+
if (signature.hasParams && signature.hasBody) {
|
|
358
|
+
inputType = `input: { params: ${signature.paramType}; body: ${signature.bodyType} }`;
|
|
359
|
+
callArgs = "input.params, input.body";
|
|
360
|
+
} else if (signature.hasParams) {
|
|
361
|
+
inputType = `params: ${signature.paramType}`;
|
|
362
|
+
callArgs = "params";
|
|
363
|
+
} else if (signature.hasBody) {
|
|
364
|
+
inputType = `body: ${signature.bodyType}`;
|
|
365
|
+
callArgs = "body";
|
|
366
|
+
}
|
|
367
|
+
const revalidateStatements = invalidationTags.length > 0 ? invalidationTags.map((tag) => ` revalidateTag('${tag}');`).join("\n") : " // No automatic revalidations";
|
|
368
|
+
return `/**
|
|
369
|
+
* ${endpoint.description || `Server action for ${name}`}
|
|
370
|
+
* @tags ${endpoint.tags?.join(", ") || "none"}
|
|
371
|
+
*/
|
|
372
|
+
export async function ${actionName}(
|
|
373
|
+
${inputType}
|
|
374
|
+
): Promise<ActionResult<${signature.responseType}>> {
|
|
375
|
+
try {
|
|
376
|
+
const result = await (serverClient as any).${name}(${callArgs});
|
|
377
|
+
|
|
378
|
+
// Revalidate related data
|
|
379
|
+
${revalidateStatements}
|
|
380
|
+
|
|
381
|
+
return { success: true, data: result };
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.error('[Server Action Error]:', error);
|
|
384
|
+
return {
|
|
385
|
+
success: false,
|
|
386
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}`;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// src/generators/queries.ts
|
|
394
|
+
import fs3 from "fs/promises";
|
|
395
|
+
import path4 from "path";
|
|
396
|
+
var ServerQueriesGenerator = class extends BaseGenerator {
|
|
397
|
+
async generate() {
|
|
398
|
+
const content = this.generateContent();
|
|
399
|
+
const outputPath = path4.join(this.context.config.outputDir, "queries.ts");
|
|
400
|
+
await fs3.mkdir(path4.dirname(outputPath), { recursive: true });
|
|
401
|
+
await fs3.writeFile(outputPath, content, "utf-8");
|
|
402
|
+
}
|
|
403
|
+
generateContent() {
|
|
404
|
+
const outputPath = path4.join(this.context.config.outputDir, "types.ts");
|
|
405
|
+
const endpointsPath = path4.join(this.context.config.endpointsPath);
|
|
406
|
+
const relativePath = path4.relative(path4.dirname(outputPath), endpointsPath).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
407
|
+
const imports = `import { cache } from 'react';
|
|
408
|
+
import { unstable_cache } from 'next/cache';
|
|
409
|
+
import { serverClient } from './server-client';
|
|
410
|
+
import { z } from 'zod';
|
|
411
|
+
import { apiConfig } from '${relativePath}';
|
|
412
|
+
`;
|
|
413
|
+
const queries = [];
|
|
414
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
415
|
+
([name, endpoint]) => {
|
|
416
|
+
if (this.isQueryEndpoint(endpoint)) {
|
|
417
|
+
queries.push(this.generateServerQuery(name, endpoint));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
return imports + "\n" + queries.join("\n\n");
|
|
422
|
+
}
|
|
423
|
+
generateServerQuery(name, endpoint) {
|
|
424
|
+
const queryName = `${name}Query`;
|
|
425
|
+
const signature = this.getEndpointSignature(name, endpoint);
|
|
426
|
+
const queryTags = this.getQueryTags(endpoint);
|
|
427
|
+
const paramDef = signature.hasParams ? `params: ${signature.paramType}` : "";
|
|
428
|
+
const queryDef = signature.hasQuery ? `query?: ${signature.queryType}` : "";
|
|
429
|
+
const paramsList = [paramDef, queryDef].filter(Boolean).join(",\n ");
|
|
430
|
+
let clientCall = "";
|
|
431
|
+
if (signature.hasParams && signature.hasQuery) {
|
|
432
|
+
clientCall = `(serverClient as any).${name}(params, query)`;
|
|
433
|
+
} else if (signature.hasParams) {
|
|
434
|
+
clientCall = `(serverClient as any).${name}(params)`;
|
|
435
|
+
} else if (signature.hasQuery) {
|
|
436
|
+
clientCall = `(serverClient as any).${name}(query)`;
|
|
437
|
+
} else {
|
|
438
|
+
clientCall = `(serverClient as any).${name}()`;
|
|
439
|
+
}
|
|
440
|
+
const cacheKeyParts = [`'${name}'`];
|
|
441
|
+
if (signature.hasParams) cacheKeyParts.push("JSON.stringify(params)");
|
|
442
|
+
if (signature.hasQuery)
|
|
443
|
+
cacheKeyParts.push("query ? JSON.stringify(query) : 'no-query'");
|
|
444
|
+
return `/**
|
|
445
|
+
* ${endpoint.description || `Server query for ${name}`}
|
|
446
|
+
* @tags ${queryTags.join(", ") || "none"}
|
|
447
|
+
*/
|
|
448
|
+
export const ${queryName} = cache(async (
|
|
449
|
+
${paramsList}
|
|
450
|
+
): Promise<${signature.responseType}> => {
|
|
451
|
+
return unstable_cache(
|
|
452
|
+
async () => ${clientCall},
|
|
453
|
+
[${cacheKeyParts.join(", ")}],
|
|
454
|
+
{
|
|
455
|
+
tags: [${queryTags.map((tag) => `'${tag}'`).join(", ")}],
|
|
456
|
+
revalidate: 3600, // 1 hour default, can be overridden
|
|
457
|
+
}
|
|
458
|
+
)();
|
|
459
|
+
});`;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// src/generators/types.ts
|
|
464
|
+
import fs4 from "fs/promises";
|
|
465
|
+
import path5 from "path";
|
|
466
|
+
var TypesGenerator = class extends BaseGenerator {
|
|
467
|
+
async generate() {
|
|
468
|
+
const content = this.generateContent();
|
|
469
|
+
const outputPath = path5.join(this.context.config.outputDir, "types.ts");
|
|
470
|
+
await fs4.mkdir(path5.dirname(outputPath), { recursive: true });
|
|
471
|
+
await fs4.writeFile(outputPath, content, "utf-8");
|
|
472
|
+
}
|
|
473
|
+
generateContent() {
|
|
474
|
+
const outputPath = path5.join(this.context.config.outputDir, "types.ts");
|
|
475
|
+
const endpointsPath = path5.join(this.context.config.endpointsPath);
|
|
476
|
+
const relativePath = path5.relative(path5.dirname(outputPath), endpointsPath).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
477
|
+
return `// Auto-generated type definitions
|
|
478
|
+
// Do not edit this file manually
|
|
479
|
+
|
|
480
|
+
import type { z } from 'zod';
|
|
481
|
+
import { apiConfig } from '${relativePath}';
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
// Re-export endpoint configuration types
|
|
485
|
+
export type { APIConfig, APIEndpoint, HTTPMethod } from '@cushin/api-runtime';
|
|
486
|
+
|
|
487
|
+
${this.generateEndpointTypes()}
|
|
488
|
+
`;
|
|
489
|
+
}
|
|
490
|
+
generateEndpointTypes() {
|
|
491
|
+
const types = [];
|
|
492
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
493
|
+
([name, endpoint]) => {
|
|
494
|
+
const cap = this.capitalize(name);
|
|
495
|
+
if (endpoint.response)
|
|
496
|
+
types.push(
|
|
497
|
+
`export type ${cap}Response = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.response`)};`
|
|
498
|
+
);
|
|
499
|
+
if (endpoint.body)
|
|
500
|
+
types.push(
|
|
501
|
+
`export type ${cap}Input = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.body`)};`
|
|
502
|
+
);
|
|
503
|
+
if (endpoint.query)
|
|
504
|
+
types.push(
|
|
505
|
+
`export type ${cap}Query = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.query`)};`
|
|
506
|
+
);
|
|
507
|
+
if (endpoint.params)
|
|
508
|
+
types.push(
|
|
509
|
+
`export type ${cap}Params = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.params`)};`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
);
|
|
513
|
+
return types.join("\n");
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// src/generators/client.ts
|
|
518
|
+
import fs5 from "fs/promises";
|
|
519
|
+
import path6 from "path";
|
|
520
|
+
var ClientGenerator = class extends BaseGenerator {
|
|
521
|
+
async generate() {
|
|
522
|
+
await this.generateClientFile();
|
|
523
|
+
if (this.context.config.provider === "nextjs") {
|
|
524
|
+
await this.generateServerClientFile();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async generateClientFile() {
|
|
528
|
+
const content = this.generateClientContent();
|
|
529
|
+
const outputPath = path6.join(this.context.config.outputDir, "client.ts");
|
|
530
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
531
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
532
|
+
}
|
|
533
|
+
async generateServerClientFile() {
|
|
534
|
+
const content = this.generateServerClientContent();
|
|
535
|
+
const outputPath = path6.join(
|
|
536
|
+
this.context.config.outputDir,
|
|
537
|
+
"server-client.ts"
|
|
538
|
+
);
|
|
539
|
+
await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
|
|
540
|
+
await fs5.writeFile(outputPath, content, "utf-8");
|
|
541
|
+
}
|
|
542
|
+
generateClientContent() {
|
|
543
|
+
const useClientDirective = this.context.config.options?.useClientDirective ?? true;
|
|
544
|
+
const outputPath = path6.join(this.context.config.outputDir, "types.ts");
|
|
545
|
+
const endpointsPath = path6.join(this.context.config.endpointsPath);
|
|
546
|
+
const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
547
|
+
return `${useClientDirective ? "'use client';\n" : ""}
|
|
548
|
+
import { createAPIClient } from '@cushin/api-runtime';
|
|
549
|
+
import type { AuthCallbacks } from '@cushin/api-runtime';
|
|
550
|
+
import { apiConfig } from '${relativePath}';
|
|
551
|
+
import { z } from 'zod';
|
|
552
|
+
|
|
553
|
+
// Type the methods based on endpoints
|
|
554
|
+
type APIClientMethods = {
|
|
555
|
+
[K in keyof typeof apiConfig.endpoints]: (typeof apiConfig.endpoints)[K] extends infer E
|
|
556
|
+
? E extends { method: "GET"; params: infer P; query: infer Q; response: infer R }
|
|
557
|
+
? (params: P extends z.ZodType ? z.infer<P> : never, query?: Q extends z.ZodType ? z.infer<Q> : never) => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
558
|
+
: E extends { method: "GET"; params: infer P; response: infer R }
|
|
559
|
+
? (params: P extends z.ZodType ? z.infer<P> : never) => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
560
|
+
: E extends { method: "GET"; query: infer Q; response: infer R }
|
|
561
|
+
? (query?: Q extends z.ZodType ? z.infer<Q> : never) => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
562
|
+
: E extends { method: "GET"; response: infer R }
|
|
563
|
+
? () => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
564
|
+
: E extends { method: string; params: infer P; body: infer B; response: infer R }
|
|
565
|
+
? (params: P extends z.ZodType ? z.infer<P> : never, body: B extends z.ZodType ? z.infer<B> : never) => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
566
|
+
: E extends { method: string; params: infer P; response: infer R }
|
|
567
|
+
? (params: P extends z.ZodType ? z.infer<P> : never) => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
568
|
+
: E extends { method: string; body: infer B; response: infer R }
|
|
569
|
+
? (body: B extends z.ZodType ? z.infer<B> : never) => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
570
|
+
: E extends { method: string; response: infer R }
|
|
571
|
+
? () => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
572
|
+
: never
|
|
573
|
+
: never;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
// Export singleton instance (will be initialized later)
|
|
578
|
+
export let baseClient: APIClientMethods & {
|
|
579
|
+
refreshAuth: () => Promise<void>;
|
|
580
|
+
updateAuthCallbacks: (callbacks: AuthCallbacks) => void;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
export const apiClient = {
|
|
584
|
+
${this.generateApiClientMethods()}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Initialize API client with auth callbacks
|
|
589
|
+
* Call this function in your auth provider setup
|
|
590
|
+
*
|
|
591
|
+
* @example
|
|
592
|
+
* const authCallbacks = {
|
|
593
|
+
* getTokens: () => getStoredTokens(),
|
|
594
|
+
* onAuthError: () => router.push('/login'),
|
|
595
|
+
* onRefreshToken: async () => {
|
|
596
|
+
* await refreshAccessToken();
|
|
597
|
+
* },
|
|
598
|
+
* };
|
|
599
|
+
*
|
|
600
|
+
* initializeAPIClient(authCallbacks);
|
|
601
|
+
*/
|
|
602
|
+
export const initializeAPIClient = (authCallbacks: AuthCallbacks) => {
|
|
603
|
+
baseClient = createAPIClient(apiConfig, authCallbacks) as any;
|
|
604
|
+
return baseClient;
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// Export for custom usage
|
|
608
|
+
export { createAPIClient };
|
|
609
|
+
export type { AuthCallbacks };
|
|
610
|
+
`;
|
|
611
|
+
}
|
|
612
|
+
generateServerClientContent() {
|
|
613
|
+
const outputPath = path6.join(this.context.config.outputDir, "types.ts");
|
|
614
|
+
const endpointsPath = path6.join(this.context.config.endpointsPath);
|
|
615
|
+
const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
616
|
+
return `import { createAPIClient } from '@cushin/api-runtime';
|
|
617
|
+
import { apiConfig } from '${relativePath}';
|
|
618
|
+
import { z } from 'zod';
|
|
619
|
+
|
|
620
|
+
// Type the methods based on endpoints
|
|
621
|
+
type APIClientMethods = {
|
|
622
|
+
[K in keyof typeof apiConfig.endpoints]: (typeof apiConfig.endpoints)[K] extends infer E
|
|
623
|
+
? E extends { method: "GET"; params: infer P; query: infer Q; response: infer R }
|
|
624
|
+
? (params: P extends z.ZodType ? z.infer<P> : never, query?: Q extends z.ZodType ? z.infer<Q> : never) => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
625
|
+
: E extends { method: "GET"; params: infer P; response: infer R }
|
|
626
|
+
? (params: P extends z.ZodType ? z.infer<P> : never) => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
627
|
+
: E extends { method: "GET"; query: infer Q; response: infer R }
|
|
628
|
+
? (query?: Q extends z.ZodType ? z.infer<Q> : never) => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
629
|
+
: E extends { method: "GET"; response: infer R }
|
|
630
|
+
? () => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
631
|
+
: E extends { method: string; params: infer P; body: infer B; response: infer R }
|
|
632
|
+
? (params: P extends z.ZodType ? z.infer<P> : never, body: B extends z.ZodType ? z.infer<B> : never) => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
633
|
+
: E extends { method: string; params: infer P; response: infer R }
|
|
634
|
+
? (params: P extends z.ZodType ? z.infer<P> : never) => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
635
|
+
: E extends { method: string; body: infer B; response: infer R }
|
|
636
|
+
? (body: B extends z.ZodType ? z.infer<B> : never) => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
637
|
+
: E extends { method: string; response: infer R }
|
|
638
|
+
? () => Promise<R extends z.ZodType ? z.infer<R> : never>
|
|
639
|
+
: never
|
|
640
|
+
: never;
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Server-side API client (no auth, direct API calls)
|
|
645
|
+
* Use this in Server Components, Server Actions, and Route Handlers
|
|
646
|
+
*/
|
|
647
|
+
export const serverClient = createAPIClient(apiConfig) as APIClientMethods;
|
|
648
|
+
`;
|
|
649
|
+
}
|
|
650
|
+
generateApiClientMethods() {
|
|
651
|
+
const methods = [];
|
|
652
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
653
|
+
([name, endpoint]) => {
|
|
654
|
+
const inferParams = this.inferNonNull(
|
|
655
|
+
`typeof apiConfig.endpoints.${name}.params`
|
|
656
|
+
);
|
|
657
|
+
const inferQuery = this.inferNonNull(
|
|
658
|
+
`typeof apiConfig.endpoints.${name}.query`
|
|
659
|
+
);
|
|
660
|
+
const inferBody = this.inferNonNull(
|
|
661
|
+
`typeof apiConfig.endpoints.${name}.body`
|
|
662
|
+
);
|
|
663
|
+
const inferResponse = this.inferNonNull(
|
|
664
|
+
`typeof apiConfig.endpoints.${name}.response`
|
|
665
|
+
);
|
|
666
|
+
if (endpoint.method === "GET") {
|
|
667
|
+
if (endpoint.params && endpoint.query) {
|
|
668
|
+
methods.push(` ${name}: (params: ${inferParams}, query?: ${inferQuery}): Promise<${inferResponse}> =>
|
|
669
|
+
(baseClient as any).${name}(params, query),`);
|
|
670
|
+
} else if (endpoint.params) {
|
|
671
|
+
methods.push(` ${name}: (params: ${inferParams}): Promise<${inferResponse}> =>
|
|
672
|
+
(baseClient as any).${name}(params),`);
|
|
673
|
+
} else if (endpoint.query) {
|
|
674
|
+
methods.push(` ${name}: (query?: ${inferQuery}): Promise<${inferResponse}> =>
|
|
675
|
+
(baseClient as any).${name}(query),`);
|
|
676
|
+
} else {
|
|
677
|
+
methods.push(` ${name}: (): Promise<${inferResponse}> =>
|
|
678
|
+
(baseClient as any).${name}(),`);
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
if (endpoint.params && endpoint.body) {
|
|
682
|
+
methods.push(` ${name}: (params: ${inferParams}, body: ${inferBody}): Promise<${inferResponse}> =>
|
|
683
|
+
(baseClient as any).${name}(params, body),`);
|
|
684
|
+
} else if (endpoint.params) {
|
|
685
|
+
methods.push(` ${name}: (params: ${inferParams}): Promise<${inferResponse}> =>
|
|
686
|
+
(baseClient as any).${name}(params),`);
|
|
687
|
+
} else if (endpoint.body) {
|
|
688
|
+
methods.push(` ${name}: (body: ${inferBody}): Promise<${inferResponse}> =>
|
|
689
|
+
(baseClient as any).${name}(body),`);
|
|
690
|
+
} else {
|
|
691
|
+
methods.push(` ${name}: (): Promise<${inferResponse}> =>
|
|
692
|
+
(baseClient as any).${name}(),`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
);
|
|
697
|
+
return methods.join("\n");
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// src/generators/query-keys.ts
|
|
702
|
+
import fs6 from "fs/promises";
|
|
703
|
+
import path7 from "path";
|
|
704
|
+
var QueryKeysGenerator = class extends BaseGenerator {
|
|
705
|
+
async generate() {
|
|
706
|
+
const content = this.generateContent();
|
|
707
|
+
const outputPath = path7.join(
|
|
708
|
+
this.context.config.outputDir,
|
|
709
|
+
"query-keys.ts"
|
|
710
|
+
);
|
|
711
|
+
await fs6.mkdir(path7.dirname(outputPath), { recursive: true });
|
|
712
|
+
await fs6.writeFile(outputPath, content, "utf-8");
|
|
713
|
+
}
|
|
714
|
+
generateContent() {
|
|
715
|
+
const outputPath = path7.join(this.context.config.outputDir, "types.ts");
|
|
716
|
+
const endpointsPath = path7.join(this.context.config.endpointsPath);
|
|
717
|
+
const relativePath = path7.relative(path7.dirname(outputPath), endpointsPath).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
718
|
+
const content = `// Auto-generated query keys
|
|
719
|
+
import { z } from 'zod';
|
|
720
|
+
import { apiConfig } from '${relativePath}';
|
|
721
|
+
|
|
722
|
+
export const queryKeys = {
|
|
723
|
+
${this.generateQueryKeysContent()}
|
|
724
|
+
} as const;
|
|
725
|
+
`;
|
|
726
|
+
return content;
|
|
727
|
+
}
|
|
728
|
+
generateQueryKeysContent() {
|
|
729
|
+
const resourceGroups = this.groupEndpointsByResource();
|
|
730
|
+
const keys = [];
|
|
731
|
+
Object.entries(resourceGroups).forEach(([resource, endpoints]) => {
|
|
732
|
+
const queryEndpoints = endpoints.filter(
|
|
733
|
+
({ endpoint }) => endpoint.method === "GET"
|
|
734
|
+
);
|
|
735
|
+
if (queryEndpoints.length === 0) return;
|
|
736
|
+
const resourceKeys = [` all: ['${resource}'] as const,`];
|
|
737
|
+
const added = /* @__PURE__ */ new Set();
|
|
738
|
+
queryEndpoints.forEach(({ name, endpoint }) => {
|
|
739
|
+
const keyName = this.getEndpointKeyName(name);
|
|
740
|
+
if (added.has(keyName)) return;
|
|
741
|
+
const inferParams = this.inferNonNull(
|
|
742
|
+
`typeof apiConfig.endpoints.${name}.params`
|
|
743
|
+
);
|
|
744
|
+
const inferQuery = this.inferNonNull(
|
|
745
|
+
`typeof apiConfig.endpoints.${name}.query`
|
|
746
|
+
);
|
|
747
|
+
if (endpoint.params || endpoint.query) {
|
|
748
|
+
const params = [];
|
|
749
|
+
if (endpoint.params) params.push(`params?: ${inferParams}`);
|
|
750
|
+
if (endpoint.query) params.push(`query?: ${inferQuery}`);
|
|
751
|
+
resourceKeys.push(` ${keyName}: (${params.join(", ")}) =>
|
|
752
|
+
['${resource}', '${keyName}', ${endpoint.params ? "params" : "undefined"}, ${endpoint.query ? "query" : "undefined"}] as const,`);
|
|
753
|
+
} else {
|
|
754
|
+
resourceKeys.push(
|
|
755
|
+
` ${keyName}: () => ['${resource}', '${keyName}'] as const,`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
added.add(keyName);
|
|
759
|
+
});
|
|
760
|
+
keys.push(` ${resource}: {
|
|
761
|
+
${resourceKeys.join("\n")}
|
|
762
|
+
},`);
|
|
763
|
+
});
|
|
764
|
+
return keys.join("\n");
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
// src/generators/query-options.ts
|
|
769
|
+
import fs7 from "fs/promises";
|
|
770
|
+
import path8 from "path";
|
|
771
|
+
var QueryOptionsGenerator = class extends BaseGenerator {
|
|
772
|
+
async generate() {
|
|
773
|
+
const content = this.generateContent();
|
|
774
|
+
const outputPath = path8.join(
|
|
775
|
+
this.context.config.outputDir,
|
|
776
|
+
"query-options.ts"
|
|
777
|
+
);
|
|
778
|
+
await fs7.mkdir(path8.dirname(outputPath), { recursive: true });
|
|
779
|
+
await fs7.writeFile(outputPath, content, "utf-8");
|
|
780
|
+
}
|
|
781
|
+
generateContent() {
|
|
782
|
+
const outputPath = path8.join(this.context.config.outputDir, "types.ts");
|
|
783
|
+
const endpointsPath = path8.join(this.context.config.endpointsPath);
|
|
784
|
+
const relativePath = path8.relative(path8.dirname(outputPath), endpointsPath).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
785
|
+
const content = `// Auto-generated query options
|
|
786
|
+
import { queryOptions } from '@tanstack/react-query';
|
|
787
|
+
import { apiClient } from './client';
|
|
788
|
+
import { queryKeys } from './query-keys';
|
|
789
|
+
import { z } from 'zod';
|
|
790
|
+
import { apiConfig } from '${relativePath}';
|
|
791
|
+
|
|
792
|
+
${this.generateQueryOptionsContent()}
|
|
793
|
+
|
|
794
|
+
export const apiQueryOptions = {
|
|
795
|
+
${this.generateQueryOptionsExports()}
|
|
796
|
+
} as const;
|
|
797
|
+
`;
|
|
798
|
+
return content;
|
|
799
|
+
}
|
|
800
|
+
generateQueryOptionsContent() {
|
|
801
|
+
const groups = this.groupEndpointsByResource();
|
|
802
|
+
const options = [];
|
|
803
|
+
Object.entries(groups).forEach(([resource, endpoints]) => {
|
|
804
|
+
const queries = endpoints.filter(
|
|
805
|
+
({ endpoint }) => endpoint.method === "GET"
|
|
806
|
+
);
|
|
807
|
+
if (queries.length === 0) return;
|
|
808
|
+
const resourceOptions = [];
|
|
809
|
+
queries.forEach(({ name, endpoint }) => {
|
|
810
|
+
const optionName = this.getEndpointKeyName(name);
|
|
811
|
+
const inferParams = this.inferNonNull(
|
|
812
|
+
`typeof apiConfig.endpoints.${name}.params`
|
|
813
|
+
);
|
|
814
|
+
const inferQuery = this.inferNonNull(
|
|
815
|
+
`typeof apiConfig.endpoints.${name}.query`
|
|
816
|
+
);
|
|
817
|
+
const inferResponse = this.inferNonNull(
|
|
818
|
+
`typeof apiConfig.endpoints.${name}.response`
|
|
819
|
+
);
|
|
820
|
+
const params = [];
|
|
821
|
+
let apiCall = "";
|
|
822
|
+
if (endpoint.params && endpoint.query) {
|
|
823
|
+
params.push(`params: ${inferParams}`, `filters?: ${inferQuery}`);
|
|
824
|
+
apiCall = `apiClient.${name}(params, filters)`;
|
|
825
|
+
} else if (endpoint.params) {
|
|
826
|
+
params.push(`params: ${inferParams}`);
|
|
827
|
+
apiCall = `apiClient.${name}(params)`;
|
|
828
|
+
} else if (endpoint.query) {
|
|
829
|
+
params.push(`filters?: ${inferQuery}`);
|
|
830
|
+
apiCall = `apiClient.${name}(filters)`;
|
|
831
|
+
} else {
|
|
832
|
+
apiCall = `apiClient.${name}()`;
|
|
833
|
+
}
|
|
834
|
+
const keyCall = this.generateQueryKeyCall(resource, name, endpoint);
|
|
835
|
+
resourceOptions.push(` ${optionName}: (${params.join(", ")}) =>
|
|
836
|
+
queryOptions({
|
|
837
|
+
queryKey: ${keyCall},
|
|
838
|
+
queryFn: (): Promise<${inferResponse}> => ${apiCall},
|
|
839
|
+
staleTime: 1000 * 60 * 5,
|
|
840
|
+
}),`);
|
|
841
|
+
});
|
|
842
|
+
options.push(
|
|
843
|
+
`const ${resource}QueryOptions = {
|
|
844
|
+
${resourceOptions.join("\n")}
|
|
845
|
+
};
|
|
846
|
+
`
|
|
847
|
+
);
|
|
848
|
+
});
|
|
849
|
+
return options.join("\n");
|
|
850
|
+
}
|
|
851
|
+
generateQueryOptionsExports() {
|
|
852
|
+
const groups = this.groupEndpointsByResource();
|
|
853
|
+
const exports = [];
|
|
854
|
+
Object.keys(groups).forEach((resource) => {
|
|
855
|
+
const hasQueries = groups[resource].some(
|
|
856
|
+
({ endpoint }) => endpoint.method === "GET"
|
|
857
|
+
);
|
|
858
|
+
if (hasQueries) exports.push(` ${resource}: ${resource}QueryOptions,`);
|
|
859
|
+
});
|
|
860
|
+
return exports.join("\n");
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
// src/generators/prefetch.ts
|
|
865
|
+
import fs8 from "fs/promises";
|
|
866
|
+
import path9 from "path";
|
|
867
|
+
var PrefetchGenerator = class extends BaseGenerator {
|
|
868
|
+
async generate() {
|
|
869
|
+
const content = this.generateContent();
|
|
870
|
+
const outputPath = path9.join(this.context.config.outputDir, "prefetchs.ts");
|
|
871
|
+
await fs8.mkdir(path9.dirname(outputPath), { recursive: true });
|
|
872
|
+
await fs8.writeFile(outputPath, content, "utf-8");
|
|
873
|
+
}
|
|
874
|
+
generateContent() {
|
|
875
|
+
const content = `// Auto-generated prefetch utilities
|
|
876
|
+
import { type QueryClient } from '@tanstack/react-query';
|
|
877
|
+
${this.hasQueryOptions() ? "import { apiQueryOptions } from './query-options';" : ""}
|
|
878
|
+
import { z } from 'zod';
|
|
879
|
+
import { apiConfig } from '../config/endpoints';
|
|
880
|
+
|
|
881
|
+
${this.generatePrefetchFunctions()}
|
|
882
|
+
`;
|
|
883
|
+
return content;
|
|
884
|
+
}
|
|
885
|
+
generatePrefetchFunctions() {
|
|
886
|
+
const funcs = [];
|
|
887
|
+
Object.entries(this.context.apiConfig.endpoints).forEach(
|
|
888
|
+
([name, endpoint]) => {
|
|
889
|
+
if (endpoint.method === "GET")
|
|
890
|
+
funcs.push(this.generatePrefetchFunction(name, endpoint));
|
|
891
|
+
}
|
|
892
|
+
);
|
|
893
|
+
return funcs.join("\n\n");
|
|
894
|
+
}
|
|
895
|
+
generatePrefetchFunction(name, endpoint) {
|
|
896
|
+
const prefetchName = `prefetch${this.capitalize(name)}`;
|
|
897
|
+
const resource = this.getResourceFromEndpoint(name, endpoint);
|
|
898
|
+
const optionName = this.getEndpointKeyName(name);
|
|
899
|
+
const inferParams = this.inferNonNull(
|
|
900
|
+
`typeof apiConfig.endpoints.${name}.params`
|
|
901
|
+
);
|
|
902
|
+
const inferQuery = this.inferNonNull(
|
|
903
|
+
`typeof apiConfig.endpoints.${name}.query`
|
|
904
|
+
);
|
|
905
|
+
const params = ["queryClient: QueryClient"];
|
|
906
|
+
const optionParams = [];
|
|
907
|
+
if (endpoint.params) {
|
|
908
|
+
params.push(`params: ${inferParams}`);
|
|
909
|
+
optionParams.push("params");
|
|
910
|
+
}
|
|
911
|
+
if (endpoint.query) {
|
|
912
|
+
params.push(`filters?: ${inferQuery}`);
|
|
913
|
+
optionParams.push("filters");
|
|
914
|
+
}
|
|
915
|
+
return `export const ${prefetchName} = async (${params.join(",\n ")}) => {
|
|
916
|
+
return await queryClient.ensureQueryData(apiQueryOptions.${resource}.${optionName}(${optionParams.join(", ")}));
|
|
917
|
+
};`;
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// src/generators/generate-index.ts
|
|
922
|
+
import fs9 from "fs/promises";
|
|
923
|
+
import path10 from "path";
|
|
924
|
+
var IndexGenerator = class extends BaseGenerator {
|
|
925
|
+
async generate() {
|
|
926
|
+
const content = this.generateContent();
|
|
927
|
+
const outputPath = path10.join(this.context.config.outputDir, "index.ts");
|
|
928
|
+
await fs9.mkdir(path10.dirname(outputPath), { recursive: true });
|
|
929
|
+
await fs9.writeFile(outputPath, content, "utf-8");
|
|
930
|
+
}
|
|
931
|
+
generateContent() {
|
|
932
|
+
const outputPath = path10.join(this.context.config.outputDir, "types.ts");
|
|
933
|
+
const endpointsPath = path10.join(this.context.config.endpointsPath);
|
|
934
|
+
const relativePath = path10.relative(path10.dirname(outputPath), endpointsPath).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
935
|
+
const content = `// Auto-generated exports
|
|
936
|
+
export * from './types';
|
|
937
|
+
export * from './client';
|
|
938
|
+
export * from './query-keys';
|
|
939
|
+
${this.hasQueryOptions() ? "export * from './query-options';" : ""}
|
|
940
|
+
export * from './hooks';
|
|
941
|
+
export * from './prefetchs';
|
|
942
|
+
export { z } from 'zod';
|
|
943
|
+
export { apiConfig } from '${relativePath}';
|
|
944
|
+
`;
|
|
945
|
+
return content;
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// src/generators/index.ts
|
|
950
|
+
var CodeGenerator = class {
|
|
951
|
+
constructor(context) {
|
|
952
|
+
this.context = context;
|
|
953
|
+
}
|
|
954
|
+
async generate() {
|
|
955
|
+
const generators = this.getGenerators();
|
|
956
|
+
for (const generator of generators) {
|
|
957
|
+
await generator.generate();
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
getGenerators() {
|
|
961
|
+
const generators = [];
|
|
962
|
+
generators.push(new TypesGenerator(this.context));
|
|
963
|
+
generators.push(new IndexGenerator(this.context));
|
|
964
|
+
if (this.context.config.generateClient) {
|
|
965
|
+
generators.push(new ClientGenerator(this.context));
|
|
966
|
+
}
|
|
967
|
+
if (this.context.config.generateHooks) {
|
|
968
|
+
generators.push(new QueryKeysGenerator(this.context));
|
|
969
|
+
generators.push(new QueryOptionsGenerator(this.context));
|
|
970
|
+
generators.push(new HooksGenerator(this.context));
|
|
971
|
+
}
|
|
972
|
+
if (this.context.config.generatePrefetch) {
|
|
973
|
+
generators.push(new PrefetchGenerator(this.context));
|
|
974
|
+
}
|
|
975
|
+
if (this.context.config.generateServerActions && this.context.config.provider === "nextjs") {
|
|
976
|
+
generators.push(new ServerActionsGenerator(this.context));
|
|
977
|
+
}
|
|
978
|
+
if (this.context.config.generateServerQueries && this.context.config.provider === "nextjs") {
|
|
979
|
+
generators.push(new ServerQueriesGenerator(this.context));
|
|
980
|
+
}
|
|
981
|
+
return generators;
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
// src/core/codegen.ts
|
|
986
|
+
import { fileURLToPath } from "url";
|
|
987
|
+
var CodegenCore = class {
|
|
988
|
+
constructor(config) {
|
|
989
|
+
this.config = config;
|
|
990
|
+
}
|
|
991
|
+
async execute() {
|
|
992
|
+
const apiConfig = await this.loadAPIConfig();
|
|
993
|
+
this.config.apiConfig = apiConfig;
|
|
994
|
+
const generator = new CodeGenerator({
|
|
995
|
+
config: this.config,
|
|
996
|
+
apiConfig
|
|
997
|
+
});
|
|
998
|
+
await generator.generate();
|
|
999
|
+
}
|
|
1000
|
+
async loadAPIConfig() {
|
|
1001
|
+
try {
|
|
1002
|
+
const jiti = createJiti(fileURLToPath(import.meta.url), {
|
|
1003
|
+
interopDefault: true
|
|
1004
|
+
});
|
|
1005
|
+
const module = await jiti.import(this.config.endpointsPath);
|
|
1006
|
+
const apiConfig = module.apiConfig || module.default?.apiConfig || module.default || module;
|
|
1007
|
+
if (!apiConfig || !apiConfig.endpoints) {
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
'Invalid API config: must export an object with "endpoints" property'
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
return apiConfig;
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
throw new Error(
|
|
1015
|
+
`Failed to load endpoints from "${this.config.endpointsPath}": ${error instanceof Error ? error.message : String(error)}`
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
export {
|
|
1021
|
+
CodeGenerator,
|
|
1022
|
+
CodegenCore,
|
|
1023
|
+
loadConfig,
|
|
1024
|
+
validateConfig
|
|
1025
|
+
};
|
|
1026
|
+
//# sourceMappingURL=index.js.map
|