docs-util: add support for workflows in markdown theme (#8485)

* add template for workflows

* initial changes

* added support for parallel steps

* added support for when

* added merge options

* fix merge options

* fix to tooltip

* clean up

* remove redirects

* fixes

* theme fixes + added merge options

* generate hook examples + fixes

* changed namespaces

* add custom autogenerator

* change type of additional data
This commit is contained in:
Shahed Nasser
2024-08-09 16:35:52 +03:00
committed by GitHub
parent 79b49c1288
commit a19c562bec
58 changed files with 2098 additions and 328 deletions
@@ -8,6 +8,7 @@ import { baseOptions } from "./base-options.js"
import path from "path"
import { rootPathPrefix } from "./general.js"
import { modules } from "./references.js"
import { getCoreFlowNamespaces } from "../utils/get-namespaces.js"
const customOptions: Record<string, Partial<TypeDocOptions>> = {
"core-flows": getOptions({
@@ -18,20 +19,7 @@ const customOptions: Record<string, Partial<TypeDocOptions>> = {
enableWorkflowsPlugins: true,
enableNamespaceGenerator: true,
// @ts-expect-error there's a typing issue in typedoc
generateNamespaces: [
{
name: "Workflows",
description:
"Workflows listed here are created by Medusa and can be imported from `@medusajs/core-flows`.",
pathPattern: "**/packages/core/core-flows/**/workflows/**",
},
{
name: "Steps",
description:
"Steps listed here are created by Medusa and can be imported from `@medusajs/core-flows`.",
pathPattern: "**/packages/core/core-flows/**/steps/**",
},
],
generateNamespaces: getCoreFlowNamespaces(),
}),
"auth-provider": getOptions({
entryPointPath: "packages/core/utils/src/auth/abstract-auth-provider.ts",
@@ -0,0 +1,77 @@
import { FormattingOptionsType } from "types"
import baseSectionsOptions from "../base-section-options.js"
const coreFlowsOptions: FormattingOptionsType = {
"^core_flows": {
expandMembers: true,
sections: {
...baseSectionsOptions,
member_getterSetter: false,
},
workflowDiagramComponent: "WorkflowDiagram",
mdxImports: [`import { TypeList, WorkflowDiagram } from "docs-ui"`],
},
"^modules/core_flows/page\\.mdx": {
reflectionDescription:
"This section of the documentation provides a reference to Medusa's workflows and steps that you can use in your customizations.",
reflectionGroups: {
Namespaces: true,
Enumerations: false,
Classes: false,
Interfaces: false,
"Type Aliases": false,
Variables: false,
"Enumeration Members": false,
Functions: false,
},
hideTocHeaders: true,
frontmatterData: {
slug: "/references/medusa-workflows",
},
reflectionTitle: {
fullReplacement: "Medusa Workflows API Reference",
},
},
"^core_flows/.*/.*(Workflows|Steps)/page\\.mdx": {
expandMembers: false,
reflectionGroups: {
Variables: false,
Properties: false,
"Type Literals": false,
},
sections: {
...baseSectionsOptions,
member_getterSetter: false,
members_categories: false,
},
hideTocHeaders: true,
},
"^core_flows/.*Workflows/functions/.*/page\\.mdx": {
reflectionDescription:
"This documentation provides a reference to the `{{alias}}`. It belongs to the `@medusajs/core-flows` package.",
frontmatterData: {
slug: "/references/medusa-workflows/{{alias}}",
sidebar_label: "{{alias}}",
},
reflectionTitle: {
kind: false,
typeParameters: false,
suffix: "- Medusa Workflows API Reference",
},
},
"^core_flows/.*Steps/functions/.*/page\\.mdx": {
reflectionDescription:
"This documentation provides a reference to the `{{alias}}`. It belongs to the `@medusajs/core-flows` package.",
frontmatterData: {
slug: "/references/medusa-workflows/steps/{{alias}}",
sidebar_label: "{{alias}}",
},
reflectionTitle: {
kind: false,
typeParameters: false,
suffix: "- Medusa Workflows API Reference",
},
},
}
export default coreFlowsOptions
@@ -11,9 +11,11 @@ import searchOptions from "./search.js"
import taxProviderOptions from "./tax-provider.js"
import workflowsOptions from "./workflows.js"
import dmlOptions from "./dml.js"
import coreFlowsOptions from "./core-flows.js"
const mergerCustomOptions: FormattingOptionsType = {
...authProviderOptions,
...coreFlowsOptions,
...dmlOptions,
...fileOptions,
...fulfillmentProviderOptions,
@@ -12,6 +12,10 @@ import { FormattingOptionType } from "types"
import { kebabToCamel, kebabToPascal, kebabToSnake, kebabToTitle } from "utils"
import baseSectionsOptions from "./base-section-options.js"
import mergerCustomOptions from "./merger-custom-options/index.js"
import {
getCoreFlowNamespaces,
getNamespaceNames,
} from "../utils/get-namespaces.js"
const mergerOptions: Partial<TypeDocOptions> = {
...baseOptions,
@@ -36,7 +40,9 @@ const mergerOptions: Partial<TypeDocOptions> = {
"helper-steps",
"workflows",
],
allReflectionsHaveOwnDocumentInNamespace: ["Utilities"],
allReflectionsHaveOwnDocumentInNamespace: [
...getNamespaceNames(getCoreFlowNamespaces()),
],
formatting: {
"*": {
showCommentsAsHeader: true,
@@ -0,0 +1,74 @@
import { readdirSync } from "fs"
import { rootPathPrefix } from "../constants/general.js"
import { NamespaceGenerateDetails } from "types"
import { capitalize, kebabToTitle } from "utils"
import path from "path"
export function getCoreFlowNamespaces(): NamespaceGenerateDetails[] {
const namespaces: NamespaceGenerateDetails[] = []
const rootFlowsPath = path.join(
rootPathPrefix,
"packages",
"core",
"core-flows",
"src"
)
// retrieve directories
const directories = readdirSync(rootFlowsPath, {
withFileTypes: true,
})
directories.forEach((directory) => {
if (!directory.isDirectory()) {
return
}
const namespaceName = kebabToTitle(directory.name)
const pathPattern = `**/packages/core/core-flows/src/${directory.name}/**`
const namespace: NamespaceGenerateDetails = {
name: namespaceName,
pathPattern,
children: [],
}
const subDirs = readdirSync(path.join(rootFlowsPath, directory.name), {
withFileTypes: true,
})
subDirs.forEach((dir) => {
if (
!dir.isDirectory() ||
(dir.name !== "workflows" && dir.name !== "steps")
) {
return
}
namespace.children!.push({
name: `${capitalize(dir.name)}_${namespaceName}`,
pathPattern: `**/packages/core/core-flows/src/${directory.name}/${dir.name}`,
})
})
namespaces.push(namespace)
})
return namespaces
}
export function getNamespaceNames(
namespaces: NamespaceGenerateDetails[]
): string[] {
const names: string[] = []
namespaces.forEach((namespace) => {
names.push(namespace.name)
if (namespace.children) {
names.push(...getNamespaceNames(namespace.children))
}
})
return names
}
@@ -2,6 +2,7 @@ import { minimatch } from "minimatch"
import {
Application,
Comment,
Context,
Converter,
DeclarationReflection,
ParameterType,
@@ -34,25 +35,26 @@ export function load(app: Application) {
"generateNamespaces"
) as unknown as NamespaceGenerateDetails[]
namespaces.forEach((namespace) => {
const genNamespace = context.createDeclarationReflection(
ReflectionKind.Namespace,
void 0,
void 0,
namespace.name
)
const generateNamespaces = (ns: NamespaceGenerateDetails[]) => {
const createdNamespaces: DeclarationReflection[] = []
ns.forEach((namespace) => {
const genNamespace = createNamespace(context, namespace)
if (namespace.description) {
genNamespace.comment = new Comment([
{
kind: "text",
text: namespace.description,
},
])
}
generatedNamespaces.set(namespace.pathPattern, genNamespace)
generatedNamespaces.set(namespace.pathPattern, genNamespace)
})
if (namespace.children) {
generateNamespaces(namespace.children).forEach((child) =>
genNamespace.addChild(child)
)
}
createdNamespaces.push(genNamespace)
})
return createdNamespaces
}
generateNamespaces(namespaces)
})
app.converter.on(
@@ -69,13 +71,61 @@ export function load(app: Application) {
return
}
generatedNamespaces.forEach((namespace, pathPattern) => {
if (!minimatch(filePath, pathPattern)) {
return
}
const namespaces = app.options.getValue(
"generateNamespaces"
) as unknown as NamespaceGenerateDetails[]
namespace.addChild(reflection)
})
const findNamespace = (
ns: NamespaceGenerateDetails[]
): DeclarationReflection | undefined => {
let found: DeclarationReflection | undefined
ns.some((namespace) => {
if (namespace.children) {
// give priorities to children
found = findNamespace(namespace.children)
if (found) {
return true
}
}
if (!minimatch(filePath, namespace.pathPattern)) {
return false
}
found = generatedNamespaces.get(namespace.pathPattern)
return found !== undefined
})
return found
}
const namespace = findNamespace(namespaces)
namespace?.addChild(reflection)
}
)
}
function createNamespace(
context: Context,
namespace: NamespaceGenerateDetails
): DeclarationReflection {
const genNamespace = context.createDeclarationReflection(
ReflectionKind.Namespace,
void 0,
void 0,
namespace.name
)
if (namespace.description) {
genNamespace.comment = new Comment([
{
kind: "text",
text: namespace.description,
},
])
}
return genNamespace
}
@@ -69,6 +69,11 @@ import dmlPropertiesHelper from "./resources/helpers/dml-properties"
import ifWorkflowStepHelper from "./resources/helpers/if-workflow-step"
import stepInputHelper from "./resources/helpers/step-input"
import stepOutputHelper from "./resources/helpers/step-output"
import ifWorkflowHelper from "./resources/helpers/if-workflow"
import workflowInputHelper from "./resources/helpers/workflow-input"
import workflowOutputHelper from "./resources/helpers/workflow-output"
import workflowDiagramHelper from "./resources/helpers/workflow-diagram"
import workflowHooksHelper from "./resources/helpers/workflow-hooks"
import { MarkdownTheme } from "./theme"
const TEMPLATE_PATH = path.join(__dirname, "resources", "templates")
@@ -166,4 +171,9 @@ export function registerHelpers(theme: MarkdownTheme) {
ifWorkflowStepHelper()
stepInputHelper(theme)
stepOutputHelper(theme)
ifWorkflowHelper()
workflowInputHelper(theme)
workflowOutputHelper(theme)
workflowDiagramHelper(theme)
workflowHooksHelper(theme)
}
@@ -1,14 +1,15 @@
import * as Handlebars from "handlebars"
import { Reflection, SignatureReflection } from "typedoc"
import { isWorkflowStep } from "utils"
import { isWorkflow, isWorkflowStep } from "utils"
export default function () {
Handlebars.registerHelper("example", function (reflection: Reflection) {
const isStep =
const isWorkflowOrStep =
reflection.variant === "signature" &&
isWorkflowStep(reflection as SignatureReflection)
(isWorkflowStep(reflection as SignatureReflection) ||
isWorkflow(reflection as SignatureReflection))
const targetReflection =
isStep && reflection.parent ? reflection.parent : reflection
isWorkflowOrStep && reflection.parent ? reflection.parent : reflection
const exampleTag = targetReflection.comment?.blockTags.find(
(tag) => tag.tag === "@example"
)
@@ -0,0 +1,12 @@
import * as Handlebars from "handlebars"
import { SignatureReflection } from "typedoc"
import { isWorkflow } from "utils"
export default function () {
Handlebars.registerHelper(
"ifWorkflow",
function (this: SignatureReflection, options: Handlebars.HelperOptions) {
return isWorkflow(this) ? options.fn(this) : options.inverse(this)
}
)
}
@@ -24,6 +24,7 @@ export default function (theme: MarkdownTheme) {
reflectionType: inputType,
project: this.project || options.data.theme.project,
maxLevel,
wrapObject: true,
})
if (!input.length) {
@@ -24,6 +24,7 @@ export default function (theme: MarkdownTheme) {
reflectionType: outputType,
project: this.project || options.data.theme.project,
maxLevel,
wrapObject: true,
})
if (!output.length) {
@@ -3,6 +3,7 @@ import {
DeclarationReflection,
ProjectReflection,
ReflectionGroup,
ReflectionKind,
} from "typedoc"
import { MarkdownTheme } from "../../theme"
import { escapeChars } from "utils"
@@ -13,13 +14,18 @@ export default function (theme: MarkdownTheme) {
function (this: ProjectReflection | DeclarationReflection) {
const md: string[] = []
const { hideInPageTOC } = theme
const { hideInPageTOC, allReflectionsHaveOwnDocumentInNamespace } = theme
const { hideTocHeaders, reflectionGroupRename = {} } =
theme.getFormattingOptionsForLocation()
const isVisible = this.groups?.some((group) =>
group.allChildrenHaveOwnDocument()
)
const isNamespaceVisible =
this.kind === ReflectionKind.Namespace &&
allReflectionsHaveOwnDocumentInNamespace.includes(this.name)
const isVisible =
isNamespaceVisible ||
this.groups?.some((group) => {
return group.allChildrenHaveOwnDocument()
})
function pushGroup(group: ReflectionGroup, md: string[]) {
const children = group.children.map(
@@ -47,7 +53,7 @@ export default function (theme: MarkdownTheme) {
md.push("\n")
})
} else {
if (!hideInPageTOC || group.allChildrenHaveOwnDocument()) {
if (!hideInPageTOC || isVisible) {
if (!hideTocHeaders) {
md.push(`${headingLevel} ${groupTitle}\n\n`)
}
@@ -0,0 +1,113 @@
import { MarkdownTheme } from "../../theme"
import * as Handlebars from "handlebars"
import { DocumentReflection, SignatureReflection } from "typedoc"
import { formatWorkflowDiagramComponent } from "../../utils/format-workflow-diagram-component"
import { getProjectChild } from "utils"
import { getWorkflowReflectionFromNamespace } from "../../utils/workflow-utils"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"workflowDiagram",
function (this: SignatureReflection): string {
const { workflowDiagramComponent } =
theme.getFormattingOptionsForLocation()
if (!this.parent?.documents?.length) {
return ""
}
const steps: Record<string, unknown>[] = []
this.parent.documents.forEach((document, index) => {
if (document.name === "when") {
const condition = getDocumentTagValue(document, "@whenCondition")
const depth = getDocumentTagValue(document, "@workflowDepth")
const whenStep = {
type: "when",
condition,
depth,
steps: [] as Record<string, unknown>[],
}
document.children?.forEach((childDocument) => {
whenStep.steps.push(
getStep({
document: childDocument,
theme,
index,
})
)
})
steps.push(whenStep)
} else {
steps.push(
getStep({
document,
theme,
index,
})
)
}
})
return (
`${Handlebars.helpers.titleLevel()} Diagram\n\n` +
formatWorkflowDiagramComponent({
component: workflowDiagramComponent,
componentItem: {
name: this.name,
steps,
},
})
)
}
)
}
function getStep({
document,
theme,
index,
}: {
document: DocumentReflection
theme: MarkdownTheme
index: number
}) {
const type = document.comment?.modifierTags.has("@workflowStep")
? "workflow"
: document.comment?.modifierTags.has("@hook")
? "hook"
: "step"
const namespaceRefl = theme.project
? getWorkflowReflectionFromNamespace(theme.project, document.name)
: undefined
const associatedReflection =
namespaceRefl ||
(theme.project ? getProjectChild(theme.project, document.name) : undefined)
const depth = getDocumentTagValue(document, `@workflowDepth`) || `${index}`
return {
type,
name: document.name,
description: associatedReflection?.comment
? Handlebars.helpers.comments(associatedReflection.comment, true)
: "",
link:
type === "hook" || !associatedReflection?.url
? `#${document.name}`
: Handlebars.helpers.relativeURL(associatedReflection.url),
depth: parseInt(depth),
}
}
function getDocumentTagValue(
document: DocumentReflection,
tag: `@${string}`
): string | undefined {
return document.comment
?.getTag(tag)
?.content.find((tagContent) => tagContent.kind === "text")?.text
}
@@ -0,0 +1,72 @@
import { MarkdownTheme } from "../../theme"
import * as Handlebars from "handlebars"
import { SignatureReflection } from "typedoc"
import { cleanUpHookInput, getProjectChild } from "utils"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"workflowHooks",
function (this: SignatureReflection): string {
if (!this.parent?.documents || !theme.project) {
return ""
}
const hooks = this.parent.documents.filter(
(document) => document.comment?.modifierTags.has("@hook")
)
if (!hooks.length) {
return ""
}
let str = `${Handlebars.helpers.titleLevel()} Hooks`
Handlebars.helpers.incrementCurrentTitleLevel()
const hooksTitleLevel = Handlebars.helpers.titleLevel()
hooks.forEach((hook) => {
// show the hook's input
const hookReflection = getProjectChild(theme.project!, hook.name)
if (
!hookReflection ||
!hookReflection.signatures?.length ||
!hookReflection.signatures[0].parameters?.length
) {
return
}
str += `\n\n${hooksTitleLevel} ${hook.name}\n\n`
const hookExample = hookReflection.comment?.getTag(`@example`)
if (hookExample) {
Handlebars.helpers.incrementCurrentTitleLevel()
const innerTitleLevel = Handlebars.helpers.titleLevel()
str += `${innerTitleLevel} Example\n\n\`\`\`ts\n${Handlebars.helpers.comment(
hookExample.content
)}\n\`\`\`\n\n${innerTitleLevel} Input\n\n`
Handlebars.helpers.decrementCurrentTitleLevel()
}
str += `Handlers consuming this hook accept the following input.\n\n`
str += Handlebars.helpers.parameterComponent.call(
cleanUpHookInput(hookReflection.signatures[0].parameters),
{
hash: {
sectionTitle: hook.name,
},
}
)
})
Handlebars.helpers.decrementCurrentTitleLevel()
return str
}
)
}
@@ -0,0 +1,44 @@
import { MarkdownTheme } from "../../theme"
import * as Handlebars from "handlebars"
import { SignatureReflection } from "typedoc"
import { getWorkflowInputType } from "utils"
import { formatParameterComponent } from "../../utils/format-parameter-component"
import { getReflectionTypeParameters } from "../../utils/reflection-type-parameters"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"workflowInput",
function (
this: SignatureReflection,
options: Handlebars.HelperOptions
): string {
const { parameterComponent, maxLevel, parameterComponentExtraProps } =
theme.getFormattingOptionsForLocation()
const inputType = getWorkflowInputType(this)
if (!inputType) {
return ""
}
const input = getReflectionTypeParameters({
reflectionType: inputType,
project: this.project || options.data.theme.project,
maxLevel,
wrapObject: true,
})
if (!input.length) {
return ""
}
const formattedComponent = formatParameterComponent({
parameterComponent,
componentItems: input,
extraProps: parameterComponentExtraProps,
sectionTitle: options.hash.sectionTitle,
})
return `${Handlebars.helpers.titleLevel()} Input\n\n${formattedComponent}`
}
)
}
@@ -0,0 +1,44 @@
import { MarkdownTheme } from "../../theme"
import * as Handlebars from "handlebars"
import { SignatureReflection } from "typedoc"
import { getWorkflowOutputType } from "utils"
import { formatParameterComponent } from "../../utils/format-parameter-component"
import { getReflectionTypeParameters } from "../../utils/reflection-type-parameters"
export default function (theme: MarkdownTheme) {
Handlebars.registerHelper(
"workflowOutput",
function (
this: SignatureReflection,
options: Handlebars.HelperOptions
): string {
const { parameterComponent, maxLevel, parameterComponentExtraProps } =
theme.getFormattingOptionsForLocation()
const outputType = getWorkflowOutputType(this)
if (!outputType) {
return ""
}
const output = getReflectionTypeParameters({
reflectionType: outputType,
project: this.project || options.data.theme.project,
maxLevel,
wrapObject: true,
})
if (!output.length) {
return ""
}
const formattedComponent = formatParameterComponent({
parameterComponent,
componentItems: output,
extraProps: parameterComponentExtraProps,
sectionTitle: options.hash.sectionTitle,
})
return `${Handlebars.helpers.titleLevel()} Output\n\n${formattedComponent}`
}
)
}
@@ -4,6 +4,12 @@
{{else}}
{{#ifWorkflow}}
{{> member.workflow}}
{{else}}
{{#ifWorkflowStep}}
{{> member.step}}
@@ -14,4 +20,6 @@
{{/ifWorkflowStep}}
{{/ifWorkflow}}
{{/ifReactQueryType}}
@@ -0,0 +1,21 @@
{{{signatureTitle accessor parent}}}
{{#if (sectionEnabled "member_signature_comment")}}
{{> comment}}
{{/if}}
{{#if (sectionEnabled "member_signature_example")}}
{{{example this}}}
{{/if}}
{{{workflowDiagram}}}
{{{workflowInput sectionTitle=name}}}
{{{workflowOutput sectionTitle=name}}}
{{{workflowHooks}}}
@@ -0,0 +1,9 @@
export function formatWorkflowDiagramComponent({
component,
componentItem,
}: {
component: string | undefined
componentItem: Record<string, unknown>
}): string {
return `<${component} workflow={${JSON.stringify(componentItem)}} />`
}
@@ -0,0 +1,23 @@
import {
DeclarationReflection,
ProjectReflection,
ReflectionKind,
} from "typedoc"
export function getWorkflowReflectionFromNamespace(
project: ProjectReflection,
reflName: string
): DeclarationReflection | undefined {
let found: DeclarationReflection | undefined
project
.getChildrenByKind(ReflectionKind.Module)
.find((moduleRef) => moduleRef.name === "core-flows")
?.getChildrenByKind(ReflectionKind.Namespace)
.some((namespace) => {
found = namespace.getChildByName(reflName) as DeclarationReflection
return found !== undefined
})
return found
}
@@ -1,6 +1,7 @@
import {
Application,
Comment,
CommentTag,
Context,
Converter,
DeclarationReflection,
@@ -14,7 +15,13 @@ import {
import ts, { SyntaxKind, VariableStatement } from "typescript"
import { WorkflowManager, WorkflowDefinition } from "@medusajs/orchestration"
import Helper from "./utils/helper"
import { isWorkflow } from "utils"
import { isWorkflow, isWorkflowStep } from "utils"
import { StepType } from "./types"
type ParsedStep = {
stepReflection: DeclarationReflection
stepType: StepType
}
/**
* A plugin that extracts a workflow's steps, hooks, their types, and attaches them as
@@ -92,12 +99,20 @@ class WorkflowsPlugin {
continue
}
this.parseSteps({
this.parseWorkflow({
workflowId,
constructorFn: initializer.arguments[1],
context,
parentReflection: reflection.parent,
})
if (!reflection.comment && reflection.parent.comment) {
reflection.comment = reflection.parent.comment
}
} else if (isWorkflowStep(reflection)) {
if (!reflection.comment && reflection.parent.comment) {
reflection.comment = reflection.parent.comment
}
}
}
}
@@ -107,7 +122,7 @@ class WorkflowsPlugin {
*
* @param param0 - The workflow's details.
*/
parseSteps({
parseWorkflow({
workflowId,
constructorFn,
context,
@@ -130,138 +145,241 @@ class WorkflowsPlugin {
parentReflection.documents = []
}
let stepDepth = 1
constructorFn.body.statements.forEach((statement) => {
let initializer: ts.CallExpression | undefined
switch (statement.kind) {
case SyntaxKind.VariableStatement:
const variableInitializer = (statement as VariableStatement)
.declarationList.declarations[0].initializer
if (
!variableInitializer ||
!ts.isCallExpression(variableInitializer)
) {
return
}
initializer = variableInitializer
break
case SyntaxKind.ExpressionStatement:
const statementInitializer = (statement as ts.ExpressionStatement)
.expression
if (!ts.isCallExpression(statementInitializer)) {
return
}
initializer = statementInitializer
}
const initializer = this.getInitializerOfNode(statement)
if (!initializer) {
return
}
const { stepId, stepReflection } =
this.parseStep({
const initializerName = this.helper.normalizeName(
initializer.expression.getText()
)
if (initializerName === "when") {
this.parseWhenStep({
initializer,
parentReflection,
context,
workflow,
stepDepth,
})
} else {
const steps = this.parseSteps({
initializer,
context,
workflow,
}) || {}
workflowVarName: parentReflection.name,
})
if (!stepId || !stepReflection) {
return
if (!steps.length) {
return
}
steps.forEach((step) => {
this.createStepDocumentReflection({
...step,
depth: stepDepth,
parentReflection,
})
})
}
const stepModifier = this.helper.getModifier(initializer)
const documentReflection = new DocumentReflection(
stepReflection.name,
stepReflection,
[],
{}
)
documentReflection.comment = new Comment()
documentReflection.comment.modifierTags.add(stepModifier)
parentReflection.documents?.push(documentReflection)
stepDepth++
})
}
/**
* Parse a step to retrieve its ID and reflection.
* Parses steps in an initializer, retrieving each of their ID and reflection.
*
* @param param0 - The step's details.
* @returns The step's ID and reflection, if found.
*/
parseStep({
parseSteps({
initializer,
context,
workflow,
workflowVarName,
}: {
initializer: ts.CallExpression
context: Context
workflow?: WorkflowDefinition
}):
| {
stepId: string
stepReflection: DeclarationReflection
}
| undefined {
workflowVarName: string
}): ParsedStep[] {
const steps: ParsedStep[] = []
const initializerName = this.helper.normalizeName(
initializer.expression.getText()
)
let stepId: string | undefined
let stepReflection: DeclarationReflection | undefined
if (
this.helper.getStepType(initializer) === "hook" &&
"symbol" in initializer.arguments[1]
) {
// get the hook's name from the first argument
stepId = this.helper.normalizeName(initializer.arguments[0].getText())
stepReflection = this.assembleHookReflection({
stepId,
context,
inputSymbol: initializer.arguments[1].symbol as ts.Symbol,
})
} else {
const initializerReflection =
context.project.getChildByName(initializerName)
if (
!initializerReflection ||
!(initializerReflection instanceof DeclarationReflection)
) {
return
if (initializerName === "parallelize") {
if (!initializer.arguments.length) {
return steps
}
const { initializer } =
this.helper.getReflectionSymbolAndInitializer({
project: context.project,
reflection: initializerReflection,
}) || {}
initializer.arguments.forEach((argument) => {
if (!ts.isCallExpression(argument)) {
return
}
steps.push(
...this.parseSteps({
initializer: argument,
context,
workflow,
workflowVarName,
})
)
})
} else {
let stepId: string | undefined
let stepReflection: DeclarationReflection | undefined
let stepType = this.helper.getStepType(initializer)
if (stepType === "hook" && "symbol" in initializer.arguments[1]) {
// get the hook's name from the first argument
stepId = this.helper.normalizeName(initializer.arguments[0].getText())
stepReflection = this.assembleHookReflection({
stepId,
context,
inputSymbol: initializer.arguments[1].symbol as ts.Symbol,
workflowName: workflowVarName,
})
} else {
const initializerReflection =
context.project.getChildByName(initializerName)
if (
!initializerReflection ||
!(initializerReflection instanceof DeclarationReflection)
) {
return steps
}
const { initializer: originalInitializer } =
this.helper.getReflectionSymbolAndInitializer({
project: context.project,
reflection: initializerReflection,
}) || {}
if (!originalInitializer) {
return steps
}
stepId = this.helper.getStepOrWorkflowId(
originalInitializer,
context.project,
true
)
stepType = this.helper.getStepType(originalInitializer)
stepReflection = initializerReflection
}
// check if is a step in the workflow
if (
stepId &&
stepType &&
stepReflection &&
workflow?.handlers_.get(stepId)
) {
steps.push({
stepReflection,
stepType,
})
}
}
return steps
}
/**
* Parses the step in a `when` condition, and creates a `when` document with the steps as child documents.
*
* @param param0 - The when stp's details.
*/
parseWhenStep({
initializer,
parentReflection,
context,
workflow,
stepDepth,
}: {
initializer: ts.CallExpression
parentReflection: DeclarationReflection
context: Context
workflow?: WorkflowDefinition
stepDepth: number
}) {
const whenInitializer = (initializer.expression as ts.CallExpression)
.expression as ts.CallExpression
const thenInitializer = initializer
if (
whenInitializer.arguments.length < 2 ||
(!ts.isFunctionExpression(whenInitializer.arguments[1]) &&
!ts.isArrowFunction(whenInitializer.arguments[1])) ||
thenInitializer.arguments.length < 1 ||
(!ts.isFunctionExpression(thenInitializer.arguments[0]) &&
!ts.isArrowFunction(thenInitializer.arguments[0]))
) {
return
}
const whenCondition = whenInitializer.arguments[1].body.getText()
const thenStatements = (thenInitializer.arguments[0].body as ts.Block)
.statements
const documentReflection = new DocumentReflection(
"when",
parentReflection,
[],
{}
)
documentReflection.comment = new Comment()
documentReflection.comment.modifierTags.add(this.helper.getModifier(`when`))
documentReflection.comment.blockTags.push(
new CommentTag(`@workflowDepth`, [
{
kind: "text",
text: `${stepDepth}`,
},
])
)
documentReflection.comment.blockTags.push(
new CommentTag(`@whenCondition`, [
{
kind: "text",
text: whenCondition,
},
])
)
thenStatements.forEach((statement) => {
const initializer = this.getInitializerOfNode(statement)
if (!initializer) {
return
}
stepId = this.helper.getStepOrWorkflowId(
this.parseSteps({
initializer,
context.project,
true
)
stepReflection = initializerReflection
}
context,
workflow,
workflowVarName: parentReflection.name,
}).forEach((step) => {
this.createStepDocumentReflection({
...step,
depth: stepDepth,
parentReflection: documentReflection,
})
})
})
// check if is a step in the workflow
if (!stepId || !stepReflection || !workflow?.handlers_.get(stepId)) {
return
}
return {
stepId,
stepReflection,
if (documentReflection.children?.length) {
parentReflection.documents?.push(documentReflection)
}
}
@@ -275,10 +393,12 @@ class WorkflowsPlugin {
stepId,
context,
inputSymbol,
workflowName,
}: {
stepId: string
context: Context
inputSymbol: ts.Symbol
workflowName: string
}): DeclarationReflection {
const declarationReflection = context.createDeclarationReflection(
ReflectionKind.Function,
@@ -286,6 +406,14 @@ class WorkflowsPlugin {
undefined,
stepId
)
declarationReflection.comment = new Comment()
declarationReflection.comment.summary = [
{
kind: "text",
text: "This step is a hook that you can inject custom functionality into.",
},
]
const signatureReflection = new SignatureReflection(
stepId,
ReflectionKind.SomeSignature,
@@ -300,6 +428,10 @@ class WorkflowsPlugin {
parameter.type = ReferenceType.createSymbolReference(inputSymbol, context)
if (parameter.type.name === "__object") {
parameter.type.name = "object"
}
signatureReflection.parameters = []
signatureReflection.parameters.push(parameter)
@@ -308,8 +440,162 @@ class WorkflowsPlugin {
declarationReflection.signatures.push(signatureReflection)
declarationReflection.comment.blockTags.push(
new CommentTag(`@example`, [
{
kind: "code",
text: this.helper.generateHookExample({
hookName: stepId,
workflowName,
parameter,
}),
},
])
)
return declarationReflection
}
/**
* Creates a document reflection for a step.
*
* @param param0 - The step's details.
*/
createStepDocumentReflection({
stepType,
stepReflection,
depth,
parentReflection,
}: ParsedStep & {
depth: number
parentReflection: DeclarationReflection | DocumentReflection
}) {
const stepModifier = this.helper.getModifier(stepType)
const documentReflection = new DocumentReflection(
stepReflection.name,
stepReflection,
[],
{}
)
documentReflection.comment = new Comment()
documentReflection.comment.modifierTags.add(stepModifier)
documentReflection.comment.blockTags.push(
new CommentTag(`@workflowDepth`, [
{
kind: "text",
text: `${depth}`,
},
])
)
if (parentReflection.isDocument()) {
parentReflection.addChild(documentReflection)
} else {
parentReflection.documents?.push(documentReflection)
}
}
/**
* Gets the initializer in a node, if available.
*
* @param node - The node to search for an initializer in.
* @returns The initializer, if found.
*/
getInitializerOfNode(node: ts.Node): ts.CallExpression | undefined {
let initializer: ts.CallExpression | undefined
switch (node.kind) {
case SyntaxKind.CallExpression:
initializer = node as ts.CallExpression
break
case SyntaxKind.VariableStatement:
const variableInitializer = (node as VariableStatement).declarationList
.declarations[0].initializer
if (!variableInitializer) {
return
}
initializer = this.findCallExpression(variableInitializer)
break
case SyntaxKind.ExpressionStatement:
const statementInitializer = (node as ts.ExpressionStatement).expression
initializer = this.findCallExpression(statementInitializer)
break
case SyntaxKind.ReturnStatement:
let returnInitializer = (node as ts.ReturnStatement).expression
if (
returnInitializer &&
ts.isNewExpression(returnInitializer) &&
returnInitializer.expression.getText().includes("WorkflowResponse") &&
returnInitializer.arguments?.length
) {
returnInitializer = this.getInitializerOfNode(
returnInitializer.arguments[0]
)
}
if (!returnInitializer) {
return
}
initializer = this.findCallExpression(returnInitializer)
break
}
return initializer ? this.cleanUpInitializer(initializer) : undefined
}
/**
* Finds a `CallExpression` in an expression and returns it, if available.
*
* @param expression - The expression to search in.
* @param skipCallCheck - Whether to skip the `CallExpression` check the first time. Useful for the {@link cleanUpInitializer} method.
* @returns The `CallExpression` if found.
*/
findCallExpression(
expression: ts.Expression,
skipCallCheck = false
): ts.CallExpression | undefined {
let initializer = expression
while (
(skipCallCheck || !ts.isCallExpression(initializer)) &&
"expression" in initializer
) {
initializer = initializer.expression as ts.Expression
skipCallCheck = false
}
return initializer && ts.isCallExpression(initializer)
? initializer
: undefined
}
/**
* Finds an inner call expression of a call expression, if the provided one is not allowed.
* This is useful for steps that chain a `.config` method, for example.
*
* @param initializer - The call expression to search in.
* @returns The call expression to be used.
*/
cleanUpInitializer(initializer: ts.CallExpression): ts.CallExpression {
if (!("name" in initializer.expression)) {
return initializer
}
const initializerName = (initializer.expression.name as ts.Identifier)
.escapedText
if (initializerName === "config") {
return this.findCallExpression(initializer, true) || initializer
}
return initializer
}
}
export default WorkflowsPlugin
@@ -1,3 +1,3 @@
export type StepType = "step" | "workflowStep" | "hook"
export type StepType = "step" | "workflowStep" | "hook" | "when"
export type StepModifier = "@step" | "@workflowStep" | "@hook"
export type StepModifier = "@step" | "@workflowStep" | "@hook" | "@when"
@@ -1,5 +1,9 @@
import { DeclarationReflection, ProjectReflection } from "typedoc"
import ts from "typescript"
import {
DeclarationReflection,
ParameterReflection,
ProjectReflection,
} from "typedoc"
import ts, { isStringLiteral } from "typescript"
import { StepModifier, StepType } from "../types"
/**
@@ -13,7 +17,27 @@ export default class Helper {
* @returns The normalized name.
*/
normalizeName(name: string) {
return name.replace(".runAsStep", "").replace(/^"/, "").replace(/"$/, "")
const nameWithoutQuotes = name.replace(/^"/, "").replace(/"$/, "")
const dotPos = nameWithoutQuotes.indexOf(".")
const parenPos = nameWithoutQuotes.indexOf("(")
// If both indices of dot and parenthesis are -1, set endIndex to -1
// if one of them is -1, use the other's value
// if both aren't -1, use the minimum
const endIndex =
dotPos === -1 && parenPos === -1
? -1
: dotPos === -1
? parenPos
: parenPos === -1
? dotPos
: Math.min(dotPos, parenPos)
return nameWithoutQuotes.substring(
0,
endIndex === -1 ? nameWithoutQuotes.length : endIndex
)
}
/**
@@ -64,13 +88,39 @@ export default class Helper {
project: ProjectReflection,
checkWorkflowStep = false
): string | undefined {
const idVar = initializer.arguments[0]
const idArg = initializer.arguments[0]
const isWorkflowStep =
checkWorkflowStep && this.getStepType(initializer) === "workflowStep"
const idVarName = this.normalizeName(idVar.getText())
const idArgValue = this.normalizeName(idArg.getText())
let stepId: string | undefined
if (ts.isObjectLiteralExpression(idArg)) {
const nameProperty = idArg.properties.find(
(property) => property.name?.getText() === "name"
)
if (nameProperty && ts.isPropertyAssignment(nameProperty)) {
const nameValue = this.normalizeName(nameProperty.initializer.getText())
stepId = ts.isStringLiteral(nameProperty.initializer)
? nameValue
: this.getValueFromReflection(nameValue, project)
}
} else if (!isStringLiteral(idArg)) {
stepId = this.getValueFromReflection(idArgValue, project)
} else {
stepId = idArgValue
}
return isWorkflowStep ? `${stepId}-as-step` : stepId
}
private getValueFromReflection(
refName: string,
project: ProjectReflection
): string | undefined {
// load it from the project
const idVarReflection = project.getChildByName(idVarName)
const idVarReflection = project.getChildByName(refName)
if (
!idVarReflection ||
@@ -80,9 +130,7 @@ export default class Helper {
return
}
const stepId = idVarReflection.type.value as string
return isWorkflowStep ? `${stepId}-as-step` : stepId
return idVarReflection.type.value as string
}
/**
@@ -97,6 +145,8 @@ export default class Helper {
return "workflowStep"
case "createHook":
return "hook"
case "when":
return "when"
default:
return "step"
}
@@ -108,9 +158,39 @@ export default class Helper {
* @param initializer - The step's initializer.
* @returns The step's modifier.
*/
getModifier(initializer: ts.CallExpression): StepModifier {
const stepType = this.getStepType(initializer)
getModifier(stepType: StepType): StepModifier {
return `@${stepType}`
}
generateHookExample({
hookName,
workflowName,
parameter,
}: {
hookName: string
workflowName: string
parameter: ParameterReflection
}): string {
let str = `import { ${workflowName} } from "@medusajs/core-flows"\n\n`
str += `${workflowName}.hooks.${hookName}(\n\tasync (({`
if (
parameter.type?.type === "reference" &&
parameter.type.reflection instanceof DeclarationReflection &&
parameter.type.reflection.children
) {
parameter.type.reflection.children.forEach((childParam, index) => {
if (index > 0) {
str += `,`
}
str += ` ${childParam.name}`
})
}
str += ` }, { container }) => {\n\t\t//TODO\n\t})\n)`
return str
}
}
+5
View File
@@ -83,6 +83,7 @@ export type FormattingOptionType = {
endSections?: string[]
shouldIncrementAfterStartSections?: boolean
hideTocHeaders?: boolean
workflowDiagramComponent?: string
}
export declare module "typedoc" {
@@ -286,4 +287,8 @@ export declare type NamespaceGenerateDetails = {
* namespace
*/
pathPattern: string
/**
* The namespace's children
*/
children?: NamespaceGenerateDetails[]
}
@@ -0,0 +1,63 @@
import {
DeclarationReflection,
IntrinsicType,
ParameterReflection,
Reflection,
} from "typedoc"
export function cleanUpHookInput(
parameters: ParameterReflection[]
): ParameterReflection[] {
return parameters.map((parameter) => {
if (parameter.type?.type !== "reference" || !parameter.type.reflection) {
return parameter
}
cleanUpReflectionType(parameter.type.reflection)
if (
parameter.type.reflection &&
parameter.type.reflection instanceof DeclarationReflection &&
parameter.type.reflection.children
) {
parameter.type.reflection.children.forEach(cleanUpReflectionType)
}
return parameter
})
}
function cleanUpReflectionType(reflection: Reflection): Reflection {
if (
!(reflection instanceof DeclarationReflection) &&
!(reflection instanceof ParameterReflection)
) {
return reflection
}
if (
reflection.type?.type === "reference" &&
reflection.type.name === "WorkflowData" &&
reflection.type.typeArguments?.length
) {
reflection.type = reflection.type.typeArguments[0]
}
if (reflection.defaultValue) {
delete reflection.defaultValue
}
if (reflection.name === "additional_data") {
reflection.type = new IntrinsicType("Record<string, unknown> | undefined")
} else if (
reflection.type?.type === "intersection" &&
reflection.type.types.length >= 2
) {
reflection.type = reflection.type.types[1]
}
if (reflection instanceof DeclarationReflection && reflection.children) {
reflection.children.forEach(cleanUpReflectionType)
}
return reflection
}
+1
View File
@@ -2,6 +2,7 @@ export * from "./dml-utils"
export * from "./get-type-children"
export * from "./get-project-child"
export * from "./get-type-str"
export * from "./hooks-util"
export * from "./step-utils"
export * from "./str-formatting"
export * from "./str-utils"
+16 -5
View File
@@ -1,5 +1,7 @@
import { ArrayType, SignatureReflection, SomeType, UnionType } from "typedoc"
const disallowedIntrinsicTypeNames = ["unknown", "void", "any", "never"]
export function isWorkflowStep(reflection: SignatureReflection): boolean {
return (
reflection.parent?.children?.some((child) => child.name === "__step__") ||
@@ -10,11 +12,7 @@ export function isWorkflowStep(reflection: SignatureReflection): boolean {
export function getStepInputType(
reflection: SignatureReflection
): SomeType | undefined {
if (!isWorkflowStep(reflection)) {
return
}
if (!reflection.parameters?.length) {
if (!isWorkflowStep(reflection) || !reflection.parameters?.length) {
return
}
@@ -28,6 +26,13 @@ export function getStepOutputType(
return
}
if (
reflection.type?.type === "intrinsic" &&
disallowedIntrinsicTypeNames.includes(reflection.type.name)
) {
return
}
if (reflection.type?.type !== "intersection") {
return reflection.type
}
@@ -52,6 +57,12 @@ function cleanUpType(itemType: SomeType | undefined): SomeType | undefined {
return cleanUpUnionType(itemType)
case "array":
return cleanUpArrayType(itemType)
case "intrinsic":
if (disallowedIntrinsicTypeNames.includes(itemType.name)) {
return undefined
}
return itemType
default:
return itemType
}
+55 -1
View File
@@ -1,4 +1,4 @@
import { SignatureReflection } from "typedoc"
import { ReferenceType, SignatureReflection, SomeType } from "typedoc"
export function isWorkflow(reflection: SignatureReflection): boolean {
return (
@@ -6,3 +6,57 @@ export function isWorkflow(reflection: SignatureReflection): boolean {
false
)
}
export function getWorkflowInputType(
reflection: SignatureReflection
): SomeType | undefined {
const exportedWorkflowType = getExportedWorkflowType(reflection)
const inputType = exportedWorkflowType?.typeArguments![0]
return isAllowedType(inputType) ? inputType : undefined
}
export function getWorkflowOutputType(
reflection: SignatureReflection
): SomeType | undefined {
const exportedWorkflowType = getExportedWorkflowType(reflection)
const outputType = exportedWorkflowType?.typeArguments![1]
return isAllowedType(outputType) ? outputType : undefined
}
function getExportedWorkflowType(
reflection: SignatureReflection
): ReferenceType | undefined {
if (
!isWorkflow(reflection) ||
reflection.type?.type !== "intersection" ||
reflection.type.types.length < 2
) {
return
}
const exportedWorkflowType = reflection.type.types[1]
if (
exportedWorkflowType.type !== "reference" ||
exportedWorkflowType.name !== "ExportedWorkflow" ||
(exportedWorkflowType.typeArguments?.length || 0) < 1
) {
return
}
return exportedWorkflowType
}
const disallowedIntrinsicTypeNames = ["unknown", "void", "any", "never"]
function isAllowedType(type: SomeType | undefined): boolean {
return (
type !== undefined &&
(type.type !== "intrinsic" ||
!disallowedIntrinsicTypeNames.includes(type.name))
)
}