Skip to content

Commit fa49352

Browse files
authored
docs: diagrams plugin tooling (medusajs#5741)
* added plugin * updated plugin + added component * dummy data TO BE REMOVED * (wip) workflow generator tool * add workflow generator tooling * updated the generator tool * added code file creation * fix design of diagrams * configured diagram theme * added build script * removed comments + unnecessary files * general fixes * refactored plugin * added README + more output types
1 parent d27b86a commit fa49352

File tree

19 files changed

+2964
-170
lines changed

19 files changed

+2964
-170
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Workflows Diagram Generator
2+
3+
An internal tool to generate [Mermaid](https://mermaid.js.org/) diagrams for workflows.
4+
5+
> Note: This tool is a beta tool created to generate diagrams that can be used in the Medusa documentation.
6+
7+
## Usage
8+
9+
After installing the dependencies, run the following command:
10+
11+
```bash
12+
yarn start run ./path/to/workflow -o ./path/to/output/dir
13+
```
14+
15+
Where:
16+
17+
- `./path/to/workflow` is the path to a file containing a Workflow, or a directory containing more than one file.
18+
- `./path/to/output/dir` is the path to the directory that outputted diagrams should be placed in.
19+
20+
### Command Options
21+
22+
#### --t, --type
23+
24+
```bash
25+
yarn start run ./path/to/workflow -o ./path/to/output/dir -t markdown
26+
```
27+
28+
The `type` of diagram to be generated. It can be one of the following:
29+
30+
- `docs` (default): For each workflow, it creates a directory holding the diagram of the workflow and its code in separate files. Diagrams are placed in `.mermaid` files.
31+
- `markdown`: Generates the diagram of each workflow in a `.md` file.
32+
- `mermaid`: Generates the diagram of each workflow in a `.mermaid` file.
33+
- `console`: Outputs the diagrams in the console.
34+
- `svg`: Generates the diagram in SVG format.
35+
- `png`: Generates the diagram in PNG format.
36+
- `pdf`: Generates the diagram in PDF format.
37+
38+
#### --no-theme
39+
40+
```bash
41+
yarn start run ./path/to/workflow -o ./path/to/output/dir --no-theme
42+
```
43+
44+
Removes Medusa's default theming from the outputted diagram. Note that Medusa's theme doesn't support dark mode.
45+
46+
#### --pretty-names
47+
48+
```bash
49+
yarn start run ./path/to/workflow -o ./path/to/output/dir --pretty-names
50+
```
51+
52+
Changes slug and camel-case names of steps to capitalized names.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "workflows-diagram-generator",
3+
"license": "MIT",
4+
"scripts": {
5+
"start": "ts-node src/index.ts",
6+
"build": "tsc",
7+
"watch": "tsc --watch",
8+
"prepublishOnly": "cross-env NODE_ENV=production tsc --build"
9+
},
10+
"publishConfig": {
11+
"access": "public"
12+
},
13+
"version": "0.0.0",
14+
"type": "module",
15+
"exports": "./dist/index.js",
16+
"bin": {
17+
"workflow-diagrams-generator": "dist/index.js"
18+
},
19+
"dependencies": {
20+
"@medusajs/workflows-sdk": "latest",
21+
"@mermaid-js/mermaid-cli": "^10.6.1",
22+
"commander": "^11.1.0",
23+
"ts-node": "^10.9.1",
24+
"typescript": "^5.1.6"
25+
},
26+
"devDependencies": {
27+
"@types/node": "^20.9.4"
28+
}
29+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { TransactionStepsDefinition } from "@medusajs/orchestration"
2+
import getRandomString from "../utils/get-random-string.js"
3+
4+
type DiagramBuilderOptions = {
5+
theme?: boolean
6+
prettyNames?: boolean
7+
}
8+
9+
type ReturnedSteps = {
10+
escapedStepNames: string[]
11+
links: string[]
12+
defsStr: string
13+
}
14+
15+
export default class DiagramBuilder {
16+
private options: DiagramBuilderOptions
17+
static SPACING = "\t"
18+
19+
constructor(options: DiagramBuilderOptions) {
20+
this.options = options
21+
}
22+
23+
buildDiagram(workflow: TransactionStepsDefinition): string {
24+
let diagram = `${this.getThemeConfig()}${
25+
this.options.theme ? this.getLinePrefix(1) : ""
26+
}flowchart TB`
27+
28+
const stepsDiagram = this.getSteps(workflow, this.options.theme ? 2 : 1)
29+
30+
diagram +=
31+
stepsDiagram.defsStr + `\n` + this.formatLinks(stepsDiagram.links)
32+
33+
return diagram
34+
}
35+
36+
getThemeConfig(): string {
37+
return this.options.theme
38+
? `%%{
39+
init: {
40+
'theme': 'base',
41+
'themeVariables': {
42+
'background': '#FFFFFF',
43+
'mainBkg': '#FFFFFF',
44+
'primaryColor': '#FFFFFF',
45+
'primaryTextColor': '#030712',
46+
'primaryBorderColor': '#D1D5DB',
47+
'nodeBorder': '#D1D5DB',
48+
'lineColor': '#11181C',
49+
'fontFamily': 'Inter',
50+
'fontSize': '13px',
51+
'tertiaryColor': '#F3F4F6',
52+
'tertiaryBorderColor': '#D1D5DB',
53+
'tertiaryTextColor': '#030712'
54+
}
55+
}
56+
}%%`
57+
: ""
58+
}
59+
60+
getSteps(
61+
flow: TransactionStepsDefinition | TransactionStepsDefinition[],
62+
level: number
63+
): ReturnedSteps {
64+
const links: string[] = []
65+
let defsStr = ""
66+
const escapedStepNames: string[] = []
67+
const linePrefix = this.getLinePrefix(level)
68+
69+
const flowArr: TransactionStepsDefinition[] | undefined = Array.isArray(
70+
flow
71+
)
72+
? flow
73+
: !flow.action && Array.isArray(flow.next)
74+
? flow.next
75+
: undefined
76+
77+
if (flowArr) {
78+
// these are steps running in parallel
79+
// since there are changes where the flowArr contains
80+
// one item, we check the length before treating the
81+
// main steps as steps running in parallel
82+
const areStepsParallel = flowArr.length > 1
83+
const parallelDefinitions: Record<string, string> = {}
84+
flowArr.forEach((flowItem) => {
85+
const flowSteps = this.getSteps(flowItem, level)
86+
if (areStepsParallel) {
87+
const escapedName = this.getEscapedStepName(flowItem.action)
88+
if (escapedName) {
89+
const itemDefinition = `${linePrefix}${escapedName}(${this.formatStepName(
90+
flowItem.action!
91+
)})`
92+
parallelDefinitions[itemDefinition] = flowSteps.defsStr.replace(
93+
itemDefinition,
94+
""
95+
)
96+
} else {
97+
// if the step doesn't have an action name
98+
// we just show it as a regular step rather than
99+
// a subgraph
100+
defsStr += `${linePrefix}${flowSteps.defsStr}`
101+
}
102+
} else {
103+
// if the steps aren't parallel
104+
// just show them as regular steps
105+
defsStr += `${linePrefix}${flowSteps.defsStr}`
106+
}
107+
links.push(...flowSteps.links)
108+
escapedStepNames.push(...flowSteps.escapedStepNames)
109+
})
110+
111+
// if there are steps in parallel,
112+
// we show them as a subgraph
113+
const definitionKeys = Object.keys(parallelDefinitions)
114+
if (definitionKeys.length) {
115+
defsStr += `${this.getSubgraph(
116+
definitionKeys.join(""),
117+
linePrefix
118+
)}${linePrefix}${Object.values(parallelDefinitions).join("")}`
119+
}
120+
} else {
121+
const flowItem = flow as TransactionStepsDefinition
122+
const escapedName = this.getEscapedStepName(flowItem.action)
123+
124+
if (escapedName.length) {
125+
escapedStepNames.push(escapedName)
126+
defsStr += `${linePrefix}${escapedName}(${this.formatStepName(
127+
flowItem.action!
128+
)})`
129+
}
130+
131+
if (flowItem.next) {
132+
const nextSteps = this.getSteps(flowItem.next, level)
133+
defsStr += `${linePrefix}${nextSteps.defsStr}`
134+
if (escapedName.length) {
135+
nextSteps.escapedStepNames.forEach((escapedStep) => {
136+
links.push(`${linePrefix}${escapedName} --> ${escapedStep}`)
137+
})
138+
} else {
139+
escapedStepNames.push(...nextSteps.escapedStepNames)
140+
}
141+
links.push(...nextSteps.links)
142+
}
143+
}
144+
145+
return {
146+
escapedStepNames,
147+
links,
148+
defsStr,
149+
}
150+
}
151+
152+
getSubgraph(defsStr: string, linePrefix: string): string {
153+
return `${linePrefix}subgraph parallel${getRandomString()} [Parallel]${linePrefix}${defsStr}${linePrefix}end`
154+
}
155+
156+
getEscapedStepName(originalName: string | undefined): string {
157+
return originalName?.replaceAll("-", "") || ""
158+
}
159+
160+
formatStepName(originalName: string): string {
161+
if (!this.options?.prettyNames) {
162+
return originalName
163+
}
164+
return originalName
165+
.replaceAll("-", " ")
166+
.replaceAll(/([A-Z])/g, " $1")
167+
.split(" ")
168+
.map((word) => `${word.charAt(0).toUpperCase()}${word.substring(1)}`)
169+
.join(" ")
170+
}
171+
172+
getLinePrefix(indentation = 0): string {
173+
return `\n${DiagramBuilder.SPACING.repeat(indentation)}`
174+
}
175+
176+
// TODO need to explore with this function
177+
// for now it just returns the joined links, but
178+
// it should split links on multiple lines in the
179+
// diagram
180+
formatLinks(links: string[], level = 2): string {
181+
const linePrefix = this.getLinePrefix(level)
182+
return links.join(linePrefix)
183+
184+
// This is used to ensure that a line doesn't get too long
185+
// let nodesInCurrentLine = 0
186+
// // TODO change this to be a command line option
187+
// const maxNodesInLine = 3
188+
189+
// if (links.length <= maxNodesInLine) {
190+
// return links.join(linePrefix)
191+
// }
192+
193+
// let finalStr = ""
194+
195+
// links.forEach((link) => {
196+
// if (nodesInCurrentLine === 0) {
197+
// finalStr += "subgraph"
198+
// }
199+
200+
// finalStr += link
201+
// ++nodesInCurrentLine
202+
// if (nodesInCurrentLine === maxNodesInLine) {
203+
// finalStr += "end"
204+
// nodesInCurrentLine = 0
205+
// }
206+
// })
207+
208+
// return finalStr
209+
}
210+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/* eslint-disable no-case-declarations */
2+
import { WorkflowManager } from "@medusajs/orchestration"
3+
import * as path from "path"
4+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs"
5+
import registerWorkflows from "../utils/register-workflows.js"
6+
import DiagramBuilder from "../classes/diagram-builder.js"
7+
import { run as runMermaid } from "@mermaid-js/mermaid-cli"
8+
9+
type Options = {
10+
output: string
11+
type: "docs" | "markdown" | "mermaid" | "console" | "svg" | "png" | "pdf"
12+
theme: boolean
13+
prettyNames: boolean
14+
}
15+
16+
export default async function (workflowPath: string, options: Options) {
17+
const workflowDefinitions = await registerWorkflows(workflowPath)
18+
19+
const diagramBuilder = new DiagramBuilder(options)
20+
21+
if (
22+
workflowDefinitions.size > 0 &&
23+
["svg", "png", "pdf"].includes(options.type)
24+
) {
25+
console.log(
26+
`Generating ${options.type} file(s) with mermaid. This may take some time...`
27+
)
28+
}
29+
30+
for (const [name, code] of workflowDefinitions) {
31+
const workflow = WorkflowManager.getWorkflow(name)
32+
33+
if (!workflow) {
34+
continue
35+
}
36+
37+
const diagram = diagramBuilder.buildDiagram(workflow.flow_)
38+
if (!existsSync(options.output)) {
39+
mkdirSync(options.output, { recursive: true })
40+
}
41+
42+
switch (options.type) {
43+
case "docs":
44+
const workflowPath = path.join(options.output, name)
45+
if (!existsSync(workflowPath)) {
46+
mkdirSync(workflowPath, { recursive: true })
47+
}
48+
// write files
49+
writeFileSync(path.join(workflowPath, "diagram.mermaid"), diagram)
50+
if (code) {
51+
writeFileSync(path.join(workflowPath, "code.ts"), code)
52+
}
53+
break
54+
case "mermaid":
55+
writeFileSync(path.join(options.output, `${name}.mermaid`), diagram)
56+
break
57+
case "markdown":
58+
writeFileSync(
59+
path.join(options.output, `${name}.md`),
60+
`\`\`\`mermaid\n${diagram}\n\`\`\``
61+
)
62+
break
63+
case "console":
64+
console.log(`Diagram for workflow ${name}:\n${diagram}`)
65+
break
66+
case "svg":
67+
case "png":
68+
case "pdf":
69+
const tempFilePath = path.join(options.output, `${name}.mermaid`)
70+
writeFileSync(tempFilePath, diagram)
71+
await runMermaid(
72+
tempFilePath,
73+
path.join(options.output, `${name}.${options.type}`),
74+
{
75+
quiet: true,
76+
}
77+
)
78+
rmSync(tempFilePath)
79+
}
80+
}
81+
82+
console.log(`Generated diagrams for ${workflowDefinitions.size} workflows.`)
83+
}

0 commit comments

Comments
 (0)