docs-util: add script to generate events info (#12340)

* docs-util: add script to generate events info

* add property and parent name to json
This commit is contained in:
Shahed Nasser
2025-05-01 14:44:24 +03:00
committed by GitHub
parent e3e02a1cc8
commit 91f5ac91a9
12 changed files with 1061 additions and 5 deletions

View File

@@ -7,9 +7,10 @@
"build": "tsc",
"watch": "tsc --watch",
"prepublishOnly": "cross-env NODE_ENV=production tsc --build",
"generate:oas": "yarn generate:route-examples && yarn start run ../../../../packages/medusa/src/api --type oas && yarn start clean:oas",
"generate:oas": "yarn generate:route-examples && yarn generate:events && 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",
"generate:route-examples": "yarn start run ../../../../packages/core/js-sdk/src --type route-examples"
"generate:route-examples": "yarn start run ../../../../packages/core/js-sdk/src --type route-examples",
"generate:events": "yarn start run ../../../../packages/core/utils/src/core-flows/events.ts --type events"
},
"publishConfig": {
"access": "public"
@@ -23,6 +24,7 @@
"commander": "^11.1.0",
"dotenv": "^16.3.1",
"eslint": "8.56.0",
"glob": "^11.0.2",
"minimatch": "^9.0.3",
"openai": "^4.29.1",
"openapi-types": "^12.1.3",

View File

@@ -0,0 +1,103 @@
import ts from "typescript"
import EventsKindGenerator from "../kinds/events.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 { getEventsOutputBasePath } from "../../utils/get-output-base-paths.js"
class EventsGenerator extends AbstractGenerator {
protected eventsKindGenerator?: EventsKindGenerator
async run() {
this.init()
this.eventsKindGenerator = new EventsKindGenerator({
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(`[EVENTS] 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 events: Record<string, unknown>[] = []
await this.eventsKindGenerator!.populateWorkflows()
const documentChild = async (node: ts.Node) => {
if (
this.eventsKindGenerator!.isAllowed(node) &&
this.eventsKindGenerator!.canDocumentNode(node)
) {
const eventsJson = await this.eventsKindGenerator!.getDocBlock(node)
events.push(...JSON.parse(eventsJson))
}
}
await Promise.all(
fileNodes.map(async (node) => await documentChild(node))
)
if (!this.options.dryRun) {
this.writeJson(events)
}
this.generatorEventManager.emit(GeneratorEvent.FINISHED_GENERATE_EVENT)
console.log(`[EVENTS] 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/core/utils/src/core-flows/events.ts",
{
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(events: Record<string, unknown>[]) {
const filePath = getEventsOutputBasePath()
const eventsJson = JSON.stringify(events, null, 2)
ts.sys.writeFile(filePath, eventsJson)
}
}
export default EventsGenerator

View File

@@ -0,0 +1,180 @@
import ts from "typescript"
import DefaultKindGenerator, { GetDocBlockOptions } from "./default.js"
import { glob } from "glob"
import getMonorepoRoot from "../../utils/get-monorepo-root.js"
import { readFile } from "fs/promises"
class EventsKindGenerator extends DefaultKindGenerator<ts.VariableDeclaration> {
protected allowedKinds: ts.SyntaxKind[] = [ts.SyntaxKind.VariableDeclaration]
public name = "events"
protected workflows: Record<string, string> = {}
protected workflowsEmittingEvents: Record<string, string> = {}
isAllowed(node: ts.Node): node is ts.VariableDeclaration {
if (
!super.isAllowed(node) ||
!node.initializer ||
!ts.isObjectLiteralExpression(node.initializer)
) {
return false
}
return node.initializer.properties.length > 0
}
async getDocBlock(
node: ts.VariableDeclaration | ts.Node,
options?: GetDocBlockOptions
): Promise<string> {
if (!this.isAllowed(node)) {
return await super.getDocBlock(node, options)
}
const properties = (node.initializer as ts.ObjectLiteralExpression)
.properties
const events: {
name: string
parentName: string
propertyName: string
payload: string
description?: string
workflows: string[]
version?: string
deprecated?: boolean
deprecated_message?: string
}[] = properties
.filter((property) => ts.isPropertyAssignment(property))
.map((property) => {
const propertyAssignment = property as ts.PropertyAssignment
const eventVariableName = node.name.getText()
const eventPropertyName = propertyAssignment.name.getText()
const workflows = this.getWorkflowsUsingEvent({
eventVariableName,
eventPropertyName,
})
if (!workflows.length) {
return null
}
const commentsAndTags = ts.getJSDocCommentsAndTags(propertyAssignment)
let payloadTag: ts.JSDocTag | undefined
let versionTag: ts.JSDocTag | undefined
let deprecatedTag: ts.JSDocTag | undefined
let description: string | undefined
commentsAndTags.forEach((comment) => {
if (!("tags" in comment)) {
return
}
if (typeof comment.comment === "string") {
description = comment.comment
}
comment.tags?.forEach((tag) => {
if (tag.tagName.getText() === "eventPayload") {
payloadTag = tag
}
if (tag.tagName.getText() === "version") {
versionTag = tag
}
if (tag.tagName.getText() === "deprecated") {
deprecatedTag = tag
}
})
})
return {
name: propertyAssignment.initializer.getText().replaceAll(`"`, ""),
parentName: eventVariableName,
propertyName: eventPropertyName,
payload: (payloadTag?.comment as string) ?? "",
description,
workflows,
version: versionTag?.comment as string,
deprecated: deprecatedTag !== undefined,
deprecated_message: deprecatedTag?.comment as string,
}
})
.filter((event) => event !== null)
return JSON.stringify(events)
}
getWorkflowsUsingEvent({
eventVariableName,
eventPropertyName,
}: {
eventVariableName: string
eventPropertyName: string
}): string[] {
const eventName = `${eventVariableName}.${eventPropertyName}`
const workflows = this.findWorkflowsUsingEvent(eventName)
return workflows
}
async populateWorkflows() {
if (Object.keys(this.workflows).length > 0) {
return
}
const files = await glob(
`${getMonorepoRoot()}/packages/core/core-flows/src/**/workflows/**/*.ts`
)
for (const file of files) {
const workflowFile = await readFile(file, "utf-8")
const workflowName = this.getWorkflowNameFromWorkflowFile(workflowFile)
if (!workflowName) {
continue
}
this.workflows[workflowName] = workflowFile
if (workflowFile.includes("emitEventStep")) {
this.workflowsEmittingEvents[workflowName] = workflowFile
}
}
}
getWorkflowNameFromWorkflowFile(workflowFile: string) {
const workflowNameMatch = workflowFile.match(
/export const\s+(\w+)\s*=\s*createWorkflow\(/
)
return workflowNameMatch ? workflowNameMatch[1] : null
}
findWorkflowsUsingEvent(eventName: string) {
const workflows = Object.keys(this.workflowsEmittingEvents).filter(
(workflowName) =>
this.workflowsEmittingEvents[workflowName].includes(eventName)
)
// find workflows using the extracted workflows
let newWorkflows: string[] = [...workflows]
while (newWorkflows.length > 0) {
// loop over the workflows and find new workflows that use the extracted workflows
const foundWorkflows: string[] = []
for (const workflowName of newWorkflows) {
foundWorkflows.push(
...Object.keys(this.workflows).filter(
(workflowKey) =>
workflowKey !== workflowName &&
this.workflows[workflowKey].match(
new RegExp(`${workflowName}[\n\\s]*\\.run`)
)
)
)
}
workflows.push(...foundWorkflows)
newWorkflows = foundWorkflows
}
return workflows
}
}
export default EventsKindGenerator

View File

@@ -6,6 +6,7 @@ import { CommonCliOptions } from "../types/index.js"
import OasGenerator from "../classes/generators/oas.js"
import DmlGenerator from "../classes/generators/dml.js"
import RouteExamplesGenerator from "../classes/generators/route-examples.js"
import EventsGenerator from "../classes/generators/events.js"
export default async function runGitChanges({
type,
@@ -63,5 +64,14 @@ export default async function runGitChanges({
await routeExamplesGenerator.run()
}
if (type === "all" || type === "events") {
const eventsGenerator = new EventsGenerator({
paths: files,
...options,
})
await eventsGenerator.run()
}
console.log(`Finished generating docs for ${files.length} files.`)
}

View File

@@ -7,6 +7,7 @@ import { CommonCliOptions } from "../types/index.js"
import { GitManager } from "../classes/helpers/git-manager.js"
import DmlGenerator from "../classes/generators/dml.js"
import RouteExamplesGenerator from "../classes/generators/route-examples.js"
import EventsGenerator from "../classes/generators/events.js"
export default async function (
commitSha: string,
@@ -71,5 +72,14 @@ export default async function (
await routeExamplesGenerator.run()
}
if (type === "all" || type === "events") {
const eventsGenerator = new EventsGenerator({
paths: filteredFiles,
...options,
})
await eventsGenerator.run()
}
console.log(`Finished generating docs for ${filteredFiles.length} files.`)
}

View File

@@ -7,6 +7,7 @@ import OasGenerator from "../classes/generators/oas.js"
import { CommonCliOptions } from "../types/index.js"
import DmlGenerator from "../classes/generators/dml.js"
import RouteExamplesGenerator from "../classes/generators/route-examples.js"
import EventsGenerator from "../classes/generators/events.js"
export default async function ({ type, tag, ...options }: CommonCliOptions) {
const gitManager = new GitManager()
@@ -69,5 +70,14 @@ export default async function ({ type, tag, ...options }: CommonCliOptions) {
await routeExamplesGenerator.run()
}
if (type === "all" || type === "events") {
const eventsGenerator = new EventsGenerator({
paths: filteredFiles,
...options,
})
await eventsGenerator.run()
}
console.log(`Finished generating docs for ${filteredFiles.length} files.`)
}

View File

@@ -1,5 +1,6 @@
import DmlGenerator from "../classes/generators/dml.js"
import DocblockGenerator from "../classes/generators/docblock.js"
import EventsGenerator from "../classes/generators/events.js"
import { Options } from "../classes/generators/index.js"
import OasGenerator from "../classes/generators/oas.js"
import RouteExamplesGenerator from "../classes/generators/route-examples.js"
@@ -47,5 +48,14 @@ export default async function run(
await routeExamplesGenerator.run()
}
if (type === "all" || type === "events") {
const eventsGenerator = new EventsGenerator({
paths,
...options,
})
await eventsGenerator.run()
}
console.log(`Finished running.`)
}

View File

@@ -14,7 +14,7 @@ program.name("docs-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", "dml", "route-examples"])
.choices(["all", "docs", "oas", "dml", "route-examples", "events"])
.default("all")
const generateExamplesOption = new Option(

View File

@@ -13,7 +13,7 @@ export declare type OpenApiOperation = Partial<OpenAPIV3.OperationObject> & {
}
export declare type CommonCliOptions = {
type: "all" | "oas" | "docs" | "dml" | "route-examples"
type: "all" | "oas" | "docs" | "dml" | "route-examples" | "events"
generateExamples?: boolean
tag?: string
}

View File

@@ -15,6 +15,19 @@ export function getDmlOutputBasePath() {
return path.join(getMonorepoRoot(), "www", "utils", "generated", "dml-output")
}
/**
* Retrieves the base path to the `events-output` directory.
*/
export function getEventsOutputBasePath() {
return path.join(
getMonorepoRoot(),
"www",
"utils",
"generated",
"events-output.json"
)
}
/**
* Retrieves the base path to the `route-examples-output` directory.
*/