docs-util: add docblock generator for models built with DML (#8296)
* docs-util: add docblock generator for models built with DML * add missing turbo task
This commit is contained in:
52
.github/workflows/generate-docblocks.yml
vendored
52
.github/workflows/generate-docblocks.yml
vendored
@@ -116,3 +116,55 @@ jobs:
|
||||
branch: "chore/generate-oas"
|
||||
branch-suffix: "timestamp"
|
||||
add-paths: www/utils/generated/oas-output/**
|
||||
generate-dml:
|
||||
name: Generated DML JSON files PR
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js 20
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Install www/utils Dependencies
|
||||
run: yarn
|
||||
working-directory: www/utils
|
||||
|
||||
- name: Build packages
|
||||
run: yarn build
|
||||
working-directory: www/utils
|
||||
|
||||
- name: Check Commit
|
||||
id: check-commit
|
||||
run: "yarn check:release-commit ${{ github.sha }}"
|
||||
working-directory: www/utils/packages/scripts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GIT_OWNER: ${{ github.repository_owner }}
|
||||
GIT_REPO: medusa
|
||||
|
||||
- name: Run docblock generator
|
||||
if: steps.check-commit.outputs.is_release_commit == 'true'
|
||||
run: "yarn generate:dml"
|
||||
working-directory: www/utils
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GIT_OWNER: ${{ github.repository_owner }}
|
||||
GIT_REPO: medusa
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.check-commit.outputs.is_release_commit == 'true'
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
title: "chore(docs): Generated DML JSON files"
|
||||
body: "This PR holds all generated DML JSON files for the upcoming release."
|
||||
branch: "chore/generate-dml-json"
|
||||
branch-suffix: "timestamp"
|
||||
add-paths: www/utils/generated/dml-output/**
|
||||
|
||||
@@ -64,4 +64,56 @@ jobs:
|
||||
www/apps/api-reference/specs
|
||||
www/utils/generated/oas-output
|
||||
branch: "docs/generate-api-ref"
|
||||
branch-suffix: "timestamp"
|
||||
branch-suffix: "timestamp"
|
||||
preview-dml:
|
||||
name: Generate DML JSON files
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/cache-deps
|
||||
with:
|
||||
extension: reference
|
||||
|
||||
- name: Build Packages
|
||||
run: yarn build
|
||||
|
||||
- name: Install www/utils Dependencies
|
||||
run: yarn
|
||||
working-directory: www/utils
|
||||
|
||||
- name: Build www/utils packages
|
||||
run: yarn build
|
||||
working-directory: www/utils
|
||||
|
||||
- name: Run docblock generator
|
||||
run: "yarn generate:dml"
|
||||
working-directory: www/utils
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GIT_OWNER: ${{ github.repository_owner }}
|
||||
GIT_REPO: medusa
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
title: "chore(docs): Generated DML JSON files"
|
||||
body: "This PR holds all generated DML JSON files for the upcoming release."
|
||||
branch: "chore/generate-dml-json"
|
||||
branch-suffix: "timestamp"
|
||||
add-paths: www/utils/generated/dml-output/**
|
||||
@@ -9,7 +9,8 @@
|
||||
"watch": "turbo run watch",
|
||||
"lint": "turbo run lint",
|
||||
"generate:references": "turbo run generate:references",
|
||||
"generate:oas": "turbo run generate:oas"
|
||||
"generate:oas": "turbo run generate:oas",
|
||||
"generate:dml": "turbo run generate:dml"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.23.0",
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch",
|
||||
"prepublishOnly": "cross-env NODE_ENV=production tsc --build",
|
||||
"generate:oas": "yarn start run ../../../../packages/medusa/src/api --type oas && yarn start clean:oas"
|
||||
"generate:oas": "yarn start run ../../../../packages/medusa/src/api --type oas && yarn start clean:oas",
|
||||
"generate:dml": "yarn start run ../../../../packages/modules --type dml && yarn start clean:dml"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import ts from "typescript"
|
||||
import DmlKindGenerator from "../kinds/dml.js"
|
||||
import AbstractGenerator from "./index.js"
|
||||
import { GeneratorEvent } from "../helpers/generator-event-manager.js"
|
||||
import { minimatch } from "minimatch"
|
||||
import getBasePath from "../../utils/get-base-path.js"
|
||||
import toJsonFormatted from "../../utils/to-json-formatted.js"
|
||||
import { DmlFile } from "../../types/index.js"
|
||||
|
||||
/**
|
||||
* A class used to generate DML JSON files with descriptions of properties.
|
||||
*/
|
||||
class DmlGenerator extends AbstractGenerator {
|
||||
protected dmlKindGenerator?: DmlKindGenerator
|
||||
|
||||
async run() {
|
||||
this.init()
|
||||
|
||||
this.dmlKindGenerator = new DmlKindGenerator({
|
||||
checker: this.checker!,
|
||||
generatorEventManager: this.generatorEventManager,
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
this.program!.getSourceFiles().map(async (file) => {
|
||||
// Ignore .d.ts files
|
||||
if (file.isDeclarationFile || !this.isFileIncluded(file.fileName)) {
|
||||
return
|
||||
}
|
||||
const fileNodes: ts.Node[] = [file]
|
||||
|
||||
console.log(`[DML] Generating for ${file.fileName}...`)
|
||||
|
||||
// since typescript's compiler API doesn't support
|
||||
// async processes, we have to retrieve the nodes first then
|
||||
// traverse them separately.
|
||||
const pushNodesToArr = (node: ts.Node) => {
|
||||
fileNodes.push(node)
|
||||
|
||||
ts.forEachChild(node, pushNodesToArr)
|
||||
}
|
||||
ts.forEachChild(file, pushNodesToArr)
|
||||
|
||||
const documentChild = async (node: ts.Node) => {
|
||||
if (
|
||||
this.dmlKindGenerator!.isAllowed(node) &&
|
||||
this.dmlKindGenerator!.canDocumentNode(node)
|
||||
) {
|
||||
const dmlJson = await this.dmlKindGenerator!.getDocBlock(node)
|
||||
|
||||
if (!this.options.dryRun) {
|
||||
const filePath =
|
||||
this.dmlKindGenerator!.getAssociatedFileName(node)
|
||||
this.writeJson(filePath, dmlJson)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
fileNodes.map(async (node) => await documentChild(node))
|
||||
)
|
||||
|
||||
this.generatorEventManager.emit(GeneratorEvent.FINISHED_GENERATE_EVENT)
|
||||
console.log(`[OAS] Finished generating OAS for ${file.fileName}.`)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the specified file path is included in the program
|
||||
* and is an API file.
|
||||
*
|
||||
* @param fileName - The file path to check
|
||||
* @returns Whether the OAS generator can run on this file.
|
||||
*/
|
||||
isFileIncluded(fileName: string): boolean {
|
||||
return (
|
||||
super.isFileIncluded(fileName) &&
|
||||
minimatch(getBasePath(fileName), "packages/modules/**/models/**", {
|
||||
matchBase: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method writes the DML JSON file. If the file already exists, it only updates
|
||||
* the data model's object in the JSON file.
|
||||
*
|
||||
* @param filePath - The path of the file to write the DML JSON to.
|
||||
* @param dataModelJson - The DML JSON.
|
||||
*/
|
||||
writeJson(filePath: string, dataModelJson: string) {
|
||||
let moduleJson = ts.sys.readFile(filePath)
|
||||
if (!moduleJson) {
|
||||
moduleJson = dataModelJson
|
||||
} else {
|
||||
// parse the JSON and replace the data model's JSON
|
||||
// with the new one
|
||||
const parsedModuleJson = JSON.parse(moduleJson) as DmlFile
|
||||
const parsedDataModelJson = JSON.parse(dataModelJson) as DmlFile
|
||||
const dataModelName = Object.keys(parsedDataModelJson)[0]
|
||||
parsedModuleJson[dataModelName] = parsedDataModelJson[dataModelName]
|
||||
|
||||
moduleJson = toJsonFormatted(parsedModuleJson)
|
||||
}
|
||||
|
||||
ts.sys.writeFile(filePath, moduleJson)
|
||||
}
|
||||
}
|
||||
|
||||
export default DmlGenerator
|
||||
@@ -4,6 +4,7 @@ import { GeneratorEvent } from "../helpers/generator-event-manager.js"
|
||||
import AbstractGenerator from "./index.js"
|
||||
import { minimatch } from "minimatch"
|
||||
import AiGenerator from "../helpers/ai-generator.js"
|
||||
import getBasePath from "../../utils/get-base-path.js"
|
||||
|
||||
/**
|
||||
* A class used to generate docblock for one or multiple file paths.
|
||||
@@ -164,7 +165,7 @@ class DocblockGenerator extends AbstractGenerator {
|
||||
isFileIncluded(fileName: string): boolean {
|
||||
return (
|
||||
super.isFileIncluded(fileName) &&
|
||||
!minimatch(this.getBasePath(fileName), "packages/medusa/**/api**/**", {
|
||||
!minimatch(getBasePath(fileName), "packages/medusa/**/api**/**", {
|
||||
matchBase: true,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import GeneratorEventManager from "../helpers/generator-event-manager.js"
|
||||
import { CommonCliOptions } from "../../types/index.js"
|
||||
import { existsSync, readdirSync, statSync } from "node:fs"
|
||||
import path from "node:path"
|
||||
import getBasePath from "../../utils/get-base-path.js"
|
||||
|
||||
export type Options = {
|
||||
paths: string[]
|
||||
@@ -76,28 +77,12 @@ abstract class AbstractGenerator {
|
||||
* @returns {boolean} Whether the file can have docblocks generated for it.
|
||||
*/
|
||||
isFileIncluded(fileName: string): boolean {
|
||||
const baseFilePath = this.getBasePath(fileName)
|
||||
const baseFilePath = getBasePath(fileName)
|
||||
return this.options.paths.some((path) =>
|
||||
baseFilePath.startsWith(this.getBasePath(path))
|
||||
baseFilePath.startsWith(getBasePath(path))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the pathname of a file without the relative part before `packages/`
|
||||
*
|
||||
* @param fileName - The file name/path
|
||||
* @returns The path without the relative part.
|
||||
*/
|
||||
getBasePath(fileName: string) {
|
||||
let basePath = fileName
|
||||
const packageIndex = fileName.indexOf("packages/")
|
||||
if (packageIndex) {
|
||||
basePath = basePath.substring(packageIndex)
|
||||
}
|
||||
|
||||
return basePath
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the generator's properties for new usage.
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,7 @@ import AbstractGenerator from "./index.js"
|
||||
import ts from "typescript"
|
||||
import OasKindGenerator from "../kinds/oas.js"
|
||||
import { GeneratorEvent } from "../helpers/generator-event-manager.js"
|
||||
import getBasePath from "../../utils/get-base-path.js"
|
||||
|
||||
/**
|
||||
* A class used to generate OAS yaml comments. The comments are written
|
||||
@@ -82,7 +83,7 @@ class OasGenerator extends AbstractGenerator {
|
||||
isFileIncluded(fileName: string): boolean {
|
||||
return (
|
||||
super.isFileIncluded(fileName) &&
|
||||
minimatch(this.getBasePath(fileName), "packages/medusa/**/api**/**", {
|
||||
minimatch(getBasePath(fileName), "packages/medusa/**/api**/**", {
|
||||
matchBase: true,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -678,6 +678,63 @@ class KnowledgeBaseFactory {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve summary and description for a property of an object, interface, or type.
|
||||
*
|
||||
* @param param0 - The property's details.
|
||||
* @returns The property's summary.
|
||||
*/
|
||||
tryToGetObjectPropertySummary({
|
||||
retrieveOptions,
|
||||
propertyDetails: { isClassOrInterface, isBoolean, classOrInterfaceName },
|
||||
}: {
|
||||
/**
|
||||
* The options used to retrieve the summary from the knowledge base, if available.
|
||||
*/
|
||||
retrieveOptions: RetrieveOptions
|
||||
/**
|
||||
* The details of the property.
|
||||
*/
|
||||
propertyDetails: {
|
||||
/**
|
||||
* Whether the property's value is a class or interface. This applies to all
|
||||
* object-like types.
|
||||
*
|
||||
* If `true`, the property is considered to represent a relationship to another
|
||||
* class / interface.
|
||||
*/
|
||||
isClassOrInterface: boolean
|
||||
/**
|
||||
* Whether the property's value is a boolean
|
||||
*/
|
||||
isBoolean: boolean
|
||||
/**
|
||||
* The name of the class / interface this property's value is associated to.
|
||||
* This is only used if {@link isClassOrInterface} is `true`.
|
||||
*/
|
||||
classOrInterfaceName?: string
|
||||
}
|
||||
}): string {
|
||||
let summary = this.tryToGetSummary(retrieveOptions)
|
||||
|
||||
if (summary) {
|
||||
return summary
|
||||
}
|
||||
|
||||
if (isClassOrInterface) {
|
||||
summary = `The associated ${classOrInterfaceName}.`
|
||||
} else if (isBoolean) {
|
||||
summary = `Whether the ${retrieveOptions.templateOptions
|
||||
?.parentName} ${snakeToWords(retrieveOptions.str)}.`
|
||||
} else {
|
||||
summary = `The ${snakeToWords(
|
||||
retrieveOptions.str
|
||||
)} of the ${retrieveOptions.templateOptions?.parentName}`
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
}
|
||||
|
||||
export default KnowledgeBaseFactory
|
||||
|
||||
@@ -4,7 +4,7 @@ import Formatter from "./formatter.js"
|
||||
import { join } from "path"
|
||||
import { DOCBLOCK_LINE_ASTRIX } from "../../constants.js"
|
||||
import ts from "typescript"
|
||||
import getOasOutputBasePath from "../../utils/get-oas-output-base-path.js"
|
||||
import { getOasOutputBasePath } from "../../utils/get-output-base-paths.js"
|
||||
import { parse } from "yaml"
|
||||
import formatOas from "../../utils/format-oas.js"
|
||||
import pluralize from "pluralize"
|
||||
|
||||
@@ -24,7 +24,7 @@ export type GeneratorOptions = {
|
||||
checker: ts.TypeChecker
|
||||
kinds?: ts.SyntaxKind[]
|
||||
generatorEventManager: GeneratorEventManager
|
||||
additionalOptions: Pick<CommonCliOptions, "generateExamples">
|
||||
additionalOptions?: Pick<CommonCliOptions, "generateExamples">
|
||||
}
|
||||
|
||||
export type GetDocBlockOptions = {
|
||||
@@ -65,7 +65,7 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
checker,
|
||||
kinds,
|
||||
generatorEventManager,
|
||||
additionalOptions,
|
||||
additionalOptions = {},
|
||||
}: GeneratorOptions) {
|
||||
this.allowedKinds = kinds || DefaultKindGenerator.DEFAULT_ALLOWED_NODE_KINDS
|
||||
this.checker = checker
|
||||
|
||||
277
www/utils/packages/docblock-generator/src/classes/kinds/dml.ts
Normal file
277
www/utils/packages/docblock-generator/src/classes/kinds/dml.ts
Normal file
@@ -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/index.js"
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1,7 +1,7 @@
|
||||
import ts from "typescript"
|
||||
import DefaultKindGenerator, { GetDocBlockOptions } from "./default.js"
|
||||
import { DOCBLOCK_END_LINE, DOCBLOCK_START } from "../../constants.js"
|
||||
import { camelToWords, snakeToWords } from "utils"
|
||||
import { camelToWords } from "utils"
|
||||
import { normalizeName } from "../../utils/str-formatting.js"
|
||||
|
||||
/**
|
||||
@@ -40,36 +40,32 @@ class DTOPropertyGenerator extends DefaultKindGenerator<ts.PropertySignature> {
|
||||
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.tryToGetSummary({
|
||||
str: node.name.getText(),
|
||||
kind: node.kind,
|
||||
templateOptions: {
|
||||
rawParentName,
|
||||
parentName,
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
||||
if (summary) {
|
||||
str += summary
|
||||
} else {
|
||||
// check if the property's type is interface/type/class
|
||||
const propertyType = this.checker.getTypeAtLocation(node)
|
||||
if (propertyType.isClassOrInterface()) {
|
||||
str += `The associated ${this.formatInterfaceName(
|
||||
this.checker.typeToString(propertyType)
|
||||
)}.`
|
||||
} else if (
|
||||
"intrinsicName" in propertyType &&
|
||||
propertyType.intrinsicName === "boolean"
|
||||
) {
|
||||
str += `Whether the ${parentName} ${snakeToWords(node.name.getText())}.`
|
||||
} else {
|
||||
// format summary
|
||||
str += `The ${snakeToWords(node.name.getText())} of the ${parentName}.`
|
||||
}
|
||||
}
|
||||
str += summary
|
||||
|
||||
return `${str}${DOCBLOCK_END_LINE}`
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "../../types/index.js"
|
||||
import formatOas from "../../utils/format-oas.js"
|
||||
import getCorrectZodTypeName from "../../utils/get-correct-zod-type-name.js"
|
||||
import getOasOutputBasePath from "../../utils/get-oas-output-base-path.js"
|
||||
import { getOasOutputBasePath } from "../../utils/get-output-base-paths.js"
|
||||
import isZodObject from "../../utils/is-zod-object.js"
|
||||
import parseOas, { ExistingOas } from "../../utils/parse-oas.js"
|
||||
import OasExamplesGenerator from "../examples/oas.js"
|
||||
|
||||
@@ -5,6 +5,7 @@ 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.
|
||||
@@ -20,6 +21,7 @@ class KindsRegistry {
|
||||
>
|
||||
) {
|
||||
this.kindInstances = [
|
||||
new DmlKindGenerator(options),
|
||||
new OasKindGenerator(options),
|
||||
new MedusaReactHooksKindGenerator(options),
|
||||
new FunctionKindGenerator(options),
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { readdirSync, existsSync, readFileSync, writeFileSync } from "fs"
|
||||
import { getDmlOutputBasePath } from "../utils/get-output-base-paths.js"
|
||||
import path from "path"
|
||||
import getMonorepoRoot from "../utils/get-monorepo-root.js"
|
||||
import { DmlFile } from "../types/index.js"
|
||||
import toJsonFormatted from "../utils/to-json-formatted.js"
|
||||
|
||||
export default async function () {
|
||||
console.log("Cleaning DML JSON files...")
|
||||
|
||||
const dmlOutputPath = getDmlOutputBasePath()
|
||||
const monorepoRoot = getMonorepoRoot()
|
||||
|
||||
const jsonFiles = readdirSync(dmlOutputPath)
|
||||
|
||||
jsonFiles.forEach((jsonFile) => {
|
||||
const jsonFilePath = path.join(dmlOutputPath, jsonFile)
|
||||
const parsedJson = JSON.parse(
|
||||
readFileSync(jsonFilePath, "utf-8")
|
||||
) as DmlFile
|
||||
|
||||
const dataModelKeys = Object.keys(parsedJson)
|
||||
let dataUpdated = false
|
||||
|
||||
dataModelKeys.forEach((dataModelName) => {
|
||||
const { filePath } = parsedJson[dataModelName]
|
||||
|
||||
const fullFilePath = path.join(monorepoRoot, filePath)
|
||||
|
||||
if (existsSync(fullFilePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
// delete data model from json object
|
||||
delete parsedJson[dataModelName]
|
||||
dataUpdated = true
|
||||
})
|
||||
|
||||
if (dataUpdated) {
|
||||
writeFileSync(jsonFilePath, toJsonFormatted(parsedJson))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import OasKindGenerator, { OasArea } from "../classes/kinds/oas.js"
|
||||
import { DEFAULT_OAS_RESPONSES } from "../constants.js"
|
||||
import { OpenApiDocument, OpenApiSchema } from "../types/index.js"
|
||||
import getMonorepoRoot from "../utils/get-monorepo-root.js"
|
||||
import getOasOutputBasePath from "../utils/get-oas-output-base-path.js"
|
||||
import { getOasOutputBasePath } from "../utils/get-output-base-paths.js"
|
||||
import parseOas from "../utils/parse-oas.js"
|
||||
|
||||
const OAS_PREFIX_REGEX = /@oas \[(?<method>(get|post|delete))\] (?<path>.+)/
|
||||
|
||||
@@ -4,6 +4,7 @@ import getMonorepoRoot from "../utils/get-monorepo-root.js"
|
||||
import { GitManager } from "../classes/helpers/git-manager.js"
|
||||
import { CommonCliOptions } from "../types/index.js"
|
||||
import OasGenerator from "../classes/generators/oas.js"
|
||||
import DmlGenerator from "../classes/generators/dml.js"
|
||||
|
||||
export default async function runGitChanges({
|
||||
type,
|
||||
@@ -43,5 +44,14 @@ export default async function runGitChanges({
|
||||
await oasGenerator.run()
|
||||
}
|
||||
|
||||
if (type === "all" || type === "dml") {
|
||||
const dmlGenerator = new DmlGenerator({
|
||||
paths: files,
|
||||
...options,
|
||||
})
|
||||
|
||||
await dmlGenerator.run()
|
||||
}
|
||||
|
||||
console.log(`Finished generating docs for ${files.length} files.`)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import DocblockGenerator from "../classes/generators/docblock.js"
|
||||
import OasGenerator from "../classes/generators/oas.js"
|
||||
import { CommonCliOptions } from "../types/index.js"
|
||||
import { GitManager } from "../classes/helpers/git-manager.js"
|
||||
import DmlGenerator from "../classes/generators/dml.js"
|
||||
|
||||
export default async function (
|
||||
commitSha: string,
|
||||
@@ -51,5 +52,14 @@ export default async function (
|
||||
await oasGenerator.run()
|
||||
}
|
||||
|
||||
if (type === "all" || type === "dml") {
|
||||
const dmlGenerator = new DmlGenerator({
|
||||
paths: filteredFiles,
|
||||
...options,
|
||||
})
|
||||
|
||||
await dmlGenerator.run()
|
||||
}
|
||||
|
||||
console.log(`Finished generating docs for ${filteredFiles.length} files.`)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import getMonorepoRoot from "../utils/get-monorepo-root.js"
|
||||
import { GitManager } from "../classes/helpers/git-manager.js"
|
||||
import OasGenerator from "../classes/generators/oas.js"
|
||||
import { CommonCliOptions } from "../types/index.js"
|
||||
import DmlGenerator from "../classes/generators/dml.js"
|
||||
|
||||
export default async function ({ type, tag, ...options }: CommonCliOptions) {
|
||||
const gitManager = new GitManager()
|
||||
@@ -49,5 +50,14 @@ export default async function ({ type, tag, ...options }: CommonCliOptions) {
|
||||
await oasGenerator.run()
|
||||
}
|
||||
|
||||
if (type === "all" || type === "dml") {
|
||||
const dmlGenerator = new DmlGenerator({
|
||||
paths: filteredFiles,
|
||||
...options,
|
||||
})
|
||||
|
||||
await dmlGenerator.run()
|
||||
}
|
||||
|
||||
console.log(`Finished generating docs for ${filteredFiles.length} files.`)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import DmlGenerator from "../classes/generators/dml.js"
|
||||
import DocblockGenerator from "../classes/generators/docblock.js"
|
||||
import { Options } from "../classes/generators/index.js"
|
||||
import OasGenerator from "../classes/generators/oas.js"
|
||||
@@ -27,5 +28,14 @@ export default async function run(
|
||||
await oasGenerator.run()
|
||||
}
|
||||
|
||||
if (type === "all" || type === "dml") {
|
||||
const dmlGenerator = new DmlGenerator({
|
||||
paths,
|
||||
...options,
|
||||
})
|
||||
|
||||
await dmlGenerator.run()
|
||||
}
|
||||
|
||||
console.log(`Finished running.`)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import runGitChanges from "./commands/run-git-changes.js"
|
||||
import runGitCommit from "./commands/run-git-commit.js"
|
||||
import runRelease from "./commands/run-release.js"
|
||||
import cleanOas from "./commands/clean-oas.js"
|
||||
import cleanDml from "./commands/clean-dml.js"
|
||||
|
||||
const program = new Command()
|
||||
|
||||
@@ -13,7 +14,7 @@ program.name("docblock-generator").description("Generate TSDoc doc-blocks")
|
||||
|
||||
// define common options
|
||||
const typeOption = new Option("--type <type>", "The type of docs to generate.")
|
||||
.choices(["all", "docs", "oas"])
|
||||
.choices(["all", "docs", "oas", "dml"])
|
||||
.default("all")
|
||||
|
||||
const generateExamplesOption = new Option(
|
||||
@@ -68,4 +69,11 @@ program
|
||||
)
|
||||
.action(cleanOas)
|
||||
|
||||
program
|
||||
.command("clean:dml")
|
||||
.description(
|
||||
"Check generated DML files under the `dml-output` directory and remove any data models that no longer exists."
|
||||
)
|
||||
.action(cleanDml)
|
||||
|
||||
program.parse()
|
||||
|
||||
@@ -12,7 +12,7 @@ export declare type OpenApiOperation = Partial<OpenAPIV3.OperationObject> & {
|
||||
}
|
||||
|
||||
export declare type CommonCliOptions = {
|
||||
type: "all" | "oas" | "docs"
|
||||
type: "all" | "oas" | "docs" | "dml"
|
||||
generateExamples?: boolean
|
||||
tag?: string
|
||||
}
|
||||
@@ -28,3 +28,12 @@ export declare interface OpenApiTagObject extends OpenAPIV3.TagObject {
|
||||
export declare interface OpenApiDocument extends OpenAPIV3.Document {
|
||||
tags?: OpenApiTagObject[]
|
||||
}
|
||||
|
||||
export declare type DmlObject = Record<string, string>
|
||||
|
||||
export declare type DmlFile = {
|
||||
[k: string]: {
|
||||
filePath: string
|
||||
properties: DmlObject
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Retrieve the pathname of a file without the relative part before `packages/`
|
||||
*
|
||||
* @param fileName - The file name/path
|
||||
* @returns The path without the relative part.
|
||||
*/
|
||||
export default function getBasePath(fileName: string) {
|
||||
let basePath = fileName
|
||||
const packageIndex = fileName.indexOf("packages/")
|
||||
if (packageIndex) {
|
||||
basePath = basePath.substring(packageIndex)
|
||||
}
|
||||
|
||||
return basePath
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import dirname from "./dirname.js"
|
||||
*
|
||||
* @returns {string} The absolute path to the monorepository.
|
||||
*/
|
||||
export default function getMonorepoRoot() {
|
||||
export default function getMonorepoRoot(): string {
|
||||
return (
|
||||
process.env.MONOREPO_ROOT_PATH ||
|
||||
path.join(dirname(import.meta.url), "..", "..", "..", "..", "..", "..")
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import path from "path"
|
||||
import getMonorepoRoot from "./get-monorepo-root.js"
|
||||
|
||||
export default function getOasOutputBasePath() {
|
||||
return path.join(getMonorepoRoot(), "www", "utils", "generated", "oas-output")
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import path from "path"
|
||||
import getMonorepoRoot from "./get-monorepo-root.js"
|
||||
|
||||
/**
|
||||
* Retrieves the base path to the `oas-output` directory.
|
||||
*/
|
||||
export function getOasOutputBasePath() {
|
||||
return path.join(getMonorepoRoot(), "www", "utils", "generated", "oas-output")
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the base path to the `dml-output` directory
|
||||
*/
|
||||
export function getDmlOutputBasePath() {
|
||||
return path.join(getMonorepoRoot(), "www", "utils", "generated", "dml-output")
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Retrieves the stringified JSON of a variable formatted.
|
||||
*
|
||||
* @param item The item to stringify
|
||||
* @returns The formatted JSON string
|
||||
*/
|
||||
export default function toJsonFormatted(item: unknown): string {
|
||||
return JSON.stringify(item, undefined, "\t")
|
||||
}
|
||||
@@ -32,6 +32,13 @@ export function snakeToWords(str: string): string {
|
||||
return str.replaceAll("_", " ").toLowerCase()
|
||||
}
|
||||
|
||||
export function snakeToPascal(str: string): string {
|
||||
return str
|
||||
.split("_")
|
||||
.map((word) => capitalize(word))
|
||||
.join("")
|
||||
}
|
||||
|
||||
export function kebabToTitle(str: string): string {
|
||||
return str
|
||||
.split("-")
|
||||
@@ -76,6 +83,10 @@ export function pascalToCamel(str: string): string {
|
||||
return `${str.charAt(0).toLowerCase()}${str.substring(1)}`
|
||||
}
|
||||
|
||||
export function pascalToWords(str: string): string {
|
||||
return str.replaceAll(/([A-Z])/g, " $1")
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove parts of the name such as DTO, Filterable, etc...
|
||||
*
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"generate:dml": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user