docs-util: added workflows typedoc plugin (#8463)

* initial changes

* finish workflow plugin

* add comments

* remove todo
This commit is contained in:
Shahed Nasser
2024-08-06 14:23:24 +03:00
committed by GitHub
parent 0ff5b975e7
commit c870302400
21 changed files with 576 additions and 5 deletions

View File

@@ -16,6 +16,8 @@
"type": "module",
"exports": "./dist/index.js",
"dependencies": {
"@medusajs/core-flows": "link:../../../../packages/core/core-flows",
"@medusajs/workflows-sdk": "link:../../../../packages/core/workflows-sdk",
"chalk": "^5.3.0",
"commander": "^11.1.0",
"dotenv": "^16.3.1",
@@ -25,6 +27,7 @@
"typedoc-plugin-custom": "*",
"typedoc-plugin-markdown-medusa": "*",
"typedoc-plugin-rename-defaults": "^0.7.0",
"typedoc-plugin-workflows": "*",
"types": "*",
"typescript": "5.5",
"utils": "*",

View File

@@ -19,6 +19,10 @@ export default async function generate(
) {
const references = names.includes("all") ? allReferences : names
if (references.includes("core-flows")) {
await import("@medusajs/core-flows")
}
for (const referenceName of references) {
const referenceType = getReferenceType(referenceName)
try {

View File

@@ -10,6 +10,13 @@ import { rootPathPrefix } from "./general.js"
import { modules } from "./references.js"
const customOptions: Record<string, Partial<TypeDocOptions>> = {
"core-flows": getOptions({
entryPointPath: "packages/core/core-flows/src/index.ts",
tsConfigName: "core-flows.json",
name: "core-flows",
plugin: ["typedoc-plugin-workflows"],
enableWorkflowsPlugins: true,
}),
"auth-provider": getOptions({
entryPointPath: "packages/core/utils/src/auth/abstract-auth-provider.ts",
tsConfigName: "utils.json",

View File

@@ -21,6 +21,7 @@ export const modules = [
const allReferences = [
...modules,
"core-flows",
"auth-provider",
"dml",
"file",

View File

@@ -1,6 +1,6 @@
import * as Handlebars from "handlebars"
import { Reflection, SignatureReflection } from "typedoc"
import { isWorkflowStep } from "../../utils/step-utils"
import { isWorkflowStep } from "utils"
export default function () {
Handlebars.registerHelper("example", function (reflection: Reflection) {

View File

@@ -1,6 +1,6 @@
import * as Handlebars from "handlebars"
import { SignatureReflection } from "typedoc"
import { isWorkflowStep } from "../../utils/step-utils"
import { isWorkflowStep } from "utils"
export default function () {
Handlebars.registerHelper(

View File

@@ -1,7 +1,7 @@
import { MarkdownTheme } from "../../theme"
import * as Handlebars from "handlebars"
import { SignatureReflection } from "typedoc"
import { getStepInputType } from "../../utils/step-utils"
import { getStepInputType } from "utils"
import { formatParameterComponent } from "../../utils/format-parameter-component"
import { getReflectionTypeParameters } from "../../utils/reflection-type-parameters"

View File

@@ -1,7 +1,7 @@
import { MarkdownTheme } from "../../theme"
import * as Handlebars from "handlebars"
import { SignatureReflection } from "typedoc"
import { getStepOutputType } from "../../utils/step-utils"
import { getStepOutputType } from "utils"
import { formatParameterComponent } from "../../utils/format-parameter-component"
import { getReflectionTypeParameters } from "../../utils/reflection-type-parameters"

View File

@@ -0,0 +1,2 @@
dist
.yarn

View File

@@ -0,0 +1,44 @@
{
"name": "typedoc-plugin-workflows",
"private": true,
"version": "0.0.0",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"description": "A plugin that generates information relevant for workflows",
"main": "./dist/index.js",
"exports": "./dist/index.js",
"files": [
"dist"
],
"author": "Shahed Nasser",
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"lint": "eslint --ext .ts src"
},
"peerDependencies": {
"typedoc": "0.26.x"
},
"devDependencies": {
"@types/eslint": "^8.56.6",
"@types/node": "^16.11.10",
"types": "*",
"typescript": "5.5"
},
"keywords": [
"typedocplugin",
"packages",
"monorepo",
"typedoc"
],
"dependencies": {
"@medusajs/core-flows": "link:../../../../packages/core/core-flows",
"@medusajs/workflows-sdk": "link:../../../../packages/core/workflows-sdk",
"eslint": "^8.53.0",
"glob": "^10.3.10",
"utils": "*",
"yaml": "^2.3.3"
}
}

View File

@@ -0,0 +1,6 @@
import { Application } from "typedoc"
import WorkflowsPlugin from "./plugin"
export function load(app: Application) {
new WorkflowsPlugin(app)
}

View File

@@ -0,0 +1,315 @@
import {
Application,
Comment,
Context,
Converter,
DeclarationReflection,
DocumentReflection,
ParameterReflection,
ParameterType,
ReferenceType,
ReflectionKind,
SignatureReflection,
} from "typedoc"
import ts, { SyntaxKind, VariableStatement } from "typescript"
import { WorkflowManager, WorkflowDefinition } from "@medusajs/orchestration"
import Helper from "./utils/helper"
import { isWorkflow } from "utils"
/**
* A plugin that extracts a workflow's steps, hooks, their types, and attaches them as
* documents to the workflow's reflection.
*/
class WorkflowsPlugin {
protected app: Application
protected helper: Helper
constructor(app: Application) {
this.app = app
this.helper = new Helper()
this.registerOptions()
this.registerEventHandlers()
}
/**
* Register the plugin's options.
*/
registerOptions() {
this.app.options.addDeclaration({
name: "enableWorkflowsPlugins",
help: "Whether to enable the workflows plugin.",
type: ParameterType.Boolean, // The default
defaultValue: false,
})
}
/**
* Register event handlers.
*/
registerEventHandlers() {
this.app.converter.on(
Converter.EVENT_RESOLVE_BEGIN,
this.handleResolve.bind(this)
)
}
/**
* When the converter begins resolving a project, this method is triggered. It finds
* all signatures that are workflows and attaches the necessary information to them.
*
* @param context - The project's context.
*/
handleResolve(context: Context) {
for (const reflection of context.project.getReflectionsByKind(
ReflectionKind.All
)) {
if (!(reflection instanceof SignatureReflection)) {
continue
}
if (isWorkflow(reflection)) {
const { initializer } =
this.helper.getReflectionSymbolAndInitializer({
project: context.project,
reflection: reflection.parent,
}) || {}
if (
!initializer ||
(!ts.isArrowFunction(initializer.arguments[1]) &&
!ts.isFunctionExpression(initializer.arguments[1]))
) {
continue
}
const workflowId = this.helper.getStepOrWorkflowId(
initializer,
context.project
)
if (!workflowId) {
continue
}
this.parseSteps({
workflowId,
constructorFn: initializer.arguments[1],
context,
parentReflection: reflection.parent,
})
}
}
}
/**
* Parse the steps of a workflow and attach them as documents to the parent reflection.
*
* @param param0 - The workflow's details.
*/
parseSteps({
workflowId,
constructorFn,
context,
parentReflection,
}: {
workflowId: string
constructorFn: ts.ArrowFunction | ts.FunctionExpression
context: Context
parentReflection: DeclarationReflection
}) {
// use the workflow manager to check whether something in the constructor
// body is a step/hook
const workflow = WorkflowManager.getWorkflow(workflowId)
if (!ts.isBlock(constructorFn.body)) {
return
}
if (!parentReflection.documents) {
parentReflection.documents = []
}
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
}
if (!initializer) {
return
}
const { stepId, stepReflection } =
this.parseStep({
initializer,
context,
workflow,
}) || {}
if (!stepId || !stepReflection) {
return
}
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)
})
}
/**
* Parse a step to retrieve its ID and reflection.
*
* @param param0 - The step's details.
* @returns The step's ID and reflection, if found.
*/
parseStep({
initializer,
context,
workflow,
}: {
initializer: ts.CallExpression
context: Context
workflow?: WorkflowDefinition
}):
| {
stepId: string
stepReflection: DeclarationReflection
}
| undefined {
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
}
const { initializer } =
this.helper.getReflectionSymbolAndInitializer({
project: context.project,
reflection: initializerReflection,
}) || {}
if (!initializer) {
return
}
stepId = this.helper.getStepOrWorkflowId(
initializer,
context.project,
true
)
stepReflection = initializerReflection
}
// check if is a step in the workflow
if (!stepId || !stepReflection || !workflow?.handlers_.get(stepId)) {
return
}
return {
stepId,
stepReflection,
}
}
/**
* This method creates a declaration reflection for a hook, since a hook doesn't have its own reflection.
*
* @param param0 - The hook's details.
* @returns The hook's reflection
*/
assembleHookReflection({
stepId,
context,
inputSymbol,
}: {
stepId: string
context: Context
inputSymbol: ts.Symbol
}): DeclarationReflection {
const declarationReflection = context.createDeclarationReflection(
ReflectionKind.Function,
undefined,
undefined,
stepId
)
const signatureReflection = new SignatureReflection(
stepId,
ReflectionKind.SomeSignature,
declarationReflection
)
const parameter = new ParameterReflection(
"input",
ReflectionKind.Parameter,
signatureReflection
)
parameter.type = ReferenceType.createSymbolReference(inputSymbol, context)
signatureReflection.parameters = []
signatureReflection.parameters.push(parameter)
declarationReflection.signatures = []
declarationReflection.signatures.push(signatureReflection)
return declarationReflection
}
}
export default WorkflowsPlugin

View File

@@ -0,0 +1,3 @@
export type StepType = "step" | "workflowStep" | "hook"
export type StepModifier = "@step" | "@workflowStep" | "@hook"

View File

@@ -0,0 +1,116 @@
import { DeclarationReflection, ProjectReflection } from "typedoc"
import ts from "typescript"
import { StepModifier, StepType } from "../types"
/**
* A class of helper methods.
*/
export default class Helper {
/**
* Remove from a name, preferrably a function expression's name, extraneous details.
*
* @param name - The name to normalize.
* @returns The normalized name.
*/
normalizeName(name: string) {
return name.replace(".runAsStep", "").replace(/^"/, "").replace(/"$/, "")
}
/**
* Get symbol and initializer of a reflection.
*
* @param param0 - The reflection's details.
* @returns The symbol and initializer, if found.
*/
getReflectionSymbolAndInitializer({
project,
reflection,
}: {
project: ProjectReflection
reflection: DeclarationReflection
}):
| {
symbol: ts.Symbol
initializer: ts.CallExpression
}
| undefined {
const symbol = project.getSymbolFromReflection(reflection)
if (
!symbol ||
!symbol.valueDeclaration ||
!("initializer" in symbol.valueDeclaration)
) {
return
}
return {
symbol,
initializer: symbol.valueDeclaration.initializer as ts.CallExpression,
}
}
/**
* Get the ID of a step or a workflow.
*
* @param initializer - The associated initializer. For example, `createWorkflow`.
* @param project - The typedoc project.
* @param checkWorkflowStep - Whether to check if a workflow is a step. If enabled, the `-as-step` suffix is added
* to the ID. This is useful when testing against the workflow's steps retrieved by the workflow manager.
* @returns The ID of the step or workflow.
*/
getStepOrWorkflowId(
initializer: ts.CallExpression,
project: ProjectReflection,
checkWorkflowStep = false
): string | undefined {
const idVar = initializer.arguments[0]
const isWorkflowStep =
checkWorkflowStep && this.getStepType(initializer) === "workflowStep"
const idVarName = this.normalizeName(idVar.getText())
// load it from the project
const idVarReflection = project.getChildByName(idVarName)
if (
!idVarReflection ||
!(idVarReflection instanceof DeclarationReflection) ||
idVarReflection.type?.type !== "literal"
) {
return
}
const stepId = idVarReflection.type.value as string
return isWorkflowStep ? `${stepId}-as-step` : stepId
}
/**
* Get the type of the step.
*
* @param initializer - The initializer of the step.
* @returns The step's type.
*/
getStepType(initializer: ts.CallExpression): StepType {
switch (initializer.expression.getText()) {
case "createWorkflow":
return "workflowStep"
case "createHook":
return "hook"
default:
return "step"
}
}
/**
* Get the modifier to use based on the step's type.
*
* @param initializer - The step's initializer.
* @returns The step's modifier.
*/
getModifier(initializer: ts.CallExpression): StepModifier {
const stepType = this.getStepType(initializer)
return `@${stepType}`
}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}

View File

@@ -257,6 +257,11 @@ export declare module "typedoc" {
* @defaultValue false
*/
normalizeDmlTypes: boolean
/**
* Whether to enable the workflows plugin.
* @defaultValue false
*/
enableWorkflowsPlugins: boolean
}
}

View File

@@ -11,7 +11,8 @@
"scripts": {
"build": "yarn clean && tsc",
"lint": "eslint --ext .ts src",
"clean": "rimraf dist"
"clean": "rimraf dist",
"watch": "tsc --watch"
},
"peerDependencies": {
"typedoc": "0.26.x"

View File

@@ -2,5 +2,7 @@ export * from "./dml-utils"
export * from "./get-type-children"
export * from "./get-project-child"
export * from "./get-type-str"
export * from "./step-utils"
export * from "./str-formatting"
export * from "./str-utils"
export * from "./workflow-utils"

View File

@@ -0,0 +1,8 @@
import { SignatureReflection } from "typedoc"
export function isWorkflow(reflection: SignatureReflection): boolean {
return (
reflection.parent.children?.some((child) => child.name === "runAsStep") ||
false
)
}

View File

@@ -615,6 +615,18 @@ __metadata:
languageName: node
linkType: hard
"@medusajs/core-flows@link:../../../../packages/core/core-flows::locator=typedoc-generate-references%40workspace%3Apackages%2Ftypedoc-generate-references":
version: 0.0.0-use.local
resolution: "@medusajs/core-flows@link:../../../../packages/core/core-flows::locator=typedoc-generate-references%40workspace%3Apackages%2Ftypedoc-generate-references"
languageName: node
linkType: soft
"@medusajs/core-flows@link:../../../../packages/core/core-flows::locator=typedoc-plugin-workflows%40workspace%3Apackages%2Ftypedoc-plugin-workflows":
version: 0.0.0-use.local
resolution: "@medusajs/core-flows@link:../../../../packages/core/core-flows::locator=typedoc-plugin-workflows%40workspace%3Apackages%2Ftypedoc-plugin-workflows"
languageName: node
linkType: soft
"@medusajs/modules-sdk@npm:^1.12.4":
version: 1.12.4
resolution: "@medusajs/modules-sdk@npm:1.12.4"
@@ -666,6 +678,18 @@ __metadata:
languageName: node
linkType: hard
"@medusajs/workflows-sdk@link:../../../../packages/core/workflows-sdk::locator=typedoc-generate-references%40workspace%3Apackages%2Ftypedoc-generate-references":
version: 0.0.0-use.local
resolution: "@medusajs/workflows-sdk@link:../../../../packages/core/workflows-sdk::locator=typedoc-generate-references%40workspace%3Apackages%2Ftypedoc-generate-references"
languageName: node
linkType: soft
"@medusajs/workflows-sdk@link:../../../../packages/core/workflows-sdk::locator=typedoc-plugin-workflows%40workspace%3Apackages%2Ftypedoc-plugin-workflows":
version: 0.0.0-use.local
resolution: "@medusajs/workflows-sdk@link:../../../../packages/core/workflows-sdk::locator=typedoc-plugin-workflows%40workspace%3Apackages%2Ftypedoc-plugin-workflows"
languageName: node
linkType: soft
"@medusajs/workflows-sdk@npm:latest":
version: 0.1.0
resolution: "@medusajs/workflows-sdk@npm:0.1.0"
@@ -5318,6 +5342,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "typedoc-generate-references@workspace:packages/typedoc-generate-references"
dependencies:
"@medusajs/core-flows": "link:../../../../packages/core/core-flows"
"@medusajs/workflows-sdk": "link:../../../../packages/core/workflows-sdk"
"@types/node": ^20.9.4
"@types/pluralize": ^0.0.33
chalk: ^5.3.0
@@ -5330,6 +5356,7 @@ __metadata:
typedoc-plugin-custom: "*"
typedoc-plugin-markdown-medusa: "*"
typedoc-plugin-rename-defaults: ^0.7.0
typedoc-plugin-workflows: "*"
types: "*"
typescript: 5.5
utils: "*"
@@ -5383,6 +5410,25 @@ __metadata:
languageName: node
linkType: hard
"typedoc-plugin-workflows@*, typedoc-plugin-workflows@workspace:packages/typedoc-plugin-workflows":
version: 0.0.0-use.local
resolution: "typedoc-plugin-workflows@workspace:packages/typedoc-plugin-workflows"
dependencies:
"@medusajs/core-flows": "link:../../../../packages/core/core-flows"
"@medusajs/workflows-sdk": "link:../../../../packages/core/workflows-sdk"
"@types/eslint": ^8.56.6
"@types/node": ^16.11.10
eslint: ^8.53.0
glob: ^10.3.10
types: "*"
typescript: 5.5
utils: "*"
yaml: ^2.3.3
peerDependencies:
typedoc: 0.26.x
languageName: unknown
linkType: soft
"typedoc@npm:^0.26.2":
version: 0.26.2
resolution: "typedoc@npm:0.26.2"