docs-util: infer resolved resources in workflow + steps (#10637)

This commit is contained in:
Shahed Nasser
2024-12-17 19:03:00 +02:00
committed by GitHub
parent 0a40b69276
commit ee62083c52
7 changed files with 331 additions and 10 deletions

View File

@@ -4,7 +4,7 @@ import { stringify } from "yaml"
import { replaceTemplateVariables } from "../../utils/reflection-template-strings"
import { Reflection } from "typedoc"
import { FrontmatterData } from "types"
import { getTagComments, getTagsAsArray } from "utils"
import { getTagComments, getTagsAsArray, getUniqueStrArray } from "utils"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper("frontmatter", function (this: Reflection) {
@@ -29,6 +29,11 @@ export default function (theme: MarkdownTheme) {
const tagContent = getTagsAsArray(tag)
resolvedFrontmatter["tags"]?.push(...tagContent)
})
if (resolvedFrontmatter["tags"]?.length) {
resolvedFrontmatter["tags"] = getUniqueStrArray(
resolvedFrontmatter["tags"]
)
}
return `---\n${stringify(resolvedFrontmatter).trim()}\n---\n\n`
})

View File

@@ -14,13 +14,21 @@ import {
} from "typedoc"
import ts, { SyntaxKind, VariableStatement } from "typescript"
import { WorkflowManager, WorkflowDefinition } from "@medusajs/orchestration"
import Helper from "./utils/helper"
import { findReflectionInNamespaces, isWorkflow, isWorkflowStep } from "utils"
import Helper, { WORKFLOW_AS_STEP_SUFFIX } from "./utils/helper"
import {
findReflectionInNamespaces,
isWorkflow,
isWorkflowStep,
addTagsToReflection,
getResolvedResourcesOfStep,
getUniqueStrArray,
} from "utils"
import { StepType } from "./types"
type ParsedStep = {
stepReflection: DeclarationReflection
stepType: StepType
resources: string[]
}
/**
@@ -30,10 +38,19 @@ type ParsedStep = {
class WorkflowsPlugin {
protected app: Application
protected helper: Helper
protected workflowsTagsMap: Map<string, string[]>
protected addTagsAfterParsing: {
[k: string]: {
id: string
workflowIds: string[]
}
}
constructor(app: Application) {
this.app = app
this.helper = new Helper()
this.workflowsTagsMap = new Map()
this.addTagsAfterParsing = {}
this.registerOptions()
this.registerEventHandlers()
@@ -110,6 +127,7 @@ class WorkflowsPlugin {
constructorFn: initializer.arguments[1],
context,
parentReflection: reflection.parent,
workflowReflection: reflection,
})
if (!reflection.comment && reflection.parent.comment) {
@@ -121,6 +139,8 @@ class WorkflowsPlugin {
}
}
}
this.handleAddTagsAfterParsing(context)
}
/**
@@ -133,15 +153,18 @@ class WorkflowsPlugin {
constructorFn,
context,
parentReflection,
workflowReflection,
}: {
workflowId: string
constructorFn: ts.ArrowFunction | ts.FunctionExpression
context: Context
parentReflection: DeclarationReflection
workflowReflection: SignatureReflection
}) {
// use the workflow manager to check whether something in the constructor
// body is a step/hook
const workflow = WorkflowManager.getWorkflow(workflowId)
const resources: string[] = []
if (!ts.isBlock(constructorFn.body)) {
return
@@ -165,19 +188,22 @@ class WorkflowsPlugin {
)
if (initializerName === "when") {
this.parseWhenStep({
const { resources: whenResources } = this.parseWhenStep({
initializer,
parentReflection,
context,
workflow,
stepDepth,
workflowReflection,
})
resources.push(...whenResources)
} else {
const steps = this.parseSteps({
initializer,
context,
workflow,
workflowVarName: parentReflection.name,
workflowReflection,
})
if (!steps.length) {
@@ -190,11 +216,15 @@ class WorkflowsPlugin {
depth: stepDepth,
parentReflection,
})
resources.push(...step.resources)
})
}
stepDepth++
})
const uniqueResources = addTagsToReflection(parentReflection, resources)
this.updateWorkflowsTagsMap(workflowId, uniqueResources)
}
/**
@@ -208,11 +238,13 @@ class WorkflowsPlugin {
context,
workflow,
workflowVarName,
workflowReflection,
}: {
initializer: ts.CallExpression
context: Context
workflow?: WorkflowDefinition
workflowVarName: string
workflowReflection: SignatureReflection
}): ParsedStep[] {
const steps: ParsedStep[] = []
const initializerName = this.helper.normalizeName(
@@ -235,6 +267,7 @@ class WorkflowsPlugin {
context,
workflow,
workflowVarName,
workflowReflection,
})
)
})
@@ -242,6 +275,7 @@ class WorkflowsPlugin {
let stepId: string | undefined
let stepReflection: DeclarationReflection | undefined
let stepType = this.helper.getStepType(initializer)
const resources: string[] = []
if (stepType === "hook" && "symbol" in initializer.arguments[1]) {
// get the hook's name from the first argument
@@ -281,6 +315,12 @@ class WorkflowsPlugin {
"step",
true
)
const stepResources = getResolvedResourcesOfStep(
originalInitializer,
stepId
)
resources.push(...stepResources)
stepType = this.helper.getStepType(originalInitializer)
stepReflection = initializerReflection
}
@@ -295,7 +335,14 @@ class WorkflowsPlugin {
steps.push({
stepReflection,
stepType,
resources,
})
if (stepId?.endsWith(WORKFLOW_AS_STEP_SUFFIX)) {
this.updateAddTagsAfterParsingMap(workflowReflection, {
id: workflow.id,
workflowId: stepId,
})
}
}
}
@@ -313,13 +360,18 @@ class WorkflowsPlugin {
context,
workflow,
stepDepth,
workflowReflection,
}: {
initializer: ts.CallExpression
parentReflection: DeclarationReflection
context: Context
workflow?: WorkflowDefinition
stepDepth: number
}) {
workflowReflection: SignatureReflection
}): {
resources: string[]
} {
const resources: string[] = []
const whenInitializer = (initializer.expression as ts.CallExpression)
.expression as ts.CallExpression
const thenInitializer = initializer
@@ -332,7 +384,9 @@ class WorkflowsPlugin {
(!ts.isFunctionExpression(thenInitializer.arguments[0]) &&
!ts.isArrowFunction(thenInitializer.arguments[0]))
) {
return
return {
resources,
}
}
const whenCondition = whenInitializer.arguments[1].body.getText()
@@ -378,18 +432,25 @@ class WorkflowsPlugin {
context,
workflow,
workflowVarName: parentReflection.name,
workflowReflection,
}).forEach((step) => {
this.createStepDocumentReflection({
...step,
depth: stepDepth,
parentReflection: documentReflection,
})
resources.push(...step.resources)
})
})
if (documentReflection.children?.length) {
parentReflection.documents?.push(documentReflection)
}
return {
resources: getUniqueStrArray(resources),
}
}
/**
@@ -473,6 +534,7 @@ class WorkflowsPlugin {
createStepDocumentReflection({
stepType,
stepReflection,
resources,
depth,
parentReflection,
}: ParsedStep & {
@@ -498,6 +560,7 @@ class WorkflowsPlugin {
},
])
)
addTagsToReflection(stepReflection, resources)
if (parentReflection.isDocument()) {
parentReflection.addChild(documentReflection)
@@ -605,6 +668,71 @@ class WorkflowsPlugin {
return initializer
}
updateAddTagsAfterParsingMap(
reflection: SignatureReflection,
{
id,
workflowId,
}: {
id: string
workflowId: string
}
) {
const existingItem = this.addTagsAfterParsing[`${reflection.id}`] || {
id,
workflowIds: [],
}
existingItem.workflowIds.push(
workflowId.replace(WORKFLOW_AS_STEP_SUFFIX, "")
)
this.addTagsAfterParsing[`${reflection.id}`] = existingItem
}
updateWorkflowsTagsMap(workflowId: string, tags: string[]) {
const existingItems = this.workflowsTagsMap.get(workflowId) || []
existingItems.push(...tags)
this.workflowsTagsMap.set(workflowId, existingItems)
}
handleAddTagsAfterParsing(context: Context) {
let keys = Object.keys(this.addTagsAfterParsing)
const handleForWorkflow = (
key: string,
{
id,
workflowIds,
}: {
id: string
workflowIds: string[]
}
) => {
const resources: string[] = []
workflowIds.forEach((workflowId) => {
// check if it exists in keys
const existingKey = keys.find(
(k) => this.addTagsAfterParsing[k].id === workflowId
)
if (existingKey) {
handleForWorkflow(existingKey, this.addTagsAfterParsing[existingKey])
}
resources.push(...(this.workflowsTagsMap.get(workflowId) || []))
})
const reflection = context.project.getReflectionById(parseInt(key))
if (reflection) {
const uniqueTags = addTagsToReflection(reflection, resources)
this.updateWorkflowsTagsMap(id, uniqueTags)
}
delete this.addTagsAfterParsing[key]
keys = Object.keys(this.addTagsAfterParsing)
}
do {
handleForWorkflow(keys[0], this.addTagsAfterParsing[keys[0]])
} while (keys.length > 0)
}
}
export default WorkflowsPlugin

View File

@@ -7,6 +7,8 @@ import ts from "typescript"
import { StepModifier, StepType } from "../types"
import { capitalize, findReflectionInNamespaces } from "utils"
export const WORKFLOW_AS_STEP_SUFFIX = `-as-step`
/**
* A class of helper methods.
*/
@@ -126,7 +128,7 @@ export default class Helper {
stepId = this._getStepOrWorkflowIdFromArrowFunction(initializer, type)
}
return isWorkflowStep ? `${stepId}-as-step` : stepId
return isWorkflowStep ? `${stepId}${WORKFLOW_AS_STEP_SUFFIX}` : stepId
}
private _getStepOrWorkflowIdFromArrowFunction(

View File

@@ -0,0 +1,136 @@
import ts from "typescript"
import { getUniqueStrArray } from "./str-utils"
import { camelToWords } from "./str-formatting"
const RESOLVE_EXPRESSIONS = [`container.resolve`, `req.scope.resolve`]
export const getResolvedResources = (
functionExpression: ts.ArrowFunction | ts.FunctionDeclaration
): string[] => {
const resources: string[] = []
if (!functionExpression.body) {
return resources
}
const body = ts.isBlock(functionExpression.body)
? functionExpression.body
: getBlockFromNode(functionExpression.body)
if (!body) {
return resources
}
body.statements.forEach((statement) => {
if (!ts.isVariableStatement(statement)) {
return
}
statement.declarationList.declarations.forEach((declaration) => {
if (
!declaration.initializer ||
!ts.isCallExpression(declaration.initializer) ||
!declaration.initializer.arguments.length ||
!("name" in declaration.initializer.arguments[0])
) {
return
}
const initializerText = declaration.initializer.getText()
const isContainerExpression = RESOLVE_EXPRESSIONS.some((exp) =>
initializerText.startsWith(exp)
)
if (!isContainerExpression) {
return
}
const resourceName = normalizeResolvedResourceName(
declaration.initializer.arguments[0]
)
if (!resourceName.length) {
return
}
resources.push(resourceName)
})
})
return resources
}
export const getResolvedResourcesOfStep = (
expression: ts.CallExpression,
stepId?: string
): string[] => {
if (
!expression.arguments ||
expression.arguments.length < 2 ||
(!ts.isArrowFunction(expression.arguments[1]) &&
!ts.isFunctionDeclaration(expression.arguments[1]))
) {
return stepId ? getResolvedResourcesByStepId(stepId) : []
}
const stepFunction: ts.ArrowFunction | ts.FunctionDeclaration =
expression.arguments[1]
let resources = getResolvedResources(stepFunction)
if (
expression.arguments.length === 3 &&
(ts.isArrowFunction(expression.arguments[2]) ||
ts.isFunctionDeclaration(expression.arguments[2]))
) {
// get resolved resources of compensation function
resources.push(...getResolvedResources(expression.arguments[2]))
// make resources unique
resources = getUniqueStrArray(resources)
}
if (!resources.length && stepId) {
return getResolvedResourcesByStepId(stepId)
}
return resources
}
const normalizeResolvedResourceName = (expression: ts.Expression): string => {
let name = ""
switch (true) {
case ts.isPropertyAccessExpression(expression):
name = expression.name.getText()
break
case ts.isStringLiteral(expression):
name = camelToWords(expression.getText())
}
return name.toLowerCase().replaceAll("_", " ")
}
const getBlockFromNode = (node: ts.Node): ts.Block | undefined => {
if ("body" in node) {
if (ts.isBlock(node.body as ts.Node)) {
return node.body as ts.Block
}
return getBlockFromNode(node.body as ts.Node)
}
if ("expression" in node) {
return getBlockFromNode(node.expression as ts.Node)
}
return undefined
}
/**
* Some steps like useQueryGraphStep are not possible
* to detect due to their implementation. For those,
* we have static resolutions
*/
const STEPS_RESOLVED_RESOURCES: Record<string, string[]> = {
"use-query-graph-step": ["query"],
}
export const getResolvedResourcesByStepId = (stepId: string): string[] => {
return STEPS_RESOLVED_RESOURCES[stepId] || []
}

View File

@@ -1,6 +1,7 @@
export * from "./dml-utils"
export * from "./get-type-children"
export * from "./get-project-child"
export * from "./get-resolved-resources"
export * from "./get-type-str"
export * from "./hooks-util"
export * from "./step-utils"

View File

@@ -21,3 +21,7 @@ export function stripLineBreaks(str: string) {
.trim()
: ""
}
export function getUniqueStrArray(str: string[]): string[] {
return Array.from(new Set(str))
}

View File

@@ -1,11 +1,17 @@
import { CommentTag, DeclarationReflection, Reflection } from "typedoc"
import { Comment, CommentTag, DeclarationReflection, Reflection } from "typedoc"
import { getUniqueStrArray } from "./str-utils"
export const getTagsAsArray = (tag: CommentTag): string[] => {
return tag.content
export const getTagsAsArray = (
tag: CommentTag,
makeUnique = true
): string[] => {
const tags = tag.content
.map((content) => content.text)
.join("")
.split(",")
.map((value) => value.trim())
return makeUnique ? getUniqueStrArray(tags) : tags
}
export const getTagComments = (reflection: Reflection): CommentTag[] => {
@@ -23,3 +29,42 @@ export const getTagComments = (reflection: Reflection): CommentTag[] => {
return tagComments
}
export const getTagsAsValue = (tags: string[]): string => {
return tags.join(",")
}
export const addTagsToReflection = (
reflection: Reflection,
tags: string[]
): string[] => {
let tempTags = [...tags]
// check if reflection has an existing tag
const existingTag = reflection.comment?.blockTags.find(
(tag) => tag.tag === `@tags`
)
if (existingTag) {
tempTags.push(...getTagsAsArray(existingTag))
}
if (!tags.length) {
return tempTags
}
// make tags unique
tempTags = getUniqueStrArray(tempTags)
if (!reflection.comment) {
reflection.comment = new Comment()
}
reflection.comment.blockTags.push(
new CommentTag(`@tags`, [
{
kind: "text",
text: getTagsAsValue(tempTags),
},
])
)
return tempTags
}