diff --git a/README.md b/README.md
index a75d08f..494302d 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
$ npm install -D @7nohe/openapi-react-query-codegen
```
- Register the command to the `scripts` property in your package.json file.
+Register the command to the `scripts` property in your package.json file.
```json
{
@@ -30,7 +30,6 @@ You can also run the command without installing it in your project using the npx
$ npx --package @7nohe/openapi-react-query-codegen openapi-rq -i ./petstore.yaml -c axios
```
-
## Usage
```
@@ -67,7 +66,10 @@ $ openapi-rq -i ./petstore.yaml
```
- openapi
- queries
- - index.ts <- custom react hooks
+ - index.ts <- main file that exports common types, variables, and hooks
+ - common.ts <- common types
+ - queries.ts <- generated query hooks
+ - suspenses.ts <- generated suspense hooks
- requests <- output code generated by OpenAPI Typescript Codegen
```
@@ -75,9 +77,7 @@ $ openapi-rq -i ./petstore.yaml
```tsx
// App.tsx
-import {
- usePetServiceFindPetsByStatus,
-} from "../openapi/queries";
+import { usePetServiceFindPetsByStatus } from "../openapi/queries";
function App() {
const { data } = usePetServiceFindPetsByStatus({ status: ["available"] });
@@ -100,10 +100,8 @@ You can also use pure TS clients.
```tsx
import { useQuery } from "@tanstack/react-query";
-import { PetService } from '../openapi/requests/services/PetService';
-import {
- usePetServiceFindPetsByStatusKey,
-} from "../openapi/queries";
+import { PetService } from "../openapi/requests/services/PetService";
+import { usePetServiceFindPetsByStatusKey } from "../openapi/queries";
function App() {
// You can still use the auto-generated query key
@@ -111,14 +109,40 @@ function App() {
queryKey: [usePetServiceFindPetsByStatusKey],
queryFn: () => {
// Do something here
- return PetService.findPetsByStatus(['available']);
- }
-});
+ return PetService.findPetsByStatus(["available"]);
+ },
+ });
+
+ return
{/* .... */}
;
+}
+
+export default App;
+```
+
+You can also use suspense hooks.
+
+```tsx
+// App.tsx
+import { useDefaultClientFindPetsSuspense } from "../openapi/queries/suspense";
+function ChildComponent() {
+ const { data } = useDefaultClientFindPetsSuspense({ tags: [], limit: 10 });
return (
-
- {/* .... */}
-
+
+ {data?.map((pet, index) => (
+ - {pet.name}
+ ))}
+
+ );
+}
+
+function ParentComponent() {
+ return (
+ <>
+ loading...>}>
+
+
+ >
);
}
@@ -126,4 +150,5 @@ export default App;
```
## License
+
MIT
diff --git a/examples/react-app/src/App.tsx b/examples/react-app/src/App.tsx
index 41ee5ff..0e7e0c4 100644
--- a/examples/react-app/src/App.tsx
+++ b/examples/react-app/src/App.tsx
@@ -8,6 +8,7 @@ import {
} from "../openapi/queries";
import { useState } from "react";
import { queryClient } from "./queryClient";
+import { SuspenseParent } from "./components/SuspenseParent";
function App() {
const [tags, _setTags] = useState([]);
@@ -19,7 +20,8 @@ function App() {
// this defaults to any - here we are showing how to override the type
// Note - this is marked as deprecated in the OpenAPI spec and being passed to the client
const { data: notDefined } = useDefaultClientGetNotDefined();
- const { mutate: mutateNotDefined } = useDefaultClientPostNotDefined();
+ const { mutate: mutateNotDefined } =
+ useDefaultClientPostNotDefined();
const { mutate: addPet } = useDefaultClientAddPet();
@@ -60,6 +62,10 @@ function App() {
>
Create a pet
+
+
Suspense Components
+
+
);
}
diff --git a/examples/react-app/src/components/SuspenseChild.tsx b/examples/react-app/src/components/SuspenseChild.tsx
new file mode 100644
index 0000000..d2f482e
--- /dev/null
+++ b/examples/react-app/src/components/SuspenseChild.tsx
@@ -0,0 +1,17 @@
+import { useDefaultClientFindPetsSuspense } from "../../openapi/queries/suspense";
+
+export const SuspenseChild = () => {
+ const { data } = useDefaultClientFindPetsSuspense({ tags: [], limit: 10 });
+
+ if (!Array.isArray(data)) {
+ return Error!
;
+ }
+
+ return (
+
+ {data?.map((pet, index) => (
+ - {pet.name}
+ ))}
+
+ );
+};
diff --git a/examples/react-app/src/components/SuspenseParent.tsx b/examples/react-app/src/components/SuspenseParent.tsx
new file mode 100644
index 0000000..d86e793
--- /dev/null
+++ b/examples/react-app/src/components/SuspenseParent.tsx
@@ -0,0 +1,12 @@
+import { Suspense } from "react";
+import { SuspenseChild } from "./SuspenseChild";
+
+export const SuspenseParent = () => {
+ return (
+ <>
+ loading...>}>
+
+
+ >
+ );
+};
diff --git a/src/common.ts b/src/common.ts
index 736a0e1..c123061 100644
--- a/src/common.ts
+++ b/src/common.ts
@@ -1,3 +1,17 @@
+import { type PathLike } from "fs";
+import { stat } from "fs/promises";
+import ts, { JSDocComment, NodeArray, SourceFile } from "typescript";
+
+export const TData = ts.factory.createIdentifier("TData");
+export const TError = ts.factory.createIdentifier("TError");
+export const TContext = ts.factory.createIdentifier("TContext");
+
+export const queryKeyGenericType =
+ ts.factory.createTypeReferenceNode("TQueryKey");
+export const queryKeyConstraint = ts.factory.createTypeReferenceNode("Array", [
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
+]);
+
export const capitalizeFirstLetter = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
@@ -5,3 +19,38 @@ export const capitalizeFirstLetter = (str: string) => {
export const lowercaseFirstLetter = (str: string) => {
return str.charAt(0).toLowerCase() + str.slice(1);
};
+
+export const getNameFromMethod = (
+ method: ts.MethodDeclaration,
+ node: ts.SourceFile
+) => {
+ return method.name.getText(node);
+};
+
+export type MethodDescription = {
+ className: string;
+ node: SourceFile;
+ method: ts.MethodDeclaration;
+ methodBlock: ts.Block;
+ httpMethodName: string;
+ jsDoc: (string | NodeArray | undefined)[];
+ isDeprecated: boolean;
+};
+
+export async function exists(f: PathLike) {
+ try {
+ await stat(f);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+const Common = "Common";
+
+export function BuildCommonTypeName(name: string | ts.Identifier) {
+ if (typeof name === "string") {
+ return ts.factory.createIdentifier(`${Common}.${name}`);
+ }
+ return ts.factory.createIdentifier(`${Common}.${name.text}`);
+}
diff --git a/src/createExports.ts b/src/createExports.ts
index c9bde14..c441d91 100644
--- a/src/createExports.ts
+++ b/src/createExports.ts
@@ -4,8 +4,9 @@ import { join } from "path";
import fs from "fs";
import { createUseQuery } from "./createUseQuery";
import { createUseMutation } from "./createUseMutation";
+import { type MethodDescription } from "./common";
-export const createExports = (generatedClientsPath: string) => {
+export function getMethodsFromService(generatedClientsPath: string) {
const services = sync(
join(generatedClientsPath, "services", "*.ts").replace(/\\/g, "/")
);
@@ -19,56 +20,214 @@ export const createExports = (generatedClientsPath: string) => {
return [
...nodes
.map((node) => {
- const klass = node
+ const foundKlass = node
.getChildren()[0]
.getChildren()
- .find(
- (child) => child.kind === ts.SyntaxKind.ClassDeclaration
- ) as ts.ClassDeclaration;
- const className = klass.name?.getText(node)!;
+ .find((child) => child.kind === ts.SyntaxKind.ClassDeclaration);
+ if (!foundKlass) {
+ throw new Error("Class not found");
+ }
+ const klass = foundKlass as ts.ClassDeclaration;
+ const className = klass.name?.getText(node);
+ if (!className) {
+ throw new Error("Class name not found");
+ }
const methods = klass.members.filter(
(node) => node.kind === ts.SyntaxKind.MethodDeclaration
) as ts.MethodDeclaration[];
- return methods
- .map((method) => {
- const methodBlock = method
- .getChildren(node)
- .find((child) => child.kind === ts.SyntaxKind.Block) as ts.Block;
- const returnStatement = methodBlock.statements.find(
- (s) => s.kind === ts.SyntaxKind.ReturnStatement
- ) as ts.ReturnStatement;
- const callExpression =
- returnStatement.expression as ts.CallExpression;
- const properties = (
- callExpression.arguments[1] as ts.ObjectLiteralExpression
- ).properties as unknown as ts.PropertyAssignment[];
- const httpMethodName = properties
- .find((p) => p.name?.getText(node) === "method")
- ?.initializer?.getText(node)!;
-
-
- const getAllChildren = (tsNode: ts.Node): Array => {
- const childItems = tsNode.getChildren(node);
- if (childItems.length) {
- const allChildren = childItems.map(getAllChildren);
- return [tsNode].concat(allChildren.flat());
- }
- return [tsNode];
+ if (!methods.length) {
+ throw new Error("No methods found");
+ }
+ return methods.map((method) => {
+ const methodBlockNode = method
+ .getChildren(node)
+ .find((child) => child.kind === ts.SyntaxKind.Block);
+
+ if (!methodBlockNode) {
+ throw new Error("Method block not found");
+ }
+ const methodBlock = methodBlockNode as ts.Block;
+ const foundReturnStatement = methodBlock.statements.find(
+ (s) => s.kind === ts.SyntaxKind.ReturnStatement
+ );
+ if (!foundReturnStatement) {
+ throw new Error("Return statement not found");
+ }
+ const returnStatement = foundReturnStatement as ts.ReturnStatement;
+ const foundCallExpression = returnStatement.expression;
+ if (!foundCallExpression) {
+ throw new Error("Call expression not found");
+ }
+ const callExpression = foundCallExpression as ts.CallExpression;
+ const properties = (
+ callExpression.arguments[1] as ts.ObjectLiteralExpression
+ ).properties as unknown as ts.PropertyAssignment[];
+ const httpMethodName = properties
+ .find((p) => p.name?.getText(node) === "method")
+ ?.initializer?.getText(node);
+
+ if (!httpMethodName) {
+ throw new Error("httpMethodName not found");
+ }
+
+ const getAllChildren = (tsNode: ts.Node): Array => {
+ const childItems = tsNode.getChildren(node);
+ if (childItems.length) {
+ const allChildren = childItems.map(getAllChildren);
+ return [tsNode].concat(allChildren.flat());
}
+ return [tsNode];
+ };
- const children = getAllChildren(method);
- const jsDoc = children.filter((c) => c.kind === ts.SyntaxKind.JSDoc).map((c) => {
- return (c as JSDoc).comment
- });
- const hasDeprecated = children
- .some((c) => c.kind === ts.SyntaxKind.JSDocDeprecatedTag);
-
- return httpMethodName === "'GET'"
- ? createUseQuery(node, className, method, jsDoc, hasDeprecated)
- : createUseMutation(node, className, method, jsDoc, hasDeprecated);
- })
- .flat();
+ const children = getAllChildren(method);
+ const jsDoc = children
+ .filter((c) => c.kind === ts.SyntaxKind.JSDoc)
+ .map((c) => (c as JSDoc).comment);
+ const isDeprecated = children.some(
+ (c) => c.kind === ts.SyntaxKind.JSDocDeprecatedTag
+ );
+
+ return {
+ className,
+ node,
+ method,
+ methodBlock,
+ httpMethodName,
+ jsDoc,
+ isDeprecated,
+ } satisfies MethodDescription;
+ });
})
.flat(),
];
+}
+
+export const createExportsV2 = (generatedClientsPath: string) => {
+ const methods = getMethodsFromService(generatedClientsPath);
+
+ const allGet = methods.filter((m) => m.httpMethodName === "'GET'");
+ const allPost = methods.filter((m) => m.httpMethodName === "'POST'");
+
+ const allQueries = allGet.map((m) => createUseQuery(m));
+ const allMutations = allPost.map((m) => createUseMutation(m));
+
+ const commonInQueries = allQueries
+ .map(({ apiResponse, returnType, key }) => [apiResponse, returnType, key])
+ .flat();
+ const commonInMutations = allMutations
+ .map(({ mutationResult }) => [mutationResult])
+ .flat();
+
+ const allCommon = [...commonInQueries, ...commonInMutations];
+
+ const mainQueries = allQueries.map(({ queryHook }) => [queryHook]).flat();
+ const mainMutations = allMutations
+ .map(({ mutationHook }) => [mutationHook])
+ .flat();
+
+ const mainExports = [...mainQueries, ...mainMutations];
+
+ const suspenseQueries = allQueries
+ .map(({ suspenseQueryHook }) => [suspenseQueryHook])
+ .flat();
+
+ const suspenseExports = [...suspenseQueries];
+
+ return {
+ /**
+ * Common types and variables between queries (regular and suspense) and mutations
+ */
+ allCommon,
+ /**
+ * Main exports are the hooks that are used in the components
+ */
+ mainExports,
+ /**
+ * Suspense exports are the hooks that are used in the suspense components
+ */
+ suspenseExports,
+ };
};
+
+// export const createExports = (generatedClientsPath: string) => {
+// const services = sync(
+// join(generatedClientsPath, "services", "*.ts").replace(/\\/g, "/")
+// );
+// const nodes = services.map((servicePath) =>
+// ts.createSourceFile(
+// servicePath, // fileName
+// fs.readFileSync(join(process.cwd(), servicePath), "utf8"),
+// ts.ScriptTarget.Latest // languageVersion
+// )
+// );
+// return [
+// ...nodes
+// .map((node) => {
+// const klass = node
+// .getChildren()[0]
+// .getChildren()
+// .find(
+// (child) => child.kind === ts.SyntaxKind.ClassDeclaration
+// ) as ts.ClassDeclaration;
+// const className = klass.name?.getText(node);
+// if (!className) {
+// throw new Error("Class name not found");
+// }
+// const methods = klass.members.filter(
+// (node) => node.kind === ts.SyntaxKind.MethodDeclaration
+// ) as ts.MethodDeclaration[];
+// return methods
+// .map((method) => {
+// const methodBlock = method
+// .getChildren(node)
+// .find((child) => child.kind === ts.SyntaxKind.Block) as ts.Block;
+// const returnStatement = methodBlock.statements.find(
+// (s) => s.kind === ts.SyntaxKind.ReturnStatement
+// ) as ts.ReturnStatement;
+// const callExpression =
+// returnStatement.expression as ts.CallExpression;
+// const properties = (
+// callExpression.arguments[1] as ts.ObjectLiteralExpression
+// ).properties as unknown as ts.PropertyAssignment[];
+// const httpMethodName = properties
+// .find((p) => p.name?.getText(node) === "method")
+// ?.initializer?.getText(node);
+
+// if (!httpMethodName) {
+// throw new Error("httpMethodName not found");
+// }
+
+// const getAllChildren = (tsNode: ts.Node): Array => {
+// const childItems = tsNode.getChildren(node);
+// if (childItems.length) {
+// const allChildren = childItems.map(getAllChildren);
+// return [tsNode].concat(allChildren.flat());
+// }
+// return [tsNode];
+// };
+
+// const children = getAllChildren(method);
+// const jsDoc = children
+// .filter((c) => c.kind === ts.SyntaxKind.JSDoc)
+// .map((c) => {
+// return (c as JSDoc).comment;
+// });
+// const hasDeprecated = children.some(
+// (c) => c.kind === ts.SyntaxKind.JSDocDeprecatedTag
+// );
+
+// return httpMethodName === "'GET'"
+// ? createUseQuery(node, className, method, jsDoc, hasDeprecated)
+// : createUseMutation(
+// node,
+// className,
+// method,
+// jsDoc,
+// hasDeprecated
+// );
+// })
+// .flat();
+// })
+// .flat(),
+// ];
+// };
diff --git a/src/createImports.ts b/src/createImports.ts
index ebc7f1f..6f74b7d 100644
--- a/src/createImports.ts
+++ b/src/createImports.ts
@@ -23,6 +23,11 @@ export const createImports = (generatedClientsPath: string) => {
undefined,
ts.factory.createIdentifier("useQuery")
),
+ ts.factory.createImportSpecifier(
+ false,
+ undefined,
+ ts.factory.createIdentifier("useSuspenseQuery")
+ ),
ts.factory.createImportSpecifier(
false,
undefined,
diff --git a/src/createSource.ts b/src/createSource.ts
index ce3aa95..6c7155c 100644
--- a/src/createSource.ts
+++ b/src/createSource.ts
@@ -1,34 +1,142 @@
-import ts from "typescript";
+import ts, { factory } from "typescript";
import { createImports } from "./createImports";
-import { createExports } from "./createExports";
+import { createExportsV2 } from "./createExports";
import { version } from "../package.json";
const createSourceFile = (outputPath: string) => {
- return ts.factory.createSourceFile(
- [...createImports(outputPath), ...createExports(outputPath)],
+ const imports = createImports(outputPath);
+ const exports = createExportsV2(outputPath);
+
+ const commonSource = ts.factory.createSourceFile(
+ [...imports, ...exports.allCommon],
ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
ts.NodeFlags.None
);
+
+ const commonImport = ts.factory.createImportDeclaration(
+ undefined,
+ ts.factory.createImportClause(
+ false,
+ ts.factory.createIdentifier("* as Common"),
+ undefined
+ ),
+ ts.factory.createStringLiteral("./common"),
+ undefined
+ );
+
+ const commonExport = ts.factory.createExportDeclaration(
+ undefined,
+ false,
+ undefined,
+ ts.factory.createStringLiteral("./common"),
+ undefined
+ );
+
+ const queriesExport = ts.factory.createExportDeclaration(
+ undefined,
+ false,
+ undefined,
+ ts.factory.createStringLiteral("./queries"),
+ undefined
+ );
+
+ const mainSource = ts.factory.createSourceFile(
+ [commonImport, ...imports, ...exports.mainExports],
+ ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
+ ts.NodeFlags.None
+ );
+
+ const suspenseSource = ts.factory.createSourceFile(
+ [commonImport, ...imports, ...exports.suspenseExports],
+ ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
+ ts.NodeFlags.None
+ );
+
+ const indexSource = ts.factory.createSourceFile(
+ [commonExport, queriesExport],
+ ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
+ ts.NodeFlags.None
+ );
+
+ return {
+ commonSource,
+ mainSource,
+ suspenseSource,
+ indexSource,
+ };
};
-export const createSource = (outputPath: string) => {
- const resultFile = ts.createSourceFile(
+export const createSources = (outputPath: string) => {
+ const queriesFile = ts.createSourceFile(
+ "queries.ts",
+ "",
+ ts.ScriptTarget.Latest,
+ false,
+ ts.ScriptKind.TS
+ );
+ const commonFile = ts.createSourceFile(
+ "common.ts",
+ "",
+ ts.ScriptTarget.Latest,
+ false,
+ ts.ScriptKind.TS
+ );
+ const suspenseFile = ts.createSourceFile(
+ "suspense.ts",
+ "",
+ ts.ScriptTarget.Latest,
+ false,
+ ts.ScriptKind.TS
+ );
+
+ const indexFile = ts.createSourceFile(
"index.ts",
"",
ts.ScriptTarget.Latest,
false,
ts.ScriptKind.TS
);
+
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
removeComments: false,
});
- const node = createSourceFile(outputPath);
+ const { commonSource, mainSource, suspenseSource, indexSource } =
+ createSourceFile(outputPath);
+
+ const commonResult =
+ `// generated with @7nohe/openapi-react-query-codegen@${version} \n` +
+ printer.printNode(ts.EmitHint.Unspecified, commonSource, commonFile);
+
+ const mainResult =
+ `// generated with @7nohe/openapi-react-query-codegen@${version} \n` +
+ printer.printNode(ts.EmitHint.Unspecified, mainSource, queriesFile);
+
+ const suspenseResult =
+ `// generated with @7nohe/openapi-react-query-codegen@${version} \n` +
+ printer.printNode(ts.EmitHint.Unspecified, suspenseSource, suspenseFile);
- const result =
+ const indexResult =
`// generated with @7nohe/openapi-react-query-codegen@${version} \n` +
- printer.printNode(ts.EmitHint.Unspecified, node, resultFile);
+ printer.printNode(ts.EmitHint.Unspecified, indexSource, indexFile);
- return result;
+ return [
+ {
+ name: "index.ts",
+ content: indexResult,
+ },
+ {
+ name: "common.ts",
+ content: commonResult,
+ },
+ {
+ name: "queries.ts",
+ content: mainResult,
+ },
+ {
+ name: "suspense.ts",
+ content: suspenseResult,
+ },
+ ];
};
diff --git a/src/createUseMutation.ts b/src/createUseMutation.ts
index 89b29d3..706a270 100644
--- a/src/createUseMutation.ts
+++ b/src/createUseMutation.ts
@@ -1,17 +1,26 @@
import ts from "typescript";
-import { capitalizeFirstLetter } from "./common";
+import {
+ BuildCommonTypeName,
+ MethodDescription,
+ TContext,
+ TData,
+ TError,
+ capitalizeFirstLetter,
+ getNameFromMethod,
+} from "./common";
import { addJSDocToNode } from "./util";
-export const createUseMutation = (
- node: ts.SourceFile,
- className: string,
- method: ts.MethodDeclaration,
- jsDoc: (string | ts.NodeArray | undefined)[] = [],
- deprecated: boolean = false
-) => {
- const methodName = method.name?.getText(node)!;
- // Awaited>
- const awaitedResponseDataType = ts.factory.createTypeReferenceNode(
+/**
+ * Awaited>
+ */
+function generateAwaitedReturnType({
+ className,
+ methodName,
+}: {
+ className: string;
+ methodName: string;
+}) {
+ return ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("Awaited"),
[
ts.factory.createTypeReferenceNode(
@@ -28,14 +37,26 @@ export const createUseMutation = (
),
]
);
+}
- const TData = ts.factory.createIdentifier("TData");
- const TError = ts.factory.createIdentifier("TError");
- const TContext = ts.factory.createIdentifier("TContext");
+export const createUseMutation = ({
+ node,
+ className,
+ method,
+ jsDoc = [],
+ isDeprecated = false,
+}: MethodDescription) => {
+ const methodName = getNameFromMethod(method, node);
+ const awaitedResponseDataType = generateAwaitedReturnType({
+ className,
+ methodName,
+ });
const mutationResult = ts.factory.createTypeAliasDeclaration(
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
- ts.factory.createIdentifier(`${className}${capitalizeFirstLetter(methodName)}MutationResult`),
+ ts.factory.createIdentifier(
+ `${className}${capitalizeFirstLetter(methodName)}MutationResult`
+ ),
undefined,
awaitedResponseDataType
);
@@ -44,7 +65,7 @@ export const createUseMutation = (
undefined,
TData,
undefined,
- ts.factory.createTypeReferenceNode(mutationResult.name)
+ ts.factory.createTypeReferenceNode(BuildCommonTypeName(mutationResult.name))
);
const methodParameters =
@@ -199,7 +220,10 @@ export const createUseMutation = (
)
);
- const hookWithJsDoc = addJSDocToNode(exportHook, node, deprecated, jsDoc);
+ const hookWithJsDoc = addJSDocToNode(exportHook, node, isDeprecated, jsDoc);
- return [mutationResult, hookWithJsDoc];
+ return {
+ mutationResult,
+ mutationHook: hookWithJsDoc,
+ };
};
diff --git a/src/createUseQuery.ts b/src/createUseQuery.ts
index 3c06227..d6deb42 100644
--- a/src/createUseQuery.ts
+++ b/src/createUseQuery.ts
@@ -1,57 +1,23 @@
import ts from "typescript";
-import { capitalizeFirstLetter } from "./common";
+import {
+ BuildCommonTypeName,
+ capitalizeFirstLetter,
+ getNameFromMethod,
+ queryKeyConstraint,
+ queryKeyGenericType,
+} from "./common";
import { addJSDocToNode } from "./util";
+import { type MethodDescription } from "./common";
+import { TData, TError } from "./common";
-export const createUseQuery = (
- node: ts.SourceFile,
- className: string,
- method: ts.MethodDeclaration,
- jsDoc: (string | ts.NodeArray | undefined)[] = [],
- deprecated: boolean = false
-) => {
- const methodName = method.name?.getText(node)!;
- let requestParam = [];
- if (method.parameters.length !== 0) {
- requestParam.push(
- ts.factory.createParameterDeclaration(
- undefined,
- undefined,
- ts.factory.createObjectBindingPattern(
- method.parameters.map((param) =>
- ts.factory.createBindingElement(
- undefined,
- undefined,
- ts.factory.createIdentifier(param.name.getText(node)),
- undefined
- )
- )
- ),
- undefined,
- ts.factory.createTypeLiteralNode(
- method.parameters.map((param) =>
- ts.factory.createPropertySignature(
- undefined,
- ts.factory.createIdentifier(param.name.getText(node)),
- param.questionToken ?? param.initializer
- ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
- : param.questionToken,
- param.type
- )
- )
- )
- )
- );
- }
-
- const customHookName = `use${className}${capitalizeFirstLetter(methodName)}`;
- const queryKey = `${customHookName}Key`;
-
- const queryKeyGenericType = ts.factory.createTypeReferenceNode("TQueryKey");
- const queryKeyConstraint = ts.factory.createTypeReferenceNode("Array", [
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
- ]);
-
- // Awaited>
+export const createApiResponseType = ({
+ className,
+ methodName,
+}: {
+ className: string;
+ methodName: string;
+}) => {
+ /** Awaited> */
const awaitedResponseDataType = ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("Awaited"),
[
@@ -69,9 +35,10 @@ export const createUseQuery = (
),
]
);
- // DefaultResponseDataType
- // export type MyClassMethodDefaultResponse = Awaited>
- const defaultApiResponse = ts.factory.createTypeAliasDeclaration(
+ /** DefaultResponseDataType
+ * export type MyClassMethodDefaultResponse = Awaited>
+ */
+ const apiResponse = ts.factory.createTypeAliasDeclaration(
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier(
`${capitalizeFirstLetter(className)}${capitalizeFirstLetter(
@@ -82,19 +49,76 @@ export const createUseQuery = (
awaitedResponseDataType
);
- const TData = ts.factory.createIdentifier("TData");
- const TError = ts.factory.createIdentifier("TError");
-
const responseDataType = ts.factory.createTypeParameterDeclaration(
undefined,
TData.text,
undefined,
- ts.factory.createTypeReferenceNode(defaultApiResponse.name)
+ ts.factory.createTypeReferenceNode(BuildCommonTypeName(apiResponse.name))
+ );
+
+ return {
+ /** DefaultResponseDataType
+ * export type MyClassMethodDefaultResponse = Awaited>
+ */
+ apiResponse,
+ /**
+ * will be the name of the type of the response type of the method
+ * MyClassMethodDefaultResponse
+ */
+ responseDataType,
+ };
+};
+
+export function getRequestParamFromMethod(
+ method: ts.MethodDeclaration,
+ node: ts.SourceFile
+) {
+ if (!method.parameters.length) {
+ return null;
+ }
+ return ts.factory.createParameterDeclaration(
+ undefined,
+ undefined,
+ ts.factory.createObjectBindingPattern(
+ method.parameters.map((param) =>
+ ts.factory.createBindingElement(
+ undefined,
+ undefined,
+ ts.factory.createIdentifier(param.name.getText(node)),
+ undefined
+ )
+ )
+ ),
+ undefined,
+ ts.factory.createTypeLiteralNode(
+ method.parameters.map((param) =>
+ ts.factory.createPropertySignature(
+ undefined,
+ ts.factory.createIdentifier(param.name.getText(node)),
+ param.questionToken ?? param.initializer
+ ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
+ : param.questionToken,
+ param.type
+ )
+ )
+ )
);
+}
- // Return Type
- // export const classNameMethodNameQueryResult = UseQueryResult;
- const returnTypeExport = ts.factory.createTypeAliasDeclaration(
+/**
+ * Return Type
+ * export const classNameMethodNameQueryResult = UseQueryResult;
+ */
+export function createReturnTypeExport({
+ className,
+ methodName,
+ defaultApiResponse,
+}: {
+ className: string;
+ methodName: string;
+ defaultApiResponse: ts.TypeAliasDeclaration;
+}) {
+ return ts.factory.createTypeAliasDeclaration(
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier(
`${capitalizeFirstLetter(className)}${capitalizeFirstLetter(
@@ -120,12 +144,24 @@ export const createUseQuery = (
[
ts.factory.createTypeReferenceNode(TData),
ts.factory.createTypeReferenceNode(TError),
- ],
- ),
+ ]
+ )
);
+}
- // QueryKey
- const queryKeyExport = ts.factory.createVariableStatement(
+/**
+ * QueryKey
+ */
+export function createQueryKeyExport({
+ className,
+ methodName,
+ queryKey,
+}: {
+ className: string;
+ methodName: string;
+ queryKey: string;
+}) {
+ return ts.factory.createVariableStatement(
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
ts.factory.createVariableDeclarationList(
[
@@ -141,14 +177,67 @@ export const createUseQuery = (
ts.NodeFlags.Const
)
);
+}
+
+function hookNameFromMethod({
+ method,
+ node,
+ className,
+}: {
+ method: ts.MethodDeclaration;
+ node: ts.SourceFile;
+ className: string;
+}) {
+ const methodName = getNameFromMethod(method, node);
+ return `use${className}${capitalizeFirstLetter(methodName)}`;
+}
+
+function createQueryKeyFromMethod({
+ method,
+ node,
+ className,
+}: {
+ method: ts.MethodDeclaration;
+ node: ts.SourceFile;
+ className: string;
+}) {
+ const customHookName = hookNameFromMethod({ method, node, className });
+ const queryKey = `${customHookName}Key`;
+ return queryKey;
+}
+
+/**
+ * Creates a custom hook for a query
+ * @param queryString The type of query to use from react-query
+ * @param suffix The suffix to append to the hook name
+ */
+function createQueryHook({
+ queryString,
+ suffix,
+ responseDataType,
+ requestParams,
+ method,
+ node,
+ className,
+}: {
+ queryString: "useSuspenseQuery" | "useQuery";
+ suffix: string;
+ responseDataType: ts.TypeParameterDeclaration;
+ requestParams: ts.ParameterDeclaration[];
+ method: ts.MethodDeclaration;
+ node: ts.SourceFile;
+ className: string;
+}) {
+ const methodName = getNameFromMethod(method, node);
+ const customHookName = hookNameFromMethod({ method, node, className });
+ const queryKey = createQueryKeyFromMethod({ method, node, className });
- // Custom hook
const hookExport = ts.factory.createVariableStatement(
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
ts.factory.createVariableDeclarationList(
[
ts.factory.createVariableDeclaration(
- ts.factory.createIdentifier(customHookName),
+ ts.factory.createIdentifier(`${customHookName}${suffix}`),
undefined,
undefined,
ts.factory.createArrowFunction(
@@ -171,7 +260,7 @@ export const createUseQuery = (
),
]),
[
- ...requestParam,
+ ...requestParams,
ts.factory.createParameterDeclaration(
undefined,
undefined,
@@ -212,7 +301,7 @@ export const createUseQuery = (
undefined,
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
ts.factory.createCallExpression(
- ts.factory.createIdentifier("useQuery"),
+ ts.factory.createIdentifier(queryString),
[
ts.factory.createTypeReferenceNode(TData),
ts.factory.createTypeReferenceNode(TError),
@@ -223,7 +312,7 @@ export const createUseQuery = (
ts.factory.createIdentifier("queryKey"),
ts.factory.createArrayLiteralExpression(
[
- ts.factory.createIdentifier(queryKey),
+ BuildCommonTypeName(queryKey),
ts.factory.createSpreadElement(
ts.factory.createParenthesizedExpression(
ts.factory.createBinaryExpression(
@@ -290,7 +379,72 @@ export const createUseQuery = (
ts.NodeFlags.Const
)
);
- const hookWithJsDoc = addJSDocToNode(hookExport, node, deprecated, jsDoc);
+ return hookExport;
+}
+
+export const createUseQuery = ({
+ node,
+ className,
+ method,
+ jsDoc = [],
+ isDeprecated: deprecated = false,
+}: MethodDescription) => {
+ const methodName = getNameFromMethod(method, node);
+ const queryKey = createQueryKeyFromMethod({ method, node, className });
+ const { apiResponse: defaultApiResponse, responseDataType } =
+ createApiResponseType({
+ className,
+ methodName,
+ });
+
+ const requestParam = getRequestParamFromMethod(method, node);
+
+ const requestParams = requestParam ? [requestParam] : [];
+
+ const queryHook = createQueryHook({
+ queryString: "useQuery",
+ suffix: "",
+ responseDataType,
+ requestParams,
+ method,
+ node,
+ className,
+ });
+ const suspenseQueryHook = createQueryHook({
+ queryString: "useSuspenseQuery",
+ suffix: "Suspense",
+ responseDataType,
+ requestParams,
+ method,
+ node,
+ className,
+ });
+
+ const hookWithJsDoc = addJSDocToNode(queryHook, node, deprecated, jsDoc);
+ const suspenseHookWithJsDoc = addJSDocToNode(
+ suspenseQueryHook,
+ node,
+ deprecated,
+ jsDoc
+ );
+
+ const returnTypeExport = createReturnTypeExport({
+ className,
+ methodName,
+ defaultApiResponse,
+ });
+
+ const queryKeyExport = createQueryKeyExport({
+ className,
+ methodName,
+ queryKey,
+ });
- return [defaultApiResponse, returnTypeExport, queryKeyExport, hookWithJsDoc];
+ return {
+ apiResponse: defaultApiResponse,
+ returnType: returnTypeExport,
+ key: queryKeyExport,
+ queryHook: hookWithJsDoc,
+ suspenseQueryHook: suspenseHookWithJsDoc,
+ };
};
diff --git a/src/generate.ts b/src/generate.ts
index 6898df6..7c96fc7 100644
--- a/src/generate.ts
+++ b/src/generate.ts
@@ -2,7 +2,7 @@ import { generate as generateTSClients } from "openapi-typescript-codegen";
import { print } from "./print";
import { CLIOptions } from "./cli";
import path from "path";
-import { createSource } from "./createSource";
+import { createSources } from "./createSource";
import { defaultOutputPath, requestsOutputPath } from "./constants";
export async function generate(options: CLIOptions) {
@@ -16,6 +16,6 @@ export async function generate(options: CLIOptions) {
httpClient: options.client,
output: openApiOutputPath,
});
- const source = createSource(openApiOutputPath);
- print(source, options);
+ const sources = createSources(openApiOutputPath);
+ print(sources, options);
}
diff --git a/src/print.ts b/src/print.ts
index 31878a8..c480b27 100644
--- a/src/print.ts
+++ b/src/print.ts
@@ -1,21 +1,40 @@
-import fs from "fs";
+import { stat, mkdir, writeFile } from "fs/promises";
import path from "path";
import { CLIOptions } from "./cli";
import { defaultOutputPath, queriesOutputPath } from "./constants";
+import { exists } from "./common";
-function printGeneratedTS(result: string, options: CLIOptions) {
+async function printGeneratedTS(
+ result: {
+ name: string;
+ content: string;
+ },
+ options: CLIOptions
+) {
const dir = path.join(options.output ?? defaultOutputPath, queriesOutputPath);
- if (!fs.existsSync(dir)) {
- fs.mkdirSync(dir, { recursive: true });
+ const dirExists = await exists(dir);
+ if (!dirExists) {
+ await mkdir(dir, { recursive: true });
}
- fs.writeFileSync(path.join(dir, "index.ts"), result);
+ await writeFile(path.join(dir, result.name), result.content);
}
-export function print(result: string, options: CLIOptions) {
+export async function print(
+ results: {
+ name: string;
+ content: string;
+ }[],
+ options: CLIOptions
+) {
const outputPath = options.output ?? defaultOutputPath;
- if (!fs.existsSync(outputPath)) {
- fs.mkdirSync(outputPath);
+ const dirExists = await exists(outputPath);
+ if (!dirExists) {
+ await mkdir(outputPath);
}
- printGeneratedTS(result, options);
+ const promises = results.map(async (result) => {
+ await printGeneratedTS(result, options);
+ });
+
+ await Promise.all(promises);
}