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:
Shahed Nasser
2024-07-29 11:11:39 +03:00
committed by GitHub
parent cb33ec5cf1
commit d127abbcda
30 changed files with 749 additions and 63 deletions

View File

@@ -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/**

View File

@@ -65,3 +65,55 @@ jobs:
www/utils/generated/oas-output
branch: "docs/generate-api-ref"
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/**

View File

@@ -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",

View File

@@ -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"

View File

@@ -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

View File

@@ -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,
})
)

View File

@@ -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.
*/

View File

@@ -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,
})
)

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View 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

View File

@@ -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({
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}.`
}
}
return `${str}${DOCBLOCK_END_LINE}`
}

View File

@@ -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"

View File

@@ -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),

View File

@@ -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))
}
})
}

View File

@@ -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>.+)/

View File

@@ -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.`)
}

View File

@@ -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.`)
}

View File

@@ -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.`)
}

View File

@@ -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.`)
}

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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), "..", "..", "..", "..", "..", "..")

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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...
*

View File

@@ -20,6 +20,11 @@
"dependsOn": [
"^build"
]
},
"generate:dml": {
"dependsOn": [
"^build"
]
}
}
}