From 60fe3bd2ad30f6719df3f56de199925ef621f337 Mon Sep 17 00:00:00 2001 From: Landon Gavin Date: Tue, 2 Apr 2024 15:30:04 -0400 Subject: [PATCH] refactor: split file generation and add suspense exports in own file --- README.md | 57 +- examples/react-app/src/App.tsx | 8 +- .../src/components/SuspenseChild.tsx | 17 + .../src/components/SuspenseParent.tsx | 12 + src/common.ts | 49 ++ src/createExports.ts | 245 ++++++-- src/createSource.ts | 128 +++- src/createUseMutation.ts | 60 +- src/createUseQuery.ts | 575 +++++++++++------- src/generate.ts | 6 +- src/print.ts | 37 +- 11 files changed, 873 insertions(+), 321 deletions(-) create mode 100644 examples/react-app/src/components/SuspenseChild.tsx create mode 100644 examples/react-app/src/components/SuspenseParent.tsx 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 ( -
- {/* .... */} -
+ + ); +} + +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/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 1695140..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: ts.ParameterDeclaration[] = []; - 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 Type - // export const classNameMethodNameQueryResult = UseQueryResult; - const returnTypeExport = ts.factory.createTypeAliasDeclaration( + 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; + */ +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,177 +177,274 @@ export const createUseQuery = ( ts.NodeFlags.Const ) ); +} - // Custom hook - /** - * 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: "useSuspenseQuery" | "useQuery", suffix: string) { - const hookExport = ts.factory.createVariableStatement( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier(`${customHookName}${suffix}`), - undefined, +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 }); + + const hookExport = ts.factory.createVariableStatement( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(`${customHookName}${suffix}`), + undefined, + undefined, + ts.factory.createArrowFunction( undefined, - ts.factory.createArrowFunction( - undefined, - ts.factory.createNodeArray([ - responseDataType, - ts.factory.createTypeParameterDeclaration( - undefined, - TError, - undefined, + ts.factory.createNodeArray([ + responseDataType, + ts.factory.createTypeParameterDeclaration( + undefined, + TError, + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) + ), + ts.factory.createTypeParameterDeclaration( + undefined, + "TQueryKey", + queryKeyConstraint, + ts.factory.createArrayTypeNode( ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) - ), - ts.factory.createTypeParameterDeclaration( - undefined, - "TQueryKey", - queryKeyConstraint, - ts.factory.createArrayTypeNode( - ts.factory.createKeywordTypeNode( - ts.SyntaxKind.UnknownKeyword - ) - ) - ), - ]), - [ - ...requestParam, - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier("queryKey"), - ts.factory.createToken(ts.SyntaxKind.QuestionToken), - queryKeyGenericType - ), - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier("options"), - ts.factory.createToken(ts.SyntaxKind.QuestionToken), - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("Omit"), - [ - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("UseQueryOptions"), - [ - ts.factory.createTypeReferenceNode(TData), - ts.factory.createTypeReferenceNode(TError), - ] + ) + ), + ]), + [ + ...requestParams, + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier("queryKey"), + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + queryKeyGenericType + ), + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier("options"), + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("Omit"), + [ + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("UseQueryOptions"), + [ + ts.factory.createTypeReferenceNode(TData), + ts.factory.createTypeReferenceNode(TError), + ] + ), + ts.factory.createUnionTypeNode([ + ts.factory.createLiteralTypeNode( + ts.factory.createStringLiteral("queryKey") ), - ts.factory.createUnionTypeNode([ - ts.factory.createLiteralTypeNode( - ts.factory.createStringLiteral("queryKey") - ), - ts.factory.createLiteralTypeNode( - ts.factory.createStringLiteral("queryFn") - ), - ts.factory.createLiteralTypeNode( - ts.factory.createStringLiteral("initialData") - ), - ]), - ] - ) - ), + ts.factory.createLiteralTypeNode( + ts.factory.createStringLiteral("queryFn") + ), + ts.factory.createLiteralTypeNode( + ts.factory.createStringLiteral("initialData") + ), + ]), + ] + ) + ), + ], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createCallExpression( + ts.factory.createIdentifier(queryString), + [ + ts.factory.createTypeReferenceNode(TData), + ts.factory.createTypeReferenceNode(TError), ], - undefined, - ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.factory.createCallExpression( - ts.factory.createIdentifier(queryString), - [ - ts.factory.createTypeReferenceNode(TData), - ts.factory.createTypeReferenceNode(TError), - ], - [ - ts.factory.createObjectLiteralExpression([ - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier("queryKey"), - ts.factory.createArrayLiteralExpression( - [ - ts.factory.createIdentifier(queryKey), - ts.factory.createSpreadElement( - ts.factory.createParenthesizedExpression( - ts.factory.createBinaryExpression( - ts.factory.createIdentifier("queryKey"), - ts.factory.createToken( - ts.SyntaxKind.QuestionQuestionToken - ), - method.parameters.length - ? ts.factory.createArrayLiteralExpression([ - ts.factory.createObjectLiteralExpression( - method.parameters.map((param) => - ts.factory.createShorthandPropertyAssignment( - ts.factory.createIdentifier( - param.name.getText(node) - ) + [ + ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier("queryKey"), + ts.factory.createArrayLiteralExpression( + [ + BuildCommonTypeName(queryKey), + ts.factory.createSpreadElement( + ts.factory.createParenthesizedExpression( + ts.factory.createBinaryExpression( + ts.factory.createIdentifier("queryKey"), + ts.factory.createToken( + ts.SyntaxKind.QuestionQuestionToken + ), + method.parameters.length + ? ts.factory.createArrayLiteralExpression([ + ts.factory.createObjectLiteralExpression( + method.parameters.map((param) => + ts.factory.createShorthandPropertyAssignment( + ts.factory.createIdentifier( + param.name.getText(node) ) ) - ), - ]) - : ts.factory.createArrayLiteralExpression([]) - ) + ) + ), + ]) + : ts.factory.createArrayLiteralExpression([]) ) - ), - ], - false - ) - ), - 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.parameters.map((param) => - ts.factory.createIdentifier( - param.name.getText(node) - ) - ) + ], + false + ) + ), + 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) ), - ts.factory.createTypeReferenceNode(TData) - ) + undefined, + method.parameters.map((param) => + ts.factory.createIdentifier( + param.name.getText(node) + ) + ) + ), + ts.factory.createTypeReferenceNode(TData) ) - ), - ts.factory.createSpreadAssignment( - ts.factory.createIdentifier("options") - ), - ]), - ] - ) + ) + ), + ts.factory.createSpreadAssignment( + ts.factory.createIdentifier("options") + ), + ]), + ] ) - ), - ], - ts.NodeFlags.Const - ) - ); - return hookExport; - } + ) + ), + ], + ts.NodeFlags.Const + ) + ); + 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 queryHook = createQueryHook("useQuery", ""); - const suspenseQueryHook = createQueryHook("useSuspenseQuery", "Suspense"); + 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 suspenseHookWithJsDoc = addJSDocToNode( + suspenseQueryHook, + node, + deprecated, + jsDoc + ); - return [ + const returnTypeExport = createReturnTypeExport({ + className, + methodName, defaultApiResponse, - returnTypeExport, - queryKeyExport, - hookWithJsDoc, - suspenseHookWithJsDoc, - ]; + }); + + const queryKeyExport = createQueryKeyExport({ + className, + methodName, + queryKey, + }); + + 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); }