react-query-lightbase-codegen 2.5.11 → 3.1.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/src/cli.ts ADDED
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ import { codegenerate } from "./index";
6
+ import type { OpenAPIConfig } from "./types/config";
7
+
8
+ const VERSION = "2.5.11";
9
+
10
+ const HELP = `
11
+ react-query-lightbase-codegen - Generate React Query clients from OpenAPI specs
12
+
13
+ USAGE:
14
+ npx react-query-lightbase-codegen [options] <spec...> -o <output>
15
+ npx react-query-lightbase-codegen --config <config-file>
16
+
17
+ ARGUMENTS:
18
+ <spec...> One or more OpenAPI spec files (local paths or URLs)
19
+
20
+ OPTIONS:
21
+ -o, --output <dir> Output directory for generated files (default: ./generated)
22
+ -c, --config <file> Path to JSON config file
23
+ -h, --help Show this help message
24
+ -v, --version Show version number
25
+
26
+ EXAMPLES:
27
+ # Single local spec
28
+ npx react-query-lightbase-codegen ./api.yaml -o ./src/generated
29
+
30
+ # Remote spec
31
+ npx react-query-lightbase-codegen https://api.example.com/openapi.json -o ./generated
32
+
33
+ # Multiple specs
34
+ npx react-query-lightbase-codegen ./auth.yaml ./users.yaml -o ./generated
35
+
36
+ # Using config file
37
+ npx react-query-lightbase-codegen --config ./codegen.json
38
+
39
+ CONFIG FILE FORMAT:
40
+ {
41
+ "specSource": "./api.yaml", // or ["./auth.yaml", "./users.yaml"]
42
+ "exportDir": "./src/generated"
43
+ }
44
+ `;
45
+
46
+ interface ParsedArgs {
47
+ specs: string[];
48
+ output: string;
49
+ config?: string;
50
+ help: boolean;
51
+ version: boolean;
52
+ }
53
+
54
+ function parseArgs(args: string[]): ParsedArgs {
55
+ const result: ParsedArgs = {
56
+ specs: [],
57
+ output: "./generated",
58
+ help: false,
59
+ version: false,
60
+ };
61
+
62
+ let i = 0;
63
+ while (i < args.length) {
64
+ const arg = args[i];
65
+
66
+ switch (arg) {
67
+ case "-h":
68
+ case "--help":
69
+ result.help = true;
70
+ break;
71
+
72
+ case "-v":
73
+ case "--version":
74
+ result.version = true;
75
+ break;
76
+
77
+ case "-o":
78
+ case "--output":
79
+ i++;
80
+ if (i >= args.length) {
81
+ throw new Error("Missing value for --output");
82
+ }
83
+ result.output = args[i];
84
+ break;
85
+
86
+ case "-c":
87
+ case "--config":
88
+ i++;
89
+ if (i >= args.length) {
90
+ throw new Error("Missing value for --config");
91
+ }
92
+ result.config = args[i];
93
+ break;
94
+
95
+ default:
96
+ if (arg.startsWith("-")) {
97
+ throw new Error(`Unknown option: ${arg}`);
98
+ }
99
+ result.specs.push(arg);
100
+ break;
101
+ }
102
+ i++;
103
+ }
104
+
105
+ return result;
106
+ }
107
+
108
+ async function loadConfigFile(configPath: string): Promise<OpenAPIConfig> {
109
+ try {
110
+ const content = await readFile(configPath, "utf-8");
111
+ const config = JSON.parse(content);
112
+
113
+ if (!config.specSource) {
114
+ throw new Error("Config file must contain 'specSource'");
115
+ }
116
+ if (!config.exportDir) {
117
+ throw new Error("Config file must contain 'exportDir'");
118
+ }
119
+
120
+ return {
121
+ specSource: config.specSource,
122
+ exportDir: resolve(process.cwd(), config.exportDir),
123
+ };
124
+ } catch (error) {
125
+ if (error instanceof Error) {
126
+ throw new Error(`Failed to load config file: ${error.message}`);
127
+ }
128
+ throw new Error("Failed to load config file");
129
+ }
130
+ }
131
+
132
+ function resolveSpecPath(spec: string): string {
133
+ // Keep URLs as-is
134
+ if (spec.startsWith("http://") || spec.startsWith("https://")) {
135
+ return spec;
136
+ }
137
+ // Resolve local paths relative to cwd
138
+ return resolve(process.cwd(), spec);
139
+ }
140
+
141
+ async function main(): Promise<void> {
142
+ const args = process.argv.slice(2);
143
+
144
+ if (args.length === 0) {
145
+ console.log(HELP);
146
+ process.exit(0);
147
+ }
148
+
149
+ let parsed: ParsedArgs;
150
+ try {
151
+ parsed = parseArgs(args);
152
+ } catch (error) {
153
+ if (error instanceof Error) {
154
+ console.error(`Error: ${error.message}`);
155
+ }
156
+ console.error("Use --help for usage information");
157
+ process.exit(1);
158
+ }
159
+
160
+ if (parsed.help) {
161
+ console.log(HELP);
162
+ process.exit(0);
163
+ }
164
+
165
+ if (parsed.version) {
166
+ console.log(`react-query-lightbase-codegen v${VERSION}`);
167
+ process.exit(0);
168
+ }
169
+
170
+ let config: OpenAPIConfig;
171
+
172
+ if (parsed.config) {
173
+ // Load from config file
174
+ config = await loadConfigFile(parsed.config);
175
+ } else {
176
+ // Build config from CLI args
177
+ if (parsed.specs.length === 0) {
178
+ console.error("Error: No spec files provided");
179
+ console.error("Use --help for usage information");
180
+ process.exit(1);
181
+ }
182
+
183
+ const resolvedSpecs = parsed.specs.map(resolveSpecPath);
184
+
185
+ config = {
186
+ specSource: resolvedSpecs.length === 1 ? resolvedSpecs[0] : resolvedSpecs,
187
+ exportDir: resolve(process.cwd(), parsed.output),
188
+ };
189
+ }
190
+
191
+ try {
192
+ console.log("Generating API client...");
193
+ console.log(
194
+ ` Specs: ${Array.isArray(config.specSource) ? config.specSource.join(", ") : config.specSource}`
195
+ );
196
+ console.log(` Output: ${config.exportDir}`);
197
+ console.log("");
198
+
199
+ await codegenerate(config);
200
+
201
+ console.log("Generation complete!");
202
+ } catch (error) {
203
+ if (error instanceof Error) {
204
+ console.error(`Error: ${error.message}`);
205
+ } else {
206
+ console.error("An unknown error occurred");
207
+ }
208
+ process.exit(1);
209
+ }
210
+ }
211
+
212
+ main();
@@ -1,63 +1,55 @@
1
1
  import type { OpenAPIV3 } from "openapi-types";
2
- import { camelCase, pascalCase, sanitizeTypeName, specTitle } from "../utils";
3
-
4
- export interface OperationInfo {
5
- method: string;
6
- path: string;
7
- operationId: string;
8
- summary?: string;
9
- description?: string;
10
- parameters?: OpenAPIV3.ParameterObject[];
11
- requestBody?: OpenAPIV3.RequestBodyObject;
12
- responses: OpenAPIV3.ResponsesObject;
13
- }
14
-
15
- function resolveSchema(
16
- schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject | undefined,
17
- spec: OpenAPIV3.Document
18
- ): OpenAPIV3.SchemaObject | undefined {
19
- if (!schema) return undefined;
20
- if ("$ref" in schema) {
21
- const index = schema.$ref.split("/").pop();
22
- return spec.components?.schemas?.[index as string] as OpenAPIV3.SchemaObject;
23
- }
24
- return schema;
25
- }
2
+ import {
3
+ type OperationInfo,
4
+ camelCase,
5
+ collectOperations,
6
+ getContentSchema,
7
+ pascalCase,
8
+ resolveSchema,
9
+ specTitle,
10
+ } from "../utils";
26
11
 
27
12
  function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document): string {
28
- const { method, path, operationId, summary, description, parameters, requestBody, responses } = operation;
13
+ const { method, path, operationId, summary, description, deprecated, parameters, requestBody, responses } =
14
+ operation;
29
15
  // Generate JSDoc
30
16
  const jsDocLines = ["/**"];
17
+ if (deprecated) jsDocLines.push(" * @deprecated");
31
18
  if (summary) jsDocLines.push(` * ${summary}`);
32
19
  if (description) jsDocLines.push(` * ${description}`);
33
20
 
34
21
  // Add parameter descriptions
35
22
  parameters?.forEach((param) => {
36
23
  const desc = param.description ? ` - ${param.description}` : "";
37
- jsDocLines.push(
38
- ` * @param ${param.in === "path" ? "params." : param.in === "query" ? "query." : ""}${param.name}${desc}`
39
- );
24
+ const prefix =
25
+ param.in === "path"
26
+ ? "params."
27
+ : param.in === "query"
28
+ ? "query."
29
+ : param.in === "header"
30
+ ? "headers."
31
+ : param.in === "cookie"
32
+ ? "cookies."
33
+ : "";
34
+ jsDocLines.push(` * @param ${prefix}${param.name}${desc}`);
40
35
  });
41
36
 
42
37
  if (requestBody && "description" in requestBody) {
43
38
  jsDocLines.push(` * @param data - ${requestBody.description}`);
44
39
  }
45
40
 
46
- // Add return type description
47
- const responseDetails = Object.entries(responses).find(([code]) => code.startsWith("2"));
41
+ // Add return type description - prefer 2xx responses, fall back to "default"
42
+ const responseDetails =
43
+ Object.entries(responses).find(([code]) => code.startsWith("2")) ||
44
+ Object.entries(responses).find(([code]) => code === "default");
48
45
  if (responseDetails) {
49
46
  const [code, response] = responseDetails;
50
47
  const responseObj = response as OpenAPIV3.ResponseObject;
51
48
  const desc = "description" in responseObj ? responseObj.description : "";
52
- const contentType =
53
- responseObj.content?.["application/ld+json"]?.schema ??
54
- responseObj.content?.["application/json"]?.schema ??
55
- responseObj.content?.["application/octet-stream"]?.schema ??
56
- responseObj.content?.["application/json;charset=UTF-8"]?.schema;
57
-
49
+ const contentSchema = getContentSchema(responseObj.content);
58
50
  const typeName = pascalCase(`${operationId}Response${code}`);
59
51
 
60
- if (contentType) {
52
+ if (contentSchema) {
61
53
  if (desc) {
62
54
  jsDocLines.push(` * @returns ${desc}`);
63
55
  }
@@ -72,6 +64,7 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
72
64
  const urlParams = parameters?.filter((p) => p.in === "path") || [];
73
65
  const queryParams = parameters?.filter((p) => p.in === "query") || [];
74
66
  const headerParams = parameters?.filter((p) => p.in === "header") || [];
67
+ const cookieParams = parameters?.filter((p) => p.in === "cookie") || [];
75
68
 
76
69
  const isFormData = requestBody && "content" in requestBody && requestBody.content?.["multipart/form-data"];
77
70
 
@@ -79,15 +72,9 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
79
72
  ? resolveSchema(requestBody.content["multipart/form-data"].schema, spec)
80
73
  : undefined;
81
74
 
82
- const content =
83
- requestBody && "content" in requestBody
84
- ? (requestBody.content?.["application/ld+json"]?.schema ??
85
- requestBody.content?.["application/json"]?.schema ??
86
- requestBody.content?.["application/octet-stream"]?.schema ??
87
- requestBody.content?.["application/json;charset=UTF-8"]?.schema)
88
- : undefined;
89
-
90
- const requestBodySchema = content ? resolveSchema(content, spec) : undefined;
75
+ const requestBodyContent =
76
+ requestBody && "content" in requestBody ? getContentSchema(requestBody.content) : undefined;
77
+ const requestBodySchema = requestBodyContent ? resolveSchema(requestBodyContent, spec) : undefined;
91
78
 
92
79
  // Check if request body is a primitive type (string, number, boolean)
93
80
  const isPrimitiveRequestBody =
@@ -102,11 +89,19 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
102
89
  const namedType = pascalCase(operationId);
103
90
 
104
91
  // Get response type from 2xx response
105
-
106
- const responseType =
107
- responseDetails?.[0] && "content" in responseDetails[1]
108
- ? `T.${`${namedType}Response${responseDetails[0]}`}`
109
- : "unknown";
92
+ const responseType = (() => {
93
+ if (!responseDetails) return "unknown";
94
+ const [code, response] = responseDetails;
95
+ // If response has content, use the generated type
96
+ if ("content" in response && response.content) {
97
+ return `T.${namedType}Response${code}`;
98
+ }
99
+ // 204 (No Content) and 205 (Reset Content) should return void
100
+ if (code === "204" || code === "205") {
101
+ return "void";
102
+ }
103
+ return "unknown";
104
+ })();
110
105
 
111
106
  const urlWithParams =
112
107
  urlParams.length > 0 ? `\`${path.replace(/{(\w+)}/g, "${encodeURIComponent(data.$1)}")}\`` : `"${path}"`;
@@ -151,27 +146,25 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
151
146
  .join("\n ")}`
152
147
  : "",
153
148
  queryParams.length > 0 ? "axiosConfig.params = { ...axiosConfig.params, ...queryData };" : "",
154
- isFormData
155
- ? "axiosConfig.headers = { ...axiosConfig.headers, 'Content-Type': 'multipart/form-data' };"
156
- : "",
157
149
  headerParams.length > 0
158
- ? `const headerData = {
159
- ${headerParams.map((p) => `["${p.name}"]: data["${p.name}"]`).join(",\n ")}
160
- };`
150
+ ? `axiosConfig.headers = { ...axiosConfig.headers, ${headerParams.map((p) => `["${p.name}"]: data["${p.name}"]`).join(", ")} };`
161
151
  : "",
162
- headerParams.length > 0 ? "axiosConfig.headers = { ...axiosConfig.headers, ...headerData };" : "",
152
+ cookieParams.length > 0
153
+ ? `axiosConfig.headers = { ...axiosConfig.headers, Cookie: [${cookieParams.map((p) => `data["${p.name}"] != null ? \`${p.name}=\${data["${p.name}"]}\` : null`).join(", ")}].filter(Boolean).join("; ") };`
154
+ : "",
155
+ // Note: Don't set Content-Type for FormData - Axios will set it automatically with the correct boundary
163
156
  requestBody
164
- ? `const res = await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || requestBodySchema?.properties ? "bodyData" : "data"}, axiosConfig);`
165
- : `const res = await apiClient.${method}<${responseType}>(url, axiosConfig);`,
166
- "return res.data;",
157
+ ? responseType === "void"
158
+ ? `await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || requestBodySchema?.properties ? "bodyData" : "data"}, axiosConfig);`
159
+ : `const res = await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || requestBodySchema?.properties ? "bodyData" : "data"}, axiosConfig);`
160
+ : responseType === "void"
161
+ ? `await apiClient.${method}<${responseType}>(url, axiosConfig);`
162
+ : `const res = await apiClient.${method}<${responseType}>(url, axiosConfig);`,
163
+ responseType !== "void" ? "return res.data;" : "",
167
164
  ]
168
165
  .filter(Boolean)
169
166
  .join("\n ");
170
167
 
171
- // ${queryParams.length > 0 ? "params: queryData," : ""}
172
- // ${requestBody ? `data: ${isFormData ? "formData" : "bodyData"},` : ""}
173
- // ${isFormData ? `config: { headers: { 'Content-Type': 'multipart/form-data', ...axiosConfig?.headers }, ...axiosConfig },` : "...axiosConfig"}
174
-
175
168
  const requestParms = hasData
176
169
  ? isPrimitiveRequestBody
177
170
  ? `props: { data: T.${pascalCase(operationId)}Params; axiosConfig?: AxiosRequestConfig; }`
@@ -186,53 +179,10 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
186
179
  }
187
180
 
188
181
  export function generateApiClient(spec: OpenAPIV3.Document): string {
189
- const operations: OperationInfo[] = [];
190
-
191
- const resolveParameters = (
192
- parameters: (OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject)[]
193
- ): OpenAPIV3.ParameterObject[] => {
194
- return parameters.map((p) => {
195
- if ("$ref" in p) {
196
- const index = p.$ref.split("/").pop();
197
- return spec.components?.schemas?.[index as string] as OpenAPIV3.ParameterObject;
198
- }
199
- return p;
200
- });
201
- };
202
-
203
- const resolveRequestBody = (
204
- requestBody: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject | undefined
205
- ): OpenAPIV3.RequestBodyObject | undefined => {
206
- if (!requestBody) return undefined;
207
- if ("$ref" in requestBody) {
208
- const index = requestBody.$ref.split("/").pop();
209
- return spec.components?.schemas?.[index as string] as OpenAPIV3.RequestBodyObject;
210
- }
211
- return requestBody;
212
- };
213
-
214
- // Collect all operations
215
- Object.entries(spec.paths || {}).forEach(([path, pathItem]) => {
216
- if (!pathItem) return;
217
- ["get", "post", "put", "delete", "patch"].forEach((method) => {
218
- const operation = pathItem[method as keyof OpenAPIV3.PathItemObject] as OpenAPIV3.OperationObject;
219
- if (!operation) return;
220
- operations.push({
221
- method: method,
222
- path,
223
- operationId: `${sanitizeTypeName(operation.operationId || `${path.replace(/\W+/g, "_")}`)}`,
224
- summary: operation.summary,
225
- description: operation.description,
226
- parameters: resolveParameters([...(pathItem.parameters || []), ...(operation.parameters || [])]),
227
- requestBody: resolveRequestBody(operation.requestBody),
228
- responses: operation.responses,
229
- });
230
- });
231
- });
232
-
182
+ const operations = collectOperations(spec);
233
183
  const title = specTitle(spec);
234
184
 
235
- return `import type { AxiosResponse, AxiosRequestConfig } from 'axios';
185
+ return `import type { AxiosRequestConfig } from 'axios';
236
186
  import { getApiClient } from './apiClient';
237
187
  import type * as T from './${title}.schema';
238
188
 
@@ -1,21 +1,15 @@
1
1
  import type { OpenAPIV3 } from "openapi-types";
2
- import { camelCase, sanitizeTypeName, specTitle } from "../utils";
3
- import type { OperationInfo } from "./clientGenerator";
4
-
5
- function resolveSchema(
6
- schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject | undefined,
7
- spec: OpenAPIV3.Document
8
- ): OpenAPIV3.SchemaObject | undefined {
9
- if (!schema) return undefined;
10
- if ("$ref" in schema) {
11
- const index = schema.$ref.split("/").pop();
12
- return spec.components?.schemas?.[index as string] as OpenAPIV3.SchemaObject;
13
- }
14
- return schema;
15
- }
2
+ import {
3
+ type OperationInfo,
4
+ camelCase,
5
+ collectOperations,
6
+ getContentSchema,
7
+ resolveSchema,
8
+ specTitle,
9
+ } from "../utils";
16
10
 
17
11
  function generateQueryOptions(operation: OperationInfo, spec: OpenAPIV3.Document): string {
18
- const { operationId, parameters, requestBody, method } = operation;
12
+ const { operationId, parameters, requestBody, deprecated } = operation;
19
13
 
20
14
  const hasData = (parameters && parameters.length > 0) || operation.requestBody;
21
15
 
@@ -32,13 +26,7 @@ function generateQueryOptions(operation: OperationInfo, spec: OpenAPIV3.Document
32
26
  return schema.required?.map((p) => `'${p}'`) || [];
33
27
  };
34
28
 
35
- const content =
36
- requestBody && "content" in requestBody
37
- ? (requestBody.content?.["application/ld+json"]?.schema ??
38
- requestBody.content?.["application/json"]?.schema ??
39
- requestBody.content?.["application/octet-stream"]?.schema)
40
- : undefined;
41
-
29
+ const content = requestBody && "content" in requestBody ? getContentSchema(requestBody.content) : undefined;
42
30
  const requestBodySchema = content ? resolveSchema(content, spec) : undefined;
43
31
 
44
32
  // Check if request body is a primitive type (string, number, boolean)
@@ -90,8 +78,10 @@ function generateQueryOptions(operation: OperationInfo, spec: OpenAPIV3.Document
90
78
  : `hasDefinedProps(${paramsVariable}, ${requiredParams.join(", ")})`
91
79
  : "true";
92
80
 
81
+ const deprecatedComment = deprecated ? "/** @deprecated */\n" : "";
82
+
93
83
  return `
94
- export const ${namedQueryOptions} = (
84
+ ${deprecatedComment}export const ${namedQueryOptions} = (
95
85
  ${hasData ? `props: Partial<Parameters<typeof apiClient.${namedQuery}>[0]>` : `props?: Partial<Parameters<typeof apiClient.${namedQuery}>[0]>`}
96
86
  ) => {
97
87
  ${destructuringLine}
@@ -104,37 +94,12 @@ export const ${namedQueryOptions} = (
104
94
  }
105
95
 
106
96
  export function generateReactQuery(spec: OpenAPIV3.Document): string {
107
- const operations: OperationInfo[] = [];
108
-
109
- // Collect operations (same as in clientGenerator)
110
- Object.entries(spec.paths || {}).forEach(([path, pathItem]) => {
111
- if (!pathItem) return;
112
-
113
- ["get", "post", "put", "delete", "patch"].forEach((method) => {
114
- const operation = pathItem[method as keyof OpenAPIV3.PathItemObject] as OpenAPIV3.OperationObject;
115
- if (!operation) return;
116
- operations.push({
117
- method: method,
118
- path,
119
- operationId: sanitizeTypeName(`${operation.operationId || `${path.replace(/\W+/g, "_")}`}`),
120
- summary: operation.summary,
121
- description: operation.description,
122
- parameters: [
123
- ...(pathItem.parameters || []),
124
- ...(operation.parameters || []),
125
- ] as OpenAPIV3.ParameterObject[],
126
- requestBody: operation.requestBody as OpenAPIV3.RequestBodyObject,
127
- responses: operation.responses,
128
- });
129
- });
130
- });
97
+ const operations = collectOperations(spec);
131
98
 
132
99
  return `import { queryOptions, skipToken } from '@tanstack/react-query';
133
100
  import * as apiClient from './${specTitle(spec)}.client';
134
- // TEMPORARY: allows for backward compatibility imports
135
- export * from './${specTitle(spec)}.client';
136
101
 
137
- const hasDefinedProps = <T extends { [P in K]?: any }, K extends PropertyKey>(
102
+ const hasDefinedProps = <T extends { [P in K]?: unknown }, K extends PropertyKey>(
138
103
  obj: T,
139
104
  ...keys: K[]
140
105
  ): obj is T & { [P in K]-?: Exclude<T[P], undefined> } => {