From ee62083c525b7e78004e83186b96e3d2b3ff5df3 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Tue, 17 Dec 2024 19:03:00 +0200 Subject: [PATCH] docs-util: infer resolved resources in workflow + steps (#10637) --- .../src/resources/helpers/frontmatter.ts | 7 +- .../typedoc-plugin-workflows/src/plugin.ts | 138 +++++++++++++++++- .../src/utils/helper.ts | 4 +- .../utils/src/get-resolved-resources.ts | 136 +++++++++++++++++ www/utils/packages/utils/src/index.ts | 1 + www/utils/packages/utils/src/str-utils.ts | 4 + www/utils/packages/utils/src/tag-utils.ts | 51 ++++++- 7 files changed, 331 insertions(+), 10 deletions(-) create mode 100644 www/utils/packages/utils/src/get-resolved-resources.ts diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/frontmatter.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/frontmatter.ts index ac80604a4e900..5b5a68d9c784f 100644 --- a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/frontmatter.ts +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/frontmatter.ts @@ -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` }) diff --git a/www/utils/packages/typedoc-plugin-workflows/src/plugin.ts b/www/utils/packages/typedoc-plugin-workflows/src/plugin.ts index 81a65fe20e1fd..1926804048284 100644 --- a/www/utils/packages/typedoc-plugin-workflows/src/plugin.ts +++ b/www/utils/packages/typedoc-plugin-workflows/src/plugin.ts @@ -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 + 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 diff --git a/www/utils/packages/typedoc-plugin-workflows/src/utils/helper.ts b/www/utils/packages/typedoc-plugin-workflows/src/utils/helper.ts index d1c8dccc44796..c255a2e5d8c5f 100644 --- a/www/utils/packages/typedoc-plugin-workflows/src/utils/helper.ts +++ b/www/utils/packages/typedoc-plugin-workflows/src/utils/helper.ts @@ -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( diff --git a/www/utils/packages/utils/src/get-resolved-resources.ts b/www/utils/packages/utils/src/get-resolved-resources.ts new file mode 100644 index 0000000000000..21e1a6488e6a9 --- /dev/null +++ b/www/utils/packages/utils/src/get-resolved-resources.ts @@ -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 = { + "use-query-graph-step": ["query"], +} + +export const getResolvedResourcesByStepId = (stepId: string): string[] => { + return STEPS_RESOLVED_RESOURCES[stepId] || [] +} diff --git a/www/utils/packages/utils/src/index.ts b/www/utils/packages/utils/src/index.ts index f18aafee71735..1bcbea5a1d061 100644 --- a/www/utils/packages/utils/src/index.ts +++ b/www/utils/packages/utils/src/index.ts @@ -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" diff --git a/www/utils/packages/utils/src/str-utils.ts b/www/utils/packages/utils/src/str-utils.ts index 50753431dd482..4a79b2ec2ac5b 100644 --- a/www/utils/packages/utils/src/str-utils.ts +++ b/www/utils/packages/utils/src/str-utils.ts @@ -21,3 +21,7 @@ export function stripLineBreaks(str: string) { .trim() : "" } + +export function getUniqueStrArray(str: string[]): string[] { + return Array.from(new Set(str)) +} diff --git a/www/utils/packages/utils/src/tag-utils.ts b/www/utils/packages/utils/src/tag-utils.ts index 6f95664d1256e..5341d0a8de2fb 100644 --- a/www/utils/packages/utils/src/tag-utils.ts +++ b/www/utils/packages/utils/src/tag-utils.ts @@ -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 +}