@cushin/api-codegen 1.0.0

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/dist/cli.js ADDED
@@ -0,0 +1,756 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { cosmiconfig } from 'cosmiconfig';
6
+ import path6 from 'path';
7
+ import { pathToFileURL } from 'url';
8
+ import fs5 from 'fs/promises';
9
+
10
+ var explorer = cosmiconfig("api-codegen", {
11
+ searchPlaces: [
12
+ "api-codegen.config.js",
13
+ "api-codegen.config.mjs",
14
+ "api-codegen.config.ts",
15
+ "api-codegen.config.json",
16
+ ".api-codegenrc",
17
+ ".api-codegenrc.json",
18
+ ".api-codegenrc.js"
19
+ ]
20
+ });
21
+ async function loadConfig(configPath) {
22
+ try {
23
+ const result = configPath ? await explorer.load(configPath) : await explorer.search();
24
+ if (!result || !result.config) {
25
+ return null;
26
+ }
27
+ const userConfig = result.config;
28
+ const rootDir = path6.dirname(result.filepath);
29
+ const endpointsPath = path6.resolve(rootDir, userConfig.endpoints);
30
+ const outputDir = path6.resolve(rootDir, userConfig.output);
31
+ const generateHooks = userConfig.generateHooks ?? true;
32
+ const generateServerActions = userConfig.generateServerActions ?? userConfig.provider === "nextjs";
33
+ const generateServerQueries = userConfig.generateServerQueries ?? userConfig.provider === "nextjs";
34
+ const generateClient = userConfig.generateClient ?? true;
35
+ return {
36
+ ...userConfig,
37
+ rootDir,
38
+ endpointsPath,
39
+ outputDir,
40
+ generateHooks,
41
+ generateServerActions,
42
+ generateServerQueries,
43
+ generateClient
44
+ };
45
+ } catch (error) {
46
+ throw new Error(
47
+ `Failed to load config: ${error instanceof Error ? error.message : String(error)}`
48
+ );
49
+ }
50
+ }
51
+ function validateConfig(config) {
52
+ if (!config.endpoints) {
53
+ throw new Error('Config error: "endpoints" path is required');
54
+ }
55
+ if (!config.provider) {
56
+ throw new Error('Config error: "provider" must be specified (vite or nextjs)');
57
+ }
58
+ if (!["vite", "nextjs"].includes(config.provider)) {
59
+ throw new Error('Config error: "provider" must be either "vite" or "nextjs"');
60
+ }
61
+ if (!config.output) {
62
+ throw new Error('Config error: "output" directory is required');
63
+ }
64
+ }
65
+
66
+ // src/generators/base.ts
67
+ var BaseGenerator = class {
68
+ constructor(context) {
69
+ this.context = context;
70
+ }
71
+ isQueryEndpoint(endpoint) {
72
+ return endpoint.method === "GET";
73
+ }
74
+ isMutationEndpoint(endpoint) {
75
+ return !this.isQueryEndpoint(endpoint);
76
+ }
77
+ capitalize(str) {
78
+ return str.charAt(0).toUpperCase() + str.slice(1);
79
+ }
80
+ getQueryTags(endpoint) {
81
+ return endpoint.tags || [];
82
+ }
83
+ getInvalidationTags(endpoint) {
84
+ const tags = endpoint.tags || [];
85
+ return tags.filter((tag) => tag !== "query" && tag !== "mutation");
86
+ }
87
+ hasParams(endpoint) {
88
+ return !!endpoint.params;
89
+ }
90
+ hasQuery(endpoint) {
91
+ return !!endpoint.query;
92
+ }
93
+ hasBody(endpoint) {
94
+ return !!endpoint.body;
95
+ }
96
+ getEndpointSignature(name, endpoint) {
97
+ const hasParams = this.hasParams(endpoint);
98
+ const hasQuery = this.hasQuery(endpoint);
99
+ const hasBody = this.hasBody(endpoint);
100
+ return {
101
+ hasParams,
102
+ hasQuery,
103
+ hasBody,
104
+ paramType: hasParams ? `ExtractParams<APIEndpoints['${name}']>` : "never",
105
+ queryType: hasQuery ? `ExtractQuery<APIEndpoints['${name}']>` : "never",
106
+ bodyType: hasBody ? `ExtractBody<APIEndpoints['${name}']>` : "never",
107
+ responseType: `ExtractResponse<APIEndpoints['${name}']>`
108
+ };
109
+ }
110
+ generateMutationCall(name, hasParams, hasBody) {
111
+ if (hasParams && hasBody) {
112
+ return `return apiClient.${name}(input.params, input.body);`;
113
+ } else if (hasParams) {
114
+ return `return apiClient.${name}(input);`;
115
+ } else if (hasBody) {
116
+ return `return apiClient.${name}(input);`;
117
+ } else {
118
+ return `return apiClient.${name}();`;
119
+ }
120
+ }
121
+ };
122
+
123
+ // src/generators/hooks.ts
124
+ var HooksGenerator = class extends BaseGenerator {
125
+ async generate() {
126
+ const content = this.generateContent();
127
+ const outputPath = path6.join(this.context.config.outputDir, "hooks.ts");
128
+ await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
129
+ await fs5.writeFile(outputPath, content, "utf-8");
130
+ }
131
+ generateContent() {
132
+ const useClientDirective = this.context.config.options?.useClientDirective ?? true;
133
+ const imports = `${useClientDirective ? "'use client';\n" : ""}
134
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
135
+ import type {
136
+ UseQueryOptions,
137
+ UseMutationOptions,
138
+ QueryKey
139
+ } from '@tanstack/react-query';
140
+ import { apiClient } from './client';
141
+ import type {
142
+ APIEndpoints,
143
+ ExtractBody,
144
+ ExtractParams,
145
+ ExtractQuery,
146
+ ExtractResponse
147
+ } from './types';
148
+ `;
149
+ const hooks = [];
150
+ Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
151
+ if (this.isQueryEndpoint(endpoint)) {
152
+ hooks.push(this.generateQueryHook(name, endpoint));
153
+ } else {
154
+ hooks.push(this.generateMutationHook(name, endpoint));
155
+ }
156
+ });
157
+ return imports + "\n" + hooks.join("\n\n");
158
+ }
159
+ generateQueryHook(name, endpoint) {
160
+ const hookPrefix = this.context.config.options?.hookPrefix || "use";
161
+ const hookName = `${hookPrefix}${this.capitalize(name)}`;
162
+ const signature = this.getEndpointSignature(name, endpoint);
163
+ const queryTags = this.getQueryTags(endpoint);
164
+ const paramDef = signature.hasParams ? `params: ${signature.paramType}` : "";
165
+ const queryDef = signature.hasQuery ? `query?: ${signature.queryType}` : "";
166
+ const optionsDef = `options?: Omit<UseQueryOptions<${signature.responseType}, Error, ${signature.responseType}, QueryKey>, 'queryKey' | 'queryFn'>`;
167
+ const paramsList = [paramDef, queryDef, optionsDef].filter(Boolean).join(",\n ");
168
+ const queryKeyParts = [
169
+ ...queryTags.map((tag) => `'${tag}'`),
170
+ signature.hasParams ? "params" : "undefined",
171
+ signature.hasQuery ? "query" : "undefined"
172
+ ];
173
+ const clientCallArgs = [];
174
+ if (signature.hasParams) clientCallArgs.push("params");
175
+ if (signature.hasQuery) clientCallArgs.push("query");
176
+ return `/**
177
+ * ${endpoint.description || `Query hook for ${name}`}
178
+ * @tags ${queryTags.join(", ") || "none"}
179
+ */
180
+ export function ${hookName}(
181
+ ${paramsList}
182
+ ) {
183
+ return useQuery({
184
+ queryKey: [${queryKeyParts.join(", ")}] as const,
185
+ queryFn: () => apiClient.${name}(${clientCallArgs.join(", ")}),
186
+ ...options,
187
+ });
188
+ }`;
189
+ }
190
+ generateMutationHook(name, endpoint) {
191
+ const hookPrefix = this.context.config.options?.hookPrefix || "use";
192
+ const hookName = `${hookPrefix}${this.capitalize(name)}`;
193
+ const signature = this.getEndpointSignature(name, endpoint);
194
+ const invalidationTags = this.getInvalidationTags(endpoint);
195
+ let inputType = "void";
196
+ if (signature.hasParams && signature.hasBody) {
197
+ inputType = `{ params: ${signature.paramType}; body: ${signature.bodyType} }`;
198
+ } else if (signature.hasParams) {
199
+ inputType = signature.paramType;
200
+ } else if (signature.hasBody) {
201
+ inputType = signature.bodyType;
202
+ }
203
+ const invalidationQueries = invalidationTags.length > 0 ? invalidationTags.map((tag) => ` queryClient.invalidateQueries({ queryKey: ['${tag}'] });`).join("\n") : " // No automatic invalidations";
204
+ return `/**
205
+ * ${endpoint.description || `Mutation hook for ${name}`}
206
+ * @tags ${endpoint.tags?.join(", ") || "none"}
207
+ */
208
+ export function ${hookName}(
209
+ options?: Omit<UseMutationOptions<${signature.responseType}, Error, ${inputType}>, 'mutationFn'>
210
+ ) {
211
+ const queryClient = useQueryClient();
212
+
213
+ return useMutation({
214
+ mutationFn: ${inputType === "void" ? "() => {" : "(input) => {"}
215
+ ${this.generateMutationCall(name, signature.hasParams, signature.hasBody)}
216
+ },
217
+ onSuccess: (data, variables, context) => {
218
+ // Invalidate related queries
219
+ ${invalidationQueries}
220
+
221
+ // Call user's onSuccess if provided
222
+ options?.onSuccess?.(data, variables, context);
223
+ },
224
+ ...options,
225
+ });
226
+ }`;
227
+ }
228
+ };
229
+ var ServerActionsGenerator = class extends BaseGenerator {
230
+ async generate() {
231
+ const content = this.generateContent();
232
+ const outputPath = path6.join(this.context.config.outputDir, "actions.ts");
233
+ await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
234
+ await fs5.writeFile(outputPath, content, "utf-8");
235
+ }
236
+ generateContent() {
237
+ const imports = `'use server';
238
+
239
+ import { revalidateTag, revalidatePath } from 'next/cache';
240
+ import { serverClient } from './server-client';
241
+ import type {
242
+ APIEndpoints,
243
+ ExtractBody,
244
+ ExtractParams,
245
+ ExtractResponse
246
+ } from './types';
247
+
248
+ export type ActionResult<T> =
249
+ | { success: true; data: T }
250
+ | { success: false; error: string };
251
+ `;
252
+ const actions = [];
253
+ Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
254
+ if (this.isMutationEndpoint(endpoint)) {
255
+ actions.push(this.generateServerAction(name, endpoint));
256
+ }
257
+ });
258
+ return imports + "\n" + actions.join("\n\n");
259
+ }
260
+ generateServerAction(name, endpoint) {
261
+ const actionSuffix = this.context.config.options?.actionSuffix || "Action";
262
+ const actionName = `${name}${actionSuffix}`;
263
+ const signature = this.getEndpointSignature(name, endpoint);
264
+ const invalidationTags = this.getInvalidationTags(endpoint);
265
+ let inputType = "";
266
+ let inputParam = "";
267
+ if (signature.hasParams && signature.hasBody) {
268
+ inputType = `input: { params: ${signature.paramType}; body: ${signature.bodyType} }`;
269
+ inputParam = "input";
270
+ } else if (signature.hasParams) {
271
+ inputType = `params: ${signature.paramType}`;
272
+ inputParam = "params";
273
+ } else if (signature.hasBody) {
274
+ inputType = `body: ${signature.bodyType}`;
275
+ inputParam = "body";
276
+ }
277
+ const revalidateStatements = invalidationTags.length > 0 ? invalidationTags.map((tag) => ` revalidateTag('${tag}');`).join("\n") : " // No automatic revalidations";
278
+ return `/**
279
+ * ${endpoint.description || `Server action for ${name}`}
280
+ * @tags ${endpoint.tags?.join(", ") || "none"}
281
+ */
282
+ export async function ${actionName}(
283
+ ${inputType}
284
+ ): Promise<ActionResult<${signature.responseType}>> {
285
+ try {
286
+ const result = await serverClient.${name}(${inputParam ? inputParam : ""});
287
+
288
+ // Revalidate related data
289
+ ${revalidateStatements}
290
+
291
+ return { success: true, data: result };
292
+ } catch (error) {
293
+ console.error('[Server Action Error]:', error);
294
+ return {
295
+ success: false,
296
+ error: error instanceof Error ? error.message : 'Unknown error'
297
+ };
298
+ }
299
+ }`;
300
+ }
301
+ };
302
+ var ServerQueriesGenerator = class extends BaseGenerator {
303
+ async generate() {
304
+ const content = this.generateContent();
305
+ const outputPath = path6.join(this.context.config.outputDir, "queries.ts");
306
+ await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
307
+ await fs5.writeFile(outputPath, content, "utf-8");
308
+ }
309
+ generateContent() {
310
+ const imports = `import { cache } from 'react';
311
+ import { unstable_cache } from 'next/cache';
312
+ import { serverClient } from './server-client';
313
+ import type {
314
+ APIEndpoints,
315
+ ExtractParams,
316
+ ExtractQuery,
317
+ ExtractResponse
318
+ } from './types';
319
+ `;
320
+ const queries = [];
321
+ Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
322
+ if (this.isQueryEndpoint(endpoint)) {
323
+ queries.push(this.generateServerQuery(name, endpoint));
324
+ }
325
+ });
326
+ return imports + "\n" + queries.join("\n\n");
327
+ }
328
+ generateServerQuery(name, endpoint) {
329
+ const queryName = `${name}Query`;
330
+ const signature = this.getEndpointSignature(name, endpoint);
331
+ const queryTags = this.getQueryTags(endpoint);
332
+ const paramDef = signature.hasParams ? `params: ${signature.paramType}` : "";
333
+ const queryDef = signature.hasQuery ? `query?: ${signature.queryType}` : "";
334
+ const paramsList = [paramDef, queryDef].filter(Boolean).join(",\n ");
335
+ const clientCallArgs = [];
336
+ if (signature.hasParams) clientCallArgs.push("params");
337
+ if (signature.hasQuery) clientCallArgs.push("query");
338
+ const cacheKeyParts = [
339
+ `'${name}'`
340
+ ];
341
+ if (signature.hasParams) cacheKeyParts.push("JSON.stringify(params)");
342
+ if (signature.hasQuery) cacheKeyParts.push("JSON.stringify(query)");
343
+ return `/**
344
+ * ${endpoint.description || `Server query for ${name}`}
345
+ * @tags ${queryTags.join(", ") || "none"}
346
+ */
347
+ export const ${queryName} = cache(async (
348
+ ${paramsList}
349
+ ): Promise<${signature.responseType}> => {
350
+ return unstable_cache(
351
+ async () => serverClient.${name}(${clientCallArgs.join(", ")}),
352
+ [${cacheKeyParts.join(", ")}],
353
+ {
354
+ tags: [${queryTags.map((tag) => `'${tag}'`).join(", ")}],
355
+ revalidate: 3600, // 1 hour default, can be overridden
356
+ }
357
+ )();
358
+ });`;
359
+ }
360
+ };
361
+ var TypesGenerator = class extends BaseGenerator {
362
+ async generate() {
363
+ const content = this.generateContent();
364
+ const outputPath = path6.join(this.context.config.outputDir, "types.ts");
365
+ await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
366
+ await fs5.writeFile(outputPath, content, "utf-8");
367
+ }
368
+ generateContent() {
369
+ return `// Auto-generated type definitions
370
+ // Do not edit this file manually
371
+
372
+ import type { z } from 'zod';
373
+
374
+ // Re-export endpoint configuration types
375
+ export type { APIConfig, APIEndpoint, HTTPMethod } from '@vietbus/api-codegen/config';
376
+
377
+ /**
378
+ * Type helper to extract params schema from an endpoint
379
+ */
380
+ export type ExtractParams<T> = T extends { params: infer P extends z.ZodType }
381
+ ? z.infer<P>
382
+ : never;
383
+
384
+ /**
385
+ * Type helper to extract query schema from an endpoint
386
+ */
387
+ export type ExtractQuery<T> = T extends { query: infer Q extends z.ZodType }
388
+ ? z.infer<Q>
389
+ : never;
390
+
391
+ /**
392
+ * Type helper to extract body schema from an endpoint
393
+ */
394
+ export type ExtractBody<T> = T extends { body: infer B extends z.ZodType }
395
+ ? z.infer<B>
396
+ : never;
397
+
398
+ /**
399
+ * Type helper to extract response schema from an endpoint
400
+ */
401
+ export type ExtractResponse<T> = T extends { response: infer R extends z.ZodType }
402
+ ? z.infer<R>
403
+ : never;
404
+
405
+ /**
406
+ * Import your API config to get typed endpoints
407
+ *
408
+ * @example
409
+ * import { apiConfig } from './config/endpoints';
410
+ * export type APIEndpoints = typeof apiConfig.endpoints;
411
+ */
412
+ export type APIEndpoints = Record<string, any>;
413
+ `;
414
+ }
415
+ };
416
+ var ClientGenerator = class extends BaseGenerator {
417
+ async generate() {
418
+ await this.generateClientFile();
419
+ if (this.context.config.provider === "nextjs") {
420
+ await this.generateServerClientFile();
421
+ }
422
+ }
423
+ async generateClientFile() {
424
+ const content = this.generateClientContent();
425
+ const outputPath = path6.join(this.context.config.outputDir, "client.ts");
426
+ await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
427
+ await fs5.writeFile(outputPath, content, "utf-8");
428
+ }
429
+ async generateServerClientFile() {
430
+ const content = this.generateServerClientContent();
431
+ const outputPath = path6.join(this.context.config.outputDir, "server-client.ts");
432
+ await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
433
+ await fs5.writeFile(outputPath, content, "utf-8");
434
+ }
435
+ generateClientContent() {
436
+ const useClientDirective = this.context.config.options?.useClientDirective ?? true;
437
+ return `${useClientDirective ? "'use client';\n" : ""}
438
+ import { createAPIClient } from '@cushin/api-codegen/client';
439
+ import type { AuthCallbacks } from '@cushin/api-codegen/client';
440
+ import { apiConfig } from '../config/endpoints';
441
+ import type { APIEndpoints } from './types';
442
+
443
+ // Type-safe API client methods
444
+ type APIClientMethods = {
445
+ [K in keyof APIEndpoints]: APIEndpoints[K] extends {
446
+ method: infer M;
447
+ params?: infer P;
448
+ query?: infer Q;
449
+ body?: infer B;
450
+ response: infer R;
451
+ }
452
+ ? M extends 'GET'
453
+ ? P extends { _type: any }
454
+ ? Q extends { _type: any }
455
+ ? (params: P['_type'], query?: Q['_type']) => Promise<R['_type']>
456
+ : (params: P['_type']) => Promise<R['_type']>
457
+ : Q extends { _type: any }
458
+ ? (query?: Q['_type']) => Promise<R['_type']>
459
+ : () => Promise<R['_type']>
460
+ : P extends { _type: any }
461
+ ? B extends { _type: any }
462
+ ? (params: P['_type'], body: B['_type']) => Promise<R['_type']>
463
+ : (params: P['_type']) => Promise<R['_type']>
464
+ : B extends { _type: any }
465
+ ? (body: B['_type']) => Promise<R['_type']>
466
+ : () => Promise<R['_type']>
467
+ : never;
468
+ };
469
+
470
+ // Export singleton instance (will be initialized later)
471
+ export let apiClient: APIClientMethods & {
472
+ refreshAuth: () => Promise<void>;
473
+ updateAuthCallbacks: (callbacks: AuthCallbacks) => void;
474
+ };
475
+
476
+ /**
477
+ * Initialize API client with auth callbacks
478
+ * Call this function in your auth provider setup
479
+ *
480
+ * @example
481
+ * const authCallbacks = {
482
+ * getTokens: () => getStoredTokens(),
483
+ * setTokens: (tokens) => storeTokens(tokens),
484
+ * clearTokens: () => clearStoredTokens(),
485
+ * onAuthError: () => router.push('/login'),
486
+ * onRefreshToken: async () => {
487
+ * const newToken = await refreshAccessToken();
488
+ * return newToken;
489
+ * },
490
+ * };
491
+ *
492
+ * initializeAPIClient(authCallbacks);
493
+ */
494
+ export const initializeAPIClient = (authCallbacks: AuthCallbacks) => {
495
+ apiClient = createAPIClient(apiConfig, authCallbacks) as any;
496
+ return apiClient;
497
+ };
498
+
499
+ // Export for custom usage
500
+ export { createAPIClient };
501
+ export type { AuthCallbacks };
502
+ `;
503
+ }
504
+ generateServerClientContent() {
505
+ return `import { createAPIClient } from '@cushin/api-codegen/client';
506
+ import { apiConfig } from '../config/endpoints';
507
+ import type { APIEndpoints } from './types';
508
+
509
+ // Type-safe API client methods for server-side
510
+ type APIClientMethods = {
511
+ [K in keyof APIEndpoints]: APIEndpoints[K] extends {
512
+ method: infer M;
513
+ params?: infer P;
514
+ query?: infer Q;
515
+ body?: infer B;
516
+ response: infer R;
517
+ }
518
+ ? M extends 'GET'
519
+ ? P extends { _type: any }
520
+ ? Q extends { _type: any }
521
+ ? (params: P['_type'], query?: Q['_type']) => Promise<R['_type']>
522
+ : (params: P['_type']) => Promise<R['_type']>
523
+ : Q extends { _type: any }
524
+ ? (query?: Q['_type']) => Promise<R['_type']>
525
+ : () => Promise<R['_type']>
526
+ : P extends { _type: any }
527
+ ? B extends { _type: any }
528
+ ? (params: P['_type'], body: B['_type']) => Promise<R['_type']>
529
+ : (params: P['_type']) => Promise<R['_type']>
530
+ : B extends { _type: any }
531
+ ? (body: B['_type']) => Promise<R['_type']>
532
+ : () => Promise<R['_type']>
533
+ : never;
534
+ };
535
+
536
+ /**
537
+ * Server-side API client (no auth, direct API calls)
538
+ * Use this in Server Components, Server Actions, and Route Handlers
539
+ */
540
+ export const serverClient = createAPIClient(apiConfig) as APIClientMethods;
541
+ `;
542
+ }
543
+ };
544
+
545
+ // src/generators/index.ts
546
+ var CodeGenerator = class {
547
+ constructor(context) {
548
+ this.context = context;
549
+ }
550
+ async generate() {
551
+ const generators = this.getGenerators();
552
+ for (const generator of generators) {
553
+ await generator.generate();
554
+ }
555
+ }
556
+ getGenerators() {
557
+ const generators = [];
558
+ generators.push(new TypesGenerator(this.context));
559
+ if (this.context.config.generateClient) {
560
+ generators.push(new ClientGenerator(this.context));
561
+ }
562
+ if (this.context.config.generateHooks) {
563
+ generators.push(new HooksGenerator(this.context));
564
+ }
565
+ if (this.context.config.generateServerActions && this.context.config.provider === "nextjs") {
566
+ generators.push(new ServerActionsGenerator(this.context));
567
+ }
568
+ if (this.context.config.generateServerQueries && this.context.config.provider === "nextjs") {
569
+ generators.push(new ServerQueriesGenerator(this.context));
570
+ }
571
+ return generators;
572
+ }
573
+ };
574
+
575
+ // src/core/codegen.ts
576
+ var CodegenCore = class {
577
+ constructor(config) {
578
+ this.config = config;
579
+ }
580
+ async execute() {
581
+ const apiConfig = await this.loadAPIConfig();
582
+ this.config.apiConfig = apiConfig;
583
+ const generator = new CodeGenerator({
584
+ config: this.config,
585
+ apiConfig
586
+ });
587
+ await generator.generate();
588
+ }
589
+ async loadAPIConfig() {
590
+ try {
591
+ const fileUrl = pathToFileURL(this.config.endpointsPath).href;
592
+ const module = await import(fileUrl);
593
+ const apiConfig = module.apiConfig || module.default?.apiConfig || module.default || module;
594
+ if (!apiConfig || !apiConfig.endpoints) {
595
+ throw new Error(
596
+ 'Invalid API config: must export an object with "endpoints" property'
597
+ );
598
+ }
599
+ return apiConfig;
600
+ } catch (error) {
601
+ throw new Error(
602
+ `Failed to load endpoints from "${this.config.endpointsPath}": ${error instanceof Error ? error.message : String(error)}`
603
+ );
604
+ }
605
+ }
606
+ };
607
+ var program = new Command();
608
+ program.name("api-codegen").description("Generate type-safe API client code from endpoint definitions").version("1.0.0");
609
+ program.command("generate").alias("gen").description("Generate API client code from configuration").option("-c, --config <path>", "Path to configuration file").option("-w, --watch", "Watch for changes and regenerate").action(async (options) => {
610
+ const spinner = ora("Loading configuration...").start();
611
+ try {
612
+ const config = await loadConfig(options.config);
613
+ if (!config) {
614
+ spinner.fail(
615
+ chalk.red(
616
+ "No configuration file found. Please create an api-codegen.config.js file."
617
+ )
618
+ );
619
+ process.exit(1);
620
+ }
621
+ spinner.text = "Validating configuration...";
622
+ validateConfig(config);
623
+ spinner.text = "Loading API endpoints...";
624
+ const codegen = new CodegenCore(config);
625
+ spinner.text = "Generating code...";
626
+ await codegen.execute();
627
+ spinner.succeed(
628
+ chalk.green(
629
+ `\u2728 Code generated successfully in ${chalk.cyan(config.outputDir)}`
630
+ )
631
+ );
632
+ console.log(chalk.dim("\nGenerated files:"));
633
+ const files = await fs5.readdir(config.outputDir);
634
+ files.forEach((file) => {
635
+ console.log(chalk.dim(` \u2022 ${file}`));
636
+ });
637
+ if (options.watch) {
638
+ console.log(chalk.yellow("\n\u{1F440} Watching for changes..."));
639
+ spinner.info(chalk.dim("Watch mode not yet implemented"));
640
+ }
641
+ } catch (error) {
642
+ spinner.fail(chalk.red("Failed to generate code"));
643
+ console.error(
644
+ chalk.red("\n" + (error instanceof Error ? error.message : String(error)))
645
+ );
646
+ if (error instanceof Error && error.stack) {
647
+ console.error(chalk.dim(error.stack));
648
+ }
649
+ process.exit(1);
650
+ }
651
+ });
652
+ program.command("init").description("Initialize a new api-codegen configuration").option("-p, --provider <provider>", "Provider type (vite or nextjs)", "vite").action(async (options) => {
653
+ const spinner = ora("Creating configuration file...").start();
654
+ try {
655
+ const configContent = generateConfigTemplate(options.provider);
656
+ const configPath = path6.join(process.cwd(), "api-codegen.config.js");
657
+ try {
658
+ await fs5.access(configPath);
659
+ spinner.warn(
660
+ chalk.yellow(
661
+ "Configuration file already exists at api-codegen.config.js"
662
+ )
663
+ );
664
+ return;
665
+ } catch {
666
+ }
667
+ await fs5.writeFile(configPath, configContent, "utf-8");
668
+ spinner.succeed(
669
+ chalk.green("\u2728 Configuration file created: api-codegen.config.js")
670
+ );
671
+ console.log(chalk.dim("\nNext steps:"));
672
+ console.log(chalk.dim(" 1. Update the endpoints path in the config"));
673
+ console.log(chalk.dim(" 2. Run: npx api-codegen generate"));
674
+ } catch (error) {
675
+ spinner.fail(chalk.red("Failed to create configuration file"));
676
+ console.error(
677
+ chalk.red("\n" + (error instanceof Error ? error.message : String(error)))
678
+ );
679
+ process.exit(1);
680
+ }
681
+ });
682
+ program.command("validate").description("Validate your API endpoints configuration").option("-c, --config <path>", "Path to configuration file").action(async (options) => {
683
+ const spinner = ora("Loading configuration...").start();
684
+ try {
685
+ const config = await loadConfig(options.config);
686
+ if (!config) {
687
+ spinner.fail(chalk.red("No configuration file found"));
688
+ process.exit(1);
689
+ }
690
+ spinner.text = "Validating configuration...";
691
+ validateConfig(config);
692
+ spinner.text = "Loading API endpoints...";
693
+ new CodegenCore(config);
694
+ const apiConfigModule = await import(pathToFileURL2(config.endpointsPath).href);
695
+ const apiConfig = apiConfigModule.apiConfig || apiConfigModule.default?.apiConfig || apiConfigModule.default;
696
+ if (!apiConfig || !apiConfig.endpoints) {
697
+ throw new Error("Invalid endpoints configuration");
698
+ }
699
+ const endpointCount = Object.keys(apiConfig.endpoints).length;
700
+ spinner.succeed(
701
+ chalk.green(
702
+ `\u2728 Configuration is valid! Found ${endpointCount} endpoint${endpointCount === 1 ? "" : "s"}`
703
+ )
704
+ );
705
+ console.log(chalk.dim("\nEndpoints:"));
706
+ Object.entries(apiConfig.endpoints).forEach(([name, endpoint]) => {
707
+ console.log(
708
+ chalk.dim(
709
+ ` \u2022 ${chalk.cyan(name)}: ${endpoint.method} ${endpoint.path}`
710
+ )
711
+ );
712
+ });
713
+ } catch (error) {
714
+ spinner.fail(chalk.red("Validation failed"));
715
+ console.error(
716
+ chalk.red("\n" + (error instanceof Error ? error.message : String(error)))
717
+ );
718
+ process.exit(1);
719
+ }
720
+ });
721
+ function generateConfigTemplate(provider) {
722
+ return `/** @type {import('@cushin/api-codegen').UserConfig} */
723
+ export default {
724
+ // Provider: 'vite' or 'nextjs'
725
+ provider: '${provider}',
726
+
727
+ // Path to your API endpoints configuration
728
+ endpoints: './lib/api/config/endpoints.ts',
729
+
730
+ // Output directory for generated files
731
+ output: './lib/api/generated',
732
+
733
+ // Base URL for API requests (optional, can be set at runtime)
734
+ baseUrl: process.env.VITE_API_URL || process.env.NEXT_PUBLIC_API_URL,
735
+
736
+ // Generation options
737
+ generateHooks: true,
738
+ generateClient: true,
739
+ ${provider === "nextjs" ? `generateServerActions: true,
740
+ generateServerQueries: true,` : ""}
741
+
742
+ // Advanced options
743
+ options: {
744
+ useClientDirective: true,
745
+ hookPrefix: 'use',
746
+ actionSuffix: 'Action',
747
+ },
748
+ };
749
+ `;
750
+ }
751
+ function pathToFileURL2(filePath) {
752
+ return new URL(`file://${path6.resolve(filePath)}`);
753
+ }
754
+ program.parse();
755
+ //# sourceMappingURL=cli.js.map
756
+ //# sourceMappingURL=cli.js.map