docs-util: add examples generator for workflows and steps (#10914)

* initial changes

* docs-util: generate examples for workflows and steps
This commit is contained in:
Shahed Nasser authored and GitHub committed 2025-01-10 19:16:43 +02:00
1 parent e82645ba55
commit c5915451b8
17 files changed
+810 -142

No files matched your search

@@ -17,7 +17,6 @@
"type": "module",
"exports": "./dist/index.js",
"dependencies": {
"@faker-js/faker": "^8.4.0",
"@octokit/core": "^5.0.2",
"chalk": "^5.4.1",
"commander": "^11.1.0",
@@ -27,7 +26,6 @@
"openai": "^4.29.1",
"openapi-types": "^12.1.3",
"pluralize": "^8.0.0",
"prettier": "^3.2.4",
"ts-node": "^10.9.1",
"typescript": "^5.6.2",
"utils": "*",
@@ -1,8 +1,13 @@
import { faker } from "@faker-js/faker"
import { OpenAPIV3 } from "openapi-types"
import { OasArea } from "../kinds/oas.js"
import { CodeSample } from "../../types/index.js"
import { capitalize, kebabToCamel, wordsToCamel, wordsToKebab } from "utils"
import {
capitalize,
getFakeStrValue,
kebabToCamel,
wordsToCamel,
wordsToKebab,
} from "utils"
import { API_ROUTE_PARAM_REGEX } from "../../constants.js"
type CodeSampleData = Omit<CodeSample, "source">
@@ -274,7 +279,7 @@ class OasExamplesGenerator {
? this.getSchemaRequiredData(
typedChildProp as OpenAPIV3.SchemaObject
)
: this.getFakeValue({
: getFakeStrValue({
name: childName,
type: typedChildProp.type,
format: typedChildProp.format,
@@ -296,7 +301,7 @@ class OasExamplesGenerator {
? this.getSchemaRequiredData(
property.items as OpenAPIV3.SchemaObject
)
: this.getFakeValue({
: getFakeStrValue({
name: propertyName,
type: propertyItems.type,
format: propertyItems.format,
@@ -305,7 +310,7 @@ class OasExamplesGenerator {
}
} else if (property.type) {
// retrieve fake value for all other types
value = this.getFakeValue({
value = getFakeStrValue({
name: propertyName,
type: property.type,
format: property.format,
@@ -320,64 +325,6 @@ class OasExamplesGenerator {
return data
}
/**
* Retrieve the fake value of a property. The value is used in examples.
*
* @param param0 - The property's details
* @returns The fake value
*/
getFakeValue({
name,
type,
format,
}: {
/**
* The name of the property. It can help when generating the fake value.
* For example, if the name is `id`, the fake value generated will be of the format `id_<randomstring>`.
*/
name: string
/**
* The type of the property.
*/
type: OpenAPIV3.NonArraySchemaObjectType | "array"
/**
* The OAS format of the property. For example, `date-time`.
*/
format?: string
}): unknown {
let value: unknown
switch (true) {
case type === "string" && format === "date-time":
value = faker.date.future().toISOString()
break
case type === "boolean":
value = faker.datatype.boolean()
break
case type === "integer" || type === "number":
value = faker.number.int()
break
case type === "array":
value = []
break
case type === "string":
value = faker.helpers
.mustache(`{{${name}}}`, {
id: () =>
`id_${faker.string.alphanumeric({
length: { min: 10, max: 20 },
})}`,
name: () => faker.person.firstName(),
email: () => faker.internet.email(),
password: () => faker.internet.password({ length: 8 }),
currency: () => faker.finance.currencyCode(),
})
.replace(`{{${name}}}`, "{value}")
}
return value !== undefined ? value : "{value}"
}
}
export default OasExamplesGenerator
@@ -4,8 +4,8 @@ import path from "path"
import dirname from "../../utils/dirname.js"
import { minimatch } from "minimatch"
import { existsSync } from "fs"
import * as prettier from "prettier"
import getRelativePaths from "../../utils/get-relative-paths.js"
import { formatWithPrettier } from "utils"
/**
* A class used to apply formatting to files using ESLint and other formatting options.
@@ -174,10 +174,7 @@ class Formatter {
content: string,
fileName: string
): Promise<string> {
const prettifiedContent = await this.formatStrWithPrettier(
content,
fileName
)
const prettifiedContent = await formatWithPrettier(content, fileName)
const relevantConfig = await this.getESLintOverridesConfigForFile(fileName)
const eslint = new ESLint({
@@ -203,27 +200,6 @@ class Formatter {
return newContent
}
/**
* Format a file's content with prettier.
*
* @param content - The content to format.
* @param fileName - The name of the file the content belongs to.
* @returns The formatted content
*/
async formatStrWithPrettier(
content: string,
fileName: string
): Promise<string> {
// load config of the file
const prettierConfig = (await prettier.resolveConfig(fileName)) || undefined
if (prettierConfig && !prettierConfig.parser) {
prettierConfig.parser = "babel-ts"
}
return await prettier.format(content, prettierConfig)
}
/**
* Applies all formatting types to a string.
*
@@ -22,6 +22,7 @@
"typedoc": "0.27.x"
},
"devDependencies": {
"@types/js-beautify": "^1.14.3",
"@types/node": "^20.12.10",
"copyfiles": "^2.4.1",
"typedoc": "^0.27.5",
@@ -36,6 +37,7 @@
],
"dependencies": {
"handlebars": "^4.7.8",
"js-beautify": "^1.15.1",
"utils": "*"
}
}
@@ -78,6 +78,8 @@ import ifMemberShowTitleHelper from "./resources/helpers/if-member-show-title.js
import signatureCommentHelper from "./resources/helpers/signature-comment.js"
import versionHelper from "./resources/helpers/version.js"
import sourceCodeLinkHelper from "./resources/helpers/source-code-link.js"
import workflowExamplesHelper from "./resources/helpers/workflow-examples.js"
import stepExamplesHelper from "./resources/helpers/step-examples.js"
import { MarkdownTheme } from "./theme.js"
import { getDirname } from "utils"
@@ -187,4 +189,6 @@ export function registerHelpers(theme: MarkdownTheme) {
signatureCommentHelper()
versionHelper()
sourceCodeLinkHelper()
workflowExamplesHelper()
stepExamplesHelper()
}
@@ -0,0 +1,60 @@
import Handlebars from "handlebars"
import { DeclarationReflection, SignatureReflection } from "typedoc"
import { getReflectionTypeFakeValueStr, getStepInputType } from "utils"
import pkg from "js-beautify"
const { js_beautify } = pkg
export default function () {
Handlebars.registerHelper(
"stepExamples",
function (this: SignatureReflection): string {
const stepReflection = this.parent
const exampleTags = stepReflection.comment?.blockTags.filter(
(tag) => tag.tag === "@example"
)
if (exampleTags?.length) {
return Handlebars.helpers.example(stepReflection)
}
return generateStepExample(stepReflection)
}
)
}
function generateStepExample(stepReflection: DeclarationReflection): string {
if (!stepReflection.signatures?.length) {
return ""
}
const inputType = getStepInputType(stepReflection.signatures[0])
const inputStr = inputType
? `${getReflectionTypeFakeValueStr({
reflectionType: inputType,
name: "",
})}`
: ""
// generate example
return `
\`\`\`ts title="src/workflows/my-workflow.ts"
${js_beautify(
`import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { ${stepReflection.name} } from "@medusajs/medusa/core-flows"
const myWorkflow = createWorkflow(
"my-workflow",
() => {
const data = ${stepReflection.name}(${inputStr})
}
)`,
{
indent_size: 2,
brace_style: "preserve-inline",
wrap_line_length: 80,
}
)}
\`\`\`
`
}
@@ -0,0 +1,179 @@
import Handlebars from "handlebars"
import pkg from "js-beautify"
import { DeclarationReflection, SignatureReflection } from "typedoc"
import { getReflectionTypeFakeValueStr, getWorkflowInputType } from "utils"
const { js_beautify } = pkg
export default function () {
Handlebars.registerHelper(
"workflowExamples",
function (this: SignatureReflection): string {
const workflowReflection = this.parent
const exampleStr: string[] = []
const exampleTags = workflowReflection.comment?.blockTags.filter(
(tag) => tag.tag === "@example"
)
if (!exampleTags?.length) {
exampleStr.push(
getExecutionCodeTabs({
exampleCode: generateWorkflowExample(workflowReflection),
workflowName: workflowReflection.name,
})
)
} else {
exampleTags.forEach((exampleTag) => {
exampleTag.content.forEach((part) => {
if (part.kind !== "code") {
exampleStr.push(part.text)
return
}
exampleStr.push(
getExecutionCodeTabs({
exampleCode: part.text,
workflowName: workflowReflection.name,
})
)
})
})
}
return exampleStr.join("\n")
}
)
}
function getExecutionCodeTabs({
exampleCode,
workflowName,
}: {
exampleCode: string
workflowName: string
}): string {
exampleCode = exampleCode.replace("```ts\n", "").replace("\n```", "")
const beautifyOptions: pkg.JSBeautifyOptions = {
indent_size: 2,
brace_style: "preserve-inline",
wrap_line_length: 80,
}
return `<CodeTabs group="workflow-exection">
<CodeTab label="Another Workflow" value="another-workflow">
\`\`\`ts title="src/workflows/my-workflow.ts"
${js_beautify(
`import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { ${workflowName} } from "@medusajs/medusa/core-flows"
const myWorkflow = createWorkflow(
"my-workflow",
() => {
${exampleCode
.replace(`{ result }`, "result")
.replace(`await `, "")
.replace(`(container)\n\t.run(`, ".runAsStep(")}
}
)`,
beautifyOptions
)}
\`\`\`
</CodeTab>
<CodeTab label="API Route" value="api-route">
\`\`\`ts title="src/api/workflow/route.ts"
${js_beautify(
`import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { ${workflowName} } from "@medusajs/medusa/core-flows"
export async function POST(
req: MedusaRequest,
res: MedusaResponse
) {
${exampleCode.replace("container", "req.scope")}
res.send(result)
}
`,
beautifyOptions
)}
\`\`\`
</CodeTab>
<CodeTab label="Subscriber" value="subscriber">
\`\`\`ts title="src/subscribers/order-placed.ts"
${js_beautify(
`import {
type SubscriberConfig,
type SubscriberArgs,
} from "@medusajs/framework"
import { ${workflowName} } from "@medusajs/medusa/core-flows"
export default async function handleOrderPlaced({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
${exampleCode}
console.log(result)
}
export const config: SubscriberConfig = {
event: "order.placed",
}`,
beautifyOptions
)}
\`\`\`
</CodeTab>
<CodeTab label="Scheduled Job" value="scheduled-job">
\`\`\`ts title="src/jobs/message-daily.ts"
${js_beautify(
`import { MedusaContainer } from "@medusajs/framework/types"
import { ${workflowName} } from "@medusajs/medusa/core-flows"
export default async function myCustomJob(
container: MedusaContainer
) {
${exampleCode}
console.log(result)
}
export const config = {
name: "run-once-a-day",
schedule: "0 0 * * *",
}`,
beautifyOptions
)}
\`\`\`
</CodeTab>
</CodeTabs>`
}
function generateWorkflowExample(
workflowReflection: DeclarationReflection
): string {
if (!workflowReflection.signatures?.length) {
return ""
}
const inputType = getWorkflowInputType(workflowReflection.signatures[0])
const inputStr = inputType
? `{\n\t\tinput: ${getReflectionTypeFakeValueStr({
reflectionType: inputType,
name: "",
})}\n\t}`
: ""
// generate example
return `const { result } = await ${workflowReflection.name}(container)\n\t.run(${inputStr})`
}
@@ -10,7 +10,7 @@
{{#if (sectionEnabled "member_signature_example")}}
{{{example this}}}
{{{stepExamples this}}}
{{/if}}
@@ -12,7 +12,7 @@
{{#if (sectionEnabled "member_signature_example")}}
{{{example this}}}
{{{workflowExamples this}}}
{{/if}}
@@ -24,6 +24,7 @@ import {
getUniqueStrArray,
} from "utils"
import { StepType } from "./types.js"
import Examples from "./utils/examples.js"
type ParsedStep = {
stepReflection: DeclarationReflection
@@ -38,6 +39,7 @@ type ParsedStep = {
class WorkflowsPlugin {
protected app: Application
protected helper: Helper
protected examplesHelper: Examples
protected workflowsTagsMap: Map<string, string[]>
protected addTagsAfterParsing: {
[k: string]: {
@@ -49,6 +51,7 @@ class WorkflowsPlugin {
constructor(app: Application) {
this.app = app
this.helper = new Helper()
this.examplesHelper = new Examples()
this.workflowsTagsMap = new Map()
this.addTagsAfterParsing = {}
@@ -526,7 +529,7 @@ class WorkflowsPlugin {
new CommentTag(`@example`, [
{
kind: "code",
text: this.helper.generateHookExample({
text: this.examplesHelper.generateHookExample({
hookName: stepId,
workflowName,
parameter,
@@ -0,0 +1,35 @@
import { DeclarationReflection, ParameterReflection } from "typedoc"
export default class Examples {
generateHookExample({
hookName,
workflowName,
parameter,
}: {
hookName: string
workflowName: string
parameter: ParameterReflection
}): string {
let str = `import { ${workflowName} } from "@medusajs/medusa/core-flows"\n\n`
str += `${workflowName}.hooks.${hookName}(\n\t(async ({`
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
}
}
@@ -1,8 +1,4 @@
import {
DeclarationReflection,
ParameterReflection,
ProjectReflection,
} from "typedoc"
import { DeclarationReflection, ProjectReflection } from "typedoc"
import ts from "typescript"
import { StepModifier, StepType } from "../types.js"
import { capitalize, findReflectionInNamespaces } from "utils"
@@ -271,38 +267,6 @@ export default class Helper {
return `@${stepType}`
}
generateHookExample({
hookName,
workflowName,
parameter,
}: {
hookName: string
workflowName: string
parameter: ParameterReflection
}): string {
let str = `import { ${workflowName} } from "@medusajs/medusa/core-flows"\n\n`
str += `${workflowName}.hooks.${hookName}(\n\t(async ({`
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
}
getCallExpressionFromBody(
body: ts.ConciseBody
): ts.CallExpression | undefined {
+2
View File
@@ -23,7 +23,9 @@
"typescript": "^5.6.2"
},
"dependencies": {
"@faker-js/faker": "^9.3.0",
"octokit": "^3.1.2",
"prettier": "^3.4.2",
"rimraf": "^5.0.5"
}
}
@@ -0,0 +1,319 @@
import { faker } from "@faker-js/faker"
import {
DeclarationReflection,
LiteralType,
ReflectionFlags,
SomeType,
} from "typedoc"
import * as prettier from "prettier"
import { getTypeChildren } from "./get-type-children.js"
const MAX_LEVEL = 7
export function isReflectionTypeOptional(
reflection: DeclarationReflection
): boolean {
return "flags" in reflection
? (reflection.flags as ReflectionFlags).isOptional
: false
}
function getReflectionTypeFakeValue({
reflectionType,
name,
level = 1,
}: {
reflectionType: SomeType
name: string
level?: number
}): unknown {
if (reflectionType.type === "literal") {
return getFakeStrValue({
name,
type: typeof reflectionType.value,
})
}
if (reflectionType.type === "intrinsic") {
return getFakeStrValue({
name,
type: reflectionType.name,
})
}
if (level > MAX_LEVEL) {
return reflectionType.type === "array" ? [] : {}
}
if (reflectionType.type === "array") {
return new Array(
getReflectionTypeFakeValue({
reflectionType: reflectionType.elementType,
name,
level: level + 1,
})
)
}
if (reflectionType.type === "reflection") {
if (reflectionType.declaration.type) {
return getReflectionTypeFakeValue({
reflectionType: reflectionType.declaration.type,
name,
level: level + 1,
})
}
if (reflectionType.declaration.children) {
const obj: Record<string, unknown> = {}
reflectionType.declaration.children.forEach((child) => {
if (!child.type || isReflectionTypeOptional(child)) {
return
}
obj[child.name] = getReflectionTypeFakeValue({
reflectionType: child.type,
name: child.name,
level: level + 1,
})
})
return obj
}
return {}
}
if (
reflectionType.type === "reference" &&
reflectionType.reflection instanceof DeclarationReflection
) {
if (reflectionType.reflection.name === "BigNumberInput") {
return getFakeStrValue({
name,
type: "number",
})
} else if (reflectionType.reflection.name === "Record") {
return getFakeStrValue({
name,
type: "object",
})
}
const obj: Record<string, unknown> = {}
const children = getTypeChildren({
reflectionType: reflectionType,
project: reflectionType.reflection.project,
})
if (!children.length) {
// check whether type is enum
if (reflectionType.reflection.type?.type === "union") {
const isEnum = reflectionType.reflection.type.types.every((type) => {
return type.type === "literal" && typeof type.value === "string"
})
if (isEnum) {
return getFakeStrValue({
name,
type: "enum",
data: {
enum: (
reflectionType.reflection.type.types as LiteralType[]
).reduce(
(acc, type) => {
acc[type.value as string] = type.value as string
return acc
},
{} as Record<string, string>
),
},
})
}
}
}
children.forEach((child) => {
if (!child.type || isReflectionTypeOptional(child)) {
return
}
obj[child.name] = getReflectionTypeFakeValue({
reflectionType: child.type,
name: child.name,
level: level + 1,
})
})
return obj
}
if (reflectionType.type === "intersection") {
const obj: Record<string, unknown> = {}
reflectionType.types?.forEach((type) => {
const value = getReflectionTypeFakeValue({
reflectionType: type,
name,
level: level + 1,
})
if (typeof value === "object") {
Object.assign(obj, value)
} else {
obj[name] = value
}
})
return obj
}
if (reflectionType.type === "union" && reflectionType.types.length) {
// check if it's type Record<>
const isRecord =
reflectionType.types[0].type === "reference" &&
reflectionType.types[0].name === "Record"
if (isRecord) {
return getFakeStrValue({
name,
type: "object",
})
}
return getReflectionTypeFakeValue({
reflectionType: reflectionType.types[0],
name,
level: level + 1,
})
}
// TODO: handle more types
return "{value}"
}
export function getReflectionTypeFakeValueStr({
reflectionType,
name,
}: {
reflectionType: SomeType
name: string
}): string {
const value = getReflectionTypeFakeValue({
reflectionType,
name,
})
return JSON.stringify(value, null, 2)
}
export function getFakeStrValue({
name,
type,
format,
data,
}: {
/**
* The name of the property. It can help when generating the fake value.
* For example, if the name is `id`, the fake value generated will be of the format `id_<randomstring>`.
*/
name: string
/**
* The type of the property, such as `string` or `boolean`.
*/
type: string
/**
* The format of the property, useful for OAS. For example, `date-time`.
*/
format?: string
/**
* Additional data to help generate the fake value.
*/
data?: {
enum: Record<string, string>
}
}): unknown {
let value: unknown
if (!format && name.endsWith("_at")) {
format = "date-time"
}
switch (true) {
case type === "string" && format === "date-time":
value = faker.date.future().toISOString()
break
case type === "boolean":
value = faker.datatype.boolean()
break
case type === "integer" || type === "number":
value = faker.number.int({ min: 0, max: 50 })
break
case type === "array":
value = []
break
case type === "enum" && data?.enum !== undefined:
value = faker.helpers.enumValue(data.enum)
break
case type === "object":
value = {}
break
case type === "string":
value = faker.helpers.mustache(`{{${name}}}`, {
id: () =>
`id_${faker.string.alphanumeric({
length: { min: 10, max: 20 },
})}`,
ids: () =>
`id_${faker.string.alphanumeric({
length: { min: 10, max: 20 },
})}`,
name: () => faker.person.firstName(),
email: () => faker.internet.email(),
password: () => faker.internet.password({ length: 8 }),
currency: () => faker.finance.currencyCode(),
title: () => faker.lorem.word(),
description: () => faker.lorem.sentence(),
url: () => faker.internet.url(),
phone: () => faker.phone.number(),
created_by: () =>
`user_${faker.string.alphanumeric({
length: { min: 10, max: 20 },
})}`,
updated_by: () =>
`user_${faker.string.alphanumeric({
length: { min: 10, max: 20 },
})}`,
rejected_by: () =>
`user_${faker.string.alphanumeric({
length: { min: 10, max: 20 },
})}`,
canceled_by: () =>
`user_${faker.string.alphanumeric({
length: { min: 10, max: 20 },
})}`,
price: () => faker.commerce.price(),
quantity: () => `${faker.number.int({ min: 1, max: 10 })}`,
})
try {
value = faker.helpers.fake(value as string)
} catch (e) {
// Ignore error
}
value = (value as string).replace(`{{${name}}}`, "{value}")
}
return value !== undefined ? value : "{value}"
}
export async function formatWithPrettier(
content: string,
fileName: string
): Promise<string> {
// load config of the file
const prettierConfig = (await prettier.resolveConfig(fileName)) || undefined
if (prettierConfig && !prettierConfig.parser) {
prettierConfig.parser = "babel-ts"
}
return await prettier.format(content, prettierConfig)
}
+1
View File
@@ -1,4 +1,5 @@
export * from "./dml-utils.js"
export * from "./examples-utils.js"
export * from "./general-utils.js"
export * from "./get-type-children.js"
export * from "./get-project-child.js"