@7nohe/openapi-react-query-codegen 0.0.0-c7f76ece287f3b22268170498e8fea9b0c797946

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/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # OpenAPI React Query Codegen
2
+
3
+ > Node.js library that generates [React Query (also called TanStack Query)](https://tanstack.com/query) hooks based on an OpenAPI specification file.
4
+
5
+ ## Features
6
+
7
+ - Supports generation of custom react hooks that use React Query's `useQuery` and `useMutation` hooks
8
+ - Supports generation of query keys for query caching
9
+ - Supports the option to use pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts)
10
+
11
+ ## Install
12
+
13
+ ```
14
+ $ npm install -D @7nohe/openapi-react-query-codegen
15
+ ```
16
+
17
+ Register the command to the `scripts` property in your package.json file.
18
+
19
+ ```json
20
+ {
21
+ "scripts": {
22
+ "codegen": "openapi-rq -i ./petstore.yaml -c axios"
23
+ }
24
+ }
25
+ ```
26
+
27
+ You can also run the command without installing it in your project using the npx command.
28
+
29
+ ```bash
30
+ $ npx --package @7nohe/openapi-react-query-codegen openapi-rq -i ./petstore.yaml -c axios
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```
36
+ $ openapi-rq --help
37
+
38
+ Usage: openapi-rq [options]
39
+
40
+ Generate React Query code based on OpenAPI
41
+
42
+ Options:
43
+ -V, --version output the version number
44
+ -i, --input <value> OpenAPI specification, can be a path, url or string content (required)
45
+ -o, --output <value> Output directory (default: "openapi")
46
+ -c, --client <value> HTTP client to generate [fetch, xhr, node, axios, angular] (default: "fetch")
47
+ --request <value> Path to custom request file
48
+ --format <value> Process output folder with formatter? ['biome', 'prettier']
49
+ --lint <value> Process output folder with linter? ['eslint', 'biome']
50
+ --operationId Use operation ID to generate operation names?
51
+ --serviceResponse <value> Define shape of returned value from service calls ['body', 'response']
52
+ --base <value> Manually set base in OpenAPI config instead of inferring from server value
53
+ --enums <value> Generate JavaScript objects from enum definitions? ['javascript', 'typescript']
54
+ --useDateType Use Date type instead of string for date types for models, this will not convert the data to a Date object
55
+ --debug Enable debug mode
56
+ --noSchemas Disable generating schemas for request and response objects
57
+ --schemaTypes <value> Define the type of schema generation ['form', 'json'] (default: "json")
58
+ -h, --help display help for command
59
+ ```
60
+
61
+ ## Example Usage
62
+
63
+ ### Command
64
+
65
+ ```
66
+ $ openapi-rq -i ./petstore.yaml
67
+ ```
68
+
69
+ ### Output directory structure
70
+
71
+ ```
72
+ - openapi
73
+ - queries
74
+ - index.ts <- main file that exports common types, variables, and queries. Does not export suspense or prefetch hooks
75
+ - common.ts <- common types
76
+ - queries.ts <- generated query hooks
77
+ - suspenses.ts <- generated suspense hooks
78
+ - prefetch.ts <- generated prefetch hooks learn more about prefetching in in link below
79
+ - requests <- output code generated by @hey-api/openapi-ts
80
+ ```
81
+
82
+ - [Prefetching docs](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#prefetching-and-dehydrating-data)
83
+
84
+ ### In your app
85
+
86
+ #### Using the generated hooks
87
+
88
+ ```tsx
89
+ // App.tsx
90
+ import { usePetServiceFindPetsByStatus } from "../openapi/queries";
91
+ function App() {
92
+ const { data } = usePetServiceFindPetsByStatus({ status: ["available"] });
93
+
94
+ return (
95
+ <div className="App">
96
+ <h1>Pet List</h1>
97
+ <ul>{data?.map((pet) => <li key={pet.id}>{pet.name}</li>)}</ul>
98
+ </div>
99
+ );
100
+ }
101
+
102
+ export default App;
103
+ ```
104
+
105
+ #### Using the generated typescript client
106
+
107
+ ```tsx
108
+ import { useQuery } from "@tanstack/react-query";
109
+ import { PetService } from "../openapi/requests/services";
110
+ import { usePetServiceFindPetsByStatusKey } from "../openapi/queries";
111
+
112
+ function App() {
113
+ // You can still use the auto-generated query key
114
+ const { data } = useQuery({
115
+ queryKey: [usePetServiceFindPetsByStatusKey],
116
+ queryFn: () => {
117
+ // Do something here
118
+ return PetService.findPetsByStatus(["available"]);
119
+ },
120
+ });
121
+
122
+ return <div className="App">{/* .... */}</div>;
123
+ }
124
+
125
+ export default App;
126
+ ```
127
+
128
+ #### Using Suspense Hooks
129
+
130
+ ```tsx
131
+ // App.tsx
132
+ import { useDefaultClientFindPetsSuspense } from "../openapi/queries/suspense";
133
+ function ChildComponent() {
134
+ const { data } = useDefaultClientFindPetsSuspense({ tags: [], limit: 10 });
135
+
136
+ return <ul>{data?.map((pet, index) => <li key={pet.id}>{pet.name}</li>)}</ul>;
137
+ }
138
+
139
+ function ParentComponent() {
140
+ return (
141
+ <>
142
+ <Suspense fallback={<>loading...</>}>
143
+ <ChildComponent />
144
+ </Suspense>
145
+ </>
146
+ );
147
+ }
148
+
149
+ function App() {
150
+ return (
151
+ <div className="App">
152
+ <h1>Pet List</h1>
153
+ <ParentComponent />
154
+ </div>
155
+ );
156
+ }
157
+
158
+ export default App;
159
+ ```
160
+
161
+ #### Runtime Configuration
162
+
163
+ You can modify the default values used by the generated service calls by modifying the OpenAPI configuration singleton object.
164
+
165
+ It's default location is `openapi/requests/core/OpenAPI.ts` and it is also exported from `openapi/index.ts`
166
+
167
+ Import the constant into your runtime and modify it before setting up the react app.
168
+
169
+ ```typescript
170
+ /** main.tsx */
171
+ import { OpenAPI as OpenAPIConfig } from './openapi/requests/core/OpenAPI';
172
+ ...
173
+ OpenAPIConfig.BASE = 'www.domain.com/api';
174
+ OpenAPIConfig.HEADERS = {
175
+ 'x-header-1': 'value-1',
176
+ 'x-header-2': 'value-2',
177
+ };
178
+ ...
179
+ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
180
+ <React.StrictMode>
181
+ <QueryClientProvider client={queryClient}>
182
+ <App />
183
+ </QueryClientProvider>
184
+ </React.StrictMode>
185
+ );
186
+
187
+ ```
188
+
189
+ ## License
190
+
191
+ MIT
package/dist/cli.mjs ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import { generate } from "./generate.mjs";
3
+ import { Command, Option } from "commander";
4
+ import { readFile } from "fs/promises";
5
+ import { dirname, join } from "path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { defaultOutputPath } from "./constants.mjs";
8
+ const program = new Command();
9
+ async function setupProgram() {
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ const file = await readFile(join(__dirname, "../package.json"), "utf-8");
13
+ const packageJson = JSON.parse(file);
14
+ const version = packageJson.version;
15
+ program
16
+ .name("openapi-rq")
17
+ .version(version)
18
+ .description("Generate React Query code based on OpenAPI")
19
+ .requiredOption("-i, --input <value>", "OpenAPI specification, can be a path, url or string content (required)")
20
+ .option("-o, --output <value>", "Output directory", defaultOutputPath)
21
+ .addOption(new Option("-c, --client <value>", "HTTP client to generate")
22
+ .choices(["angular", "axios", "fetch", "node", "xhr"])
23
+ .default("fetch"))
24
+ .option("--request <value>", "Path to custom request file")
25
+ .addOption(new Option("--format <value>", "Process output folder with formatter?").choices(["biome", "prettier"]))
26
+ .addOption(new Option("--lint <value>", "Process output folder with linter?").choices(["biome", "eslint"]))
27
+ .option("--operationId", "Use operation ID to generate operation names?")
28
+ .addOption(new Option("--serviceResponse <value>", "Define shape of returned value from service calls").choices(["body", "response"]))
29
+ .option("--base <value>", "Manually set base in OpenAPI config instead of inferring from server value")
30
+ .addOption(new Option("--enums <value>", "Generate JavaScript objects from enum definitions?").choices(["javascript", "typescript"]))
31
+ .option("--useDateType", "Use Date type instead of string for date types for models, this will not convert the data to a Date object")
32
+ .option("--debug", "Run in debug mode?")
33
+ .option("--noSchemas", "Disable generating JSON schemas")
34
+ .addOption(new Option("--schemaType <value>", "Type of JSON schema [Default: 'json']").choices(["form", "json"]))
35
+ .parse();
36
+ const options = program.opts();
37
+ await generate(options, version);
38
+ }
39
+ setupProgram();
@@ -0,0 +1,135 @@
1
+ import { stat } from "fs/promises";
2
+ import ts from "typescript";
3
+ import path from "path";
4
+ import { queriesOutputPath, requestsOutputPath } from "./constants.mjs";
5
+ export const TData = ts.factory.createIdentifier("TData");
6
+ export const TError = ts.factory.createIdentifier("TError");
7
+ export const TContext = ts.factory.createIdentifier("TContext");
8
+ export const queryKeyGenericType = ts.factory.createTypeReferenceNode("TQueryKey");
9
+ export const queryKeyConstraint = ts.factory.createTypeReferenceNode("Array", [
10
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
11
+ ]);
12
+ export const capitalizeFirstLetter = (str) => {
13
+ return str.charAt(0).toUpperCase() + str.slice(1);
14
+ };
15
+ export const lowercaseFirstLetter = (str) => {
16
+ return str.charAt(0).toLowerCase() + str.slice(1);
17
+ };
18
+ export const getNameFromMethod = (method) => {
19
+ const methodName = method.getName();
20
+ if (!methodName) {
21
+ throw new Error("Method name not found");
22
+ }
23
+ return methodName;
24
+ };
25
+ export async function exists(f) {
26
+ try {
27
+ await stat(f);
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ const Common = "Common";
35
+ /**
36
+ * Build a common type name by prepending the Common namespace.
37
+ */
38
+ export function BuildCommonTypeName(name) {
39
+ if (typeof name === "string") {
40
+ return ts.factory.createIdentifier(`${Common}.${name}`);
41
+ }
42
+ return ts.factory.createIdentifier(`${Common}.${name.text}`);
43
+ }
44
+ /**
45
+ * Safely parse a value into a number. Checks for NaN and Infinity.
46
+ * Returns NaN if the string is not a valid number.
47
+ * @param value The value to parse.
48
+ * @returns The parsed number or NaN if the value is not a valid number.
49
+ */
50
+ export function safeParseNumber(value) {
51
+ const parsed = Number(value);
52
+ if (!isNaN(parsed) && isFinite(parsed)) {
53
+ return parsed;
54
+ }
55
+ return NaN;
56
+ }
57
+ export function extractPropertiesFromObjectParam(param) {
58
+ const referenced = param.findReferences()[0];
59
+ const def = referenced.getDefinition();
60
+ const paramNodes = def
61
+ .getNode()
62
+ .getType()
63
+ .getProperties()
64
+ .map((prop) => ({
65
+ name: prop.getName(),
66
+ optional: prop.isOptional(),
67
+ type: prop.getValueDeclaration()?.getType(),
68
+ }));
69
+ return paramNodes;
70
+ }
71
+ /**
72
+ * Replace the import("...") surrounding the type if there is one.
73
+ * This can happen when the type is imported from another file, but
74
+ * we are already importing all the types from that file.
75
+ *
76
+ * https://regex101.com/r/3DyHaQ/1
77
+ *
78
+ * TODO: Replace with a more robust solution.
79
+ */
80
+ export function getShortType(type) {
81
+ return type.replaceAll(/import\(".*"\)\./g, "");
82
+ }
83
+ export function getClassesFromService(node) {
84
+ const klasses = node.getClasses();
85
+ if (!klasses.length) {
86
+ throw new Error("No classes found");
87
+ }
88
+ return klasses.map((klass) => {
89
+ const className = klass.getName();
90
+ if (!className) {
91
+ throw new Error("Class name not found");
92
+ }
93
+ return {
94
+ className,
95
+ klass,
96
+ };
97
+ });
98
+ }
99
+ export function getClassNameFromClassNode(klass) {
100
+ const className = klass.getName();
101
+ if (!className) {
102
+ throw new Error("Class name not found");
103
+ }
104
+ return className;
105
+ }
106
+ export function formatOptions(options) {
107
+ // loop through properties on the options object
108
+ // if the property is a string of number then convert it to a number
109
+ // if the property is a string of boolean then convert it to a boolean
110
+ const formattedOptions = Object.entries(options).reduce((acc, [key, value]) => {
111
+ const typedKey = key;
112
+ const typedValue = value;
113
+ const parsedNumber = safeParseNumber(typedValue);
114
+ if (value === "true" || value === true) {
115
+ acc[typedKey] = true;
116
+ }
117
+ else if (value === "false" || value === false) {
118
+ acc[typedKey] = false;
119
+ }
120
+ else if (!isNaN(parsedNumber)) {
121
+ acc[typedKey] = parsedNumber;
122
+ }
123
+ else {
124
+ acc[typedKey] = typedValue;
125
+ }
126
+ return acc;
127
+ }, options);
128
+ return formattedOptions;
129
+ }
130
+ export function buildRequestsOutputPath(outputPath) {
131
+ return path.join(outputPath, requestsOutputPath);
132
+ }
133
+ export function buildQueriesOutputPath(outputPath) {
134
+ return path.join(outputPath, queriesOutputPath);
135
+ }
@@ -0,0 +1,12 @@
1
+ export const defaultOutputPath = "openapi";
2
+ export const queriesOutputPath = "queries";
3
+ export const requestsOutputPath = "requests";
4
+ export const serviceFileName = "services.gen";
5
+ export const modalsFileName = "types.gen";
6
+ export const OpenApiRqFiles = {
7
+ queries: "queries",
8
+ common: "common",
9
+ suspense: "suspense",
10
+ index: "index",
11
+ prefetch: "prefetch",
12
+ };
@@ -0,0 +1,63 @@
1
+ import { createUseQuery } from "./createUseQuery.mjs";
2
+ import { createUseMutation } from "./createUseMutation.mjs";
3
+ import { createPrefetch } from "./createPrefetch.mjs";
4
+ export const createExports = (service) => {
5
+ const { klasses } = service;
6
+ const methods = klasses.map((k) => k.methods).flat();
7
+ const allGet = methods.filter((m) => m.httpMethodName.toUpperCase().includes("GET"));
8
+ const allPost = methods.filter((m) => m.httpMethodName.toUpperCase().includes("POST"));
9
+ const allPut = methods.filter((m) => m.httpMethodName.toUpperCase().includes("PUT"));
10
+ const allPatch = methods.filter((m) => m.httpMethodName.toUpperCase().includes("PATCH"));
11
+ const allDelete = methods.filter((m) => m.httpMethodName.toUpperCase().includes("DELETE"));
12
+ const allGetQueries = allGet.map((m) => createUseQuery(m));
13
+ const allPrefetchQueries = allGet.map((m) => createPrefetch(m));
14
+ const allPostMutations = allPost.map((m) => createUseMutation(m));
15
+ const allPutMutations = allPut.map((m) => createUseMutation(m));
16
+ const allPatchMutations = allPatch.map((m) => createUseMutation(m));
17
+ const allDeleteMutations = allDelete.map((m) => createUseMutation(m));
18
+ const allQueries = [...allGetQueries];
19
+ const allMutations = [
20
+ ...allPostMutations,
21
+ ...allPutMutations,
22
+ ...allPatchMutations,
23
+ ...allDeleteMutations,
24
+ ];
25
+ const commonInQueries = allQueries
26
+ .map(({ apiResponse, returnType, key }) => [apiResponse, returnType, key])
27
+ .flat();
28
+ const commonInMutations = allMutations
29
+ .map(({ mutationResult }) => [mutationResult])
30
+ .flat();
31
+ const allCommon = [...commonInQueries, ...commonInMutations];
32
+ const mainQueries = allQueries.map(({ queryHook }) => [queryHook]).flat();
33
+ const mainMutations = allMutations
34
+ .map(({ mutationHook }) => [mutationHook])
35
+ .flat();
36
+ const mainExports = [...mainQueries, ...mainMutations];
37
+ const suspenseQueries = allQueries
38
+ .map(({ suspenseQueryHook }) => [suspenseQueryHook])
39
+ .flat();
40
+ const suspenseExports = [...suspenseQueries];
41
+ const allPrefetches = allPrefetchQueries
42
+ .map(({ prefetchHook }) => [prefetchHook])
43
+ .flat();
44
+ const allPrefetchExports = [...allPrefetches];
45
+ return {
46
+ /**
47
+ * Common types and variables between queries (regular and suspense) and mutations
48
+ */
49
+ allCommon,
50
+ /**
51
+ * Main exports are the hooks that are used in the components
52
+ */
53
+ mainExports,
54
+ /**
55
+ * Suspense exports are the hooks that are used in the suspense components
56
+ */
57
+ suspenseExports,
58
+ /**
59
+ * Prefetch exports are the hooks that are used in the prefetch components
60
+ */
61
+ allPrefetchExports,
62
+ };
63
+ };
@@ -0,0 +1,42 @@
1
+ import ts from "typescript";
2
+ import { posix } from "path";
3
+ import { modalsFileName, serviceFileName } from "./constants.mjs";
4
+ const { join } = posix;
5
+ export const createImports = ({ serviceEndName, project, }) => {
6
+ const modelsFile = project
7
+ .getSourceFiles()
8
+ .find((sourceFile) => sourceFile.getFilePath().includes(modalsFileName));
9
+ const serviceFile = project.getSourceFileOrThrow(`${serviceFileName}.ts`);
10
+ if (!modelsFile) {
11
+ console.warn(`
12
+ ⚠️ WARNING: No models file found.
13
+ This may be an error if \`.components.schemas\` or \`.components.parameters\` is defined in your OpenAPI input.`);
14
+ }
15
+ const modelNames = modelsFile
16
+ ? Array.from(modelsFile.getExportedDeclarations().keys())
17
+ : [];
18
+ const serviceExports = Array.from(serviceFile.getExportedDeclarations().keys());
19
+ const serviceNames = serviceExports.filter((name) => name.endsWith(serviceEndName));
20
+ const imports = [
21
+ ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports([
22
+ ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("useQuery")),
23
+ ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("useSuspenseQuery")),
24
+ ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("useMutation")),
25
+ ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("UseQueryResult")),
26
+ ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("UseQueryOptions")),
27
+ ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("UseMutationOptions")),
28
+ ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("UseMutationResult")),
29
+ ])), ts.factory.createStringLiteral("@tanstack/react-query"), undefined),
30
+ ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports([
31
+ // import all class names from service file
32
+ ...serviceNames.map((serviceName) => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(serviceName))),
33
+ ])), ts.factory.createStringLiteral(join("../requests", serviceFileName)), undefined),
34
+ ];
35
+ if (modelsFile) {
36
+ // import all the models by name
37
+ imports.push(ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports([
38
+ ...modelNames.map((modelName) => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(modelName))),
39
+ ])), ts.factory.createStringLiteral(join("../requests/", modalsFileName)), undefined));
40
+ }
41
+ return imports;
42
+ };
@@ -0,0 +1,58 @@
1
+ import ts from "typescript";
2
+ import { BuildCommonTypeName, extractPropertiesFromObjectParam, getNameFromMethod, } from "./common.mjs";
3
+ import { createQueryKeyFromMethod, getRequestParamFromMethod, hookNameFromMethod, } from "./createUseQuery.mjs";
4
+ import { addJSDocToNode } from "./util.mjs";
5
+ /**
6
+ * Creates a prefetch function for a query
7
+ */
8
+ function createPrefetchHook({ requestParams, method, className, }) {
9
+ const methodName = getNameFromMethod(method);
10
+ const queryName = hookNameFromMethod({ method, className });
11
+ const customHookName = `prefetch${queryName.charAt(0).toUpperCase() + queryName.slice(1)}`;
12
+ const queryKey = createQueryKeyFromMethod({ method, className });
13
+ // const
14
+ const hookExport = ts.factory.createVariableStatement(
15
+ // export
16
+ [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([
17
+ ts.factory.createVariableDeclaration(ts.factory.createIdentifier(customHookName), undefined, undefined, ts.factory.createArrowFunction(undefined, undefined, [
18
+ ts.factory.createParameterDeclaration(undefined, undefined, "queryClient", undefined, ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("QueryClient"))),
19
+ ...requestParams,
20
+ ], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createCallExpression(ts.factory.createIdentifier("queryClient.prefetchQuery"), undefined, [
21
+ ts.factory.createObjectLiteralExpression([
22
+ ts.factory.createPropertyAssignment(ts.factory.createIdentifier("queryKey"), ts.factory.createArrayLiteralExpression([
23
+ BuildCommonTypeName(queryKey),
24
+ method.getParameters().length
25
+ ? ts.factory.createArrayLiteralExpression([
26
+ ts.factory.createObjectLiteralExpression(method
27
+ .getParameters()
28
+ .map((param) => extractPropertiesFromObjectParam(param).map((p) => ts.factory.createShorthandPropertyAssignment(ts.factory.createIdentifier(p.name))))
29
+ .flat()),
30
+ ])
31
+ : ts.factory.createArrayLiteralExpression([]),
32
+ ], false)),
33
+ ts.factory.createPropertyAssignment(ts.factory.createIdentifier("queryFn"), ts.factory.createArrowFunction(undefined, undefined, [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(className), ts.factory.createIdentifier(methodName)), undefined, method.getParameters().length
34
+ ? [
35
+ ts.factory.createObjectLiteralExpression(method
36
+ .getParameters()
37
+ .map((param) => extractPropertiesFromObjectParam(param).map((p) => ts.factory.createShorthandPropertyAssignment(ts.factory.createIdentifier(p.name))))
38
+ .flat()),
39
+ ]
40
+ : undefined))),
41
+ ]),
42
+ ]))),
43
+ ], ts.NodeFlags.Const));
44
+ return hookExport;
45
+ }
46
+ export const createPrefetch = ({ className, method, jsDoc, }) => {
47
+ const requestParam = getRequestParamFromMethod(method);
48
+ const requestParams = requestParam ? [requestParam] : [];
49
+ const prefetchHook = createPrefetchHook({
50
+ requestParams,
51
+ method,
52
+ className,
53
+ });
54
+ const hookWithJsDoc = addJSDocToNode(prefetchHook, jsDoc);
55
+ return {
56
+ prefetchHook: hookWithJsDoc,
57
+ };
58
+ };
@@ -0,0 +1,84 @@
1
+ import ts from "typescript";
2
+ import { Project } from "ts-morph";
3
+ import { join } from "path";
4
+ import { OpenApiRqFiles } from "./constants.mjs";
5
+ import { createImports } from "./createImports.mjs";
6
+ import { createExports } from "./createExports.mjs";
7
+ import { getServices } from "./service.mjs";
8
+ const createSourceFile = async (outputPath, serviceEndName) => {
9
+ const project = new Project({
10
+ // Optionally specify compiler options, tsconfig.json, in-memory file system, and more here.
11
+ // If you initialize with a tsconfig.json, then it will automatically populate the project
12
+ // with the associated source files.
13
+ // Read more: https://ts-morph.com/setup/
14
+ skipAddingFilesFromTsConfig: true,
15
+ });
16
+ const sourceFiles = join(process.cwd(), outputPath);
17
+ project.addSourceFilesAtPaths(`${sourceFiles}/**/*`);
18
+ const service = await getServices(project);
19
+ const imports = createImports({
20
+ serviceEndName,
21
+ project,
22
+ });
23
+ const exports = createExports(service);
24
+ const commonSource = ts.factory.createSourceFile([...imports, ...exports.allCommon], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
25
+ const commonImport = ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, ts.factory.createIdentifier("* as Common"), undefined), ts.factory.createStringLiteral(`./${OpenApiRqFiles.common}`), undefined);
26
+ const commonExport = ts.factory.createExportDeclaration(undefined, false, undefined, ts.factory.createStringLiteral(`./${OpenApiRqFiles.common}`), undefined);
27
+ const queriesExport = ts.factory.createExportDeclaration(undefined, false, undefined, ts.factory.createStringLiteral(`./${OpenApiRqFiles.queries}`), undefined);
28
+ const mainSource = ts.factory.createSourceFile([commonImport, ...imports, ...exports.mainExports], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
29
+ const suspenseSource = ts.factory.createSourceFile([commonImport, ...imports, ...exports.suspenseExports], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
30
+ const indexSource = ts.factory.createSourceFile([commonExport, queriesExport], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
31
+ const prefetchSource = ts.factory.createSourceFile([commonImport, ...imports, ...exports.allPrefetchExports], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
32
+ return {
33
+ commonSource,
34
+ mainSource,
35
+ suspenseSource,
36
+ indexSource,
37
+ prefetchSource,
38
+ };
39
+ };
40
+ export const createSource = async ({ outputPath, version, serviceEndName, }) => {
41
+ const queriesFile = ts.createSourceFile(`${OpenApiRqFiles.queries}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
42
+ const commonFile = ts.createSourceFile(`${OpenApiRqFiles.common}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
43
+ const suspenseFile = ts.createSourceFile(`${OpenApiRqFiles.suspense}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
44
+ const indexFile = ts.createSourceFile(`${OpenApiRqFiles.index}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
45
+ const prefetchFile = ts.createSourceFile(`${OpenApiRqFiles.prefetch}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
46
+ const printer = ts.createPrinter({
47
+ newLine: ts.NewLineKind.LineFeed,
48
+ removeComments: false,
49
+ });
50
+ const { commonSource, mainSource, suspenseSource, indexSource, prefetchSource, } = await createSourceFile(outputPath, serviceEndName);
51
+ const comment = `// generated with @7nohe/openapi-react-query-codegen@${version} \n\n`;
52
+ const commonResult = comment +
53
+ printer.printNode(ts.EmitHint.Unspecified, commonSource, commonFile);
54
+ const mainResult = comment +
55
+ printer.printNode(ts.EmitHint.Unspecified, mainSource, queriesFile);
56
+ const suspenseResult = comment +
57
+ printer.printNode(ts.EmitHint.Unspecified, suspenseSource, suspenseFile);
58
+ const indexResult = comment +
59
+ printer.printNode(ts.EmitHint.Unspecified, indexSource, indexFile);
60
+ const prefetchResult = comment +
61
+ printer.printNode(ts.EmitHint.Unspecified, prefetchSource, prefetchFile);
62
+ return [
63
+ {
64
+ name: `${OpenApiRqFiles.index}.ts`,
65
+ content: indexResult,
66
+ },
67
+ {
68
+ name: `${OpenApiRqFiles.common}.ts`,
69
+ content: commonResult,
70
+ },
71
+ {
72
+ name: `${OpenApiRqFiles.queries}.ts`,
73
+ content: mainResult,
74
+ },
75
+ {
76
+ name: `${OpenApiRqFiles.suspense}.ts`,
77
+ content: suspenseResult,
78
+ },
79
+ {
80
+ name: `${OpenApiRqFiles.prefetch}.ts`,
81
+ content: prefetchResult,
82
+ },
83
+ ];
84
+ };
@@ -0,0 +1,85 @@
1
+ import ts from "typescript";
2
+ import { BuildCommonTypeName, TContext, TData, TError, capitalizeFirstLetter, extractPropertiesFromObjectParam, getNameFromMethod, getShortType, } from "./common.mjs";
3
+ import { addJSDocToNode } from "./util.mjs";
4
+ /**
5
+ * Awaited<ReturnType<typeof myClass.myMethod>>
6
+ */
7
+ function generateAwaitedReturnType({ className, methodName, }) {
8
+ return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Awaited"), [
9
+ ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("ReturnType"), [
10
+ ts.factory.createTypeQueryNode(ts.factory.createQualifiedName(ts.factory.createIdentifier(className), ts.factory.createIdentifier(methodName)), undefined),
11
+ ]),
12
+ ]);
13
+ }
14
+ export const createUseMutation = ({ className, method, jsDoc, }) => {
15
+ const methodName = getNameFromMethod(method);
16
+ const awaitedResponseDataType = generateAwaitedReturnType({
17
+ className,
18
+ methodName,
19
+ });
20
+ const mutationResult = ts.factory.createTypeAliasDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(`${className}${capitalizeFirstLetter(methodName)}MutationResult`), undefined, awaitedResponseDataType);
21
+ const responseDataType = ts.factory.createTypeParameterDeclaration(undefined, TData, undefined, ts.factory.createTypeReferenceNode(BuildCommonTypeName(mutationResult.name)));
22
+ const methodParameters = method.getParameters().length !== 0
23
+ ? ts.factory.createTypeLiteralNode(method
24
+ .getParameters()
25
+ .map((param) => {
26
+ const paramNodes = extractPropertiesFromObjectParam(param);
27
+ return paramNodes.map((refParam) => ts.factory.createPropertySignature(undefined, ts.factory.createIdentifier(refParam.name), refParam.optional
28
+ ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
29
+ : undefined, ts.factory.createTypeReferenceNode(getShortType(refParam.type.getText(param)))));
30
+ })
31
+ .flat())
32
+ : ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword);
33
+ const exportHook = ts.factory.createVariableStatement([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([
34
+ ts.factory.createVariableDeclaration(ts.factory.createIdentifier(`use${className}${capitalizeFirstLetter(methodName)}`), undefined, undefined, ts.factory.createArrowFunction(undefined, ts.factory.createNodeArray([
35
+ responseDataType,
36
+ ts.factory.createTypeParameterDeclaration(undefined, TError, undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)),
37
+ ts.factory.createTypeParameterDeclaration(undefined, TContext, undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)),
38
+ ]), [
39
+ ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createIdentifier("options"), ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Omit"), [
40
+ ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("UseMutationOptions"), [
41
+ ts.factory.createTypeReferenceNode(TData),
42
+ ts.factory.createTypeReferenceNode(TError),
43
+ methodParameters,
44
+ ts.factory.createTypeReferenceNode(TContext),
45
+ ]),
46
+ ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral("mutationFn")),
47
+ ]), undefined),
48
+ ], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createCallExpression(ts.factory.createIdentifier("useMutation"), [
49
+ ts.factory.createTypeReferenceNode(TData),
50
+ ts.factory.createTypeReferenceNode(TError),
51
+ methodParameters,
52
+ ts.factory.createTypeReferenceNode(TContext),
53
+ ], [
54
+ ts.factory.createObjectLiteralExpression([
55
+ ts.factory.createPropertyAssignment(ts.factory.createIdentifier("mutationFn"), ts.factory.createArrowFunction(undefined, undefined, method.getParameters().length !== 0
56
+ ? [
57
+ ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createObjectBindingPattern(method
58
+ .getParameters()
59
+ .map((param) => {
60
+ const paramNodes = extractPropertiesFromObjectParam(param);
61
+ return paramNodes.map((refParam) => ts.factory.createBindingElement(undefined, undefined, ts.factory.createIdentifier(refParam.name), undefined));
62
+ })
63
+ .flat()), undefined, undefined, undefined),
64
+ ]
65
+ : [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createAsExpression(ts.factory.createAsExpression(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(className), ts.factory.createIdentifier(methodName)), undefined, method.getParameters().length !== 0
66
+ ? [
67
+ ts.factory.createObjectLiteralExpression(method
68
+ .getParameters()
69
+ .map((params) => {
70
+ const paramNodes = extractPropertiesFromObjectParam(params);
71
+ return paramNodes.map((refParam) => ts.factory.createShorthandPropertyAssignment(refParam.name));
72
+ })
73
+ .flat()),
74
+ ]
75
+ : []), ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)), ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Promise"), [ts.factory.createTypeReferenceNode(TData)])))),
76
+ ts.factory.createSpreadAssignment(ts.factory.createIdentifier("options")),
77
+ ]),
78
+ ]))),
79
+ ], ts.NodeFlags.Const));
80
+ const hookWithJsDoc = addJSDocToNode(exportHook, jsDoc);
81
+ return {
82
+ mutationResult,
83
+ mutationHook: hookWithJsDoc,
84
+ };
85
+ };
@@ -0,0 +1,186 @@
1
+ import ts from "typescript";
2
+ import { BuildCommonTypeName, capitalizeFirstLetter, extractPropertiesFromObjectParam, getNameFromMethod, getShortType, queryKeyConstraint, queryKeyGenericType, TData, TError, } from "./common.mjs";
3
+ import { addJSDocToNode } from "./util.mjs";
4
+ export const createApiResponseType = ({ className, methodName, }) => {
5
+ /** Awaited<ReturnType<typeof myClass.myMethod>> */
6
+ const awaitedResponseDataType = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Awaited"), [
7
+ ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("ReturnType"), [
8
+ ts.factory.createTypeQueryNode(ts.factory.createQualifiedName(ts.factory.createIdentifier(className), ts.factory.createIdentifier(methodName)), undefined),
9
+ ]),
10
+ ]);
11
+ /** DefaultResponseDataType
12
+ * export type MyClassMethodDefaultResponse = Awaited<ReturnType<typeof myClass.myMethod>>
13
+ */
14
+ const apiResponse = ts.factory.createTypeAliasDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(`${capitalizeFirstLetter(className)}${capitalizeFirstLetter(methodName)}DefaultResponse`), undefined, awaitedResponseDataType);
15
+ const responseDataType = ts.factory.createTypeParameterDeclaration(undefined, TData.text, undefined, ts.factory.createTypeReferenceNode(BuildCommonTypeName(apiResponse.name)));
16
+ return {
17
+ /** DefaultResponseDataType
18
+ * export type MyClassMethodDefaultResponse = Awaited<ReturnType<typeof myClass.myMethod>>
19
+ */
20
+ apiResponse,
21
+ /**
22
+ * will be the name of the type of the response type of the method
23
+ * MyClassMethodDefaultResponse
24
+ */
25
+ responseDataType,
26
+ };
27
+ };
28
+ export function getRequestParamFromMethod(method) {
29
+ if (!method.getParameters().length) {
30
+ return null;
31
+ }
32
+ const params = method
33
+ .getParameters()
34
+ .map((param) => {
35
+ const paramNodes = extractPropertiesFromObjectParam(param);
36
+ return paramNodes.map((refParam) => ({
37
+ name: refParam.name,
38
+ typeName: getShortType(refParam.type.getText()),
39
+ optional: refParam.optional,
40
+ }));
41
+ })
42
+ .flat();
43
+ const areAllPropertiesOptional = params.every((param) => param.optional);
44
+ return ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createObjectBindingPattern(params.map((refParam) => ts.factory.createBindingElement(undefined, undefined, ts.factory.createIdentifier(refParam.name), undefined))), undefined, ts.factory.createTypeLiteralNode(params.map((refParam) => {
45
+ return ts.factory.createPropertySignature(undefined, ts.factory.createIdentifier(refParam.name), refParam.optional
46
+ ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
47
+ : undefined, ts.factory.createTypeReferenceNode(refParam.typeName));
48
+ })),
49
+ // if all params are optional, we create an empty object literal
50
+ // so the hook can be called without any parameters
51
+ areAllPropertiesOptional
52
+ ? ts.factory.createObjectLiteralExpression()
53
+ : undefined);
54
+ }
55
+ /**
56
+ * Return Type
57
+ * export const classNameMethodNameQueryResult<TData = MyClassMethodDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>;
58
+ */
59
+ export function createReturnTypeExport({ className, methodName, defaultApiResponse, }) {
60
+ return ts.factory.createTypeAliasDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(`${capitalizeFirstLetter(className)}${capitalizeFirstLetter(methodName)}QueryResult`), [
61
+ ts.factory.createTypeParameterDeclaration(undefined, TData, undefined, ts.factory.createTypeReferenceNode(defaultApiResponse.name)),
62
+ ts.factory.createTypeParameterDeclaration(undefined, TError, undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)),
63
+ ], ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("UseQueryResult"), [
64
+ ts.factory.createTypeReferenceNode(TData),
65
+ ts.factory.createTypeReferenceNode(TError),
66
+ ]));
67
+ }
68
+ /**
69
+ * QueryKey
70
+ */
71
+ export function createQueryKeyExport({ className, methodName, queryKey, }) {
72
+ return ts.factory.createVariableStatement([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([
73
+ ts.factory.createVariableDeclaration(ts.factory.createIdentifier(queryKey), undefined, undefined, ts.factory.createStringLiteral(`${className}${capitalizeFirstLetter(methodName)}`)),
74
+ ], ts.NodeFlags.Const));
75
+ }
76
+ export function hookNameFromMethod({ method, className, }) {
77
+ const methodName = getNameFromMethod(method);
78
+ return `use${className}${capitalizeFirstLetter(methodName)}`;
79
+ }
80
+ export function createQueryKeyFromMethod({ method, className, }) {
81
+ const customHookName = hookNameFromMethod({ method, className });
82
+ const queryKey = `${customHookName}Key`;
83
+ return queryKey;
84
+ }
85
+ /**
86
+ * Creates a custom hook for a query
87
+ * @param queryString The type of query to use from react-query
88
+ * @param suffix The suffix to append to the hook name
89
+ */
90
+ export function createQueryHook({ queryString, suffix, responseDataType, requestParams, method, className, }) {
91
+ const methodName = getNameFromMethod(method);
92
+ const customHookName = hookNameFromMethod({ method, className });
93
+ const queryKey = createQueryKeyFromMethod({ method, className });
94
+ const hookExport = ts.factory.createVariableStatement([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([
95
+ ts.factory.createVariableDeclaration(ts.factory.createIdentifier(`${customHookName}${suffix}`), undefined, undefined, ts.factory.createArrowFunction(undefined, ts.factory.createNodeArray([
96
+ responseDataType,
97
+ ts.factory.createTypeParameterDeclaration(undefined, TError, undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)),
98
+ ts.factory.createTypeParameterDeclaration(undefined, "TQueryKey", queryKeyConstraint, ts.factory.createArrayTypeNode(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword))),
99
+ ]), [
100
+ ...requestParams,
101
+ ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createIdentifier("queryKey"), ts.factory.createToken(ts.SyntaxKind.QuestionToken), queryKeyGenericType),
102
+ ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createIdentifier("options"), ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Omit"), [
103
+ ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("UseQueryOptions"), [
104
+ ts.factory.createTypeReferenceNode(TData),
105
+ ts.factory.createTypeReferenceNode(TError),
106
+ ]),
107
+ ts.factory.createUnionTypeNode([
108
+ ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral("queryKey")),
109
+ ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral("queryFn")),
110
+ ]),
111
+ ])),
112
+ ], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createCallExpression(ts.factory.createIdentifier(queryString), [
113
+ ts.factory.createTypeReferenceNode(TData),
114
+ ts.factory.createTypeReferenceNode(TError),
115
+ ], [
116
+ ts.factory.createObjectLiteralExpression([
117
+ ts.factory.createPropertyAssignment(ts.factory.createIdentifier("queryKey"), ts.factory.createArrayLiteralExpression([
118
+ BuildCommonTypeName(queryKey),
119
+ ts.factory.createSpreadElement(ts.factory.createParenthesizedExpression(ts.factory.createBinaryExpression(ts.factory.createIdentifier("queryKey"), ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken), method.getParameters().length
120
+ ? ts.factory.createArrayLiteralExpression([
121
+ ts.factory.createObjectLiteralExpression(method
122
+ .getParameters()
123
+ .map((param) => extractPropertiesFromObjectParam(param).map((p) => ts.factory.createShorthandPropertyAssignment(ts.factory.createIdentifier(p.name))))
124
+ .flat()),
125
+ ])
126
+ : ts.factory.createArrayLiteralExpression([])))),
127
+ ], false)),
128
+ ts.factory.createPropertyAssignment(ts.factory.createIdentifier("queryFn"), ts.factory.createArrowFunction(undefined, undefined, [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createAsExpression(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(className), ts.factory.createIdentifier(methodName)), undefined, method.getParameters().length
129
+ ? [
130
+ ts.factory.createObjectLiteralExpression(method
131
+ .getParameters()
132
+ .map((param) => extractPropertiesFromObjectParam(param).map((p) => ts.factory.createShorthandPropertyAssignment(ts.factory.createIdentifier(p.name))))
133
+ .flat()),
134
+ ]
135
+ : undefined), ts.factory.createTypeReferenceNode(TData)))),
136
+ ts.factory.createSpreadAssignment(ts.factory.createIdentifier("options")),
137
+ ]),
138
+ ]))),
139
+ ], ts.NodeFlags.Const));
140
+ return hookExport;
141
+ }
142
+ export const createUseQuery = ({ className, method, jsDoc, }) => {
143
+ const methodName = getNameFromMethod(method);
144
+ const queryKey = createQueryKeyFromMethod({ method, className });
145
+ const { apiResponse: defaultApiResponse, responseDataType } = createApiResponseType({
146
+ className,
147
+ methodName,
148
+ });
149
+ const requestParam = getRequestParamFromMethod(method);
150
+ const requestParams = requestParam ? [requestParam] : [];
151
+ const queryHook = createQueryHook({
152
+ queryString: "useQuery",
153
+ suffix: "",
154
+ responseDataType,
155
+ requestParams,
156
+ method,
157
+ className,
158
+ });
159
+ const suspenseQueryHook = createQueryHook({
160
+ queryString: "useSuspenseQuery",
161
+ suffix: "Suspense",
162
+ responseDataType,
163
+ requestParams,
164
+ method,
165
+ className,
166
+ });
167
+ const hookWithJsDoc = addJSDocToNode(queryHook, jsDoc);
168
+ const suspenseHookWithJsDoc = addJSDocToNode(suspenseQueryHook, jsDoc);
169
+ const returnTypeExport = createReturnTypeExport({
170
+ className,
171
+ methodName,
172
+ defaultApiResponse,
173
+ });
174
+ const queryKeyExport = createQueryKeyExport({
175
+ className,
176
+ methodName,
177
+ queryKey,
178
+ });
179
+ return {
180
+ apiResponse: defaultApiResponse,
181
+ returnType: returnTypeExport,
182
+ key: queryKeyExport,
183
+ queryHook: hookWithJsDoc,
184
+ suspenseQueryHook: suspenseHookWithJsDoc,
185
+ };
186
+ };
@@ -0,0 +1,21 @@
1
+ import { IndentationText, NewLineKind, Project, QuoteKind } from "ts-morph";
2
+ export const formatOutput = async (outputPath) => {
3
+ const project = new Project({
4
+ skipAddingFilesFromTsConfig: true,
5
+ manipulationSettings: {
6
+ indentationText: IndentationText.TwoSpaces,
7
+ newLineKind: NewLineKind.LineFeed,
8
+ quoteKind: QuoteKind.Double,
9
+ usePrefixAndSuffixTextForRename: false,
10
+ useTrailingCommas: true,
11
+ },
12
+ });
13
+ const sourceFiles = project.addSourceFilesAtPaths(`${outputPath}/**/*`);
14
+ const tasks = sourceFiles.map((sourceFile) => {
15
+ sourceFile.formatText();
16
+ sourceFile.fixMissingImports();
17
+ sourceFile.organizeImports();
18
+ return sourceFile.save();
19
+ });
20
+ await Promise.all(tasks);
21
+ };
@@ -0,0 +1,44 @@
1
+ import { createClient } from "@hey-api/openapi-ts";
2
+ import { print } from "./print.mjs";
3
+ import { createSource } from "./createSource.mjs";
4
+ import { buildQueriesOutputPath, buildRequestsOutputPath, formatOptions, } from "./common.mjs";
5
+ import { formatOutput } from "./format.mjs";
6
+ export async function generate(options, version) {
7
+ const openApiOutputPath = buildRequestsOutputPath(options.output);
8
+ const formattedOptions = formatOptions(options);
9
+ const config = {
10
+ base: formattedOptions.base,
11
+ client: formattedOptions.client,
12
+ debug: formattedOptions.debug,
13
+ dryRun: false,
14
+ enums: formattedOptions.enums,
15
+ exportCore: true,
16
+ format: formattedOptions.format,
17
+ input: formattedOptions.input,
18
+ lint: formattedOptions.lint,
19
+ output: openApiOutputPath,
20
+ request: formattedOptions.request,
21
+ schemas: {
22
+ export: !formattedOptions.noSchemas,
23
+ type: formattedOptions.schemaType,
24
+ },
25
+ services: {
26
+ export: true,
27
+ response: formattedOptions.serviceResponse,
28
+ },
29
+ types: {
30
+ dates: formattedOptions.useDateType,
31
+ export: true,
32
+ },
33
+ useOptions: true,
34
+ };
35
+ await createClient(config);
36
+ const source = await createSource({
37
+ outputPath: openApiOutputPath,
38
+ version,
39
+ serviceEndName: "Service", // we are hard coding this because changing the service end name was depreciated in @hey-api/openapi-ts
40
+ });
41
+ await print(source, formattedOptions);
42
+ const queriesOutputPath = buildQueriesOutputPath(options.output);
43
+ await formatOutput(queriesOutputPath);
44
+ }
package/dist/print.mjs ADDED
@@ -0,0 +1,22 @@
1
+ import { mkdir, writeFile } from "fs/promises";
2
+ import path from "path";
3
+ import { buildQueriesOutputPath, exists } from "./common.mjs";
4
+ async function printGeneratedTS(result, options) {
5
+ const dir = buildQueriesOutputPath(options.output);
6
+ const dirExists = await exists(dir);
7
+ if (!dirExists) {
8
+ await mkdir(dir, { recursive: true });
9
+ }
10
+ await writeFile(path.join(dir, result.name), result.content);
11
+ }
12
+ export async function print(results, options) {
13
+ const outputPath = options.output;
14
+ const dirExists = await exists(outputPath);
15
+ if (!dirExists) {
16
+ await mkdir(outputPath);
17
+ }
18
+ const promises = results.map(async (result) => {
19
+ await printGeneratedTS(result, options);
20
+ });
21
+ await Promise.all(promises);
22
+ }
@@ -0,0 +1,79 @@
1
+ import ts from "typescript";
2
+ import { getClassNameFromClassNode, getClassesFromService, } from "./common.mjs";
3
+ import { serviceFileName } from "./constants.mjs";
4
+ export async function getServices(project) {
5
+ const node = project
6
+ .getSourceFiles()
7
+ .find((sourceFile) => sourceFile.getFilePath().includes(serviceFileName));
8
+ if (!node) {
9
+ throw new Error("No service node found");
10
+ }
11
+ const klasses = getClassesFromService(node);
12
+ return {
13
+ klasses: klasses.map(({ klass, className }) => ({
14
+ className,
15
+ klass,
16
+ methods: getMethodsFromService(node, klass),
17
+ })),
18
+ node,
19
+ };
20
+ }
21
+ function getMethodsFromService(node, klass) {
22
+ const methods = klass.getMethods();
23
+ if (!methods.length) {
24
+ throw new Error("No methods found");
25
+ }
26
+ return methods.map((method) => {
27
+ const methodBlockNode = method.compilerNode
28
+ .getChildren(node.compilerNode)
29
+ .find((child) => child.kind === ts.SyntaxKind.Block);
30
+ if (!methodBlockNode) {
31
+ throw new Error("Method block not found");
32
+ }
33
+ const methodBlock = methodBlockNode;
34
+ const foundReturnStatement = methodBlock.statements.find((s) => s.kind === ts.SyntaxKind.ReturnStatement);
35
+ if (!foundReturnStatement) {
36
+ throw new Error("Return statement not found");
37
+ }
38
+ const returnStatement = foundReturnStatement;
39
+ const foundCallExpression = returnStatement.expression;
40
+ if (!foundCallExpression) {
41
+ throw new Error("Call expression not found");
42
+ }
43
+ const callExpression = foundCallExpression;
44
+ const properties = callExpression.arguments[1].properties;
45
+ const httpMethodName = properties
46
+ .find((p) => p.name?.getText(node.compilerNode) === "method")
47
+ ?.initializer?.getText(node.compilerNode);
48
+ if (!httpMethodName) {
49
+ throw new Error("httpMethodName not found");
50
+ }
51
+ const getAllChildren = (tsNode) => {
52
+ const childItems = tsNode.getChildren(node.compilerNode);
53
+ if (childItems.length) {
54
+ const allChildren = childItems.map(getAllChildren);
55
+ return [tsNode].concat(allChildren.flat());
56
+ }
57
+ return [tsNode];
58
+ };
59
+ const children = getAllChildren(method.compilerNode);
60
+ // get all JSDoc comments
61
+ // this should be an array of 1 or 0
62
+ const jsDocs = children
63
+ .filter((c) => c.kind === ts.SyntaxKind.JSDoc)
64
+ .map((c) => c.getText(node.compilerNode));
65
+ // get the first JSDoc comment
66
+ const jsDoc = jsDocs?.[0];
67
+ const isDeprecated = children.some((c) => c.kind === ts.SyntaxKind.JSDocDeprecatedTag);
68
+ const className = getClassNameFromClassNode(klass);
69
+ return {
70
+ className,
71
+ node,
72
+ method,
73
+ methodBlock,
74
+ httpMethodName,
75
+ jsDoc,
76
+ isDeprecated,
77
+ };
78
+ });
79
+ }
package/dist/util.mjs ADDED
@@ -0,0 +1,16 @@
1
+ import ts from "typescript";
2
+ export function addJSDocToNode(node, jsDoc) {
3
+ if (!jsDoc) {
4
+ return node;
5
+ }
6
+ // replace the first /** with *
7
+ // we do this because ts.addSyntheticLeadingComment will add /* to the beginning but we want /**
8
+ const removedFirstLine = jsDoc.trim().replace(/^\/\*\*/, "*");
9
+ // remove the last */ because ts.addSyntheticLeadingComment will add it
10
+ const removedSecondLine = removedFirstLine.replace(/\*\/$/, "");
11
+ const split = removedSecondLine.split("\n");
12
+ const trimmed = split.map((line) => line.trim());
13
+ const joined = trimmed.join("\n");
14
+ const nodeWithJSDoc = ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, joined, true);
15
+ return nodeWithJSDoc;
16
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@7nohe/openapi-react-query-codegen",
3
+ "version": "0.0.0-c7f76ece287f3b22268170498e8fea9b0c797946",
4
+ "description": "OpenAPI React Query Codegen",
5
+ "bin": {
6
+ "openapi-rq": "dist/cli.mjs"
7
+ },
8
+ "type": "module",
9
+ "workspaces": [
10
+ "examples/*"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/7nohe/openapi-react-query-codegen.git"
15
+ },
16
+ "homepage": "https://github.com/7nohe/openapi-react-query-codegen",
17
+ "bugs": "https://github.com/7nohe/openapi-react-query-codegen/issues",
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "keywords": [
22
+ "codegen",
23
+ "react-query",
24
+ "react",
25
+ "openapi",
26
+ "swagger",
27
+ "typescript",
28
+ "openapi-typescript-codegen",
29
+ "@hey-api/openapi-ts"
30
+ ],
31
+ "author": "Daiki Urata (@7nohe)",
32
+ "license": "MIT",
33
+ "devDependencies": {
34
+ "@hey-api/openapi-ts": "0.42.1",
35
+ "@types/node": "^20.10.6",
36
+ "@vitest/coverage-v8": "^1.5.0",
37
+ "commander": "^12.0.0",
38
+ "glob": "^10.3.10",
39
+ "rimraf": "^5.0.5",
40
+ "ts-morph": "^22.0.0",
41
+ "typescript": "^5.3.3",
42
+ "vitest": "^1.5.0"
43
+ },
44
+ "peerDependencies": {
45
+ "@hey-api/openapi-ts": "0.42.1",
46
+ "commander": "12.x",
47
+ "glob": "10.x",
48
+ "ts-morph": "22.x",
49
+ "typescript": "5.x"
50
+ },
51
+ "engines": {
52
+ "node": ">=14"
53
+ },
54
+ "scripts": {
55
+ "build": "rimraf dist && tsc -p tsconfig.json",
56
+ "preview": "npm run build && npm -C examples/react-app run generate:api",
57
+ "release": "npx git-ensure -a && npx bumpp --commit --tag --push",
58
+ "test": "vitest --coverage.enabled true"
59
+ }
60
+ }