Files
medusa-store/docs-util/packages/docblock-generator/src/classes/kinds/medusa-react-hooks.ts
2024-01-24 11:13:40 +01:00

196 lines
5.8 KiB
TypeScript

import ts from "typescript"
import FunctionKindGenerator, {
FunctionNode,
FunctionOrVariableNode,
} from "./function.js"
import {
DOCBLOCK_NEW_LINE,
DOCBLOCK_END_LINE,
DOCBLOCK_START,
DOCBLOCK_DOUBLE_LINES,
} from "../../constants.js"
import nodeHasComments from "../../utils/node-has-comments.js"
import {
CUSTOM_NAMESPACE_TAG,
getCustomNamespaceTag,
} from "../../utils/medusa-react-utils.js"
/**
* Docblock generate for medusa-react hooks. Since hooks are essentially functions,
* it extends the {@link FunctionKindGenerator} class.
*/
class MedusaReactHooksKindGenerator extends FunctionKindGenerator {
/**
* Checks whether the generator can retrieve the docblock of the specified node. It uses the parent generator
* to check that the node is a function, then checks if the function is a mutation using the {@link isMutation} method,
* or a query using the {@link isQuery} method.
*
* @param {ts.Node} node - The node to check.
* @returns {boolean} Whether this generator can be used on this node.
*/
isAllowed(node: ts.Node): node is FunctionOrVariableNode {
if (!super.isAllowed(node)) {
return false
}
const actualNode = ts.isVariableStatement(node)
? this.extractFunctionNode(node)
: node
return (
actualNode !== undefined &&
(this.isMutation(actualNode) || this.isQuery(actualNode))
)
}
/**
* Checks whether a function node is a mutation.
*
* @param {FunctionNode} node - The function node to check.
* @returns {boolean} Whether the node is a mutation.
*/
isMutation(node: FunctionNode): boolean {
const nodeType = this.getReturnType(node)
const callSignatures = nodeType.getCallSignatures()
return (
callSignatures.length > 0 &&
this.checker
.typeToString(this.checker.getReturnTypeOfSignature(callSignatures[0]))
.startsWith("UseMutationResult")
)
}
/**
* Checks whether a function node is a query.
*
* @param {FunctionNode} node - The function node to check.
* @returns {boolean} Whether the node is a query.
*/
isQuery(node: FunctionNode): boolean {
return node.parameters.some(
(parameter) =>
parameter.type?.getText().startsWith("UseQueryOptionsWrapper")
)
}
/**
* Retrieves the docblock of the medusa-react hook or mutation.
*
* @param {FunctionNode & ts.VariableDeclaration} node - The node to retrieve its docblock.
* @returns {string} The node's docblock.
*/
getDocBlock(node: FunctionNode & ts.VariableDeclaration): string {
if (!this.isAllowed(node)) {
return super.getDocBlock(node)
}
const actualNode = ts.isVariableStatement(node)
? this.extractFunctionNode(node)
: node
if (!actualNode) {
return super.getDocBlock(node)
}
const isMutation = this.isMutation(actualNode)
let str = `${DOCBLOCK_START}This hook ${this.getFunctionSummary(node)}`
// add example
str += this.getFunctionExample()
// loop over parameters that aren't query/mutation parameters
// and add docblock to them
this.getActualParameters(actualNode).forEach((parameter) => {
ts.addSyntheticLeadingComment(
parameter,
ts.SyntaxKind.MultiLineCommentTrivia,
super.getDocBlock(parameter),
true
)
})
// check if mutation parameter is an intrinsic type and, if so, add the `@typeParamDefinition`
// tag to the hook
if (isMutation) {
const typeArg = this.getMutationRequestTypeArg(actualNode)
if (typeArg) {
str += `${DOCBLOCK_DOUBLE_LINES}@typeParamDefinition ${this.checker.typeToString(
typeArg
)} - {summary}`
}
}
// add common docs
str += this.getCommonDocs(node, {
prefixWithLineBreaks: true,
})
// add namespace in case it's not added
if (!str.includes(CUSTOM_NAMESPACE_TAG)) {
str += `${DOCBLOCK_DOUBLE_LINES}${getCustomNamespaceTag(actualNode)}`
}
// add the category
str += `${DOCBLOCK_NEW_LINE}@category ${
isMutation ? "Mutations" : "Queries"
}`
return `${str}${DOCBLOCK_END_LINE}`
}
/**
* Retrieves the parameters of a function node that aren't query/mutation options.
*
* @param {FunctionNode} node - The function node to retrieve its parameters.
* @returns {ts.ParameterDeclaration[]} - The function's actual parameters.
*/
getActualParameters(node: FunctionNode): ts.ParameterDeclaration[] {
return node.parameters.filter((parameter) => {
const parameterTypeStr = parameter.type?.getText()
return (
!parameterTypeStr?.startsWith("UseQueryOptionsWrapper") &&
!parameterTypeStr?.startsWith("UseMutationOptions") &&
!nodeHasComments(parameter)
)
})
}
/**
* Retreives a mutation's intrinsic request type, if available, which is specified as the third type argument of `UseMutationOptions`.
*
* @param {FunctionNode} node - The function node to retrieve its request type.
* @returns {ts.Type | undefined} The mutation's request type, if available.
*/
getMutationRequestTypeArg(node: FunctionNode): ts.Type | undefined {
const parameter = node.parameters.find(
(parameter) => parameter.type?.getText().startsWith("UseMutationOptions")
)
if (!parameter) {
return
}
const parameterType = this.checker.getTypeFromTypeNode(parameter.type!)
const typeArgs =
parameterType.aliasTypeArguments ||
("resolvedTypeArguments" in parameterType
? (parameterType.resolvedTypeArguments as ts.Type[])
: [])
if (
!typeArgs ||
typeArgs.length < 3 ||
!("intrinsicName" in typeArgs[2]) ||
["void", "unknown"].includes(typeArgs[2].intrinsicName as string)
) {
return
}
// find request in third type argument
return typeArgs[2]
}
}
export default MedusaReactHooksKindGenerator