docs-util: rename docblock-generator to docs-generator (#8331)
* docs-util: rename docblock-generator to docs-generator * change program name * fix action
This commit is contained in:
@@ -0,0 +1,600 @@
|
||||
import ts from "typescript"
|
||||
import {
|
||||
DOCBLOCK_START,
|
||||
DOCBLOCK_END_LINE,
|
||||
DOCBLOCK_DOUBLE_LINES,
|
||||
DOCBLOCK_NEW_LINE,
|
||||
SUMMARY_PLACEHOLDER,
|
||||
} from "../../constants.js"
|
||||
import getSymbol from "../../utils/get-symbol.js"
|
||||
import KnowledgeBaseFactory, {
|
||||
RetrieveOptions,
|
||||
} from "../helpers/knowledge-base-factory.js"
|
||||
import {
|
||||
getCustomNamespaceTag,
|
||||
shouldHaveCustomNamespace,
|
||||
} from "../../utils/medusa-react-utils.js"
|
||||
import GeneratorEventManager from "../helpers/generator-event-manager.js"
|
||||
import { CommonCliOptions } from "../../types/index.js"
|
||||
import AiGenerator from "../helpers/ai-generator.js"
|
||||
import { camelToWords, capitalize } from "utils"
|
||||
import { normalizeName } from "../../utils/str-formatting.js"
|
||||
|
||||
export type GeneratorOptions = {
|
||||
checker: ts.TypeChecker
|
||||
kinds?: ts.SyntaxKind[]
|
||||
generatorEventManager: GeneratorEventManager
|
||||
additionalOptions?: Pick<CommonCliOptions, "generateExamples">
|
||||
}
|
||||
|
||||
export type GetDocBlockOptions = {
|
||||
addEnd?: boolean
|
||||
summaryPrefix?: string
|
||||
aiGenerator?: AiGenerator
|
||||
}
|
||||
|
||||
type CommonDocsOptions = {
|
||||
addDefaultSummary?: boolean
|
||||
prefixWithLineBreaks?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Class used to generate docblocks for basic kinds. It can be
|
||||
* extended for kinds requiring more elaborate TSDocs.
|
||||
*/
|
||||
class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
static DEFAULT_ALLOWED_NODE_KINDS = [
|
||||
ts.SyntaxKind.SourceFile,
|
||||
ts.SyntaxKind.ClassDeclaration,
|
||||
ts.SyntaxKind.EnumDeclaration,
|
||||
ts.SyntaxKind.EnumMember,
|
||||
ts.SyntaxKind.ModuleDeclaration,
|
||||
ts.SyntaxKind.PropertyDeclaration,
|
||||
ts.SyntaxKind.InterfaceDeclaration,
|
||||
ts.SyntaxKind.TypeAliasDeclaration,
|
||||
ts.SyntaxKind.PropertySignature,
|
||||
]
|
||||
public name = "default"
|
||||
protected allowedKinds: ts.SyntaxKind[]
|
||||
protected checker: ts.TypeChecker
|
||||
protected knowledgeBaseFactory: KnowledgeBaseFactory
|
||||
protected generatorEventManager: GeneratorEventManager
|
||||
protected options: Pick<CommonCliOptions, "generateExamples">
|
||||
|
||||
constructor({
|
||||
checker,
|
||||
kinds,
|
||||
generatorEventManager,
|
||||
additionalOptions = {},
|
||||
}: GeneratorOptions) {
|
||||
this.allowedKinds = kinds || DefaultKindGenerator.DEFAULT_ALLOWED_NODE_KINDS
|
||||
this.checker = checker
|
||||
this.knowledgeBaseFactory = new KnowledgeBaseFactory()
|
||||
this.generatorEventManager = generatorEventManager
|
||||
this.options = additionalOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the kinds that are handled by this generator.
|
||||
*/
|
||||
getAllowedKinds(): ts.SyntaxKind[] {
|
||||
return this.allowedKinds
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this generator can be used for a node based on the node's kind.
|
||||
*
|
||||
* @param {ts.Node} node - The node to check for.
|
||||
* @returns {boolean} Whether this generator can be used with the specified node.
|
||||
*/
|
||||
isAllowed(node: ts.Node): node is T {
|
||||
return this.allowedKinds.includes(node.kind)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the doc block for the passed node.
|
||||
*
|
||||
* @param {T | ts.Node} node - The node to retrieve the docblock for.
|
||||
* @param {GetDocBlockOptions} options - Options useful for children classes of this class to specify the formatting of the docblock.
|
||||
* @returns {string} The node's docblock.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getDocBlock(
|
||||
node: T | ts.Node,
|
||||
options: GetDocBlockOptions = { addEnd: true }
|
||||
): Promise<string> {
|
||||
let str = DOCBLOCK_START
|
||||
const summary = this.getNodeSummary({ node })
|
||||
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.EnumDeclaration:
|
||||
str += `@enum${DOCBLOCK_DOUBLE_LINES}${summary}`
|
||||
break
|
||||
case ts.SyntaxKind.TypeAliasDeclaration:
|
||||
str += `@interface${DOCBLOCK_DOUBLE_LINES}${summary}`
|
||||
break
|
||||
default:
|
||||
str += summary
|
||||
}
|
||||
|
||||
str += this.getCommonDocs(node, {
|
||||
prefixWithLineBreaks: true,
|
||||
})
|
||||
|
||||
return `${str}${options.addEnd ? DOCBLOCK_END_LINE : ""}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the summary comment of a node. It gives precedense to the node's symbol if it's provided/retrieved and if it's available using the {@link getSymbolDocBlock}.
|
||||
* Otherwise, it retrieves the comments of the type using the {@link getTypeDocBlock}
|
||||
* @returns {string} The summary comment.
|
||||
*/
|
||||
getNodeSummary({
|
||||
node,
|
||||
symbol,
|
||||
nodeType,
|
||||
knowledgeBaseOptions: overrideOptions,
|
||||
}: {
|
||||
/**
|
||||
* The node to retrieve the summary comment for.
|
||||
*/
|
||||
node: T | ts.Node
|
||||
/**
|
||||
* Optionally provide the node's symbol. If not provided, the
|
||||
* method will try to retrieve it.
|
||||
*/
|
||||
symbol?: ts.Symbol
|
||||
/**
|
||||
* Optionally provide the node's type. If not provided, the method
|
||||
* will try to retrieve it.
|
||||
*/
|
||||
nodeType?: ts.Type
|
||||
/**
|
||||
* Override any of the default knowledge base options
|
||||
* inferred using the {@link getKnowledgeBaseOptions} method
|
||||
*/
|
||||
knowledgeBaseOptions?: Partial<RetrieveOptions>
|
||||
}): string {
|
||||
const syntheticComments = ts.getSyntheticLeadingComments(node)
|
||||
if (syntheticComments?.length) {
|
||||
return syntheticComments.map((comment) => comment.text).join(" ")
|
||||
}
|
||||
const knowledgeBaseOptions = {
|
||||
...this.getKnowledgeBaseOptions(node),
|
||||
...overrideOptions,
|
||||
}
|
||||
if (!nodeType) {
|
||||
nodeType =
|
||||
"type" in node && node.type && ts.isTypeNode(node.type as ts.Node)
|
||||
? this.checker.getTypeFromTypeNode(node.type as ts.TypeNode)
|
||||
: symbol
|
||||
? this.checker.getTypeOfSymbolAtLocation(symbol, node)
|
||||
: this.checker.getTypeAtLocation(node)
|
||||
}
|
||||
|
||||
if (!symbol) {
|
||||
symbol = getSymbol(node, this.checker)
|
||||
}
|
||||
|
||||
let summary = ""
|
||||
|
||||
if (symbol) {
|
||||
summary = this.getSymbolDocBlock(symbol, knowledgeBaseOptions)
|
||||
}
|
||||
|
||||
if (!summary.length) {
|
||||
summary = this.getTypeDocBlock(nodeType, knowledgeBaseOptions)
|
||||
}
|
||||
|
||||
return summary.length > 0 ? summary : SUMMARY_PLACEHOLDER
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the summary comment of a type. It tries to retrieve from the alias symbol, type arguments, or {@link KnowledgeBaseFactory}.
|
||||
* If no summary comments are found, the {@link defaultSummary} is used.
|
||||
*
|
||||
* @param {ts.Type} nodeType - The type of a node.
|
||||
* @returns {string} The summary comment.
|
||||
*/
|
||||
protected getTypeDocBlock(
|
||||
nodeType: ts.Type,
|
||||
knowledgeBaseOptions?: Partial<RetrieveOptions>
|
||||
): string {
|
||||
if (nodeType.aliasSymbol || nodeType.symbol) {
|
||||
const symbolDoc = this.getSymbolDocBlock(
|
||||
nodeType.aliasSymbol || nodeType.symbol
|
||||
)
|
||||
|
||||
if (symbolDoc.length) {
|
||||
return symbolDoc
|
||||
}
|
||||
}
|
||||
|
||||
const typeArguments = this.checker.getTypeArguments(
|
||||
nodeType as ts.TypeReference
|
||||
)
|
||||
|
||||
if (typeArguments.length) {
|
||||
// take only the first type argument to account
|
||||
const typeArgumentDoc = this.getTypeDocBlock(typeArguments[0])
|
||||
|
||||
if (!typeArgumentDoc.length) {
|
||||
const tryKnowledgeSummary = this.knowledgeBaseFactory.tryToGetSummary({
|
||||
...knowledgeBaseOptions,
|
||||
str: this.checker.typeToString(nodeType),
|
||||
})
|
||||
|
||||
if (tryKnowledgeSummary?.length) {
|
||||
return tryKnowledgeSummary
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.checker.isArrayType(nodeType)) {
|
||||
return typeArgumentDoc
|
||||
}
|
||||
|
||||
// do some formatting if the encapsulating type is an array
|
||||
return `The list of ${capitalize(typeArgumentDoc) || SUMMARY_PLACEHOLDER}`
|
||||
}
|
||||
|
||||
return (
|
||||
this.knowledgeBaseFactory.tryToGetSummary({
|
||||
...knowledgeBaseOptions,
|
||||
str: this.checker.typeToString(nodeType),
|
||||
}) || ""
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the docblock of a symbol. It tries to retrieve it using the symbol's `getDocumentationComment` and `getJsDocTags`
|
||||
* methods. If both methods don't return any comments, it tries to get the comments from the {@link KnowledgeBaseFactory}.
|
||||
*
|
||||
* @param {ts.Symbol} symbol - The symbol to retrieve its docblock.
|
||||
* @returns {string} The symbol's docblock.
|
||||
*/
|
||||
protected getSymbolDocBlock(
|
||||
symbol: ts.Symbol,
|
||||
knowledgeBaseOptions?: Partial<RetrieveOptions>
|
||||
): string {
|
||||
const commentDisplayParts = symbol.getDocumentationComment(this.checker)
|
||||
if (!commentDisplayParts.length) {
|
||||
// try to get description from the first JSDoc comment
|
||||
const jsdocComments = symbol.getJsDocTags(this.checker)
|
||||
|
||||
if (jsdocComments.length) {
|
||||
jsdocComments
|
||||
.find((tag) => (tag.text?.length || 0) > 0)
|
||||
?.text!.forEach((tagText) => {
|
||||
commentDisplayParts.push(tagText)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!commentDisplayParts.length) {
|
||||
return (
|
||||
this.knowledgeBaseFactory.tryToGetSummary({
|
||||
...knowledgeBaseOptions,
|
||||
str: this.checker.typeToString(this.checker.getTypeOfSymbol(symbol)),
|
||||
}) ||
|
||||
this.knowledgeBaseFactory.tryToGetSummary({
|
||||
...knowledgeBaseOptions,
|
||||
str: symbol.name,
|
||||
}) ||
|
||||
""
|
||||
)
|
||||
}
|
||||
|
||||
return ts
|
||||
.displayPartsToString(commentDisplayParts)
|
||||
.replaceAll("\n", DOCBLOCK_NEW_LINE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves docblocks based on decorators used on a symbol.
|
||||
*
|
||||
* @param {ts.Symbol} symbol - The symbol to retrieve its decorators docblock.
|
||||
* @returns {string} The symbol's decorators docblock.
|
||||
*/
|
||||
getDecoratorDocs(symbol: ts.Symbol): string {
|
||||
let str = ""
|
||||
|
||||
symbol.declarations?.forEach((declaration) => {
|
||||
const modifiers =
|
||||
"modifiers" in declaration && declaration.modifiers
|
||||
? (declaration.modifiers as ts.NodeArray<ts.Modifier>)
|
||||
: []
|
||||
|
||||
modifiers.forEach((modifier) => {
|
||||
if (!ts.isDecorator(modifier)) {
|
||||
return
|
||||
}
|
||||
|
||||
// check for decorator text
|
||||
;(modifier as ts.Decorator).forEachChild((childNode) => {
|
||||
if (ts.isCallExpression(childNode)) {
|
||||
const childNodeExpression = (childNode as ts.CallExpression)
|
||||
.expression
|
||||
if (ts.isIdentifier(childNodeExpression)) {
|
||||
switch (childNodeExpression.escapedText) {
|
||||
case "FeatureFlagEntity":
|
||||
// add the `@featureFlag` tag.
|
||||
str += `${DOCBLOCK_DOUBLE_LINES}@featureFlag [flag_name]`
|
||||
break
|
||||
case "BeforeInsert":
|
||||
case "BeforeLoad":
|
||||
case "AfterLoad":
|
||||
// add `@apiIgnore` tag
|
||||
str += `${DOCBLOCK_DOUBLE_LINES}@apiIgnore`
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve docblocks that are common to all nodes, despite their kind.
|
||||
*
|
||||
* @param {T | ts.Node} node - The node to retrieve its common doc blocks.
|
||||
* @param {CommonDocsOptions} options - Formatting options.
|
||||
* @returns {string} The common docblocks.
|
||||
*/
|
||||
getCommonDocs(
|
||||
node: T | ts.Node,
|
||||
options: CommonDocsOptions = { addDefaultSummary: false }
|
||||
): string {
|
||||
const tags = new Set<string>()
|
||||
|
||||
const symbol = getSymbol(node, this.checker)
|
||||
|
||||
if (!symbol) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (ts.isSourceFile(node)) {
|
||||
// comments for source files must start with this tag
|
||||
tags.add(`@packageDocumentation`)
|
||||
}
|
||||
|
||||
if (options.addDefaultSummary) {
|
||||
tags.add(SUMMARY_PLACEHOLDER)
|
||||
}
|
||||
|
||||
// check for private or protected modifiers
|
||||
// and if found, add the `@ignore` tag.
|
||||
symbol.declarations?.some((declaration) => {
|
||||
if (!("modifiers" in declaration) || !declaration.modifiers) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hasPrivateOrProtected = (
|
||||
declaration.modifiers as ts.NodeArray<ts.Modifier>
|
||||
).find((modifier) => {
|
||||
modifier.kind === ts.SyntaxKind.PrivateKeyword ||
|
||||
modifier.kind === ts.SyntaxKind.ProtectedKeyword
|
||||
})
|
||||
|
||||
if (!hasPrivateOrProtected) {
|
||||
return false
|
||||
}
|
||||
|
||||
tags.add("@ignore")
|
||||
return true
|
||||
})
|
||||
|
||||
// if a symbol's name starts with `_` then we
|
||||
// should add the `@ignore` tag
|
||||
if (symbol.getName().startsWith("_")) {
|
||||
tags.add("@ignore")
|
||||
}
|
||||
|
||||
// check if any docs can be added for the symbol's
|
||||
// decorators
|
||||
this.getDecoratorDocs(symbol)
|
||||
.split(`${DOCBLOCK_DOUBLE_LINES}`)
|
||||
.filter((docItem) => docItem.length > 0)
|
||||
.forEach((docItem) => tags.add(docItem))
|
||||
|
||||
// add `@expandable` tag if the resource is
|
||||
if (ts.isPropertyDeclaration(node)) {
|
||||
const symbolType = this.checker.getTypeOfSymbol(symbol)
|
||||
if (
|
||||
symbolType.symbol?.declarations?.length &&
|
||||
ts.isClassDeclaration(symbolType.symbol?.declarations[0]) &&
|
||||
this.isEntity({
|
||||
heritageClauses: (
|
||||
symbolType.symbol?.declarations[0] as ts.ClassDeclaration
|
||||
).heritageClauses,
|
||||
node: symbolType.symbol?.declarations[0],
|
||||
})
|
||||
) {
|
||||
tags.add(`@expandable`)
|
||||
}
|
||||
}
|
||||
|
||||
// check if custom namespace should be added
|
||||
if (shouldHaveCustomNamespace(node)) {
|
||||
tags.add(getCustomNamespaceTag(node))
|
||||
}
|
||||
|
||||
// check for default value
|
||||
const defaultValue = this.getDefaultValue(node)
|
||||
if (defaultValue?.length) {
|
||||
tags.add(`@defaultValue ${defaultValue}`)
|
||||
}
|
||||
|
||||
let str = ""
|
||||
tags.forEach((tag) => {
|
||||
if (str.length > 0) {
|
||||
str += `${DOCBLOCK_DOUBLE_LINES}`
|
||||
}
|
||||
str += `${tag}`
|
||||
})
|
||||
|
||||
if (str.length && options.prefixWithLineBreaks) {
|
||||
str = `${DOCBLOCK_DOUBLE_LINES}${str}`
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node is a Medusa entity.
|
||||
* @returns {boolean} Whether the node is a Medusa entity.
|
||||
*/
|
||||
isEntity({
|
||||
/**
|
||||
* The inherit/extend keywords of the node.
|
||||
*/
|
||||
heritageClauses,
|
||||
/**
|
||||
* Optionally provide the node to accurately retrieve its type name.
|
||||
*/
|
||||
node,
|
||||
}: {
|
||||
heritageClauses?: ts.NodeArray<ts.HeritageClause>
|
||||
node?: ts.Node
|
||||
}): boolean {
|
||||
return (
|
||||
heritageClauses?.some((heritageClause) => {
|
||||
return heritageClause.types.some((heritageClauseType) => {
|
||||
const symbolType = this.checker.getTypeAtLocation(
|
||||
heritageClauseType.expression
|
||||
)
|
||||
|
||||
if (
|
||||
this.checker
|
||||
.typeToString(symbolType, node, undefined)
|
||||
.includes("BaseEntity")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (
|
||||
symbolType.symbol?.valueDeclaration &&
|
||||
"heritageClauses" in symbolType.symbol.valueDeclaration
|
||||
) {
|
||||
return this.isEntity({
|
||||
heritageClauses: symbolType.symbol.valueDeclaration
|
||||
.heritageClauses as ts.NodeArray<ts.HeritageClause>,
|
||||
node,
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}) || false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get knowledge base options for a specified node.
|
||||
*
|
||||
* @param node - The node to retrieve its knowledge base options.
|
||||
* @returns The knowledge base options.
|
||||
*/
|
||||
getKnowledgeBaseOptions(node: ts.Node): Partial<RetrieveOptions> {
|
||||
const rawParentName =
|
||||
"name" in node.parent &&
|
||||
node.parent.name &&
|
||||
ts.isIdentifier(node.parent.name as ts.Node)
|
||||
? (node.parent.name as ts.Identifier).getText()
|
||||
: undefined
|
||||
return {
|
||||
kind: node.kind,
|
||||
templateOptions: {
|
||||
rawParentName,
|
||||
parentName: rawParentName
|
||||
? camelToWords(normalizeName(rawParentName))
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default value of a node.
|
||||
*
|
||||
* @param node - The node to get its default value.
|
||||
* @returns The default value, if any.
|
||||
*/
|
||||
getDefaultValue(node: ts.Node): string | undefined {
|
||||
if (
|
||||
"initializer" in node &&
|
||||
node.initializer &&
|
||||
ts.isExpression(node.initializer as ts.Node)
|
||||
) {
|
||||
const initializer = node.initializer as ts.Expression
|
||||
|
||||
// retrieve default value only if the value is numeric, string, or boolean
|
||||
const defaultValue =
|
||||
ts.isNumericLiteral(initializer) || ts.isStringLiteral(initializer)
|
||||
? initializer.getText()
|
||||
: initializer.kind === ts.SyntaxKind.FalseKeyword
|
||||
? "false"
|
||||
: initializer.kind === ts.SyntaxKind.TrueKeyword
|
||||
? "true"
|
||||
: ""
|
||||
|
||||
if (defaultValue.length) {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a node can be documented.
|
||||
*
|
||||
* @param {ts.Node} node - The node to check for.
|
||||
* @returns {boolean} Whether the node can be documented.
|
||||
*/
|
||||
canDocumentNode(node: ts.Node): boolean {
|
||||
// check if node already has docblock
|
||||
return !this.nodeHasComments(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comments range of a node.
|
||||
* @param node - The node to get its comment range.
|
||||
* @returns The comment range of the node if available.
|
||||
*/
|
||||
getNodeCommentsRange(node: ts.Node): ts.CommentRange[] | undefined {
|
||||
return ts.getLeadingCommentRanges(
|
||||
node.getSourceFile().getFullText(),
|
||||
node.getFullStart()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a node's comment from its range.
|
||||
*
|
||||
* @param node - The node to get its comment range.
|
||||
* @returns The comment if available.
|
||||
*/
|
||||
getNodeCommentsFromRange(node: ts.Node): string | undefined {
|
||||
const commentRange = this.getNodeCommentsRange(node)
|
||||
|
||||
if (!commentRange?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
return node
|
||||
.getSourceFile()
|
||||
.getFullText()
|
||||
.slice(commentRange[0].pos, commentRange[0].end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a node has comments.
|
||||
*
|
||||
* @param node - The node to check.
|
||||
* @returns Whether the node has comments.
|
||||
*/
|
||||
nodeHasComments(node: ts.Node): boolean {
|
||||
return this.getNodeCommentsFromRange(node) !== undefined
|
||||
}
|
||||
}
|
||||
|
||||
export default DefaultKindGenerator
|
||||
@@ -0,0 +1,277 @@
|
||||
import ts from "typescript"
|
||||
import DefaultKindGenerator, { GetDocBlockOptions } from "./default.js"
|
||||
import getBasePath from "../../utils/get-base-path.js"
|
||||
import { getDmlOutputBasePath } from "../../utils/get-output-base-paths.js"
|
||||
import path from "path"
|
||||
import { camelToWords, RELATION_NAMES, snakeToPascal } from "utils"
|
||||
import toJsonFormatted from "../../utils/to-json-formatted.js"
|
||||
import { DmlFile, DmlObject } from "types"
|
||||
|
||||
/**
|
||||
* DML generator for data models created with DML.
|
||||
*/
|
||||
class DmlKindGenerator extends DefaultKindGenerator<ts.CallExpression> {
|
||||
protected allowedKinds: ts.SyntaxKind[] = [ts.SyntaxKind.CallExpression]
|
||||
public name = "dml"
|
||||
|
||||
/**
|
||||
* Checks whether the node is a call expression, is `model.define`, and has at least
|
||||
* two arguments.
|
||||
*
|
||||
* @param node - The node to check
|
||||
* @returns Whether this kind generator can be used for this node.
|
||||
*/
|
||||
isAllowed(node: ts.Node): node is ts.CallExpression {
|
||||
if (!super.isAllowed(node)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isModelUtility =
|
||||
"expression" in node.expression &&
|
||||
(node.expression.expression as ts.Node).getText() === "model"
|
||||
const isDefineMethod =
|
||||
"name" in node.expression &&
|
||||
(node.expression.name as ts.Identifier).getText() === "define"
|
||||
const hasAllArguments = node.arguments.length >= 2
|
||||
|
||||
return isModelUtility && isDefineMethod && hasAllArguments
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the DML object from the node's associated file, if it exists.
|
||||
*
|
||||
* @param node - The node to retrieve its DML object.
|
||||
* @param dataModelName - The data model's name
|
||||
* @returns The DML object, if available.
|
||||
*/
|
||||
getExistingDmlObjectFromFile(
|
||||
node: ts.CallExpression,
|
||||
dataModelName: string
|
||||
): DmlObject | undefined {
|
||||
const filePath = this.getAssociatedFileName(node)
|
||||
const existingJson = ts.sys.readFile(filePath)
|
||||
|
||||
if (!existingJson) {
|
||||
return
|
||||
}
|
||||
|
||||
const parsedJson = JSON.parse(existingJson) as DmlFile
|
||||
if (!Object.hasOwn(parsedJson, dataModelName)) {
|
||||
return
|
||||
}
|
||||
|
||||
return parsedJson[dataModelName].properties
|
||||
}
|
||||
|
||||
/**
|
||||
* Get new or updated JSON dml object of the data model.
|
||||
*
|
||||
* @param node - The node to get its JSON DML object.
|
||||
* @param options - General options
|
||||
* @returns The JSON dml object.
|
||||
*/
|
||||
async getDocBlock(
|
||||
node: ts.CallExpression | ts.Node,
|
||||
options?: GetDocBlockOptions
|
||||
): Promise<string> {
|
||||
if (!this.isAllowed(node)) {
|
||||
return await super.getDocBlock(node, options)
|
||||
}
|
||||
const dataModelName = this.getDataModelName(node.arguments[0])
|
||||
|
||||
const existingDmlObject = this.getExistingDmlObjectFromFile(
|
||||
node,
|
||||
dataModelName
|
||||
)
|
||||
|
||||
const properties = existingDmlObject
|
||||
? this.updateExistingDmlObject({
|
||||
node,
|
||||
dmlObject: existingDmlObject,
|
||||
dataModelName,
|
||||
})
|
||||
: this.getNewDmlObject({
|
||||
node,
|
||||
dataModelName,
|
||||
})
|
||||
|
||||
const dmlFile: DmlFile = {
|
||||
[dataModelName]: {
|
||||
filePath: getBasePath(node.getSourceFile().fileName),
|
||||
properties,
|
||||
},
|
||||
}
|
||||
|
||||
return toJsonFormatted(dmlFile)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new DML object for a node.
|
||||
*
|
||||
* @param param0 - The node and data model's details
|
||||
* @returns The DML object.
|
||||
*/
|
||||
getNewDmlObject({
|
||||
node,
|
||||
dataModelName,
|
||||
}: {
|
||||
node: ts.CallExpression
|
||||
dataModelName: string
|
||||
}): DmlObject {
|
||||
return this.getPropertiesFromNode(node, dataModelName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing dml object by removing properties no longer
|
||||
* available and adding new ones.
|
||||
*
|
||||
* @param param0 - The node and data model's details.
|
||||
* @returns The updated DML object.
|
||||
*/
|
||||
updateExistingDmlObject({
|
||||
node,
|
||||
dmlObject,
|
||||
dataModelName,
|
||||
}: {
|
||||
node: ts.CallExpression
|
||||
dmlObject: DmlObject
|
||||
dataModelName: string
|
||||
}): DmlObject {
|
||||
const newDmlObject = Object.assign({}, dmlObject)
|
||||
const newProperties = this.getPropertiesFromNode(node, dataModelName)
|
||||
|
||||
const newPropertyNames = Object.keys(newProperties)
|
||||
const oldPropertyNames = Object.keys(newDmlObject)
|
||||
|
||||
// delete properties not available anymore
|
||||
oldPropertyNames.forEach((oldPropertyName) => {
|
||||
if (!newPropertyNames.includes(oldPropertyName)) {
|
||||
delete newDmlObject[oldPropertyName]
|
||||
}
|
||||
})
|
||||
|
||||
// add new properties
|
||||
newPropertyNames.forEach((newPropertyName) => {
|
||||
if (oldPropertyNames.includes(newPropertyName)) {
|
||||
return
|
||||
}
|
||||
|
||||
newDmlObject[newPropertyName] = newProperties[newPropertyName]
|
||||
})
|
||||
|
||||
return newDmlObject
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data model's name. It's either a string passed to `model.define`, or
|
||||
* in the `name` property of the object passed as a parameter to `model.define`.
|
||||
*
|
||||
* @param node - The node to retrieve its data model name.
|
||||
* @returns The data model's name.
|
||||
*/
|
||||
getDataModelName(node: ts.Node): string {
|
||||
let name = node.getText()
|
||||
if (ts.isObjectLiteralExpression(node)) {
|
||||
const nameProperty = node.properties.find((propertyNode) => {
|
||||
return (
|
||||
propertyNode.name?.getText() === "name" &&
|
||||
"initializer" in propertyNode
|
||||
)
|
||||
}) as ts.ObjectLiteralElementLike & {
|
||||
initializer: ts.Node
|
||||
}
|
||||
|
||||
if (nameProperty) {
|
||||
name = nameProperty.initializer.getText()
|
||||
}
|
||||
}
|
||||
|
||||
return snakeToPascal(name.replace(/^"/, "").replace(/"$/, ""))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the properties of a node.
|
||||
*
|
||||
* @param node - The node to get its properties.
|
||||
* @param dataModelName - The data model's name.
|
||||
* @returns The properties and their description.
|
||||
*/
|
||||
getPropertiesFromNode(
|
||||
node: ts.CallExpression,
|
||||
dataModelName: string
|
||||
): DmlObject {
|
||||
const formattedDataModelName = this.formatDataModelName(dataModelName)
|
||||
const propertyNodes =
|
||||
"properties" in node.arguments[1]
|
||||
? (node.arguments[1].properties as ts.Node[])
|
||||
: []
|
||||
|
||||
const properties: DmlObject = {}
|
||||
|
||||
propertyNodes.forEach((propertyNode) => {
|
||||
const propertyName =
|
||||
"name" in propertyNode
|
||||
? (propertyNode.name as ts.Identifier).getText()
|
||||
: propertyNode.getText()
|
||||
const propertyType = this.checker.getTypeAtLocation(propertyNode)
|
||||
const propertyTypeStr = this.checker.typeToString(propertyType)
|
||||
|
||||
const isRelation = RELATION_NAMES.some((relationName) =>
|
||||
propertyTypeStr.includes(relationName)
|
||||
)
|
||||
const isBoolean = propertyTypeStr.includes("BooleanProperty")
|
||||
const relationName = isRelation ? camelToWords(propertyName) : undefined
|
||||
|
||||
let propertyDescription =
|
||||
this.knowledgeBaseFactory.tryToGetObjectPropertySummary({
|
||||
retrieveOptions: {
|
||||
str: propertyName,
|
||||
kind: propertyNode.kind,
|
||||
templateOptions: {
|
||||
parentName: formattedDataModelName,
|
||||
rawParentName: dataModelName,
|
||||
},
|
||||
},
|
||||
propertyDetails: {
|
||||
isClassOrInterface: isRelation,
|
||||
isBoolean,
|
||||
classOrInterfaceName: relationName,
|
||||
},
|
||||
})
|
||||
|
||||
if (isRelation) {
|
||||
propertyDescription += `\n\n@expandable`
|
||||
}
|
||||
|
||||
properties[propertyName] = propertyDescription
|
||||
})
|
||||
|
||||
return properties
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the output JSON file associated with a node.
|
||||
*
|
||||
* @param node - The node to get its file name.
|
||||
* @returns The file name.
|
||||
*/
|
||||
getAssociatedFileName(node: ts.Node): string {
|
||||
const filePath = getBasePath(node.getSourceFile().fileName).split("/")
|
||||
// since modules are at packages/modules/<name>, the name should be at index 2
|
||||
const moduleName = filePath[2]
|
||||
|
||||
return path.join(getDmlOutputBasePath(), `${moduleName}.json`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the data model name for presentation purposes.
|
||||
*
|
||||
* @param name - The raw data model's name.
|
||||
* @returns The formatted name.
|
||||
*/
|
||||
formatDataModelName(name: string): string {
|
||||
return camelToWords(name)
|
||||
}
|
||||
}
|
||||
|
||||
export default DmlKindGenerator
|
||||
@@ -0,0 +1,98 @@
|
||||
import ts from "typescript"
|
||||
import DefaultKindGenerator, { GetDocBlockOptions } from "./default.js"
|
||||
import { DOCBLOCK_END_LINE, DOCBLOCK_START } from "../../constants.js"
|
||||
import { camelToWords } from "utils"
|
||||
import { normalizeName } from "../../utils/str-formatting.js"
|
||||
|
||||
/**
|
||||
* A class that generates doc blocks for properties in a DTO interface/type.
|
||||
*/
|
||||
class DTOPropertyGenerator extends DefaultKindGenerator<ts.PropertySignature> {
|
||||
protected allowedKinds: ts.SyntaxKind[] = [ts.SyntaxKind.PropertySignature]
|
||||
public name = "dto-property"
|
||||
|
||||
/**
|
||||
* Check that the generator can handle generating for the node.
|
||||
*
|
||||
* @param {ts.Node} node - The node to check.
|
||||
* @returns {boolean} Whether the generator can handle generating for the node.
|
||||
*/
|
||||
isAllowed(node: ts.Node): node is ts.PropertySignature {
|
||||
if (!super.isAllowed(node)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
this.getParentName((node as ts.PropertySignature).parent).endsWith(
|
||||
"DTO"
|
||||
) || false
|
||||
)
|
||||
}
|
||||
|
||||
async getDocBlock(
|
||||
node: ts.PropertyDeclaration | ts.Node,
|
||||
options?: GetDocBlockOptions
|
||||
): Promise<string> {
|
||||
if (!this.isAllowed(node)) {
|
||||
return await super.getDocBlock(node, options)
|
||||
}
|
||||
|
||||
let str = DOCBLOCK_START
|
||||
const rawParentName = this.getParentName(node.parent)
|
||||
const parentName = this.formatInterfaceName(rawParentName)
|
||||
// check if the property's type is interface/type/class
|
||||
const propertyType = this.checker.getTypeAtLocation(node)
|
||||
const isPropertyClassOrInterface = propertyType.isClassOrInterface()
|
||||
|
||||
// try first to retrieve the summary from the knowledge base if it exists.
|
||||
const summary = this.knowledgeBaseFactory.tryToGetObjectPropertySummary({
|
||||
retrieveOptions: {
|
||||
str: node.name.getText(),
|
||||
kind: node.kind,
|
||||
templateOptions: {
|
||||
rawParentName,
|
||||
parentName,
|
||||
},
|
||||
},
|
||||
propertyDetails: {
|
||||
isClassOrInterface: isPropertyClassOrInterface,
|
||||
isBoolean:
|
||||
"intrinsicName" in propertyType &&
|
||||
propertyType.intrinsicName === "boolean",
|
||||
classOrInterfaceName: isPropertyClassOrInterface
|
||||
? this.formatInterfaceName(this.checker.typeToString(propertyType))
|
||||
: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
str += summary
|
||||
|
||||
return `${str}${DOCBLOCK_END_LINE}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the name of the interface/type.
|
||||
*
|
||||
* @param {string} name - The name to format.
|
||||
* @returns {string} The formatted name.
|
||||
*/
|
||||
formatInterfaceName(name: string): string {
|
||||
return camelToWords(normalizeName(name))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the parent interface/type.
|
||||
*
|
||||
* @param {ts.InterfaceDeclaration | ts.TypeLiteralNode} parent - The parent node.
|
||||
* @returns {string} The name of the parent.
|
||||
*/
|
||||
getParentName(parent: ts.InterfaceDeclaration | ts.TypeLiteralNode): string {
|
||||
if (ts.isInterfaceDeclaration(parent)) {
|
||||
return parent.name.getText()
|
||||
}
|
||||
|
||||
return this.checker.typeToString(this.checker.getTypeFromTypeNode(parent))
|
||||
}
|
||||
}
|
||||
|
||||
export default DTOPropertyGenerator
|
||||
@@ -0,0 +1,386 @@
|
||||
import ts from "typescript"
|
||||
import DefaultKindGenerator, { GetDocBlockOptions } from "./default.js"
|
||||
import {
|
||||
DOCBLOCK_NEW_LINE,
|
||||
DOCBLOCK_END_LINE,
|
||||
DOCBLOCK_START,
|
||||
DOCBLOCK_DOUBLE_LINES,
|
||||
SUMMARY_PLACEHOLDER,
|
||||
} from "../../constants.js"
|
||||
import getSymbol from "../../utils/get-symbol.js"
|
||||
import AiGenerator from "../helpers/ai-generator.js"
|
||||
import path from "path"
|
||||
|
||||
export type FunctionNode =
|
||||
| ts.MethodDeclaration
|
||||
| ts.MethodSignature
|
||||
| ts.FunctionDeclaration
|
||||
| ts.ArrowFunction
|
||||
|
||||
export type VariableNode = ts.VariableDeclaration | ts.VariableStatement
|
||||
|
||||
export type FunctionOrVariableNode = FunctionNode | ts.VariableStatement
|
||||
|
||||
/**
|
||||
* Docblock generator for functions.
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode> {
|
||||
protected methodKinds: ts.SyntaxKind[] = [
|
||||
ts.SyntaxKind.MethodDeclaration,
|
||||
ts.SyntaxKind.MethodSignature,
|
||||
]
|
||||
protected functionKinds: ts.SyntaxKind[] = [ts.SyntaxKind.FunctionDeclaration]
|
||||
protected allowedKinds: ts.SyntaxKind[] = [
|
||||
...this.methodKinds,
|
||||
...this.functionKinds,
|
||||
]
|
||||
public name = "function"
|
||||
static EXAMPLE_PLACEHOLDER = `{example-code}`
|
||||
protected aiParameterExceptions = ["sharedContext"]
|
||||
|
||||
/**
|
||||
* Checks whether a node is considered a function node. A node is considered a function node if:
|
||||
*
|
||||
* 1. It is a method declaration (typically in classes), a method signature (typically in interfaces), or a function declaration.
|
||||
* 2. An arrow function. However, for better docblock placement and formatting, we detect the variable statement surrounding the arrow function
|
||||
* rather than the arrow function itself.
|
||||
*
|
||||
* @param {ts.Node} node - The node to check.
|
||||
* @returns {boolean} Whether the node is a function node and can be handled by this generator.
|
||||
*/
|
||||
isAllowed(node: ts.Node): node is FunctionOrVariableNode {
|
||||
if (!super.isAllowed(node)) {
|
||||
return ts.isVariableStatement(node) && this.isFunctionVariable(node)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a node is a variable statement/declaration with underlying node function
|
||||
* using the {@link extractFunctionNode} method.
|
||||
*
|
||||
* @param {ts.Node} node - The node to check.
|
||||
* @returns {boolean} Whether the node is a variable statement/declaration with underlying node function.
|
||||
*/
|
||||
isFunctionVariable(node: ts.Node): node is VariableNode {
|
||||
if (ts.isVariableStatement(node)) {
|
||||
return node.declarationList.declarations.some((declaration) => {
|
||||
return this.isFunctionVariable(declaration)
|
||||
})
|
||||
} else if (ts.isVariableDeclaration(node)) {
|
||||
return this.extractFunctionNode(node) !== undefined
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the underlying function/method/arrow function of a variable statement or declaration.
|
||||
*
|
||||
* @param {ts.Node} node - The variable statement/declaration to retrieve the function/method from.
|
||||
* @returns The function/method if found.
|
||||
*/
|
||||
extractFunctionNode(node: VariableNode): FunctionNode | undefined {
|
||||
if (ts.isVariableStatement(node)) {
|
||||
const variableDeclaration = node.declarationList.declarations.find(
|
||||
(declaration) => ts.isVariableDeclaration(declaration)
|
||||
)
|
||||
|
||||
return variableDeclaration
|
||||
? this.extractFunctionNode(variableDeclaration)
|
||||
: undefined
|
||||
} else if (
|
||||
node.initializer &&
|
||||
(this.isAllowed(node.initializer) || ts.isArrowFunction(node.initializer))
|
||||
) {
|
||||
return node.initializer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a node refers to a method.
|
||||
*
|
||||
* @param {FunctionNode} node - The node to check.
|
||||
* @returns {boolean} Whether the node is a method.
|
||||
*/
|
||||
isMethod(
|
||||
node: FunctionNode
|
||||
): node is ts.MethodDeclaration | ts.MethodSignature {
|
||||
return this.methodKinds.includes(node.kind)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a type, typically the type of a function's signature, has return data.
|
||||
*
|
||||
* @param {string} typeStr - The type's string representation.
|
||||
* @returns {boolean} Whether the type has return data.
|
||||
*/
|
||||
hasReturnData(typeStr: string): boolean {
|
||||
return (
|
||||
typeStr !== "void" &&
|
||||
typeStr !== "never" &&
|
||||
typeStr !== "Promise<void>" &&
|
||||
typeStr !== "Promise<never>"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the return type of a function.
|
||||
*
|
||||
* @param {FunctionNode} node - The function's node.
|
||||
* @returns {ts.Type} The function's return type.
|
||||
*/
|
||||
getReturnType(node: FunctionNode): ts.Type {
|
||||
return node.type
|
||||
? this.checker.getTypeFromTypeNode(node.type)
|
||||
: this.checker.getTypeAtLocation(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the summary comment of a function.
|
||||
*
|
||||
* @param {FunctionNode} node - The function's options.
|
||||
* @returns {string} The function's summary comment.
|
||||
*/
|
||||
getFunctionSummary({
|
||||
node,
|
||||
symbol,
|
||||
parentSymbol,
|
||||
returnType,
|
||||
}: {
|
||||
/**
|
||||
* The node's function.
|
||||
*/
|
||||
node: FunctionNode
|
||||
/**
|
||||
* The node's symbol. If provided, the method will try to retrieve the summary from the {@link KnowledgeBaseFactory}.
|
||||
*/
|
||||
symbol?: ts.Symbol
|
||||
/**
|
||||
* The node's parent symbol. This is useful to pass along the parent name to the knowledge base.
|
||||
*/
|
||||
parentSymbol?: ts.Symbol
|
||||
/**
|
||||
* The node's return type. Useful for the {@link KnowledgeBaseFactory}
|
||||
*/
|
||||
returnType?: string
|
||||
}): string {
|
||||
return symbol
|
||||
? this.knowledgeBaseFactory.tryToGetFunctionSummary({
|
||||
symbol: symbol,
|
||||
kind: node.kind,
|
||||
templateOptions: {
|
||||
rawParentName: parentSymbol?.getName(),
|
||||
pluralIndicatorStr: returnType,
|
||||
},
|
||||
}) || this.getNodeSummary({ node, symbol })
|
||||
: this.getNodeSummary({ node, symbol })
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the function's example comment.
|
||||
*
|
||||
* @param {ts.Symbol} symbol - The function's symbol. If provided, the method will try to retrieve the example from the {@link KnowledgeBaseFactory}.
|
||||
* @returns {string} The function's example comment.
|
||||
*/
|
||||
getFunctionPlaceholderExample(): string {
|
||||
return this.formatExample(FunctionKindGenerator.EXAMPLE_PLACEHOLDER)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a function's example using the AiGenerator
|
||||
*
|
||||
* @param node - The function's node.
|
||||
* @param aiGenerator - An instance of the AiGenerator
|
||||
* @returns the example code
|
||||
*/
|
||||
async getFunctionExampleAi(
|
||||
node: FunctionOrVariableNode,
|
||||
aiGenerator: AiGenerator,
|
||||
withTag = true
|
||||
): Promise<string> {
|
||||
const actualNode = ts.isVariableStatement(node)
|
||||
? this.extractFunctionNode(node)
|
||||
: node
|
||||
|
||||
if (!actualNode) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const symbol = getSymbol(node, this.checker)
|
||||
|
||||
const example = await aiGenerator.generateExample({
|
||||
className: this.isMethod(actualNode)
|
||||
? getSymbol(node.parent, this.checker)?.name
|
||||
: undefined,
|
||||
functionName: symbol?.name || "",
|
||||
signature: node.getText(),
|
||||
fileName: path.basename(node.getSourceFile().fileName),
|
||||
})
|
||||
|
||||
return this.formatExample(
|
||||
example.length
|
||||
? `${example}${DOCBLOCK_NEW_LINE}`
|
||||
: FunctionKindGenerator.EXAMPLE_PLACEHOLDER,
|
||||
withTag
|
||||
)
|
||||
}
|
||||
|
||||
formatExample(example: string, withTag = true): string {
|
||||
return `${
|
||||
withTag ? `${DOCBLOCK_DOUBLE_LINES}@example${DOCBLOCK_NEW_LINE}` : ""
|
||||
}${example}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the full docblock of a function.
|
||||
*
|
||||
* @param {FunctionOrVariableNode | ts.Node} node - The function node. If a variable statement is provided, the underlying function is retrieved.
|
||||
* If a different node type is provided, the parent generator is used to retrieve the docblock comment.
|
||||
* @param {GetDocBlockOptions} options - Formatting options.
|
||||
* @returns {string} The function's docblock.
|
||||
*/
|
||||
async getDocBlock(
|
||||
node: FunctionOrVariableNode | ts.Node,
|
||||
options: GetDocBlockOptions = { addEnd: true }
|
||||
): Promise<string> {
|
||||
if (!this.isAllowed(node)) {
|
||||
return await super.getDocBlock(node, options)
|
||||
}
|
||||
|
||||
const actualNode = ts.isVariableStatement(node)
|
||||
? this.extractFunctionNode(node)
|
||||
: node
|
||||
|
||||
if (!actualNode) {
|
||||
return await super.getDocBlock(node, options)
|
||||
}
|
||||
|
||||
let existingComments = this.getNodeCommentsFromRange(node)
|
||||
|
||||
if (existingComments?.includes(FunctionKindGenerator.EXAMPLE_PLACEHOLDER)) {
|
||||
// just replace the existing comment and return it
|
||||
if (options.aiGenerator) {
|
||||
existingComments = existingComments.replace(
|
||||
FunctionKindGenerator.EXAMPLE_PLACEHOLDER,
|
||||
await this.getFunctionExampleAi(
|
||||
actualNode,
|
||||
options.aiGenerator,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return existingComments.replace("/*", "").replace("*/", "")
|
||||
}
|
||||
|
||||
const nodeSymbol = getSymbol(node, this.checker)
|
||||
const nodeParentSymbol = getSymbol(node.parent, this.checker)
|
||||
const nodeType = this.getReturnType(actualNode)
|
||||
const returnTypeStr = this.checker.typeToString(nodeType)
|
||||
const normalizedTypeStr = returnTypeStr.startsWith("Promise<")
|
||||
? returnTypeStr.replace(/^Promise</, "").replace(/>$/, "")
|
||||
: returnTypeStr
|
||||
|
||||
let str = DOCBLOCK_START
|
||||
|
||||
// add summary
|
||||
str += `${
|
||||
options.summaryPrefix ||
|
||||
(this.isMethod(actualNode) ? `This method` : `This function`)
|
||||
} ${this.getFunctionSummary({
|
||||
node: actualNode,
|
||||
symbol: nodeSymbol,
|
||||
parentSymbol: nodeParentSymbol,
|
||||
returnType: normalizedTypeStr,
|
||||
})}${DOCBLOCK_NEW_LINE}`
|
||||
|
||||
actualNode.parameters.map((parameterNode) => {
|
||||
const symbol = getSymbol(parameterNode, this.checker)
|
||||
if (!symbol) {
|
||||
return
|
||||
}
|
||||
|
||||
const symbolType = this.checker.getTypeOfSymbolAtLocation(
|
||||
symbol,
|
||||
parameterNode
|
||||
)
|
||||
|
||||
const parameterName = symbol.getName()
|
||||
const parameterSummary = this.getNodeSummary({
|
||||
node: parameterNode,
|
||||
symbol,
|
||||
nodeType: symbolType,
|
||||
knowledgeBaseOptions: {
|
||||
templateOptions: {
|
||||
rawParentName: nodeParentSymbol?.getName(),
|
||||
pluralIndicatorStr: this.checker.typeToString(symbolType),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
str += `${DOCBLOCK_NEW_LINE}@param {${this.checker.typeToString(
|
||||
symbolType
|
||||
)}} ${parameterName} - ${parameterSummary}`
|
||||
})
|
||||
|
||||
// add returns
|
||||
const possibleReturnSummary = !this.hasReturnData(returnTypeStr)
|
||||
? `Resolves when ${SUMMARY_PLACEHOLDER}`
|
||||
: this.getNodeSummary({
|
||||
node: actualNode,
|
||||
nodeType,
|
||||
})
|
||||
|
||||
str += `${DOCBLOCK_NEW_LINE}@returns {${returnTypeStr}} ${
|
||||
nodeSymbol
|
||||
? this.knowledgeBaseFactory.tryToGetFunctionReturns({
|
||||
symbol: nodeSymbol,
|
||||
kind: actualNode.kind,
|
||||
templateOptions: {
|
||||
rawParentName: nodeParentSymbol?.getName(),
|
||||
pluralIndicatorStr: normalizedTypeStr,
|
||||
},
|
||||
}) || possibleReturnSummary
|
||||
: possibleReturnSummary
|
||||
}`
|
||||
|
||||
// add example
|
||||
if (!options.aiGenerator) {
|
||||
str += this.getFunctionPlaceholderExample()
|
||||
} else {
|
||||
str += await this.getFunctionExampleAi(actualNode, options.aiGenerator)
|
||||
}
|
||||
|
||||
// add common docs
|
||||
str += this.getCommonDocs(node, {
|
||||
prefixWithLineBreaks: true,
|
||||
})
|
||||
|
||||
if (options.addEnd) {
|
||||
str += DOCBLOCK_END_LINE
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows documenting (updating) a node if it has the example placeholder.
|
||||
*
|
||||
* @param node - The node to document.
|
||||
* @returns Whether the node can be documented.
|
||||
*/
|
||||
canDocumentNode(node: ts.Node): boolean {
|
||||
const comments = this.getNodeCommentsFromRange(node)
|
||||
|
||||
return (
|
||||
!comments ||
|
||||
comments?.includes(FunctionKindGenerator.EXAMPLE_PLACEHOLDER) ||
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default FunctionKindGenerator
|
||||
@@ -0,0 +1,202 @@
|
||||
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 {
|
||||
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 {
|
||||
public name = "medusa-react"
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
async getDocBlock(
|
||||
node: FunctionNode & ts.VariableDeclaration
|
||||
): Promise<string> {
|
||||
// TODO use the AiGenerator to generate summary + examples
|
||||
if (!this.isAllowed(node)) {
|
||||
return await super.getDocBlock(node)
|
||||
}
|
||||
|
||||
const actualNode = ts.isVariableStatement(node)
|
||||
? this.extractFunctionNode(node)
|
||||
: node
|
||||
|
||||
if (!actualNode) {
|
||||
return await super.getDocBlock(node)
|
||||
}
|
||||
const isMutation = this.isMutation(actualNode)
|
||||
|
||||
let str = `${DOCBLOCK_START}This hook ${this.getFunctionSummary({
|
||||
node,
|
||||
})}`
|
||||
|
||||
// add example
|
||||
str += this.getFunctionPlaceholderExample()
|
||||
|
||||
// loop over parameters that aren't query/mutation parameters
|
||||
// and add docblock to them
|
||||
await Promise.all(
|
||||
this.getActualParameters(actualNode).map(async (parameter) => {
|
||||
ts.addSyntheticLeadingComment(
|
||||
parameter,
|
||||
ts.SyntaxKind.MultiLineCommentTrivia,
|
||||
await 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") &&
|
||||
!this.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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
import ts from "typescript"
|
||||
import FunctionKindGenerator from "./function.js"
|
||||
import DefaultKindGenerator, { GeneratorOptions } from "./default.js"
|
||||
import MedusaReactHooksKindGenerator from "./medusa-react-hooks.js"
|
||||
import SourceFileKindGenerator from "./source-file.js"
|
||||
import DTOPropertyGenerator from "./dto-property.js"
|
||||
import OasKindGenerator from "./oas.js"
|
||||
import DmlKindGenerator from "./dml.js"
|
||||
|
||||
/**
|
||||
* A class that is used as a registry for the kind generators.
|
||||
*/
|
||||
class KindsRegistry {
|
||||
protected kindInstances: DefaultKindGenerator[]
|
||||
protected defaultKindGenerator: DefaultKindGenerator
|
||||
|
||||
constructor(
|
||||
options: Pick<
|
||||
GeneratorOptions,
|
||||
"checker" | "generatorEventManager" | "additionalOptions"
|
||||
>
|
||||
) {
|
||||
this.kindInstances = [
|
||||
new DmlKindGenerator(options),
|
||||
new OasKindGenerator(options),
|
||||
new MedusaReactHooksKindGenerator(options),
|
||||
new FunctionKindGenerator(options),
|
||||
new SourceFileKindGenerator(options),
|
||||
new DTOPropertyGenerator(options),
|
||||
]
|
||||
this.defaultKindGenerator = new DefaultKindGenerator(options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the generator for a node based on its kind, if any.
|
||||
*
|
||||
* @param {ts.Node} node - The node to retrieve its docblock generator.
|
||||
* @returns {DefaultKindGenerator | undefined} The generator that can handle the node's kind, if any.
|
||||
*/
|
||||
getKindGenerator(node: ts.Node): DefaultKindGenerator | undefined {
|
||||
return (
|
||||
this.kindInstances.find((generator) => generator.isAllowed(node)) ||
|
||||
(this.defaultKindGenerator.isAllowed(node)
|
||||
? this.defaultKindGenerator
|
||||
: undefined)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a node has a kind generator.
|
||||
*
|
||||
* @param {ts.Node} node - The node to check for.
|
||||
* @returns {boolean} Whether the node has a kind generator.
|
||||
*/
|
||||
hasGenerator(node: ts.Node): boolean {
|
||||
return this.getKindGenerator(node) !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a generator by its name attribute.
|
||||
*
|
||||
* @param name - The name of the generator to retrieve.
|
||||
* @returns The generator, if exists.
|
||||
*/
|
||||
getKindGeneratorByName(name: string): DefaultKindGenerator | undefined {
|
||||
return this.kindInstances.find((generator) => generator.name === name)
|
||||
}
|
||||
}
|
||||
|
||||
export default KindsRegistry
|
||||
@@ -0,0 +1,38 @@
|
||||
import ts from "typescript"
|
||||
import DefaultKindGenerator, { GetDocBlockOptions } from "./default.js"
|
||||
import { DOCBLOCK_END_LINE, DOCBLOCK_START } from "../../constants.js"
|
||||
import { shouldHaveCustomNamespace } from "../../utils/medusa-react-utils.js"
|
||||
|
||||
/**
|
||||
* A generator used to retrieve doc blocks for a source file.
|
||||
*/
|
||||
class SourceFileKindGenerator extends DefaultKindGenerator<ts.SourceFile> {
|
||||
protected allowedKinds: ts.SyntaxKind[] = [ts.SyntaxKind.SourceFile]
|
||||
public name = "source-file"
|
||||
|
||||
/**
|
||||
* Retrieve the docblock of a source file.
|
||||
*
|
||||
* @param {ts.SourceFile | ts.Node} node - The node to retrieve its docblocks.
|
||||
* @param {GetDocBlockOptions} options - The formatting options.
|
||||
* @returns {string} The node's docblock.
|
||||
*/
|
||||
async getDocBlock(
|
||||
node: ts.SourceFile | ts.Node,
|
||||
options?: GetDocBlockOptions
|
||||
): Promise<string> {
|
||||
if (!this.isAllowed(node)) {
|
||||
return await super.getDocBlock(node, options)
|
||||
}
|
||||
|
||||
if (shouldHaveCustomNamespace(node)) {
|
||||
return `${DOCBLOCK_START}${this.getCommonDocs(node, {
|
||||
addDefaultSummary: true,
|
||||
})}${DOCBLOCK_END_LINE}`
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export default SourceFileKindGenerator
|
||||
Reference in New Issue
Block a user