@cushin/api-codegen 1.0.2 → 1.0.4

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/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { cosmiconfig } from 'cosmiconfig';
2
2
  import path6 from 'path';
3
3
  import ky, { HTTPError } from 'ky';
4
- import { pathToFileURL } from 'url';
4
+ import { createJiti } from 'jiti';
5
5
  import fs5 from 'fs/promises';
6
+ import { fileURLToPath } from 'url';
6
7
 
7
8
  // src/config/schema.ts
8
9
  function defineConfig(config) {
@@ -183,9 +184,9 @@ var APIClient = class {
183
184
  })();
184
185
  return this.refreshPromise;
185
186
  }
186
- buildPath(path7, params) {
187
- if (!params) return path7;
188
- let finalPath = path7;
187
+ buildPath(path9, params) {
188
+ if (!params) return path9;
189
+ let finalPath = path9;
189
190
  Object.entries(params).forEach(([key, value]) => {
190
191
  finalPath = finalPath.replace(
191
192
  `:${key}`,
@@ -217,7 +218,7 @@ var APIClient = class {
217
218
  }
218
219
  async request(endpoint, params, query, body) {
219
220
  try {
220
- const path7 = this.buildPath(endpoint.path, params);
221
+ const path9 = this.buildPath(endpoint.path, params);
221
222
  const client = this.getClientForEndpoint(endpoint);
222
223
  const options = {
223
224
  method: endpoint.method
@@ -241,7 +242,7 @@ var APIClient = class {
241
242
  options.json = body;
242
243
  }
243
244
  }
244
- const response = await client(path7, options);
245
+ const response = await client(path9, options);
245
246
  const data = await response.json();
246
247
  if (endpoint.response) {
247
248
  return endpoint.response.parse(data);
@@ -383,6 +384,49 @@ var BaseGenerator = class {
383
384
  return `return apiClient.${name}();`;
384
385
  }
385
386
  }
387
+ inferNonNull(expr) {
388
+ return `z.infer<NonNullable<${expr}>>`;
389
+ }
390
+ toCamelCase(str) {
391
+ return str.toLowerCase().replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toLowerCase());
392
+ }
393
+ getResourceFromEndpoint(_name, endpoint) {
394
+ const tag = endpoint.tags?.find((t) => t !== "query" && t !== "mutation");
395
+ if (tag) return this.toCamelCase(tag);
396
+ const match = endpoint.path.match(/^\/([^/]+)/);
397
+ return match ? this.toCamelCase(match[1]) : "general";
398
+ }
399
+ groupEndpointsByResource() {
400
+ const groups = {};
401
+ Object.entries(this.context.apiConfig.endpoints).forEach(
402
+ ([name, endpoint]) => {
403
+ const res = this.getResourceFromEndpoint(name, endpoint);
404
+ if (!groups[res]) groups[res] = [];
405
+ groups[res].push({ name, endpoint });
406
+ }
407
+ );
408
+ return groups;
409
+ }
410
+ resourceHasQueryEndpoints(resource) {
411
+ return this.groupEndpointsByResource()[resource]?.some(
412
+ ({ endpoint }) => endpoint.method === "GET"
413
+ ) ?? false;
414
+ }
415
+ getEndpointKeyName(name) {
416
+ return name.startsWith("get") ? name[3].toLowerCase() + name.slice(4) : name;
417
+ }
418
+ generateQueryKeyCall(resource, name, endpoint) {
419
+ const key = this.getEndpointKeyName(name);
420
+ const args = [];
421
+ if (endpoint.params) args.push("params");
422
+ if (endpoint.query) args.push("filters");
423
+ return args.length ? `queryKeys.${resource}.${key}(${args.join(", ")})` : `queryKeys.${resource}.${key}()`;
424
+ }
425
+ hasQueryOptions() {
426
+ return Object.values(this.context.apiConfig.endpoints).some(
427
+ (e) => e.method === "GET"
428
+ );
429
+ }
386
430
  };
387
431
 
388
432
  // src/generators/hooks.ts
@@ -395,7 +439,10 @@ var HooksGenerator = class extends BaseGenerator {
395
439
  }
396
440
  generateContent() {
397
441
  const useClientDirective = this.context.config.options?.useClientDirective ?? true;
398
- const imports = `${useClientDirective ? "'use client';\n" : ""}
442
+ const outputPath = path6.join(this.context.config.outputDir, "types.ts");
443
+ const endpointsPath = path6.join(this.context.config.endpointsPath);
444
+ const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
445
+ const content = `${useClientDirective ? "'use client';\n" : ""}
399
446
  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
400
447
  import type {
401
448
  UseQueryOptions,
@@ -410,85 +457,126 @@ import type {
410
457
  ExtractQuery,
411
458
  ExtractResponse
412
459
  } from './types';
460
+ import { queryKeys } from './query-keys';
461
+ ${this.hasQueryOptions() ? "import { apiQueryOptions } from './query-options';" : ""}
462
+ import { z } from 'zod';
463
+ import { apiConfig } from '${relativePath}';
464
+
465
+ ${this.generateQueryHooks()}
466
+ ${this.generateMutationHooks()}
413
467
  `;
468
+ return content;
469
+ }
470
+ generateQueryHooks() {
414
471
  const hooks = [];
415
- Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
416
- if (this.isQueryEndpoint(endpoint)) {
417
- hooks.push(this.generateQueryHook(name, endpoint));
418
- } else {
419
- hooks.push(this.generateMutationHook(name, endpoint));
472
+ Object.entries(this.context.apiConfig.endpoints).forEach(
473
+ ([name, endpoint]) => {
474
+ if (endpoint.method === "GET")
475
+ hooks.push(this.generateQueryHook(name, endpoint));
420
476
  }
421
- });
422
- return imports + "\n" + hooks.join("\n\n");
477
+ );
478
+ return hooks.join("\n\n");
423
479
  }
424
480
  generateQueryHook(name, endpoint) {
425
- const hookPrefix = this.context.config.options?.hookPrefix || "use";
426
- const hookName = `${hookPrefix}${this.capitalize(name)}`;
427
- const signature = this.getEndpointSignature(name, endpoint);
481
+ const hookName = `use${this.capitalize(name)}`;
482
+ const resource = this.getResourceFromEndpoint(name, endpoint);
483
+ const optionName = this.getEndpointKeyName(name);
484
+ const inferParams = this.inferNonNull(
485
+ `typeof apiConfig.endpoints.${name}.params`
486
+ );
487
+ const inferQuery = this.inferNonNull(
488
+ `typeof apiConfig.endpoints.${name}.query`
489
+ );
490
+ const inferResponse = this.inferNonNull(
491
+ `typeof apiConfig.endpoints.${name}.response`
492
+ );
493
+ const params = [];
494
+ const optionParams = [];
428
495
  const queryTags = this.getQueryTags(endpoint);
429
- const paramDef = signature.hasParams ? `params: ${signature.paramType}` : "";
430
- const queryDef = signature.hasQuery ? `query?: ${signature.queryType}` : "";
431
- const optionsDef = `options?: Omit<UseQueryOptions<${signature.responseType}, Error, ${signature.responseType}, QueryKey>, 'queryKey' | 'queryFn'>`;
432
- const paramsList = [paramDef, queryDef, optionsDef].filter(Boolean).join(",\n ");
433
- const queryKeyParts = [
434
- ...queryTags.map((tag) => `'${tag}'`),
435
- signature.hasParams ? "params" : "undefined",
436
- signature.hasQuery ? "query" : "undefined"
437
- ];
438
- const clientCallArgs = [];
439
- if (signature.hasParams) clientCallArgs.push("params");
440
- if (signature.hasQuery) clientCallArgs.push("query");
496
+ if (endpoint.params) {
497
+ params.push(`params: ${inferParams}`);
498
+ optionParams.push("params");
499
+ }
500
+ if (endpoint.query) {
501
+ params.push(`filters?: ${inferQuery}`);
502
+ optionParams.push("filters");
503
+ }
504
+ params.push(`options?: {
505
+ enabled?: boolean;
506
+ select?: <TData = ${inferResponse}>(data: ${inferResponse}) => TData;
507
+ }`);
441
508
  return `/**
442
509
  * ${endpoint.description || `Query hook for ${name}`}
443
510
  * @tags ${queryTags.join(", ") || "none"}
444
511
  */
445
- export function ${hookName}(
446
- ${paramsList}
447
- ) {
512
+ export function ${hookName}(${params.join(",\n ")}) {
448
513
  return useQuery({
449
- queryKey: [${queryKeyParts.join(", ")}] as const,
450
- queryFn: () => apiClient.${name}(${clientCallArgs.join(", ")}),
514
+ ...apiQueryOptions.${resource}.${optionName}(${optionParams.join(", ")}),
451
515
  ...options,
452
516
  });
453
517
  }`;
454
518
  }
519
+ generateMutationHooks() {
520
+ const hooks = [];
521
+ Object.entries(this.context.apiConfig.endpoints).forEach(
522
+ ([name, endpoint]) => {
523
+ if (endpoint.method !== "GET")
524
+ hooks.push(this.generateMutationHook(name, endpoint));
525
+ }
526
+ );
527
+ return hooks.join("\n\n");
528
+ }
455
529
  generateMutationHook(name, endpoint) {
456
- const hookPrefix = this.context.config.options?.hookPrefix || "use";
457
- const hookName = `${hookPrefix}${this.capitalize(name)}`;
458
- const signature = this.getEndpointSignature(name, endpoint);
459
- const invalidationTags = this.getInvalidationTags(endpoint);
460
- let inputType = "void";
461
- if (signature.hasParams && signature.hasBody) {
462
- inputType = `{ params: ${signature.paramType}; body: ${signature.bodyType} }`;
463
- } else if (signature.hasParams) {
464
- inputType = signature.paramType;
465
- } else if (signature.hasBody) {
466
- inputType = signature.bodyType;
530
+ const hookName = `use${this.capitalize(name)}`;
531
+ const resource = this.getResourceFromEndpoint(name, endpoint);
532
+ const inferParams = this.inferNonNull(
533
+ `typeof apiConfig.endpoints.${name}.params`
534
+ );
535
+ const inferBody = this.inferNonNull(
536
+ `typeof apiConfig.endpoints.${name}.body`
537
+ );
538
+ const inferResponse = this.inferNonNull(
539
+ `typeof apiConfig.endpoints.${name}.response`
540
+ );
541
+ const resourceHasQueries = this.resourceHasQueryEndpoints(resource);
542
+ let inputType;
543
+ let fnBody;
544
+ if (endpoint.params && endpoint.body) {
545
+ inputType = `{ params: ${inferParams}; body: ${inferBody}; }`;
546
+ fnBody = `({ params, body }: ${inputType}) => apiClient.${name}(params, body)`;
547
+ } else if (endpoint.params) {
548
+ inputType = `${inferParams}`;
549
+ fnBody = `(params: ${inputType}) => apiClient.${name}(params)`;
550
+ } else if (endpoint.body) {
551
+ inputType = `${inferBody}`;
552
+ fnBody = `(body: ${inputType}) => apiClient.${name}(body)`;
553
+ } else {
554
+ inputType = "void";
555
+ fnBody = `() => apiClient.${name}()`;
467
556
  }
468
- const invalidationQueries = invalidationTags.length > 0 ? invalidationTags.map((tag) => ` queryClient.invalidateQueries({ queryKey: ['${tag}'] });`).join("\n") : " // No automatic invalidations";
557
+ const invalidate = resourceHasQueries ? `queryClient.invalidateQueries({ queryKey: queryKeys.${resource}.all });` : "";
469
558
  return `/**
470
559
  * ${endpoint.description || `Mutation hook for ${name}`}
471
560
  * @tags ${endpoint.tags?.join(", ") || "none"}
472
561
  */
473
- export function ${hookName}(
474
- options?: Omit<UseMutationOptions<${signature.responseType}, Error, ${inputType}>, 'mutationFn'>
475
- ) {
476
- const queryClient = useQueryClient();
477
-
478
- return useMutation({
479
- mutationFn: ${inputType === "void" ? "() => {" : "(input) => {"}
480
- ${this.generateMutationCall(name, signature.hasParams, signature.hasBody)}
481
- },
482
- onSuccess: (data, variables, context) => {
483
- // Invalidate related queries
484
- ${invalidationQueries}
485
-
486
- // Call user's onSuccess if provided
487
- options?.onSuccess?.(data, variables, context);
488
- },
489
- ...options,
490
- });
491
- }`;
562
+ export function ${hookName}(options?: {
563
+ onSuccess?: (data: ${inferResponse}, variables: ${inputType}, context: unknown) => void;
564
+ onError?: (error: Error, variables: ${inputType}, context: unknown) => void;
565
+ onSettled?: (data: ${inferResponse} | undefined, error: Error | null, variables: ${inputType}, context: unknown) => void;
566
+ onMutate?: (variables: ${inputType}) => Promise<unknown> | unknown;
567
+ }) {
568
+ ${invalidate ? "const queryClient = useQueryClient();" : ""}
569
+ return useMutation({
570
+ mutationFn: ${fnBody},
571
+ onSuccess: (data, variables, context) => {
572
+ ${invalidate}
573
+ options?.onSuccess?.(data, variables, context);
574
+ },
575
+ onError: options?.onError,
576
+ onSettled: options?.onSettled,
577
+ onMutate: options?.onMutate,
578
+ });
579
+ }`;
492
580
  }
493
581
  };
494
582
  var ServerActionsGenerator = class extends BaseGenerator {
@@ -583,11 +671,13 @@ import type {
583
671
  } from './types';
584
672
  `;
585
673
  const queries = [];
586
- Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
587
- if (this.isQueryEndpoint(endpoint)) {
588
- queries.push(this.generateServerQuery(name, endpoint));
674
+ Object.entries(this.context.apiConfig.endpoints).forEach(
675
+ ([name, endpoint]) => {
676
+ if (this.isQueryEndpoint(endpoint)) {
677
+ queries.push(this.generateServerQuery(name, endpoint));
678
+ }
589
679
  }
590
- });
680
+ );
591
681
  return imports + "\n" + queries.join("\n\n");
592
682
  }
593
683
  generateServerQuery(name, endpoint) {
@@ -600,9 +690,7 @@ import type {
600
690
  const clientCallArgs = [];
601
691
  if (signature.hasParams) clientCallArgs.push("params");
602
692
  if (signature.hasQuery) clientCallArgs.push("query");
603
- const cacheKeyParts = [
604
- `'${name}'`
605
- ];
693
+ const cacheKeyParts = [`'${name}'`];
606
694
  if (signature.hasParams) cacheKeyParts.push("JSON.stringify(params)");
607
695
  if (signature.hasQuery) cacheKeyParts.push("JSON.stringify(query)");
608
696
  return `/**
@@ -631,13 +719,18 @@ var TypesGenerator = class extends BaseGenerator {
631
719
  await fs5.writeFile(outputPath, content, "utf-8");
632
720
  }
633
721
  generateContent() {
722
+ const outputPath = path6.join(this.context.config.outputDir, "types.ts");
723
+ const endpointsPath = path6.join(this.context.config.endpointsPath);
724
+ const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
634
725
  return `// Auto-generated type definitions
635
726
  // Do not edit this file manually
636
727
 
637
728
  import type { z } from 'zod';
729
+ import { apiConfig } from '${relativePath}';
730
+
638
731
 
639
732
  // Re-export endpoint configuration types
640
- export type { APIConfig, APIEndpoint, HTTPMethod } from '@vietbus/api-codegen/config';
733
+ export type { APIConfig, APIEndpoint, HTTPMethod } from '@cushin/api-codegen/schema';
641
734
 
642
735
  /**
643
736
  * Type helper to extract params schema from an endpoint
@@ -670,13 +763,35 @@ export type ExtractResponse<T> = T extends { response: infer R extends z.ZodType
670
763
  /**
671
764
  * Import your API config to get typed endpoints
672
765
  *
673
- * @example
674
- * import { apiConfig } from './config/endpoints';
675
- * export type APIEndpoints = typeof apiConfig.endpoints;
676
766
  */
677
- export type APIEndpoints = Record<string, any>;
767
+ export type APIEndpoints = typeof apiConfig.endpoints;
768
+
769
+ ${this.generateEndpointTypes()}
678
770
  `;
679
771
  }
772
+ generateEndpointTypes() {
773
+ const types = [];
774
+ Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
775
+ const cap = this.capitalize(name);
776
+ if (endpoint.response)
777
+ types.push(
778
+ `export type ${cap}Response = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.response`)};`
779
+ );
780
+ if (endpoint.body)
781
+ types.push(
782
+ `export type ${cap}Input = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.body`)};`
783
+ );
784
+ if (endpoint.query)
785
+ types.push(
786
+ `export type ${cap}Query = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.query`)};`
787
+ );
788
+ if (endpoint.params)
789
+ types.push(
790
+ `export type ${cap}Params = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.params`)};`
791
+ );
792
+ });
793
+ return types.join("\n");
794
+ }
680
795
  };
681
796
  var ClientGenerator = class extends BaseGenerator {
682
797
  async generate() {
@@ -699,11 +814,15 @@ var ClientGenerator = class extends BaseGenerator {
699
814
  }
700
815
  generateClientContent() {
701
816
  const useClientDirective = this.context.config.options?.useClientDirective ?? true;
817
+ const outputPath = path6.join(this.context.config.outputDir, "types.ts");
818
+ const endpointsPath = path6.join(this.context.config.endpointsPath);
819
+ const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
702
820
  return `${useClientDirective ? "'use client';\n" : ""}
703
821
  import { createAPIClient } from '@cushin/api-codegen/client';
704
822
  import type { AuthCallbacks } from '@cushin/api-codegen/client';
705
- import { apiConfig } from '../config/endpoints';
823
+ import { apiConfig } from '${relativePath}';
706
824
  import type { APIEndpoints } from './types';
825
+ import { z } from 'zod';
707
826
 
708
827
  // Type-safe API client methods
709
828
  type APIClientMethods = {
@@ -733,11 +852,15 @@ type APIClientMethods = {
733
852
  };
734
853
 
735
854
  // Export singleton instance (will be initialized later)
736
- export let apiClient: APIClientMethods & {
855
+ export let baseClient: APIClientMethods & {
737
856
  refreshAuth: () => Promise<void>;
738
857
  updateAuthCallbacks: (callbacks: AuthCallbacks) => void;
739
858
  };
740
859
 
860
+ export const apiClient = {
861
+ ${this.generateApiClientMethods()}
862
+ };
863
+
741
864
  /**
742
865
  * Initialize API client with auth callbacks
743
866
  * Call this function in your auth provider setup
@@ -757,8 +880,8 @@ export let apiClient: APIClientMethods & {
757
880
  * initializeAPIClient(authCallbacks);
758
881
  */
759
882
  export const initializeAPIClient = (authCallbacks: AuthCallbacks) => {
760
- apiClient = createAPIClient(apiConfig, authCallbacks) as any;
761
- return apiClient;
883
+ baseClient = createAPIClient(apiConfig, authCallbacks) as any;
884
+ return baseClient;
762
885
  };
763
886
 
764
887
  // Export for custom usage
@@ -805,6 +928,202 @@ type APIClientMethods = {
805
928
  export const serverClient = createAPIClient(apiConfig) as APIClientMethods;
806
929
  `;
807
930
  }
931
+ generateApiClientMethods() {
932
+ const methods = [];
933
+ Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
934
+ const inferParams = this.inferNonNull(
935
+ `typeof apiConfig.endpoints.${name}.params`
936
+ );
937
+ const inferQuery = this.inferNonNull(
938
+ `typeof apiConfig.endpoints.${name}.query`
939
+ );
940
+ const inferBody = this.inferNonNull(
941
+ `typeof apiConfig.endpoints.${name}.body`
942
+ );
943
+ const inferResponse = this.inferNonNull(
944
+ `typeof apiConfig.endpoints.${name}.response`
945
+ );
946
+ if (endpoint.method === "GET") {
947
+ if (endpoint.params && endpoint.query) {
948
+ methods.push(` ${name}: (params: ${inferParams}, query?: ${inferQuery}): Promise<${inferResponse}> =>
949
+ (baseClient as any).${name}(params, query),`);
950
+ } else if (endpoint.params) {
951
+ methods.push(` ${name}: (params: ${inferParams}): Promise<${inferResponse}> =>
952
+ (baseClient as any).${name}(params),`);
953
+ } else if (endpoint.query) {
954
+ methods.push(` ${name}: (query?: ${inferQuery}): Promise<${inferResponse}> =>
955
+ (baseClient as any).${name}(query),`);
956
+ } else {
957
+ methods.push(` ${name}: (): Promise<${inferResponse}> =>
958
+ (baseClient as any).${name}(),`);
959
+ }
960
+ } else {
961
+ if (endpoint.params && endpoint.body) {
962
+ methods.push(` ${name}: (params: ${inferParams}, body: ${inferBody}): Promise<${inferResponse}> =>
963
+ (baseClient as any).${name}(params, body),`);
964
+ } else if (endpoint.params) {
965
+ methods.push(` ${name}: (params: ${inferParams}): Promise<${inferResponse}> =>
966
+ (baseClient as any).${name}(params),`);
967
+ } else if (endpoint.body) {
968
+ methods.push(` ${name}: (body: ${inferBody}): Promise<${inferResponse}> =>
969
+ (baseClient as any).${name}(body),`);
970
+ } else {
971
+ methods.push(` ${name}: (): Promise<${inferResponse}> =>
972
+ (baseClient as any).${name}(),`);
973
+ }
974
+ }
975
+ });
976
+ return methods.join("\n");
977
+ }
978
+ };
979
+ var QueryKeysGenerator = class extends BaseGenerator {
980
+ async generate() {
981
+ const content = this.generateContent();
982
+ const outputPath = path6.join(
983
+ this.context.config.outputDir,
984
+ "query-keys.ts"
985
+ );
986
+ await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
987
+ await fs5.writeFile(outputPath, content, "utf-8");
988
+ }
989
+ generateContent() {
990
+ const content = `// Auto-generated query keys
991
+ import { z } from 'zod';
992
+ import { apiConfig } from '../config/endpoints';
993
+
994
+ export const queryKeys = {
995
+ ${this.generateQueryKeysContent()}
996
+ } as const;
997
+ `;
998
+ return content;
999
+ }
1000
+ generateQueryKeysContent() {
1001
+ const resourceGroups = this.groupEndpointsByResource();
1002
+ const keys = [];
1003
+ Object.entries(resourceGroups).forEach(([resource, endpoints]) => {
1004
+ const queryEndpoints = endpoints.filter(
1005
+ ({ endpoint }) => endpoint.method === "GET"
1006
+ );
1007
+ if (queryEndpoints.length === 0) return;
1008
+ const resourceKeys = [` all: ['${resource}'] as const,`];
1009
+ const added = /* @__PURE__ */ new Set();
1010
+ queryEndpoints.forEach(({ name, endpoint }) => {
1011
+ const keyName = this.getEndpointKeyName(name);
1012
+ if (added.has(keyName)) return;
1013
+ const inferParams = this.inferNonNull(
1014
+ `typeof apiConfig.endpoints.${name}.params`
1015
+ );
1016
+ const inferQuery = this.inferNonNull(
1017
+ `typeof apiConfig.endpoints.${name}.query`
1018
+ );
1019
+ if (endpoint.params || endpoint.query) {
1020
+ const params = [];
1021
+ if (endpoint.params) params.push(`params?: ${inferParams}`);
1022
+ if (endpoint.query) params.push(`query?: ${inferQuery}`);
1023
+ resourceKeys.push(` ${keyName}: (${params.join(", ")}) =>
1024
+ ['${resource}', '${keyName}', ${endpoint.params ? "params" : "undefined"}, ${endpoint.query ? "query" : "undefined"}] as const,`);
1025
+ } else {
1026
+ resourceKeys.push(
1027
+ ` ${keyName}: () => ['${resource}', '${keyName}'] as const,`
1028
+ );
1029
+ }
1030
+ added.add(keyName);
1031
+ });
1032
+ keys.push(` ${resource}: {
1033
+ ${resourceKeys.join("\n")}
1034
+ },`);
1035
+ });
1036
+ return keys.join("\n");
1037
+ }
1038
+ };
1039
+ var QueryOptionsGenerator = class extends BaseGenerator {
1040
+ async generate() {
1041
+ const content = this.generateContent();
1042
+ const outputPath = path6.join(
1043
+ this.context.config.outputDir,
1044
+ "query-options.ts"
1045
+ );
1046
+ await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
1047
+ await fs5.writeFile(outputPath, content, "utf-8");
1048
+ }
1049
+ generateContent() {
1050
+ const content = `// Auto-generated query options
1051
+ import { queryOptions } from '@tanstack/react-query';
1052
+ import { apiClient } from './api-client';
1053
+ import { queryKeys } from './query-keys';
1054
+ import { z } from 'zod';
1055
+ import { apiConfig } from '../config/endpoints';
1056
+
1057
+ ${this.generateQueryOptionsContent()}
1058
+
1059
+ export const apiQueryOptions = {
1060
+ ${this.generateQueryOptionsExports()}
1061
+ } as const;
1062
+ `;
1063
+ return content;
1064
+ }
1065
+ generateQueryOptionsContent() {
1066
+ const groups = this.groupEndpointsByResource();
1067
+ const options = [];
1068
+ Object.entries(groups).forEach(([resource, endpoints]) => {
1069
+ const queries = endpoints.filter(
1070
+ ({ endpoint }) => endpoint.method === "GET"
1071
+ );
1072
+ if (queries.length === 0) return;
1073
+ const resourceOptions = [];
1074
+ queries.forEach(({ name, endpoint }) => {
1075
+ const optionName = this.getEndpointKeyName(name);
1076
+ const inferParams = this.inferNonNull(
1077
+ `typeof apiConfig.endpoints.${name}.params`
1078
+ );
1079
+ const inferQuery = this.inferNonNull(
1080
+ `typeof apiConfig.endpoints.${name}.query`
1081
+ );
1082
+ const inferResponse = this.inferNonNull(
1083
+ `typeof apiConfig.endpoints.${name}.response`
1084
+ );
1085
+ const params = [];
1086
+ let apiCall = "";
1087
+ if (endpoint.params && endpoint.query) {
1088
+ params.push(`params: ${inferParams}`, `filters?: ${inferQuery}`);
1089
+ apiCall = `apiClient.${name}(params, filters)`;
1090
+ } else if (endpoint.params) {
1091
+ params.push(`params: ${inferParams}`);
1092
+ apiCall = `apiClient.${name}(params)`;
1093
+ } else if (endpoint.query) {
1094
+ params.push(`filters?: ${inferQuery}`);
1095
+ apiCall = `apiClient.${name}(filters)`;
1096
+ } else {
1097
+ apiCall = `apiClient.${name}()`;
1098
+ }
1099
+ const keyCall = this.generateQueryKeyCall(resource, name, endpoint);
1100
+ resourceOptions.push(` ${optionName}: (${params.join(", ")}) =>
1101
+ queryOptions({
1102
+ queryKey: ${keyCall},
1103
+ queryFn: (): Promise<${inferResponse}> => ${apiCall},
1104
+ staleTime: 1000 * 60 * 5,
1105
+ }),`);
1106
+ });
1107
+ options.push(
1108
+ `const ${resource}QueryOptions = {
1109
+ ${resourceOptions.join("\n")}
1110
+ };
1111
+ `
1112
+ );
1113
+ });
1114
+ return options.join("\n");
1115
+ }
1116
+ generateQueryOptionsExports() {
1117
+ const groups = this.groupEndpointsByResource();
1118
+ const exports$1 = [];
1119
+ Object.keys(groups).forEach((resource) => {
1120
+ const hasQueries = groups[resource].some(
1121
+ ({ endpoint }) => endpoint.method === "GET"
1122
+ );
1123
+ if (hasQueries) exports$1.push(` ${resource}: ${resource}QueryOptions,`);
1124
+ });
1125
+ return exports$1.join("\n");
1126
+ }
808
1127
  };
809
1128
 
810
1129
  // src/generators/index.ts
@@ -825,6 +1144,8 @@ var CodeGenerator = class {
825
1144
  generators.push(new ClientGenerator(this.context));
826
1145
  }
827
1146
  if (this.context.config.generateHooks) {
1147
+ generators.push(new QueryKeysGenerator(this.context));
1148
+ generators.push(new QueryOptionsGenerator(this.context));
828
1149
  generators.push(new HooksGenerator(this.context));
829
1150
  }
830
1151
  if (this.context.config.generateServerActions && this.context.config.provider === "nextjs") {
@@ -836,8 +1157,6 @@ var CodeGenerator = class {
836
1157
  return generators;
837
1158
  }
838
1159
  };
839
-
840
- // src/core/codegen.ts
841
1160
  var CodegenCore = class {
842
1161
  constructor(config) {
843
1162
  this.config = config;
@@ -853,8 +1172,10 @@ var CodegenCore = class {
853
1172
  }
854
1173
  async loadAPIConfig() {
855
1174
  try {
856
- const fileUrl = pathToFileURL(this.config.endpointsPath).href;
857
- const module = await import(fileUrl);
1175
+ const jiti = createJiti(fileURLToPath(import.meta.url), {
1176
+ interopDefault: true
1177
+ });
1178
+ const module = await jiti.import(this.config.endpointsPath);
858
1179
  const apiConfig = module.apiConfig || module.default?.apiConfig || module.default || module;
859
1180
  if (!apiConfig || !apiConfig.endpoints) {
860
1181
  throw new Error(