diff --git a/.gitignore b/.gitignore index 5ff99b1..a3b3145 100644 --- a/.gitignore +++ b/.gitignore @@ -208,7 +208,7 @@ public gatsby-types.d.ts # Generated -!generated +generated # Turbo .turbo diff --git a/packages/apicraft/apicraft.config.ts b/packages/apicraft/apicraft.config.ts index 75b9455..1d89ce0 100644 --- a/packages/apicraft/apicraft.config.ts +++ b/packages/apicraft/apicraft.config.ts @@ -6,11 +6,11 @@ import { apicraft } from './dist/esm/index.mjs'; const apicraftConfig = apicraft([ { - input: 'example-apiV1.yaml', - output: 'generated/apiV1', - instance: 'fetches', + input: 'example-apiV2.yaml', + output: 'generated/apiV2-class', + instance: 'axios', nameBy: 'path', - groupBy: 'tag', + groupBy: 'paths', plugins: ['tanstack'] } ]); diff --git a/packages/apicraft/bin/generate.ts b/packages/apicraft/bin/generate.ts index 5941dc9..8e86ff0 100644 --- a/packages/apicraft/bin/generate.ts +++ b/packages/apicraft/bin/generate.ts @@ -51,14 +51,16 @@ export const generate = { option.instance === name || (typeof option.instance === 'object' && option.instance.name === name); + const generateOutput = + typeof option.output === 'string' ? option.output : option.output.path; + const runtimeInstancePath = + typeof option.instance === 'object' ? option.instance.runtimeInstancePath : undefined; + if (matchInstance('axios')) { plugins.push( defineAxiosPlugin({ - generateOutput: - typeof option.output === 'string' ? option.output : option.output.path, - ...(typeof option.instance === 'object' && { - runtimeInstancePath: option.instance.runtimeInstancePath - }), + generateOutput, + runtimeInstancePath, exportFromIndex: true, nameBy: option.nameBy, groupBy: option.groupBy @@ -66,16 +68,11 @@ export const generate = { ); } - const generateOutput = - typeof option.output === 'string' ? option.output : option.output.path; - if (matchInstance('fetches')) { plugins.push( defineFetchesPlugin({ generateOutput, - ...(typeof option.instance === 'object' && { - runtimeInstancePath: option.instance.runtimeInstancePath - }), + runtimeInstancePath, exportFromIndex: true, nameBy: option.nameBy, groupBy: option.groupBy diff --git a/packages/apicraft/bin/plugins/axios/class/plugin.ts b/packages/apicraft/bin/plugins/axios/class/plugin.ts new file mode 100644 index 0000000..dc0bd1b --- /dev/null +++ b/packages/apicraft/bin/plugins/axios/class/plugin.ts @@ -0,0 +1,240 @@ +import * as nodePath from 'node:path'; +import ts from 'typescript'; + +import { + capitalize, + generateRequestName, + getImportRuntimeInstance, + getRequestInfo +} from '@/bin/plugins/helpers'; + +import type { AxiosPlugin } from '../types'; + +import { + getAxiosRequestCallExpression, + getAxiosRequestParameterDeclaration, + getAxiosRequestParamsType, + getImportAxios, + getImportAxiosRequestParams +} from '../helpers'; + +const CLASS_NAME = 'ApiInstance'; + +export const classHandler: AxiosPlugin['Handler'] = ({ plugin }) => { + const classFilePath = nodePath.normalize(`${plugin.output}/instance`); + const classFolderPath = nodePath.dirname(`${plugin.config.generateOutput}/${classFilePath}`); + const classFile = plugin.createFile({ + id: 'axiosInstance', + path: classFilePath + }); + + const typeImportNames = new Set(); + const typeStatements: ts.Statement[] = []; + const classElements: ts.ClassElement[] = []; + + plugin.forEach('operation', (event) => { + if (event.type !== 'operation') return; + + const request = event.operation; + const requestName = generateRequestName(request, plugin.config.nameBy); + const requestInfo = getRequestInfo({ request }); + + const requestDataTypeName = `${capitalize(request.id)}Data`; + typeImportNames.add(requestDataTypeName); + + const requestResponseTypeName = `${capitalize(request.id)}Response`; + if (requestInfo.hasResponse) typeImportNames.add(requestResponseTypeName); + + const requestParamsTypeName = `${capitalize(requestName)}RequestParams`; + typeStatements.push( + getAxiosRequestParamsType({ + requestDataTypeName, + requestParamsTypeName + }) + ); + + // ({ path, body, query, config }: RequestParams) + const requestParameter = getAxiosRequestParameterDeclaration({ + request, + requestInfo, + requestParamsTypeName + }); + + const requestBody = ts.factory.createBlock( + [ + ts.factory.createReturnStatement( + // instance.request({ method, url, data, params }) + getAxiosRequestCallExpression({ + request, + requestInfo, + requestResponseTypeName, + instanceVariant: 'class' + }) + ) + ], + true + ); + + classElements.push( + ts.factory.createMethodDeclaration( + undefined, + undefined, + ts.factory.createIdentifier(requestName), + undefined, + undefined, + [requestParameter], + undefined, + requestBody + ) + ); + }); + + // import type { AxiosRequestParams } from '@siberiacancode/apicraft'; + const importAxiosRequestParams = getImportAxiosRequestParams(); + + // import type { RequestData, RequestResponse, ... } from './types.gen'; + const importTypes = ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + true, + undefined, + ts.factory.createNamedImports( + Array.from(typeImportNames).map((typeImportName) => + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(typeImportName) + ) + ) + ) + ), + ts.factory.createStringLiteral('./types.gen') + ); + + // import type { AxiosInstance, CreateAxiosDefaults } from 'axios'; + const importAxiosTypes = ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + true, + undefined, + ts.factory.createNamedImports([ + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier('AxiosInstance') + ), + ...(!plugin.config.runtimeInstancePath + ? [ + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier('CreateAxiosDefaults') + ) + ] + : []) + ]) + ), + ts.factory.createStringLiteral('axios'), + undefined + ); + + // private instance: AxiosInstance; + const classInstanceProperty = ts.factory.createPropertyDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.PrivateKeyword)], + ts.factory.createIdentifier('instance'), + undefined, + ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('AxiosInstance'), undefined), + undefined + ); + + // constructor(config?: CreateAxiosDefaults) { this.instance = axios.create(config); } + const constructorDeclaration = ts.factory.createConstructorDeclaration( + undefined, + !plugin.config.runtimeInstancePath + ? [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier('config'), + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier('CreateAxiosDefaults'), + undefined + ), + undefined + ) + ] + : [], + ts.factory.createBlock( + [ + ts.factory.createExpressionStatement( + ts.factory.createBinaryExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createThis(), + ts.factory.createIdentifier('instance') + ), + ts.factory.createToken(ts.SyntaxKind.EqualsToken), + plugin.config.runtimeInstancePath + ? ts.factory.createIdentifier('runtimeInstance') + : ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('axios'), + ts.factory.createIdentifier('create') + ), + undefined, + !plugin.config.runtimeInstancePath ? [ts.factory.createIdentifier('config')] : [] + ) + ) + ) + ], + true + ) + ); + + // export class ApiInstance {...} + const classDeclaration = ts.factory.createClassDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier(CLASS_NAME), + undefined, + undefined, + [classInstanceProperty, constructorDeclaration, ...classElements] + ); + + // export const instance = new ApiInstance(); + const classInstance = ts.factory.createVariableStatement( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier('instance'), + undefined, + undefined, + ts.factory.createNewExpression(ts.factory.createIdentifier(CLASS_NAME), undefined, []) + ) + ], + ts.NodeFlags.Const + ) + ); + + classFile.add(importAxiosRequestParams); + classFile.add(importTypes); + classFile.add(importAxiosTypes); + + if (plugin.config.runtimeInstancePath) { + // import { instance as runtimeInstance } from runtimeInstancePath; + classFile.add( + getImportRuntimeInstance({ + folderPath: classFolderPath, + runtimeInstancePath: plugin.config.runtimeInstancePath + }) + ); + } + if (!plugin.config.runtimeInstancePath) { + // import axios from 'axios'; + classFile.add(getImportAxios()); + } + + typeStatements.forEach((alias) => classFile.add(alias)); + classFile.add(classDeclaration); + classFile.add(classInstance); +}; diff --git a/packages/apicraft/bin/plugins/axios/composed/plugin.ts b/packages/apicraft/bin/plugins/axios/composed/plugin.ts new file mode 100644 index 0000000..6626198 --- /dev/null +++ b/packages/apicraft/bin/plugins/axios/composed/plugin.ts @@ -0,0 +1,143 @@ +import * as nodePath from 'node:path'; +import ts from 'typescript'; + +import { + capitalize, + generateRequestName, + getImportInstance, + getRequestFilePaths, + getRequestInfo +} from '@/bin/plugins/helpers'; + +import type { AxiosPlugin } from '../types'; + +import { + addInstanceFile, + getAxiosRequestCallExpression, + getAxiosRequestParameterDeclaration, + getAxiosRequestParamsType, + getImportAxiosRequestParams +} from '../helpers'; + +export const composedHandler: AxiosPlugin['Handler'] = ({ plugin }) => { + if (!plugin.config.runtimeInstancePath) addInstanceFile(plugin); + + plugin.forEach('operation', (event) => { + if (event.type !== 'operation') return; + + const request = event.operation; + const requestInfo = getRequestInfo({ request }); + const requestName = generateRequestName(request, plugin.config.nameBy); + + const requestFilePaths = getRequestFilePaths({ + groupBy: plugin.config.groupBy, + output: plugin.output, + requestName, + request + }); + + requestFilePaths.forEach((requestFilePath) => { + const requestFile = plugin.createFile({ + id: requestFilePath, + path: requestFilePath + }); + + const requestParamsTypeName = `${capitalize(requestName)}RequestParams`; + const requestDataTypeName = `${capitalize(request.id)}Data`; + const requestResponseTypeName = `${capitalize(request.id)}Response`; + + // import type { AxiosRequestParams } from '@siberiacancode/apicraft'; + const importAxiosRequestParams = getImportAxiosRequestParams(); + const requestFolderPath = nodePath.dirname( + `${plugin.config.generateOutput}/${requestFilePath}` + ); + + // import type { RequestData, RequestResponse } from 'generated/types.gen'; + const importTypes = ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + true, + undefined, + ts.factory.createNamedImports([ + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(requestDataTypeName) + ), + ...(requestInfo.hasResponse + ? [ + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(requestResponseTypeName) + ) + ] + : []) + ]) + ), + ts.factory.createStringLiteral( + nodePath.relative( + requestFolderPath, + nodePath.normalize(`${plugin.config.generateOutput}/types.gen`) + ) + ) + ); + + // import { instance } from "../../instance.gen"; + const importInstance = getImportInstance({ + folderPath: requestFolderPath, + output: plugin.output, + generateOutput: plugin.config.generateOutput, + runtimeInstancePath: plugin.config.runtimeInstancePath + }); + + // type RequestParams = AxiosRequestParams; + const requestParamsType = getAxiosRequestParamsType({ + requestDataTypeName, + requestParamsTypeName + }); + + // --- export const request = ({ path, body, query, config }) => ... + const requestFunction = ts.factory.createVariableStatement( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(requestName), + undefined, + undefined, + ts.factory.createArrowFunction( + undefined, + undefined, + [ + // ({ path, body, query, config }: RequestParams) + getAxiosRequestParameterDeclaration({ + request, + requestInfo, + requestParamsTypeName + }) + ], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + // instance.request({ method, url, data, params }) + getAxiosRequestCallExpression({ + request, + requestInfo, + requestResponseTypeName, + instanceVariant: 'function' + }) + ) + ) + ], + ts.NodeFlags.Const + ) + ); + + requestFile.add(importAxiosRequestParams); + requestFile.add(importTypes); + requestFile.add(importInstance); + requestFile.add(requestParamsType); + requestFile.add(requestFunction); + }); + }); +}; diff --git a/packages/apicraft/bin/plugins/axios/helpers/addInstanceFile.ts b/packages/apicraft/bin/plugins/axios/helpers/addInstanceFile.ts index aad6683..6139f97 100644 --- a/packages/apicraft/bin/plugins/axios/helpers/addInstanceFile.ts +++ b/packages/apicraft/bin/plugins/axios/helpers/addInstanceFile.ts @@ -3,6 +3,8 @@ import type { DefinePlugin } from '@hey-api/openapi-ts'; import * as nodePath from 'node:path'; import ts from 'typescript'; +import { getImportAxios } from './getImportAxios'; + export const addInstanceFile = (plugin: DefinePlugin['Instance']) => { const instanceFile = plugin.createFile({ id: 'axiosInstance', @@ -10,11 +12,7 @@ export const addInstanceFile = (plugin: DefinePlugin['Instance']) => { }); // import axios from 'axios'; - const importAxios = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause(false, ts.factory.createIdentifier('axios'), undefined), - ts.factory.createStringLiteral('axios') - ); + const importAxios = getImportAxios(); // export const instance = axios.create(); const createInstance = ts.factory.createVariableStatement( diff --git a/packages/apicraft/bin/plugins/axios/helpers/getAxiosRequestCallExpression.ts b/packages/apicraft/bin/plugins/axios/helpers/getAxiosRequestCallExpression.ts new file mode 100644 index 0000000..0ce01d5 --- /dev/null +++ b/packages/apicraft/bin/plugins/axios/helpers/getAxiosRequestCallExpression.ts @@ -0,0 +1,78 @@ +import type { IR } from '@hey-api/openapi-ts'; + +import ts from 'typescript'; + +import type { GetRequestInfoResult } from '@/bin/plugins/helpers'; + +import { buildRequestParamsPath } from '@/bin/plugins/helpers'; + +interface GetAxiosRequestCallExpressionParams { + instanceVariant: 'class' | 'function'; + request: IR.OperationObject; + requestInfo: GetRequestInfoResult; + requestResponseTypeName: string; +} + +// instance.request({ method, url, data, params }) +export const getAxiosRequestCallExpression = ({ + request, + requestInfo, + requestResponseTypeName, + instanceVariant +}: GetAxiosRequestCallExpressionParams) => + ts.factory.createCallExpression( + instanceVariant === 'class' + ? ts.factory.createPropertyAccessExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createThis(), + ts.factory.createIdentifier('instance') + ), + ts.factory.createIdentifier('request') + ) + : ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('instance'), + ts.factory.createIdentifier('request') + ), + requestInfo.hasResponse + ? [ + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier(requestResponseTypeName), + undefined + ) + ] + : undefined, + [ + ts.factory.createObjectLiteralExpression( + [ + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier('method'), + ts.factory.createStringLiteral(request.method.toUpperCase()) + ), + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier('url'), + requestInfo.hasPathParam + ? buildRequestParamsPath(request.path) + : ts.factory.createStringLiteral(request.path) + ), + ...(request.body + ? [ + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier('data'), + ts.factory.createIdentifier('body') + ) + ] + : []), + ...(request.parameters?.query + ? [ + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier('params'), + ts.factory.createIdentifier('query') + ) + ] + : []), + ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config')) + ], + true + ) + ] + ); diff --git a/packages/apicraft/bin/plugins/axios/helpers/getAxiosRequestParameterDeclaration.ts b/packages/apicraft/bin/plugins/axios/helpers/getAxiosRequestParameterDeclaration.ts new file mode 100644 index 0000000..f7550d8 --- /dev/null +++ b/packages/apicraft/bin/plugins/axios/helpers/getAxiosRequestParameterDeclaration.ts @@ -0,0 +1,66 @@ +import type { IR } from '@hey-api/openapi-ts'; + +import ts from 'typescript'; + +import type { GetRequestInfoResult } from '../getRequestInfo'; + +interface GetAxiosRequestParameterDeclarationParams { + request: IR.OperationObject; + requestInfo: GetRequestInfoResult; + requestParamsTypeName: string; +} + +// ({ path, body, query, config }: RequestParams) +export const getAxiosRequestParameterDeclaration = ({ + request, + requestInfo, + requestParamsTypeName +}: GetAxiosRequestParameterDeclarationParams) => + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createObjectBindingPattern([ + ts.factory.createBindingElement( + undefined, + undefined, + ts.factory.createIdentifier('config'), + undefined + ), + ...(request.body + ? [ + ts.factory.createBindingElement( + undefined, + undefined, + ts.factory.createIdentifier('body'), + undefined + ) + ] + : []), + ...(request.parameters?.query + ? [ + ts.factory.createBindingElement( + undefined, + undefined, + ts.factory.createIdentifier('query'), + undefined + ) + ] + : []), + ...(requestInfo.hasPathParam + ? [ + ts.factory.createBindingElement( + undefined, + undefined, + ts.factory.createIdentifier('path'), + undefined + ) + ] + : []) + ]), + undefined, + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier(requestParamsTypeName), + undefined + ), + !requestInfo.hasRequiredParam ? ts.factory.createObjectLiteralExpression([], false) : undefined + ); diff --git a/packages/apicraft/bin/plugins/axios/helpers/getAxiosRequestParamsType.ts b/packages/apicraft/bin/plugins/axios/helpers/getAxiosRequestParamsType.ts new file mode 100644 index 0000000..f3646a2 --- /dev/null +++ b/packages/apicraft/bin/plugins/axios/helpers/getAxiosRequestParamsType.ts @@ -0,0 +1,23 @@ +import ts from 'typescript'; + +interface GetAxiosRequestParamsTypeParams { + requestDataTypeName: string; + requestParamsTypeName: string; +} + +// type RequestParams = AxiosRequestParams; +export const getAxiosRequestParamsType = ({ + requestDataTypeName, + requestParamsTypeName +}: GetAxiosRequestParamsTypeParams) => + ts.factory.createTypeAliasDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier(requestParamsTypeName), + undefined, + ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('AxiosRequestParams'), [ + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier(requestDataTypeName), + undefined + ) + ]) + ); diff --git a/packages/apicraft/bin/plugins/axios/helpers/getImportAxios.ts b/packages/apicraft/bin/plugins/axios/helpers/getImportAxios.ts new file mode 100644 index 0000000..792f55a --- /dev/null +++ b/packages/apicraft/bin/plugins/axios/helpers/getImportAxios.ts @@ -0,0 +1,9 @@ +import ts from 'typescript'; + +// import axios from 'axios'; +export const getImportAxios = () => + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause(false, ts.factory.createIdentifier('axios'), undefined), + ts.factory.createStringLiteral('axios') + ); diff --git a/packages/apicraft/bin/plugins/axios/helpers/getImportAxiosRequestParams.ts b/packages/apicraft/bin/plugins/axios/helpers/getImportAxiosRequestParams.ts new file mode 100644 index 0000000..2726eb6 --- /dev/null +++ b/packages/apicraft/bin/plugins/axios/helpers/getImportAxiosRequestParams.ts @@ -0,0 +1,19 @@ +import ts from 'typescript'; + +// import type { AxiosRequestParams } from '@siberiacancode/apicraft'; +export const getImportAxiosRequestParams = () => + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + true, + undefined, + ts.factory.createNamedImports([ + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier('AxiosRequestParams') + ) + ]) + ), + ts.factory.createStringLiteral('@siberiacancode/apicraft') + ); diff --git a/packages/apicraft/bin/plugins/axios/helpers/index.ts b/packages/apicraft/bin/plugins/axios/helpers/index.ts index 146069b..4d25b88 100644 --- a/packages/apicraft/bin/plugins/axios/helpers/index.ts +++ b/packages/apicraft/bin/plugins/axios/helpers/index.ts @@ -1 +1,6 @@ -export { addInstanceFile } from './addInstanceFile'; +export * from './addInstanceFile'; +export * from './getAxiosRequestCallExpression'; +export * from './getAxiosRequestParameterDeclaration'; +export * from './getAxiosRequestParamsType'; +export * from './getImportAxios'; +export * from './getImportAxiosRequestParams'; diff --git a/packages/apicraft/bin/plugins/axios/plugin.ts b/packages/apicraft/bin/plugins/axios/plugin.ts index 94ecfe7..505ea72 100644 --- a/packages/apicraft/bin/plugins/axios/plugin.ts +++ b/packages/apicraft/bin/plugins/axios/plugin.ts @@ -1,264 +1,13 @@ -import * as nodePath from 'node:path'; -import ts from 'typescript'; - import type { AxiosPlugin } from './types'; -import { - buildRequestParamsPath, - capitalize, - checkRequestHasRequiredParam, - generateRequestName, - getRequestFilePaths -} from '../helpers'; -import { addInstanceFile } from './helpers'; +import { classHandler } from './class/plugin'; +import { composedHandler } from './composed/plugin'; export const handler: AxiosPlugin['Handler'] = ({ plugin }) => { - if (!plugin.config.runtimeInstancePath) addInstanceFile(plugin); - - plugin.forEach('operation', (event) => { - if (event.type !== 'operation') return; - - const request = event.operation; - const requestName = generateRequestName(request, plugin.config.nameBy); - - const requestFilePaths = getRequestFilePaths({ - groupBy: plugin.config.groupBy, - output: plugin.output, - requestName, - request - }); - - requestFilePaths.forEach((requestFilePath) => { - const requestFile = plugin.createFile({ - id: requestFilePath, - path: requestFilePath - }); - - const requestParamsTypeName = `${capitalize(requestName)}RequestParams`; - const requestDataTypeName = `${capitalize(request.id)}Data`; - const requestResponseTypeName = `${capitalize(request.id)}Response`; - - // import type { AxiosRequestParams } from '@siberiacancode/apicraft'; - const importAxiosRequestParams = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - true, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier('AxiosRequestParams') - ) - ]) - ), - ts.factory.createStringLiteral('@siberiacancode/apicraft') - ); - - const requestHasResponse = Object.values(request.responses ?? {}).some( - (response) => response?.schema.$ref || response?.schema.type !== 'unknown' - ); - const requestFolderPath = nodePath.dirname( - `${plugin.config.generateOutput}/${requestFilePath}` - ); - - // import type { RequestData, RequestResponse } from 'generated/types.gen'; - const importTypes = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - true, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier(requestDataTypeName) - ), - ...(requestHasResponse - ? [ - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier(requestResponseTypeName) - ) - ] - : []) - ]) - ), - ts.factory.createStringLiteral( - nodePath.relative( - requestFolderPath, - nodePath.normalize(`${plugin.config.generateOutput}/types.gen`) - ) - ) - ); - - // import { instance } from "generated/instance.gen"; - const importInstance = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - false, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier('instance') - ) - ]) - ), - ts.factory.createStringLiteral( - nodePath.relative( - requestFolderPath, - plugin.config.runtimeInstancePath ?? - nodePath.normalize(`${plugin.config.generateOutput}/${plugin.output}/instance.gen`) - ) - ) - ); - - // type RequestParams = AxiosRequestParams; - const requestParamsType = ts.factory.createTypeAliasDeclaration( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier(requestParamsTypeName), - undefined, - ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('AxiosRequestParams'), [ - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier(requestDataTypeName), - undefined - ) - ]) - ); - - const requestHasPathParam = !!Object.keys(request.parameters?.path ?? {}).length; - const requestHasRequiredParam = checkRequestHasRequiredParam(request); - - // --- export const request = ({ path, body, query, config }) => ... - const requestFunction = ts.factory.createVariableStatement( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier(requestName), - undefined, - undefined, - ts.factory.createArrowFunction( - undefined, - undefined, - [ - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createObjectBindingPattern([ - ts.factory.createBindingElement( - undefined, - undefined, - ts.factory.createIdentifier('config'), - undefined - ), - ...(request.body - ? [ - ts.factory.createBindingElement( - undefined, - undefined, - ts.factory.createIdentifier('body'), - undefined - ) - ] - : []), - ...(request.parameters?.query - ? [ - ts.factory.createBindingElement( - undefined, - undefined, - ts.factory.createIdentifier('query'), - undefined - ) - ] - : []), - - ...(requestHasPathParam - ? [ - ts.factory.createBindingElement( - undefined, - undefined, - ts.factory.createIdentifier('path'), - undefined - ) - ] - : []) - ]), - undefined, - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier(requestParamsTypeName), - undefined - ), - !requestHasRequiredParam - ? ts.factory.createObjectLiteralExpression([], false) - : undefined - ) - ], - undefined, - ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('instance'), - ts.factory.createIdentifier('request') - ), - requestHasResponse - ? [ - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier(requestResponseTypeName), - undefined - ) - ] - : undefined, - [ - ts.factory.createObjectLiteralExpression( - [ - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier('method'), - ts.factory.createStringLiteral(request.method.toUpperCase()) - ), - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier('url'), - requestHasPathParam - ? buildRequestParamsPath(request.path) - : ts.factory.createStringLiteral(request.path) - ), - ...(request.body - ? [ - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier('data'), - ts.factory.createIdentifier('body') - ) - ] - : []), - ...(request.parameters?.query - ? [ - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier('params'), - ts.factory.createIdentifier('query') - ) - ] - : []), - ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config')) - ], - true - ) - ] - ) - ) - ) - ], - ts.NodeFlags.Const - ) - ); - - requestFile.add(importAxiosRequestParams); - requestFile.add(importTypes); - requestFile.add(importInstance); - requestFile.add(requestParamsType); - requestFile.add(requestFunction); - }); - }); + if (plugin.config.groupBy === 'class') { + classHandler({ plugin }); + } + if (plugin.config.groupBy === 'paths' || plugin.config.groupBy === 'tags') { + composedHandler({ plugin }); + } }; diff --git a/packages/apicraft/bin/plugins/fetches/plugin.ts b/packages/apicraft/bin/plugins/fetches/plugin.ts index bf6be5f..9b7ed52 100644 --- a/packages/apicraft/bin/plugins/fetches/plugin.ts +++ b/packages/apicraft/bin/plugins/fetches/plugin.ts @@ -6,9 +6,10 @@ import type { FetchesPlugin } from './types'; import { buildRequestParamsPath, capitalize, - checkRequestHasRequiredParam, generateRequestName, - getRequestFilePaths + getImportInstance, + getRequestFilePaths, + getRequestInfo } from '../helpers'; import { addInstanceFile } from './helpers'; @@ -19,6 +20,7 @@ export const handler: FetchesPlugin['Handler'] = ({ plugin }) => { if (event.type !== 'operation') return; const request = event.operation; + const requestInfo = getRequestInfo({ request }); const requestName = generateRequestName(request, plugin.config.nameBy); const requestFilePaths = getRequestFilePaths({ @@ -55,9 +57,6 @@ export const handler: FetchesPlugin['Handler'] = ({ plugin }) => { ts.factory.createStringLiteral('@siberiacancode/apicraft') ); - const requestHasResponse = Object.values(request.responses ?? {}).some( - (response) => response?.schema.$ref || response?.schema.type !== 'unknown' - ); const requestFolderPath = nodePath.dirname( `${plugin.config.generateOutput}/${requestFilePath}` ); @@ -74,7 +73,7 @@ export const handler: FetchesPlugin['Handler'] = ({ plugin }) => { undefined, ts.factory.createIdentifier(requestDataTypeName) ), - ...(requestHasResponse + ...(requestInfo.hasResponse ? [ ts.factory.createImportSpecifier( false, @@ -93,28 +92,13 @@ export const handler: FetchesPlugin['Handler'] = ({ plugin }) => { ) ); - // import { instance } from "generated/instance.gen"; - const importInstance = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - false, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier('instance') - ) - ]) - ), - ts.factory.createStringLiteral( - nodePath.relative( - requestFolderPath, - plugin.config.runtimeInstancePath ?? - nodePath.normalize(`${plugin.config.generateOutput}/${plugin.output}/instance.gen`) - ) - ) - ); + // import { instance } from "../../instance.gen"; + const importInstance = getImportInstance({ + folderPath: requestFolderPath, + output: plugin.output, + generateOutput: plugin.config.generateOutput, + runtimeInstancePath: plugin.config.runtimeInstancePath + }); // type RequestNameParams = FetchesRequestParams; const requestParamsType = ts.factory.createTypeAliasDeclaration( @@ -129,9 +113,6 @@ export const handler: FetchesPlugin['Handler'] = ({ plugin }) => { ]) ); - const requestHasPathParam = !!Object.keys(request.parameters?.path ?? {}).length; - const requestHasRequiredParam = checkRequestHasRequiredParam(request); - // --- export const requestName = ({ path, body, query, config }) => ... const requestFunction = ts.factory.createVariableStatement( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], @@ -176,7 +157,7 @@ export const handler: FetchesPlugin['Handler'] = ({ plugin }) => { ] : []), - ...(requestHasPathParam + ...(requestInfo.hasPathParam ? [ ts.factory.createBindingElement( undefined, @@ -192,7 +173,7 @@ export const handler: FetchesPlugin['Handler'] = ({ plugin }) => { ts.factory.createIdentifier(requestParamsTypeName), undefined ), - !requestHasRequiredParam + !requestInfo.hasRequiredParam ? ts.factory.createObjectLiteralExpression([], false) : undefined ) @@ -204,7 +185,7 @@ export const handler: FetchesPlugin['Handler'] = ({ plugin }) => { ts.factory.createIdentifier('instance'), ts.factory.createIdentifier('call') ), - requestHasResponse + requestInfo.hasResponse ? [ ts.factory.createTypeReferenceNode( ts.factory.createIdentifier(requestResponseTypeName), @@ -214,7 +195,7 @@ export const handler: FetchesPlugin['Handler'] = ({ plugin }) => { : undefined, [ ts.factory.createStringLiteral(request.method.toUpperCase()), - requestHasPathParam + requestInfo.hasPathParam ? buildRequestParamsPath(request.path) : ts.factory.createStringLiteral(request.path), ts.factory.createObjectLiteralExpression( diff --git a/packages/apicraft/bin/plugins/helpers/checkRequestHasRequiredParam.ts b/packages/apicraft/bin/plugins/helpers/checkRequestHasRequiredParam.ts deleted file mode 100644 index 6c986d2..0000000 --- a/packages/apicraft/bin/plugins/helpers/checkRequestHasRequiredParam.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { IR } from '@hey-api/openapi-ts'; - -export const checkRequestHasRequiredParam = (request: IR.OperationObject) => - Object.values(request.parameters?.query ?? {}).some((queryParam) => queryParam.required) || - Object.values(request.parameters?.path ?? {}).some((pathParam) => pathParam.required) || - request.body?.required; diff --git a/packages/apicraft/bin/plugins/helpers/generatePathRequestName.ts b/packages/apicraft/bin/plugins/helpers/generatePathRequestName.ts index 8d32079..7d62ec2 100644 --- a/packages/apicraft/bin/plugins/helpers/generatePathRequestName.ts +++ b/packages/apicraft/bin/plugins/helpers/generatePathRequestName.ts @@ -7,8 +7,12 @@ export const generatePathRequestName = (method: string, path: string) => { let prevPart: string | undefined; for (let pathPart of pathParts) { - pathPart = pathPart.split('-').filter(Boolean).map(capitalize).join(''); const isParam = pathPart.startsWith('{') && pathPart.endsWith('}'); + pathPart = pathPart + .split(/[^a-z0-9]+/i) + .filter(Boolean) + .map(capitalize) + .join(''); if (!isParam) { nameParts.push(capitalize(pathPart)); @@ -23,9 +27,7 @@ export const generatePathRequestName = (method: string, path: string) => { prevPart.slice(0, -1).charAt(0).toUpperCase() + prevPart.slice(1, -1); } - const paramName = pathPart.slice(1, -1); - nameParts.push(`By${capitalize(paramName)}`); - + nameParts.push(`By${capitalize(pathPart)}`); prevPart = pathPart; } diff --git a/packages/apicraft/bin/plugins/helpers/getRequestFilePaths.ts b/packages/apicraft/bin/plugins/helpers/getRequestFilePaths.ts index f4cd316..cee391c 100644 --- a/packages/apicraft/bin/plugins/helpers/getRequestFilePaths.ts +++ b/packages/apicraft/bin/plugins/helpers/getRequestFilePaths.ts @@ -17,17 +17,21 @@ export const getRequestFilePaths = ({ groupBy, output }: GetRequestFilePathsParams) => { - if (groupBy === 'tag') { + if (groupBy === 'tags') { const tags = request.tags ?? ['default']; return tags.map((tag) => nodePath.normalize(`${output}/requests/${tag}/${requestName}`)); } - if (groupBy === 'path') { + if (groupBy === 'paths') { return [ nodePath.normalize(`${output}/requests/${request.path}/${request.method.toLowerCase()}`) ]; } + if (groupBy === 'class') { + return [nodePath.normalize(`${output}/instance.gen`)]; + } + throw new Error(`Unsupported groupBy option ${groupBy}`); }; diff --git a/packages/apicraft/bin/plugins/helpers/getRequestInfo.ts b/packages/apicraft/bin/plugins/helpers/getRequestInfo.ts new file mode 100644 index 0000000..049e627 --- /dev/null +++ b/packages/apicraft/bin/plugins/helpers/getRequestInfo.ts @@ -0,0 +1,22 @@ +import type { IR } from '@hey-api/openapi-ts'; + +interface GetRequestInfoParams { + request: IR.OperationObject; +} + +export interface GetRequestInfoResult { + hasPathParam: boolean; + hasRequiredParam: boolean; + hasResponse: boolean; +} + +export const getRequestInfo = ({ request }: GetRequestInfoParams): GetRequestInfoResult => ({ + hasResponse: Object.values(request.responses ?? {}).some( + (response) => response?.schema.$ref || response?.schema.type !== 'unknown' + ), + hasPathParam: !!Object.keys(request.parameters?.path ?? {}).length, + hasRequiredParam: + Object.values(request.parameters?.query ?? {}).some((queryParam) => queryParam.required) || + Object.values(request.parameters?.path ?? {}).some((pathParam) => pathParam.required) || + !!request.body?.required +}); diff --git a/packages/apicraft/bin/plugins/helpers/imports/getApicraftTypeImport.ts b/packages/apicraft/bin/plugins/helpers/imports/getApicraftTypeImport.ts new file mode 100644 index 0000000..1192b55 --- /dev/null +++ b/packages/apicraft/bin/plugins/helpers/imports/getApicraftTypeImport.ts @@ -0,0 +1,17 @@ +import ts from 'typescript'; + +// import type { name } from '@siberiacancode/apicraft'; +export const getApicraftTypeImport = (name: string | string[]) => + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + true, + undefined, + ts.factory.createNamedImports([ + ...(Array.isArray(name) ? name : [name]).map((name) => + ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(name)) + ) + ]) + ), + ts.factory.createStringLiteral('@siberiacancode/apicraft') + ); diff --git a/packages/apicraft/bin/plugins/helpers/imports/getImportInstance.ts b/packages/apicraft/bin/plugins/helpers/imports/getImportInstance.ts new file mode 100644 index 0000000..86cff65 --- /dev/null +++ b/packages/apicraft/bin/plugins/helpers/imports/getImportInstance.ts @@ -0,0 +1,33 @@ +import nodePath from 'node:path'; +import ts from 'typescript'; + +interface GetImportInstanceParams { + folderPath: string; + generateOutput: string; + output: string; + runtimeInstancePath?: string; +} + +// import { instance } from '../../instance.gen'; +export const getImportInstance = ({ + output, + generateOutput, + runtimeInstancePath, + folderPath +}: GetImportInstanceParams) => + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports([ + ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier('instance')) + ]) + ), + ts.factory.createStringLiteral( + nodePath.relative( + folderPath, + runtimeInstancePath ?? nodePath.normalize(`${generateOutput}/${output}/instance.gen`) + ) + ) + ); diff --git a/packages/apicraft/bin/plugins/helpers/imports/getImportRequest.ts b/packages/apicraft/bin/plugins/helpers/imports/getImportRequest.ts new file mode 100644 index 0000000..24b8962 --- /dev/null +++ b/packages/apicraft/bin/plugins/helpers/imports/getImportRequest.ts @@ -0,0 +1,30 @@ +import nodePath from 'node:path'; +import ts from 'typescript'; + +interface GetImportRequestParams { + folderPath: string; + generateOutput: string; + requestFilePath: string; + requestName: string; +} + +// import type { requestName } from './requestName.gen'; +export const getImportRequest = ({ + folderPath, + requestFilePath, + requestName, + generateOutput +}: GetImportRequestParams) => + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports([ + ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(requestName)) + ]) + ), + ts.factory.createStringLiteral( + nodePath.relative(folderPath, `${generateOutput}/${requestFilePath}.gen`) + ) + ); diff --git a/packages/apicraft/bin/plugins/helpers/imports/getImportRuntimeInstance.ts b/packages/apicraft/bin/plugins/helpers/imports/getImportRuntimeInstance.ts new file mode 100644 index 0000000..09ca4be --- /dev/null +++ b/packages/apicraft/bin/plugins/helpers/imports/getImportRuntimeInstance.ts @@ -0,0 +1,29 @@ +import nodePath from 'node:path'; +import ts from 'typescript'; + +interface GetImportRuntimeInstanceParams { + folderPath: string; + runtimeInstancePath: string; +} + +// import { instance as runtimeInstance } from runtimeInstancePath; +export const getImportRuntimeInstance = ({ + folderPath, + runtimeInstancePath +}: GetImportRuntimeInstanceParams) => + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports([ + ts.factory.createImportSpecifier( + false, + ts.factory.createIdentifier('instance'), + ts.factory.createIdentifier('runtimeInstance') + ) + ]) + ), + ts.factory.createStringLiteral(nodePath.relative(folderPath, runtimeInstancePath)), + undefined + ); diff --git a/packages/apicraft/bin/plugins/helpers/imports/index.ts b/packages/apicraft/bin/plugins/helpers/imports/index.ts new file mode 100644 index 0000000..c6bc18c --- /dev/null +++ b/packages/apicraft/bin/plugins/helpers/imports/index.ts @@ -0,0 +1,4 @@ +export * from './getApicraftTypeImport'; +export * from './getImportInstance'; +export * from './getImportRequest'; +export * from './getImportRuntimeInstance'; diff --git a/packages/apicraft/bin/plugins/helpers/index.ts b/packages/apicraft/bin/plugins/helpers/index.ts index 3ed8094..6b4137c 100644 --- a/packages/apicraft/bin/plugins/helpers/index.ts +++ b/packages/apicraft/bin/plugins/helpers/index.ts @@ -1,6 +1,7 @@ export * from './buildRequestParamsPath'; export * from './capitalize'; -export * from './checkRequestHasRequiredParam'; export * from './generatePathRequestName'; export * from './generateRequestName'; export * from './getRequestFilePaths'; +export * from './getRequestInfo'; +export * from './imports'; diff --git a/packages/apicraft/bin/plugins/tanstack/class/plugin.ts b/packages/apicraft/bin/plugins/tanstack/class/plugin.ts new file mode 100644 index 0000000..0d46f72 --- /dev/null +++ b/packages/apicraft/bin/plugins/tanstack/class/plugin.ts @@ -0,0 +1,70 @@ +import type ts from 'typescript'; + +import { + capitalize, + generateRequestName, + getApicraftTypeImport, + getImportInstance +} from '@/bin/plugins/helpers'; + +import type { TanstackPlugin } from '../types'; + +import { getMutationHook, getQueryHook, getSuspenseQueryHook, getTanstackImport } from '../helpers'; + +export const classHandler: TanstackPlugin['Handler'] = ({ plugin }) => { + const hooksFile = plugin.createFile({ + id: 'hooks', + path: `${plugin.output}/hooks` + }); + + const imports: ts.ImportDeclaration[] = [ + getTanstackImport(['useQuery', 'useMutation', 'queryOptions', 'useSuspenseQuery']), + getApicraftTypeImport([ + 'TanstackQuerySettings', + 'TanstackMutationSettings', + 'TanstackSuspenseQuerySettings' + ]), + getImportInstance({ + output: plugin.output, + folderPath: plugin.output, + generateOutput: plugin.config.generateOutput + }) + ]; + + const hooks: ts.VariableStatement[] = []; + + plugin.forEach('operation', (event) => { + if (event.type !== 'operation') return; + + const request = event.operation; + const requestName = generateRequestName(request, plugin.config.nameBy); + + hooks.push( + ...getQueryHook({ + hookName: `use${capitalize(requestName)}Query`, + plugin, + request, + requestName + }) + ); + hooks.push( + ...getMutationHook({ + hookName: `use${capitalize(requestName)}Mutation`, + plugin, + requestName + }) + ); + hooks.push( + ...getSuspenseQueryHook({ + hookName: `use${capitalize(requestName)}SuspenseQuery`, + optionsFunctionName: `${requestName}Options`, + plugin, + request, + requestName + }) + ); + }); + + hooksFile.add(...imports); + hooksFile.add(...hooks); +}; diff --git a/packages/apicraft/bin/plugins/tanstack/composed/helpers/generateMutationHookFile.ts b/packages/apicraft/bin/plugins/tanstack/composed/helpers/generateMutationHookFile.ts new file mode 100644 index 0000000..51ca5a2 --- /dev/null +++ b/packages/apicraft/bin/plugins/tanstack/composed/helpers/generateMutationHookFile.ts @@ -0,0 +1,66 @@ +import type { DefinePlugin } from '@hey-api/openapi-ts'; + +import * as nodePath from 'node:path'; + +import { + capitalize, + getApicraftTypeImport, + getImportInstance, + getImportRequest +} from '@/bin/plugins/helpers'; + +import type { TanstackPluginConfig } from '../../types'; + +import { getMutationHook, getTanstackImport } from '../../helpers'; + +interface GenerateMutationHookFileParams { + plugin: DefinePlugin['Instance']; + requestFilePath: string; + requestName: string; +} + +export const generateMutationHookFile = ({ + plugin, + requestName, + requestFilePath +}: GenerateMutationHookFileParams) => { + const hookName = `use${capitalize(requestName)}Mutation`; + const hookFilePath = `${nodePath.dirname(requestFilePath).replace('requests', 'hooks')}/${hookName}`; + const hookFolderPath = nodePath.dirname(`${plugin.config.generateOutput}/${hookFilePath}`); + const hookFile = plugin.createFile({ + id: hookName, + path: hookFilePath + }); + + // import type { TanstackMutationSettings } from '@siberiacancode/apicraft'; + hookFile.add(getApicraftTypeImport('TanstackMutationSettings')); + + // import { useMutation } from '@tanstack/react-query'; + hookFile.add(getTanstackImport('useMutation')); + + if (plugin.config.groupBy === 'class') { + // import { instance } from '../../instance.gen'; + hookFile.add( + getImportInstance({ + output: plugin.output, + folderPath: hookFolderPath, + generateOutput: plugin.config.generateOutput + }) + ); + } + if (plugin.config.groupBy === 'paths' || plugin.config.groupBy === 'tags') { + // import type { requestName } from './requestName.gen'; + hookFile.add( + getImportRequest({ + folderPath: hookFolderPath, + requestFilePath, + requestName, + generateOutput: plugin.config.generateOutput + }) + ); + } + + // const requestNameMutationKey = requestName; + // const useRequestNameMutation = (settings: TanstackMutationSettings) => useMutation + hookFile.add(...getMutationHook({ hookName, plugin, requestName })); +}; diff --git a/packages/apicraft/bin/plugins/tanstack/composed/helpers/generateQueryHookFile.ts b/packages/apicraft/bin/plugins/tanstack/composed/helpers/generateQueryHookFile.ts new file mode 100644 index 0000000..a24b445 --- /dev/null +++ b/packages/apicraft/bin/plugins/tanstack/composed/helpers/generateQueryHookFile.ts @@ -0,0 +1,68 @@ +import type { IR } from '@hey-api/openapi-ts'; + +import * as nodePath from 'node:path'; + +import { + capitalize, + getApicraftTypeImport, + getImportInstance, + getImportRequest +} from '@/bin/plugins/helpers'; + +import type { TanstackPlugin } from '../../types'; + +import { getQueryHook, getTanstackImport } from '../../helpers'; + +interface GenerateQueryHookParams { + plugin: TanstackPlugin['Instance']; + request: IR.OperationObject; + requestFilePath: string; + requestName: string; +} + +export const generateQueryHookFile = ({ + plugin, + request, + requestName, + requestFilePath +}: GenerateQueryHookParams) => { + const hookName = `use${capitalize(requestName)}Query`; + const hookFilePath = `${nodePath.dirname(requestFilePath).replace('requests', 'hooks')}/${hookName}`; + const hookFolderPath = nodePath.dirname(`${plugin.config.generateOutput}/${hookFilePath}`); + const hookFile = plugin.createFile({ + id: hookName, + path: hookFilePath + }); + + // import type { TanstackQuerySettings } from '@siberiacancode/apicraft'; + hookFile.add(getApicraftTypeImport('TanstackQuerySettings')); + + // import { useQuery } from '@tanstack/react-query'; + hookFile.add(getTanstackImport('useQuery')); + + if (plugin.config.groupBy === 'class') { + // import { instance } from '../../instance.gen'; + hookFile.add( + getImportInstance({ + output: plugin.output, + folderPath: hookFolderPath, + generateOutput: plugin.config.generateOutput + }) + ); + } + if (plugin.config.groupBy === 'paths' || plugin.config.groupBy === 'tags') { + // import type { requestName } from './requestName.gen'; + hookFile.add( + getImportRequest({ + folderPath: hookFolderPath, + requestFilePath, + requestName, + generateOutput: plugin.config.generateOutput + }) + ); + } + + // const requestNameQueryKey = requestName; + // const useRequestNameQuery = (settings: TanstackQuerySettings) => useQuery + hookFile.add(...getQueryHook({ hookName, request, plugin, requestName })); +}; diff --git a/packages/apicraft/bin/plugins/tanstack/composed/helpers/generateSuspenseQueryHookFile.ts b/packages/apicraft/bin/plugins/tanstack/composed/helpers/generateSuspenseQueryHookFile.ts new file mode 100644 index 0000000..4c07b62 --- /dev/null +++ b/packages/apicraft/bin/plugins/tanstack/composed/helpers/generateSuspenseQueryHookFile.ts @@ -0,0 +1,77 @@ +import type { DefinePlugin, IR } from '@hey-api/openapi-ts'; + +import * as nodePath from 'node:path'; + +import { + capitalize, + getApicraftTypeImport, + getImportInstance, + getImportRequest +} from '@/bin/plugins/helpers'; + +import type { TanstackPluginConfig } from '../../types'; + +import { getSuspenseQueryHook, getTanstackImport } from '../../helpers'; + +interface GenerateSuspenseQueryHookParams { + plugin: DefinePlugin['Instance']; + request: IR.OperationObject; + requestFilePath: string; + requestName: string; +} + +export const generateSuspenseQueryHookFile = ({ + plugin, + request, + requestName, + requestFilePath +}: GenerateSuspenseQueryHookParams) => { + const hookName = `use${capitalize(requestName)}SuspenseQuery`; + const hookFilePath = `${nodePath.dirname(requestFilePath).replace('requests', 'hooks')}/${hookName}`; + const hookFolderPath = nodePath.dirname(`${plugin.config.generateOutput}/${hookFilePath}`); + const hookFile = plugin.createFile({ + id: hookName, + path: hookFilePath + }); + + // import type { TanstackSuspenseQuerySettings } from '@siberiacancode/apicraft'; + hookFile.add(getApicraftTypeImport('TanstackSuspenseQuerySettings')); + + // import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'; + hookFile.add(getTanstackImport(['queryOptions', 'useSuspenseQuery'])); + + if (plugin.config.groupBy === 'class') { + // import { instance } from '../../instance.gen'; + hookFile.add( + getImportInstance({ + output: plugin.output, + folderPath: hookFolderPath, + generateOutput: plugin.config.generateOutput + }) + ); + } + if (plugin.config.groupBy === 'paths' || plugin.config.groupBy === 'tags') { + // import type { requestName } from './requestName.gen'; + hookFile.add( + getImportRequest({ + folderPath: hookFolderPath, + requestFilePath, + requestName, + generateOutput: plugin.config.generateOutput + }) + ); + } + + // const requestNameSuspenseQueryKey = requestName; + // const requestNameOptions = queryOptions({...}) + // const useRequestNameSuspenseQuery = (settings: TanstackSuspenseQuerySettings) => useSuspenseQuery + hookFile.add( + ...getSuspenseQueryHook({ + optionsFunctionName: `${requestName}Options`, + hookName, + plugin, + request, + requestName + }) + ); +}; diff --git a/packages/apicraft/bin/plugins/tanstack/composed/helpers/index.ts b/packages/apicraft/bin/plugins/tanstack/composed/helpers/index.ts new file mode 100644 index 0000000..cc8eba0 --- /dev/null +++ b/packages/apicraft/bin/plugins/tanstack/composed/helpers/index.ts @@ -0,0 +1,3 @@ +export * from './generateMutationHookFile'; +export * from './generateQueryHookFile'; +export * from './generateSuspenseQueryHookFile'; diff --git a/packages/apicraft/bin/plugins/tanstack/composed/plugin.ts b/packages/apicraft/bin/plugins/tanstack/composed/plugin.ts new file mode 100644 index 0000000..0dc299c --- /dev/null +++ b/packages/apicraft/bin/plugins/tanstack/composed/plugin.ts @@ -0,0 +1,30 @@ +import { generateRequestName, getRequestFilePaths } from '@/bin/plugins/helpers'; + +import type { TanstackPlugin } from '../types'; + +import { + generateMutationHookFile, + generateQueryHookFile, + generateSuspenseQueryHookFile +} from './helpers'; + +export const composedHandler: TanstackPlugin['Handler'] = ({ plugin }) => + plugin.forEach('operation', (event) => { + if (event.type !== 'operation') return; + + const request = event.operation; + const requestName = generateRequestName(request, plugin.config.nameBy); + + const requestFilePaths = getRequestFilePaths({ + groupBy: plugin.config.groupBy, + output: plugin.output, + requestName, + request + }); + + requestFilePaths.forEach((requestFilePath) => { + generateQueryHookFile({ plugin, requestFilePath, request, requestName }); + generateSuspenseQueryHookFile({ plugin, requestFilePath, request, requestName }); + generateMutationHookFile({ plugin, requestFilePath, requestName }); + }); + }); diff --git a/packages/apicraft/bin/plugins/tanstack/helpers/generateMutationHookFile.ts b/packages/apicraft/bin/plugins/tanstack/helpers/generateMutationHookFile.ts deleted file mode 100644 index 0f74a70..0000000 --- a/packages/apicraft/bin/plugins/tanstack/helpers/generateMutationHookFile.ts +++ /dev/null @@ -1,211 +0,0 @@ -import type { DefinePlugin, IR } from '@hey-api/openapi-ts'; - -import * as nodePath from 'node:path'; -import ts from 'typescript'; - -import type { TanstackPluginConfig } from '../types'; - -import { capitalize } from '../../helpers'; -import { getRequestParamsHookKeys } from './getRequestParamsHookKeys'; - -interface GenerateMutationHookFileParams { - plugin: Parameters['Handler']>[0]['plugin']; - request: IR.OperationObject; - requestFilePath: string; - requestName: string; -} - -export const generateMutationHookFile = ({ - plugin, - request, - requestName, - requestFilePath -}: GenerateMutationHookFileParams) => { - const hookFolderPath = nodePath.dirname(requestFilePath).replace('requests', 'hooks'); - const hookName = `use${capitalize(requestName)}Mutation`; - - const hookFile = plugin.createFile({ - id: `${hookFolderPath}/${hookName}`, - path: `${hookFolderPath}/${hookName}` - }); - - // import { useMutation } from '@tanstack/react-query'; - const importUseMutation = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - false, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier('useMutation') - ) - ]) - ), - ts.factory.createStringLiteral('@tanstack/react-query') - ); - - // import type { TanstackMutationSettings } from '@siberiacancode/apicraft'; - const importTanstackMutationSettings = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - true, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier('TanstackMutationSettings') - ) - ]) - ), - ts.factory.createStringLiteral('@siberiacancode/apicraft') - ); - - // import type { requestName } from './requestName.gen'; - const importRequest = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - false, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(requestName)) - ]) - ), - ts.factory.createStringLiteral(nodePath.relative(hookFolderPath, `${requestFilePath}.gen`)) - ); - - const requestParamsHookKeys = getRequestParamsHookKeys(request); - - // const useRequestNameMutation = (settings: TanstackMutationSettings) => useMutation - const hookFunction = ts.factory.createVariableStatement( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier(hookName), - undefined, - undefined, - ts.factory.createArrowFunction( - undefined, - undefined, - [ - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier('settings'), - ts.factory.createToken(ts.SyntaxKind.QuestionToken), - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('TanstackMutationSettings'), - [ts.factory.createTypeQueryNode(ts.factory.createIdentifier(requestName))] - ) - ) - ], - undefined, - ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - // mutationKey: ['requestName', settings?.request?.path?.pathPart, settings?.request?.query?.someQuery], - ts.factory.createCallExpression(ts.factory.createIdentifier('useMutation'), undefined, [ - ts.factory.createObjectLiteralExpression( - [ - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier('mutationKey'), - ts.factory.createArrayLiteralExpression( - [ - ts.factory.createStringLiteral(requestName), - ...requestParamsHookKeys.path.map((requestPathParam) => - ts.factory.createPropertyAccessChain( - ts.factory.createPropertyAccessChain( - ts.factory.createPropertyAccessChain( - ts.factory.createIdentifier('settings'), - ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), - ts.factory.createIdentifier('request') - ), - ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), - ts.factory.createIdentifier('path') - ), - ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), - ts.factory.createIdentifier(requestPathParam) - ) - ), - ...requestParamsHookKeys.query.map((requestQueryParam) => - ts.factory.createPropertyAccessChain( - ts.factory.createPropertyAccessChain( - ts.factory.createPropertyAccessChain( - ts.factory.createIdentifier('settings'), - ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), - ts.factory.createIdentifier('request') - ), - ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), - ts.factory.createIdentifier('query') - ), - ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), - ts.factory.createIdentifier(requestQueryParam) - ) - ) - ], - false - ) - ), - // mutationFn: async (params) => requestName({ ...settings?.request, ...params }), - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier('mutationFn'), - ts.factory.createArrowFunction( - [ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword)], - undefined, - [ - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier('params') - ) - ], - undefined, - ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.factory.createCallExpression( - ts.factory.createIdentifier(requestName), - undefined, - [ - ts.factory.createObjectLiteralExpression( - [ - ts.factory.createSpreadAssignment( - ts.factory.createPropertyAccessChain( - ts.factory.createIdentifier('settings'), - ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), - ts.factory.createIdentifier('request') - ) - ), - ts.factory.createSpreadAssignment( - ts.factory.createIdentifier('params') - ) - ], - false - ) - ] - ) - ) - ), - // ...settings?.params - ts.factory.createSpreadAssignment( - ts.factory.createPropertyAccessChain( - ts.factory.createIdentifier('settings'), - ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), - ts.factory.createIdentifier('params') - ) - ) - ], - true - ) - ]) - ) - ) - ], - ts.NodeFlags.Const - ) - ); - - hookFile.add(importUseMutation); - hookFile.add(importTanstackMutationSettings); - hookFile.add(importRequest); - hookFile.add(hookFunction); -}; diff --git a/packages/apicraft/bin/plugins/tanstack/helpers/generateQueryHookFile.ts b/packages/apicraft/bin/plugins/tanstack/helpers/generateQueryHookFile.ts deleted file mode 100644 index dde73cb..0000000 --- a/packages/apicraft/bin/plugins/tanstack/helpers/generateQueryHookFile.ts +++ /dev/null @@ -1,213 +0,0 @@ -import type { DefinePlugin, IR } from '@hey-api/openapi-ts'; - -import * as nodePath from 'node:path'; -import ts from 'typescript'; - -import type { TanstackPluginConfig } from '../types'; - -import { capitalize, checkRequestHasRequiredParam } from '../../helpers'; -import { getRequestParamsHookKeys } from './getRequestParamsHookKeys'; - -interface GenerateQueryHookParams { - plugin: Parameters['Handler']>[0]['plugin']; - request: IR.OperationObject; - requestFilePath: string; - requestName: string; -} - -export const generateQueryHookFile = ({ - plugin, - request, - requestName, - requestFilePath -}: GenerateQueryHookParams) => { - const hookFolderPath = nodePath.dirname(requestFilePath).replace('requests', 'hooks'); - const hookName = `use${capitalize(requestName)}Query`; - - const hookFile = plugin.createFile({ - id: `${hookFolderPath}/${hookName}`, - path: `${hookFolderPath}/${hookName}` - }); - - // import { useQuery } from '@tanstack/react-query'; - const importUseQuery = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - false, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier('useQuery')) - ]) - ), - ts.factory.createStringLiteral('@tanstack/react-query') - ); - - // import type { TanstackQuerySettings } from '@siberiacancode/apicraft'; - const importTanstackQuerySettings = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - true, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier('TanstackQuerySettings') - ) - ]) - ), - ts.factory.createStringLiteral('@siberiacancode/apicraft') - ); - - // import type { requestName } from './requestName.gen'; - const importRequest = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - false, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(requestName)) - ]) - ), - ts.factory.createStringLiteral(nodePath.relative(hookFolderPath, `${requestFilePath}.gen`)) - ); - - const requestParamsHookKeys = getRequestParamsHookKeys(request); - const requestHasRequiredParam = checkRequestHasRequiredParam(request); - - // const useRequestNameQuery = (settings: TanstackQuerySettings) => useQuery - const hookFunction = ts.factory.createVariableStatement( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier(hookName), - undefined, - undefined, - ts.factory.createArrowFunction( - undefined, - undefined, - [ - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier('settings'), - !requestHasRequiredParam - ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) - : undefined, - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('TanstackQuerySettings'), - [ts.factory.createTypeQueryNode(ts.factory.createIdentifier(requestName))] - ) - ) - ], - undefined, - ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.factory.createCallExpression(ts.factory.createIdentifier('useQuery'), undefined, [ - ts.factory.createObjectLiteralExpression( - [ - // queryKey: ['requestName', settings.request.path.pathPart] - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier('queryKey'), - ts.factory.createArrayLiteralExpression( - [ - ts.factory.createStringLiteral(requestName), - ...requestParamsHookKeys.path.map((requestPathParam) => - ts.factory.createPropertyAccessChain( - ts.factory.createPropertyAccessChain( - ts.factory.createPropertyAccessChain( - ts.factory.createIdentifier('settings'), - !requestHasRequiredParam - ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) - : undefined, - ts.factory.createIdentifier('request') - ), - !requestHasRequiredParam - ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) - : undefined, - ts.factory.createIdentifier('path') - ), - ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), - ts.factory.createIdentifier(requestPathParam) - ) - ), - ...requestParamsHookKeys.query.map((requestQueryParam) => - ts.factory.createPropertyAccessChain( - ts.factory.createPropertyAccessChain( - ts.factory.createPropertyAccessChain( - ts.factory.createIdentifier('settings'), - !requestHasRequiredParam - ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) - : undefined, - ts.factory.createIdentifier('request') - ), - !requestHasRequiredParam - ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) - : undefined, - ts.factory.createIdentifier('query') - ), - ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), - ts.factory.createIdentifier(requestQueryParam) - ) - ) - ], - false - ) - ), - // queryFn: async () => requestName({ ...settings.request }) - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier('queryFn'), - ts.factory.createArrowFunction( - [ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword)], - undefined, - [], - undefined, - ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.factory.createCallExpression( - ts.factory.createIdentifier(requestName), - undefined, - [ - ts.factory.createObjectLiteralExpression( - [ - ts.factory.createSpreadAssignment( - ts.factory.createPropertyAccessChain( - ts.factory.createIdentifier('settings'), - !requestHasRequiredParam - ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) - : undefined, - ts.factory.createIdentifier('request') - ) - ) - ], - false - ) - ] - ) - ) - ), - // ...settings.params - ts.factory.createSpreadAssignment( - ts.factory.createPropertyAccessChain( - ts.factory.createIdentifier('settings'), - !requestHasRequiredParam - ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) - : undefined, - ts.factory.createIdentifier('params') - ) - ) - ], - true - ) - ]) - ) - ) - ], - ts.NodeFlags.Const - ) - ); - - hookFile.add(importUseQuery); - hookFile.add(importTanstackQuerySettings); - hookFile.add(importRequest); - hookFile.add(hookFunction); -}; diff --git a/packages/apicraft/bin/plugins/tanstack/helpers/getMutationHook.ts b/packages/apicraft/bin/plugins/tanstack/helpers/getMutationHook.ts new file mode 100644 index 0000000..0f84688 --- /dev/null +++ b/packages/apicraft/bin/plugins/tanstack/helpers/getMutationHook.ts @@ -0,0 +1,139 @@ +import ts from 'typescript'; + +import type { TanstackPlugin } from '../types'; + +interface GetMutationHookParams { + hookName: string; + plugin: TanstackPlugin['Instance']; + requestName: string; +} + +// export const requestNameMutationKey = requestName +// const useRequestNameMutation = (settings: TanstackMutationSettings) => useMutation +export const getMutationHook = ({ hookName, plugin, requestName }: GetMutationHookParams) => { + // export const requestNameMutationKey = requestName; + const mutationKey = ts.factory.createVariableStatement( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(`${requestName}MutationKey`), + undefined, + undefined, + ts.factory.createStringLiteral(requestName) + ) + ], + ts.NodeFlags.Const + ) + ); + + // const useRequestNameMutation = (settings: TanstackMutationSettings) => useMutation + const hookFunction = ts.factory.createVariableStatement( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(hookName), + undefined, + undefined, + ts.factory.createArrowFunction( + undefined, + undefined, + [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier('settings'), + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier('TanstackMutationSettings'), + [ + ts.factory.createTypeQueryNode( + plugin.config.groupBy === 'class' + ? ts.factory.createQualifiedName( + ts.factory.createIdentifier('instance'), + ts.factory.createIdentifier(requestName) + ) + : ts.factory.createIdentifier(requestName) + ) + ] + ) + ) + ], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + // mutationKey: [requestNameMutationKey], + ts.factory.createCallExpression(ts.factory.createIdentifier('useMutation'), undefined, [ + ts.factory.createObjectLiteralExpression( + [ + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier('mutationKey'), + ts.factory.createArrayLiteralExpression( + [ts.factory.createStringLiteral(`${requestName}MutationKey`)], + false + ) + ), + // mutationFn: async (params) => requestName({ ...settings?.request, ...params }), + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier('mutationFn'), + ts.factory.createArrowFunction( + [ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword)], + undefined, + [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier('params') + ) + ], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createCallExpression( + plugin.config.groupBy === 'class' + ? ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('instance'), + ts.factory.createIdentifier(requestName) + ) + : ts.factory.createIdentifier(requestName), + undefined, + [ + ts.factory.createObjectLiteralExpression( + [ + ts.factory.createSpreadAssignment( + ts.factory.createPropertyAccessChain( + ts.factory.createIdentifier('settings'), + ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), + ts.factory.createIdentifier('request') + ) + ), + ts.factory.createSpreadAssignment( + ts.factory.createIdentifier('params') + ) + ], + false + ) + ] + ) + ) + ), + // ...settings?.params + ts.factory.createSpreadAssignment( + ts.factory.createPropertyAccessChain( + ts.factory.createIdentifier('settings'), + ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), + ts.factory.createIdentifier('params') + ) + ) + ], + true + ) + ]) + ) + ) + ], + ts.NodeFlags.Const + ) + ); + + return [mutationKey, hookFunction]; +}; diff --git a/packages/apicraft/bin/plugins/tanstack/helpers/getQueryHook.ts b/packages/apicraft/bin/plugins/tanstack/helpers/getQueryHook.ts new file mode 100644 index 0000000..d0303a1 --- /dev/null +++ b/packages/apicraft/bin/plugins/tanstack/helpers/getQueryHook.ts @@ -0,0 +1,160 @@ +import type { IR } from '@hey-api/openapi-ts'; + +import ts from 'typescript'; + +import { getRequestInfo } from '@/bin/plugins/helpers/'; + +import type { TanstackPlugin } from '../types'; + +interface GetQueryHookParams { + hookName: string; + plugin: TanstackPlugin['Instance']; + request: IR.OperationObject; + requestName: string; +} + +// const requestNameQueryKey = requestName; +// const useRequestNameQuery = (settings: TanstackQuerySettings) => useQuery +export const getQueryHook = ({ hookName, request, plugin, requestName }: GetQueryHookParams) => { + const requestInfo = getRequestInfo({ request }); + + // export const requestNameQueryKey = requestName; + const queryKey = ts.factory.createVariableStatement( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(`${requestName}QueryKey`), + undefined, + undefined, + ts.factory.createStringLiteral(requestName) + ) + ], + ts.NodeFlags.Const + ) + ); + + // const useRequestNameQuery = (settings: TanstackQuerySettings) => useQuery + const hookFunction = ts.factory.createVariableStatement( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(hookName), + undefined, + undefined, + ts.factory.createArrowFunction( + undefined, + undefined, + [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier('settings'), + !requestInfo.hasRequiredParam + ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) + : undefined, + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier('TanstackQuerySettings'), + [ + ts.factory.createTypeQueryNode( + plugin.config.groupBy === 'class' + ? ts.factory.createQualifiedName( + ts.factory.createIdentifier('instance'), + ts.factory.createIdentifier(requestName) + ) + : ts.factory.createIdentifier(requestName) + ) + ] + ) + ) + ], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createCallExpression(ts.factory.createIdentifier('useQuery'), undefined, [ + ts.factory.createObjectLiteralExpression( + [ + // queryKey: [requestNameQueryKey, settings.request.path, settings.request.query, settings.request.body] + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier('queryKey'), + ts.factory.createArrayLiteralExpression( + [ + ts.factory.createStringLiteral(`${requestName}QueryKey`), + ...['path', 'query', 'body'].map((field) => + ts.factory.createPropertyAccessChain( + ts.factory.createPropertyAccessChain( + ts.factory.createIdentifier('settings'), + !requestInfo.hasRequiredParam + ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) + : undefined, + ts.factory.createIdentifier('request') + ), + !requestInfo.hasRequiredParam + ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) + : undefined, + ts.factory.createIdentifier(field) + ) + ) + ], + false + ) + ), + // queryFn: async () => requestName({ ...settings.request }) + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier('queryFn'), + ts.factory.createArrowFunction( + [ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword)], + undefined, + [], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createCallExpression( + plugin.config.groupBy === 'class' + ? ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('instance'), + ts.factory.createIdentifier(requestName) + ) + : ts.factory.createIdentifier(requestName), + undefined, + [ + ts.factory.createObjectLiteralExpression( + [ + ts.factory.createSpreadAssignment( + ts.factory.createPropertyAccessChain( + ts.factory.createIdentifier('settings'), + !requestInfo.hasRequiredParam + ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) + : undefined, + ts.factory.createIdentifier('request') + ) + ) + ], + false + ) + ] + ) + ) + ), + // ...settings.params + ts.factory.createSpreadAssignment( + ts.factory.createPropertyAccessChain( + ts.factory.createIdentifier('settings'), + !requestInfo.hasRequiredParam + ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) + : undefined, + ts.factory.createIdentifier('params') + ) + ) + ], + true + ) + ]) + ) + ) + ], + ts.NodeFlags.Const + ) + ); + + return [queryKey, hookFunction]; +}; diff --git a/packages/apicraft/bin/plugins/tanstack/helpers/getRequestParamsHookKeys.ts b/packages/apicraft/bin/plugins/tanstack/helpers/getRequestParamsHookKeys.ts deleted file mode 100644 index 8f3cd0b..0000000 --- a/packages/apicraft/bin/plugins/tanstack/helpers/getRequestParamsHookKeys.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { IR } from '@hey-api/openapi-ts'; - -export const getRequestParamsHookKeys = (request: IR.OperationObject) => ({ - path: Object.values(request.parameters?.path ?? {}) - .filter((param) => param.required) - .map((param) => param.name), - query: Object.values(request.parameters?.query ?? {}) - .filter((param) => param.required) - .map((param) => param.name) -}); diff --git a/packages/apicraft/bin/plugins/tanstack/helpers/generateSuspenseQueryHookFile.ts b/packages/apicraft/bin/plugins/tanstack/helpers/getSuspenseQueryHook.ts similarity index 51% rename from packages/apicraft/bin/plugins/tanstack/helpers/generateSuspenseQueryHookFile.ts rename to packages/apicraft/bin/plugins/tanstack/helpers/getSuspenseQueryHook.ts index 1eb9e90..5670b22 100644 --- a/packages/apicraft/bin/plugins/tanstack/helpers/generateSuspenseQueryHookFile.ts +++ b/packages/apicraft/bin/plugins/tanstack/helpers/getSuspenseQueryHook.ts @@ -1,90 +1,47 @@ -import type { DefinePlugin, IR } from '@hey-api/openapi-ts'; +import type { IR } from '@hey-api/openapi-ts'; -import * as nodePath from 'node:path'; import ts from 'typescript'; -import type { TanstackPluginConfig } from '../types'; +import { getRequestInfo } from '@/bin/plugins/helpers'; -import { capitalize, checkRequestHasRequiredParam } from '../../helpers'; -import { getRequestParamsHookKeys } from './getRequestParamsHookKeys'; +import type { TanstackPlugin } from '../types'; -interface GenerateSuspenseQueryHookParams { - plugin: Parameters['Handler']>[0]['plugin']; +interface GetSuspenseQueryHookParams { + hookName: string; + optionsFunctionName: string; + plugin: TanstackPlugin['Instance']; request: IR.OperationObject; - requestFilePath: string; requestName: string; } -export const generateSuspenseQueryHookFile = ({ +// export const requestNameSuspenseQueryKey = requestName +// const requestNameOptions = queryOptions({...}) +// const useRequestNameSuspenseQuery = (settings: TanstackSuspenseQuerySettings) => useSuspenseQuery +export const getSuspenseQueryHook = ({ plugin, + optionsFunctionName, request, - requestName, - requestFilePath -}: GenerateSuspenseQueryHookParams) => { - const hookFolderPath = nodePath.dirname(requestFilePath).replace('requests', 'hooks'); - const hookName = `use${capitalize(requestName)}SuspenseQuery`; + hookName, + requestName +}: GetSuspenseQueryHookParams) => { + const requestInfo = getRequestInfo({ request }); - const hookFile = plugin.createFile({ - id: `${hookFolderPath}/${hookName}`, - path: `${hookFolderPath}/${hookName}` - }); - - // import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'; - const importUseSuspenseQuery = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - false, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier('queryOptions') - ), - ts.factory.createImportSpecifier( - false, + // export const requestNameSuspenseQueryKey = requestName + const suspenseQueryKey = ts.factory.createVariableStatement( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(`${requestName}SuspenseQueryKey`), undefined, - ts.factory.createIdentifier('useSuspenseQuery') - ) - ]) - ), - ts.factory.createStringLiteral('@tanstack/react-query') - ); - - // import type { TanstackSuspenseQuerySettings } from '@siberiacancode/apicraft'; - const importTanstackSuspenseQuerySettings = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - true, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier( - false, undefined, - ts.factory.createIdentifier('TanstackSuspenseQuerySettings') + ts.factory.createStringLiteral(requestName) ) - ]) - ), - ts.factory.createStringLiteral('@siberiacancode/apicraft') - ); - - // import type { requestName } from './requestName.gen'; - const importRequest = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - false, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(requestName)) - ]) - ), - ts.factory.createStringLiteral(nodePath.relative(hookFolderPath, `${requestFilePath}.gen`)) + ], + ts.NodeFlags.Const + ) ); - const optionsFunctionName = `${requestName}Options`; - const requestParamsHookKeys = getRequestParamsHookKeys(request); - const requestHasRequiredParam = checkRequestHasRequiredParam(request); - // const requestNameOptions = queryOptions({...}) const optionsFunction = ts.factory.createVariableStatement( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], @@ -102,12 +59,21 @@ export const generateSuspenseQueryHookFile = ({ undefined, undefined, ts.factory.createIdentifier('settings'), - !requestHasRequiredParam + !requestInfo.hasRequiredParam ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, ts.factory.createTypeReferenceNode( ts.factory.createIdentifier('TanstackSuspenseQuerySettings'), - [ts.factory.createTypeQueryNode(ts.factory.createIdentifier(requestName))] + [ + ts.factory.createTypeQueryNode( + plugin.config.groupBy === 'class' + ? ts.factory.createQualifiedName( + ts.factory.createIdentifier('instance'), + ts.factory.createIdentifier(requestName) + ) + : ts.factory.createIdentifier(requestName) + ) + ] ) ) ], @@ -119,48 +85,25 @@ export const generateSuspenseQueryHookFile = ({ [ ts.factory.createObjectLiteralExpression( [ - // queryKey: ['requestName', settings.request.path.pathPart] + // queryKey: [requestNameSuspenseQueryKey, settings.request.path, settings.request.query, settings.request.body] ts.factory.createPropertyAssignment( ts.factory.createIdentifier('queryKey'), ts.factory.createArrayLiteralExpression( [ - ts.factory.createStringLiteral(requestName), - ...requestParamsHookKeys.path.map((requestPathParam) => - ts.factory.createPropertyAccessChain( - ts.factory.createPropertyAccessChain( - ts.factory.createPropertyAccessChain( - ts.factory.createIdentifier('settings'), - !requestHasRequiredParam - ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) - : undefined, - ts.factory.createIdentifier('request') - ), - !requestHasRequiredParam - ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) - : undefined, - ts.factory.createIdentifier('path') - ), - ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), - ts.factory.createIdentifier(requestPathParam) - ) - ), - ...requestParamsHookKeys.query.map((requestQueryParam) => + ts.factory.createStringLiteral(`${requestName}SuspenseQueryKey`), + ...['path', 'query', 'body'].map((field) => ts.factory.createPropertyAccessChain( ts.factory.createPropertyAccessChain( - ts.factory.createPropertyAccessChain( - ts.factory.createIdentifier('settings'), - !requestHasRequiredParam - ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) - : undefined, - ts.factory.createIdentifier('request') - ), - !requestHasRequiredParam + ts.factory.createIdentifier('settings'), + !requestInfo.hasRequiredParam ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) : undefined, - ts.factory.createIdentifier('query') + ts.factory.createIdentifier('request') ), - ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), - ts.factory.createIdentifier(requestQueryParam) + !requestInfo.hasRequiredParam + ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) + : undefined, + ts.factory.createIdentifier(field) ) ) ], @@ -177,7 +120,12 @@ export const generateSuspenseQueryHookFile = ({ undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createCallExpression( - ts.factory.createIdentifier(requestName), + plugin.config.groupBy === 'class' + ? ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('instance'), + ts.factory.createIdentifier(requestName) + ) + : ts.factory.createIdentifier(requestName), undefined, [ ts.factory.createObjectLiteralExpression( @@ -185,7 +133,7 @@ export const generateSuspenseQueryHookFile = ({ ts.factory.createSpreadAssignment( ts.factory.createPropertyAccessChain( ts.factory.createIdentifier('settings'), - !requestHasRequiredParam + !requestInfo.hasRequiredParam ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) : undefined, ts.factory.createIdentifier('request') @@ -202,7 +150,7 @@ export const generateSuspenseQueryHookFile = ({ ts.factory.createSpreadAssignment( ts.factory.createPropertyAccessChain( ts.factory.createIdentifier('settings'), - !requestHasRequiredParam + !requestInfo.hasRequiredParam ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) : undefined, ts.factory.createIdentifier('params') @@ -220,6 +168,7 @@ export const generateSuspenseQueryHookFile = ({ ) ); + // const useRequestNameSuspenseQuery = (settings: TanstackSuspenseQuerySettings) => useSuspenseQuery const hookFunction = ts.factory.createVariableStatement( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList( @@ -262,9 +211,5 @@ export const generateSuspenseQueryHookFile = ({ ) ); - hookFile.add(importUseSuspenseQuery); - hookFile.add(importTanstackSuspenseQuerySettings); - hookFile.add(importRequest); - hookFile.add(optionsFunction); - hookFile.add(hookFunction); + return [suspenseQueryKey, optionsFunction, hookFunction]; }; diff --git a/packages/apicraft/bin/plugins/tanstack/helpers/getTanstackImport.ts b/packages/apicraft/bin/plugins/tanstack/helpers/getTanstackImport.ts new file mode 100644 index 0000000..cde9044 --- /dev/null +++ b/packages/apicraft/bin/plugins/tanstack/helpers/getTanstackImport.ts @@ -0,0 +1,17 @@ +import ts from 'typescript'; + +// import { name } from '@tanstack/react-query'; +export const getTanstackImport = (name: string | string[]) => + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports([ + ...(Array.isArray(name) ? name : [name]).map((name) => + ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(name)) + ) + ]) + ), + ts.factory.createStringLiteral('@tanstack/react-query') + ); diff --git a/packages/apicraft/bin/plugins/tanstack/helpers/index.ts b/packages/apicraft/bin/plugins/tanstack/helpers/index.ts index cc8eba0..5611b1c 100644 --- a/packages/apicraft/bin/plugins/tanstack/helpers/index.ts +++ b/packages/apicraft/bin/plugins/tanstack/helpers/index.ts @@ -1,3 +1,4 @@ -export * from './generateMutationHookFile'; -export * from './generateQueryHookFile'; -export * from './generateSuspenseQueryHookFile'; +export * from './getMutationHook'; +export * from './getQueryHook'; +export * from './getSuspenseQueryHook'; +export * from './getTanstackImport'; diff --git a/packages/apicraft/bin/plugins/tanstack/plugin.ts b/packages/apicraft/bin/plugins/tanstack/plugin.ts index 7d3bb55..d6fc303 100644 --- a/packages/apicraft/bin/plugins/tanstack/plugin.ts +++ b/packages/apicraft/bin/plugins/tanstack/plugin.ts @@ -1,29 +1,13 @@ import type { TanstackPlugin } from './types'; -import { generateRequestName, getRequestFilePaths } from '../helpers'; -import { - generateMutationHookFile, - generateQueryHookFile, - generateSuspenseQueryHookFile -} from './helpers'; - -export const handler: TanstackPlugin['Handler'] = ({ plugin }) => - plugin.forEach('operation', (event) => { - if (event.type !== 'operation') return; - - const request = event.operation; - const requestName = generateRequestName(request, plugin.config.nameBy); - - const requestFilePaths = getRequestFilePaths({ - groupBy: plugin.config.groupBy, - output: plugin.output, - requestName, - request - }); - - requestFilePaths.forEach((requestFilePath) => { - generateQueryHookFile({ plugin, requestFilePath, request, requestName }); - generateSuspenseQueryHookFile({ plugin, requestFilePath, request, requestName }); - generateMutationHookFile({ plugin, requestFilePath, request, requestName }); - }); - }); +import { classHandler } from './class/plugin'; +import { composedHandler } from './composed/plugin'; + +export const handler: TanstackPlugin['Handler'] = ({ plugin }) => { + if (plugin.config.groupBy === 'class') { + classHandler({ plugin }); + } + if (plugin.config.groupBy === 'paths' || plugin.config.groupBy === 'tags') { + composedHandler({ plugin }); + } +}; diff --git a/packages/apicraft/bin/schemas/index.ts b/packages/apicraft/bin/schemas/index.ts index f3571f1..93b073e 100644 --- a/packages/apicraft/bin/schemas/index.ts +++ b/packages/apicraft/bin/schemas/index.ts @@ -123,7 +123,7 @@ export const apicraftOptionSchema = z .optional(), instance: z.union([instanceNameSchema, instanceSchema]).optional(), nameBy: z.enum(['path', 'operationId']).default('operationId').optional(), - groupBy: z.enum(['path', 'tag']).default('tag').optional(), + groupBy: z.enum(['paths', 'tags', 'class']).default('tags').optional(), plugins: z .array(pluginNameSchema.or(z.object({ name: pluginNameSchema }).passthrough())) .optional()