diff --git a/www/apps/resources/app/references/[...slug]/page.tsx b/www/apps/resources/app/references/[...slug]/page.tsx index 10ff165f0b..d5be782a90 100644 --- a/www/apps/resources/app/references/[...slug]/page.tsx +++ b/www/apps/resources/app/references/[...slug]/page.tsx @@ -5,6 +5,7 @@ import { notFound } from "next/navigation" import { typeListLinkFixerPlugin, localLinksRehypePlugin, + workflowDiagramLinkFixerPlugin, } from "remark-rehype-plugins" import MDXComponents from "@/components/MDXComponents" import mdxOptions from "../../../mdx-options.mjs" @@ -47,7 +48,20 @@ export default async function ReferencesPage({ params }: PageProps) { mdxOptions: { rehypePlugins: [ ...mdxOptions.options.rehypePlugins, - [typeListLinkFixerPlugin, pluginOptions], + [ + typeListLinkFixerPlugin, + { + ...pluginOptions, + checkLinksType: "md", + }, + ], + [ + workflowDiagramLinkFixerPlugin, + { + ...pluginOptions, + checkLinksType: "value", + }, + ], [localLinksRehypePlugin, pluginOptions], ], remarkPlugins: mdxOptions.options.remarkPlugins, diff --git a/www/apps/resources/components/MDXComponents/index.tsx b/www/apps/resources/components/MDXComponents/index.tsx index 582c664de3..03d07a1cc6 100644 --- a/www/apps/resources/components/MDXComponents/index.tsx +++ b/www/apps/resources/components/MDXComponents/index.tsx @@ -1,10 +1,16 @@ import type { MDXComponents as MDXComponentsType } from "mdx/types" -import { Link, MDXComponents as UiMdxComponents, TypeList } from "docs-ui" +import { + Link, + MDXComponents as UiMdxComponents, + TypeList, + WorkflowDiagram, +} from "docs-ui" const MDXComponents: MDXComponentsType = { ...UiMdxComponents, a: Link, TypeList, + WorkflowDiagram, } export default MDXComponents diff --git a/www/apps/resources/next.config.mjs b/www/apps/resources/next.config.mjs index bfbfcbc74c..f31b291a9c 100644 --- a/www/apps/resources/next.config.mjs +++ b/www/apps/resources/next.config.mjs @@ -3,6 +3,7 @@ import { brokenLinkCheckerPlugin, localLinksRehypePlugin, typeListLinkFixerPlugin, + workflowDiagramLinkFixerPlugin, } from "remark-rehype-plugins" import { slugChanges } from "./generated/slug-changes.mjs" import mdxPluginOptions from "./mdx-options.mjs" @@ -15,6 +16,7 @@ const withMDX = mdx({ [brokenLinkCheckerPlugin], [localLinksRehypePlugin], [typeListLinkFixerPlugin], + [workflowDiagramLinkFixerPlugin], ], remarkPlugins: mdxPluginOptions.options.remarkPlugins, jsx: true, @@ -29,14 +31,16 @@ const nextConfig = { transpilePackages: ["docs-ui"], basePath: process.env.NEXT_PUBLIC_BASE_PATH || "/v2/resources", - async redirects() { - // redirect original file paths to the rewrite - return slugChanges.map((item) => ({ - source: item.origSlug, - destination: item.newSlug, - permanent: true, - })) - }, + // Redirects shouldn't be necessary anymore since we have remark / rehype + // plugins that fix links. But leaving this here in case we need it again. + // async redirects() { + // // redirect original file paths to the rewrite + // return slugChanges.map((item) => ({ + // source: item.origSlug, + // destination: item.newSlug, + // permanent: true, + // })) + // }, } export default withMDX(nextConfig) diff --git a/www/apps/resources/sidebar.mjs b/www/apps/resources/sidebar.mjs index 3a647b7822..a9fbb0da85 100644 --- a/www/apps/resources/sidebar.mjs +++ b/www/apps/resources/sidebar.mjs @@ -1888,6 +1888,13 @@ export const sidebar = sidebarAttachHrefCommonOptions([ isChildSidebar: true, autogenerate_path: "/references/helper_steps/functions", }, + // TODO uncomment once available. + // { + // title: "Medusa Workflows Reference", + // path: "/medusa-workflows-reference", + // isChildSidebar: true, + // custom_autogenerate: "core-flows", + // }, ], }, { diff --git a/www/packages/build-scripts/src/generate-sidebar.ts b/www/packages/build-scripts/src/generate-sidebar.ts index 2bba5fdf9f..4978d1d67e 100644 --- a/www/packages/build-scripts/src/generate-sidebar.ts +++ b/www/packages/build-scripts/src/generate-sidebar.ts @@ -1,15 +1,17 @@ import type { RawSidebarItemType } from "types" -import findPageHeading from "./utils/find-page-heading.js" -import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "fs" +import { existsSync, mkdirSync, readdirSync, statSync } from "fs" import path from "path" -import { sidebarAttachHrefCommonOptions } from "./index.js" -import { getFrontMatterUtil } from "remark-rehype-plugins" -import findMetadataTitle from "./utils/find-metadata-title.js" +import { getSidebarItemLink, sidebarAttachHrefCommonOptions } from "./index.js" +import getCoreFlowsRefSidebarChildren from "./utils/get-core-flows-ref-sidebar-children.js" -type ItemsToAdd = RawSidebarItemType & { +export type ItemsToAdd = RawSidebarItemType & { sidebar_position?: number } +const customGenerators: Record Promise> = { + "core-flows": getCoreFlowsRefSidebarChildren, +} + async function getSidebarItems( dir: string, nested = false @@ -52,32 +54,17 @@ async function getSidebarItems( continue } - const frontmatter = await getFrontMatterUtil(filePath) - if (frontmatter.sidebar_autogenerate_exclude) { + const newItem = await getSidebarItemLink({ + filePath, + basePath, + fileBasename, + }) + + if (!newItem) { continue } - const fileContent = frontmatter.sidebar_label - ? "" - : readFileSync(filePath, "utf-8") - - const newItem = sidebarAttachHrefCommonOptions([ - { - path: - frontmatter.slug || - filePath.replace(basePath, "").replace(`/${fileBasename}`, ""), - title: - frontmatter.sidebar_label || - findMetadataTitle(fileContent) || - findPageHeading(fileContent) || - "", - }, - ])[0] - - items.push({ - ...newItem, - sidebar_position: frontmatter.sidebar_position, - }) + items.push(newItem) mainPageIndex = items.length - 1 } @@ -108,9 +95,12 @@ async function checkItem( return child } ) - } - - if (item.children) { + } else if ( + item.custom_autogenerate && + Object.hasOwn(customGenerators, item.custom_autogenerate) + ) { + item.children = await customGenerators[item.custom_autogenerate]() + } else if (item.children) { item.children = await Promise.all( item.children.map(async (childItem) => await checkItem(childItem)) ) diff --git a/www/packages/build-scripts/src/index.ts b/www/packages/build-scripts/src/index.ts index 2a26c0e0f7..aac08a0e2d 100644 --- a/www/packages/build-scripts/src/index.ts +++ b/www/packages/build-scripts/src/index.ts @@ -2,4 +2,6 @@ export * from "./generate-sidebar.js" export * from "./utils/find-metadata-title.js" export * from "./utils/find-page-heading.js" +export * from "./utils/get-core-flows-ref-sidebar-children.js" +export * from "./utils/get-sidebar-item-link.js" export * from "./utils/sidebar-attach-href-common-options.js" diff --git a/www/packages/build-scripts/src/utils/get-core-flows-ref-sidebar-children.ts b/www/packages/build-scripts/src/utils/get-core-flows-ref-sidebar-children.ts new file mode 100644 index 0000000000..dee20cdbd3 --- /dev/null +++ b/www/packages/build-scripts/src/utils/get-core-flows-ref-sidebar-children.ts @@ -0,0 +1,88 @@ +import { getSidebarItemLink, ItemsToAdd } from "build-scripts" +import { existsSync, readdirSync } from "fs" +import path from "path" + +export default async function getCoreFlowsRefSidebarChildren(): Promise< + ItemsToAdd[] +> { + const projPath = path.resolve() + const basePath = path.join(projPath, "references", "core_flows") + + const directories = readdirSync(basePath, { + withFileTypes: true, + }) + + const sidebarItems: ItemsToAdd[] = [] + + for (const directory of directories) { + if ( + !directory.isDirectory() || + directory.name.startsWith("core_flows.") || + directory.name === "types" || + directory.name === "interfaces" + ) { + continue + } + + const namespaceBasePath = path.join(basePath, directory.name, "functions") + + if (!existsSync(namespaceBasePath)) { + continue + } + + const childDirs = readdirSync(namespaceBasePath, { + withFileTypes: true, + }) + + const workflowItems: ItemsToAdd[] = [] + const stepItems: ItemsToAdd[] = [] + + for (const childDir of childDirs) { + if (!childDir.isDirectory()) { + continue + } + + const childDirPath = path.join(namespaceBasePath, childDir.name) + const childFile = readdirSync(childDirPath) + + const sidebarItem = await getSidebarItemLink({ + filePath: path.join(childDirPath, childFile[0]), + basePath: projPath, + fileBasename: childFile[0], + }) + + if (sidebarItem) { + if (childDir.name.endsWith("Workflow")) { + workflowItems.push(sidebarItem) + } else { + stepItems.push(sidebarItem) + } + } + } + + if (workflowItems.length || stepItems.length) { + const item: ItemsToAdd = { + title: directory.name.replaceAll("_", " "), + children: [], + } + + if (workflowItems.length) { + item.children!.push({ + title: "Workflows", + children: workflowItems, + }) + } + + if (stepItems.length) { + item.children!.push({ + title: "Steps", + children: stepItems, + }) + } + + sidebarItems.push(item) + } + } + + return sidebarItems +} diff --git a/www/packages/build-scripts/src/utils/get-sidebar-item-link.ts b/www/packages/build-scripts/src/utils/get-sidebar-item-link.ts new file mode 100644 index 0000000000..fb61050a66 --- /dev/null +++ b/www/packages/build-scripts/src/utils/get-sidebar-item-link.ts @@ -0,0 +1,42 @@ +import { getFrontMatterUtil } from "remark-rehype-plugins" +import { ItemsToAdd, sidebarAttachHrefCommonOptions } from "../index.js" +import { readFileSync } from "fs" +import findMetadataTitle from "./find-metadata-title.js" +import findPageHeading from "./find-page-heading.js" + +export async function getSidebarItemLink({ + filePath, + basePath, + fileBasename, +}: { + filePath: string + basePath: string + fileBasename: string +}): Promise { + const frontmatter = await getFrontMatterUtil(filePath) + if (frontmatter.sidebar_autogenerate_exclude) { + return + } + + const fileContent = frontmatter.sidebar_label + ? "" + : readFileSync(filePath, "utf-8") + + const newItem = sidebarAttachHrefCommonOptions([ + { + path: + frontmatter.slug || + filePath.replace(basePath, "").replace(`/${fileBasename}`, ""), + title: + frontmatter.sidebar_label || + findMetadataTitle(fileContent) || + findPageHeading(fileContent) || + "", + }, + ])[0] + + return { + ...newItem, + sidebar_position: frontmatter.sidebar_position, + } +} diff --git a/www/packages/docs-ui/package.json b/www/packages/docs-ui/package.json index dbacafea67..d3eaf33c21 100644 --- a/www/packages/docs-ui/package.json +++ b/www/packages/docs-ui/package.json @@ -68,6 +68,7 @@ "@uidotdev/usehooks": "^2.4.1", "algoliasearch": "^4.20.0", "copy-text-to-clipboard": "^3.2.0", + "framer-motion": "^11.3.21", "mermaid": "^10.9.0", "npm-to-yarn": "^2.1.0", "prism-react-renderer": "2.3.1", diff --git a/www/packages/docs-ui/src/components/Navbar/MobileMenu/Button/index.tsx b/www/packages/docs-ui/src/components/Navbar/MobileMenu/Button/index.tsx index 837be6c593..a24c6bd6fb 100644 --- a/www/packages/docs-ui/src/components/Navbar/MobileMenu/Button/index.tsx +++ b/www/packages/docs-ui/src/components/Navbar/MobileMenu/Button/index.tsx @@ -3,7 +3,7 @@ import React from "react" import { NavbarIconButton, NavbarIconButtonProps } from "../../IconButton" import clsx from "clsx" -import { Sidebar, XMark } from "@medusajs/icons" +import { SidebarLeft, XMark } from "@medusajs/icons" export type NavbarMobileMenuButtonProps = { buttonProps?: NavbarIconButtonProps @@ -28,7 +28,7 @@ export const NavbarMobileMenuButton = ({ } }} > - {!mobileSidebarOpen && } + {!mobileSidebarOpen && } {mobileSidebarOpen && } ) diff --git a/www/packages/docs-ui/src/components/WorkflowDiagram/Arrow/End/index.tsx b/www/packages/docs-ui/src/components/WorkflowDiagram/Arrow/End/index.tsx new file mode 100644 index 0000000000..eb0bfaa24b --- /dev/null +++ b/www/packages/docs-ui/src/components/WorkflowDiagram/Arrow/End/index.tsx @@ -0,0 +1,19 @@ +import React from "react" + +export const WorkflowDiagramArrowEnd = () => { + return ( + + + + ) +} diff --git a/www/packages/docs-ui/src/components/WorkflowDiagram/Arrow/Horizontal/index.tsx b/www/packages/docs-ui/src/components/WorkflowDiagram/Arrow/Horizontal/index.tsx new file mode 100644 index 0000000000..e57f6dcade --- /dev/null +++ b/www/packages/docs-ui/src/components/WorkflowDiagram/Arrow/Horizontal/index.tsx @@ -0,0 +1,18 @@ +import React from "react" + +export const WorkflowDiagramArrowHorizontal = () => { + return ( + + + + ) +} diff --git a/www/packages/docs-ui/src/components/WorkflowDiagram/Arrow/Middle/index.tsx b/www/packages/docs-ui/src/components/WorkflowDiagram/Arrow/Middle/index.tsx new file mode 100644 index 0000000000..90a00a2fa6 --- /dev/null +++ b/www/packages/docs-ui/src/components/WorkflowDiagram/Arrow/Middle/index.tsx @@ -0,0 +1,19 @@ +import React from "react" + +export const WorkflowDiagramArrowMiddle = () => { + return ( + + + + ) +} diff --git a/www/packages/docs-ui/src/components/WorkflowDiagram/Arrow/index.tsx b/www/packages/docs-ui/src/components/WorkflowDiagram/Arrow/index.tsx new file mode 100644 index 0000000000..8de1e27ac7 --- /dev/null +++ b/www/packages/docs-ui/src/components/WorkflowDiagram/Arrow/index.tsx @@ -0,0 +1,35 @@ +import React from "react" +import { WorkflowDiagramArrowHorizontal } from "./Horizontal" +import { WorkflowDiagramArrowEnd } from "./End" +import { WorkflowDiagramArrowMiddle } from "./Middle" + +export type WorkflowDiagramArrowProps = { + depth: number +} + +export const WorkflowDiagramArrow = ({ depth }: WorkflowDiagramArrowProps) => { + if (depth === 1) { + return + } + + if (depth === 2) { + return ( +
+ + +
+ ) + } + + const inbetween = Array.from({ length: depth - 2 }).map((_, index) => ( + + )) + + return ( +
+ + {inbetween} + +
+ ) +} diff --git a/www/packages/docs-ui/src/components/WorkflowDiagram/Depth/index.tsx b/www/packages/docs-ui/src/components/WorkflowDiagram/Depth/index.tsx new file mode 100644 index 0000000000..b38bb15749 --- /dev/null +++ b/www/packages/docs-ui/src/components/WorkflowDiagram/Depth/index.tsx @@ -0,0 +1,27 @@ +"use client" + +import React from "react" +import { WorkflowStepUi } from "types" +import { WorkflowDiagramStepNode } from "../Node" +import { WorkflowDiagramLine } from "../Line" + +export type WorkflowDiagramDepthProps = { + cluster: WorkflowStepUi[] + next: WorkflowStepUi[] +} + +export const WorkflowDiagramDepth = ({ + cluster, + next, +}: WorkflowDiagramDepthProps) => { + return ( +
+
+ {cluster.map((step) => ( + + ))} +
+ +
+ ) +} diff --git a/www/packages/docs-ui/src/components/WorkflowDiagram/Line/index.tsx b/www/packages/docs-ui/src/components/WorkflowDiagram/Line/index.tsx new file mode 100644 index 0000000000..a3bd76ba56 --- /dev/null +++ b/www/packages/docs-ui/src/components/WorkflowDiagram/Line/index.tsx @@ -0,0 +1,26 @@ +import React from "react" +import { WorkflowDiagramArrow } from "../Arrow" +import { WorkflowStepUi } from "types" + +export type WorkflowDiagramLineProps = { + step: WorkflowStepUi[] +} + +export const WorkflowDiagramLine = ({ step }: WorkflowDiagramLineProps) => { + if (!step) { + return <> + } + + return ( +
+
+
+
+
+
+ +
+
+
+ ) +} diff --git a/www/packages/docs-ui/src/components/WorkflowDiagram/Node/index.tsx b/www/packages/docs-ui/src/components/WorkflowDiagram/Node/index.tsx new file mode 100644 index 0000000000..adb04d83ab --- /dev/null +++ b/www/packages/docs-ui/src/components/WorkflowDiagram/Node/index.tsx @@ -0,0 +1,76 @@ +"use client" + +import { Text } from "@medusajs/ui" +import clsx from "clsx" +import Link from "next/link" +import React from "react" +import { WorkflowStepUi } from "types" +import { InlineCode, MarkdownContent, Tooltip } from "../../.." +import { Bolt, InformationCircle } from "@medusajs/icons" + +export type WorkflowDiagramNodeProps = { + step: WorkflowStepUi +} + +export const WorkflowDiagramStepNode = ({ step }: WorkflowDiagramNodeProps) => { + const stepId = step.name.split(".").pop() + + return ( + +

{step.name}

+ {step.when && ( + + Runs only if a when condition is + satisfied. + + )} + {step.description && ( + + {step.description} + + )} + + } + clickable={true} + > + +
+ {step.type === "hook" && ( +
+ +
+ )} + {step.when && ( +
+ +
+ )} + + {stepId} + +
+ +
+ ) +} diff --git a/www/packages/docs-ui/src/components/WorkflowDiagram/index.tsx b/www/packages/docs-ui/src/components/WorkflowDiagram/index.tsx new file mode 100644 index 0000000000..c78932a244 --- /dev/null +++ b/www/packages/docs-ui/src/components/WorkflowDiagram/index.tsx @@ -0,0 +1,218 @@ +"use client" + +import clsx from "clsx" +import { + useAnimationControls, + useDragControls, + useMotionValue, + motion, +} from "framer-motion" +import React, { Suspense, useEffect, useRef, useState } from "react" +import { ArrowPathMini, MinusMini, PlusMini } from "@medusajs/icons" +import { DropdownMenu, Text } from "@medusajs/ui" +import { createNodeClusters, getNextCluster } from "../../utils" +import { Workflow } from "types" +import { WorkflowDiagramDepth } from "./Depth" +import { Loading } from "../.." + +export type WorkflowDiagramProps = { + workflow: Workflow +} + +type ZoomScale = 0.5 | 0.75 | 1 + +const defaultState = { + x: -1000, + y: -1020, + scale: 1, +} + +const MAX_ZOOM = 1.5 +const MIN_ZOOM = 0.5 +const ZOOM_STEP = 0.25 + +const WorkflowDiagramContent = ({ workflow }: WorkflowDiagramProps) => { + const [zoom, setZoom] = useState(1) + const [isDragging, setIsDragging] = useState(false) + + const scale = useMotionValue(defaultState.scale) + const x = useMotionValue(defaultState.x) + const y = useMotionValue(defaultState.y) + + const controls = useAnimationControls() + + const dragControls = useDragControls() + const dragConstraints = useRef(null) + + const canZoomIn = zoom < MAX_ZOOM + const canZoomOut = zoom > MIN_ZOOM + + useEffect(() => { + const unsubscribe = scale.on("change", (latest) => { + setZoom(latest as ZoomScale) + }) + + return () => { + unsubscribe() + } + }, [scale]) + + // TODO pass steps here. + const clusters = createNodeClusters(workflow.steps) + + function scaleXandY( + prevScale: number, + newScale: number, + x: number, + y: number + ) { + const scaleRatio = newScale / prevScale + return { + x: x * scaleRatio, + y: y * scaleRatio, + } + } + + const changeZoom = (newScale: number) => { + const { x: newX, y: newY } = scaleXandY(zoom, newScale, x.get(), y.get()) + + setZoom(newScale) + controls.set({ scale: newScale, x: newX, y: newY }) + } + + const zoomIn = () => { + const curr = scale.get() + + if (curr < 1.5) { + const newScale = curr + ZOOM_STEP + changeZoom(newScale) + } + } + + const zoomOut = () => { + const curr = scale.get() + + if (curr > 0.5) { + const newScale = curr - ZOOM_STEP + changeZoom(newScale) + } + } + + const resetCanvas = async () => await controls.start(defaultState) + + return ( +
+
+
+
+ setIsDragging(true)} + onMouseUp={() => setIsDragging(false)} + drag + dragConstraints={dragConstraints} + dragElastic={0} + dragMomentum={false} + dragControls={dragControls} + initial={false} + animate={controls} + transition={{ duration: 0.25 }} + style={{ + x, + y, + scale, + }} + className={clsx( + "bg-medusa-bg-subtle relative size-[500rem] origin-top-left items-start justify-start overflow-hidden", + "bg-[radial-gradient(var(--docs-border-base)_1.5px,transparent_0)] bg-[length:20px_20px] bg-repeat", + !isDragging && "cursor-grab", + isDragging && "cursor-grabbing" + )} + > +
+
+ {Object.entries(clusters).map(([depth, cluster]) => { + const next = getNextCluster(clusters, Number(depth)) + + return ( + + ) + })} +
+
+
+
+
+
+
+ +
+ + + + {Math.round(zoom * 100)}% + + + + {[50, 75, 100, 125, 150].map((value) => ( + changeZoom(value / 100)} + className="px-docs_0.5 py-[6px]" + > + {value}% + + ))} + + +
+ +
+ +
+
+
+ ) +} + +export const WorkflowDiagram = (props: WorkflowDiagramProps) => { + return ( + }> + + + ) +} diff --git a/www/packages/docs-ui/src/components/index.ts b/www/packages/docs-ui/src/components/index.ts index 7c61f7f49b..a9f9012137 100644 --- a/www/packages/docs-ui/src/components/index.ts +++ b/www/packages/docs-ui/src/components/index.ts @@ -74,4 +74,5 @@ export * from "./ThemeImage" export * from "./Toggle" export * from "./Tooltip" export * from "./TypeList" +export * from "./WorkflowDiagram" export * from "./ZoomImg" diff --git a/www/packages/docs-ui/src/utils/index.ts b/www/packages/docs-ui/src/utils/index.ts index 824941baa9..c5f2b62699 100644 --- a/www/packages/docs-ui/src/utils/index.ts +++ b/www/packages/docs-ui/src/utils/index.ts @@ -15,3 +15,4 @@ export * from "./learning-paths" export * from "./set-obj-value" export * from "./sidebar-attach-href-common-options" export * from "./swr-fetcher" +export * from "./workflow-diagram-utils" diff --git a/www/packages/docs-ui/src/utils/workflow-diagram-utils.ts b/www/packages/docs-ui/src/utils/workflow-diagram-utils.ts new file mode 100644 index 0000000000..fb7fa05f30 --- /dev/null +++ b/www/packages/docs-ui/src/utils/workflow-diagram-utils.ts @@ -0,0 +1,32 @@ +import { WorkflowSteps, WorkflowStepUi } from "types" + +export const createNodeClusters = (steps: WorkflowSteps) => { + const clusters: Record = {} + + steps.forEach((step) => { + if (!clusters[step.depth]) { + clusters[step.depth] = [] + } + + if (step.type === "when") { + const whenSteps = step.steps.map((whenStep) => ({ + ...whenStep, + depth: step.depth, + when: step, + })) + clusters[step.depth].push(...whenSteps) + } else { + clusters[step.depth].push(step) + } + }) + + return clusters +} + +export const getNextCluster = ( + clusters: Record, + depth: number +) => { + const nextDepth = depth + 1 + return clusters[nextDepth] +} diff --git a/www/packages/remark-rehype-plugins/src/index.ts b/www/packages/remark-rehype-plugins/src/index.ts index 28a8635841..8922ce71bc 100644 --- a/www/packages/remark-rehype-plugins/src/index.ts +++ b/www/packages/remark-rehype-plugins/src/index.ts @@ -5,6 +5,7 @@ export * from "./local-links.js" export * from "./page-number.js" export * from "./resolve-admonitions.js" export * from "./type-list-link-fixer.js" +export * from "./workflow-diagram-link-fixer.js" export * from "./utils/fix-link.js" export * from "./utils/get-file-slug.js" diff --git a/www/packages/remark-rehype-plugins/src/type-list-link-fixer.ts b/www/packages/remark-rehype-plugins/src/type-list-link-fixer.ts index 494dca0039..7724f47700 100644 --- a/www/packages/remark-rehype-plugins/src/type-list-link-fixer.ts +++ b/www/packages/remark-rehype-plugins/src/type-list-link-fixer.ts @@ -1,126 +1,9 @@ -import path from "path" import { Transformer } from "unified" -import { - ExpressionJsVar, - TypeListLinkFixerOptions, - UnistNodeWithData, - UnistTree, -} from "./types/index.js" -import { FixLinkOptions, fixLinkUtil } from "./index.js" -import getAttribute from "./utils/get-attribute.js" -import { estreeToJs } from "./utils/estree-to-js.js" -import { - isExpressionJsVarLiteral, - isExpressionJsVarObj, -} from "./utils/expression-is-utils.js" - -const LINK_REGEX = /\[(.*?)\]\((?.*?)\)/gm - -function matchLinks( - str: string, - linkOptions: Omit -) { - let linkMatches - while ((linkMatches = LINK_REGEX.exec(str)) !== null) { - if (!linkMatches.groups?.link) { - return - } - - const newUrl = fixLinkUtil({ - ...linkOptions, - linkedPath: linkMatches.groups.link, - }) - - str = str.replace(linkMatches.groups.link, newUrl) - } - - return str -} - -function traverseTypes( - types: ExpressionJsVar[] | ExpressionJsVar, - linkOptions: Omit -) { - if (Array.isArray(types)) { - types.forEach((item) => traverseTypes(item, linkOptions)) - } else if (isExpressionJsVarLiteral(types)) { - types.original.value = matchLinks( - types.original.value as string, - linkOptions - ) - types.original.raw = JSON.stringify(types.original.value) - } else { - Object.values(types).forEach((value) => { - if (Array.isArray(value) || isExpressionJsVarObj(value)) { - return traverseTypes(value, linkOptions) - } - - if (!isExpressionJsVarLiteral(value)) { - return - } - - value.original.value = matchLinks( - value.original.value as string, - linkOptions - ) - value.original.raw = JSON.stringify(value.original.value) - }) - } -} +import { ComponentLinkFixerOptions } from "./types/index.js" +import { componentLinkFixer } from "./utils/component-link-fixer.js" export function typeListLinkFixerPlugin( - options?: TypeListLinkFixerOptions + options?: ComponentLinkFixerOptions ): Transformer { - const { filePath, basePath } = options || {} - return async (tree, file) => { - if (!file.cwd) { - return - } - - if (!file.history.length) { - if (!filePath) { - return - } - - file.history.push(filePath) - } - - const { visit } = await import("unist-util-visit") - - const currentPageFilePath = file.history[0].replace( - `/${path.basename(file.history[0])}`, - "" - ) - const appsPath = basePath || path.join(file.cwd, "app") - visit(tree as UnistTree, "mdxJsxFlowElement", (node: UnistNodeWithData) => { - if (node.name !== "TypeList") { - return - } - - const typesAttribute = getAttribute(node, "types") - - if ( - !typesAttribute || - typeof typesAttribute.value === "string" || - !typesAttribute.value.data?.estree - ) { - return - } - - const linkOptions = { - currentPageFilePath, - appsPath, - } - - // let newItems: Record[] - - const typesJsVar = estreeToJs(typesAttribute.value.data.estree) - - if (!typesJsVar) { - return - } - - traverseTypes(typesJsVar, linkOptions) - }) - } + return componentLinkFixer("TypeList", "types", options) } diff --git a/www/packages/remark-rehype-plugins/src/types/index.ts b/www/packages/remark-rehype-plugins/src/types/index.ts index ffc90f9fa5..5cffb878ac 100644 --- a/www/packages/remark-rehype-plugins/src/types/index.ts +++ b/www/packages/remark-rehype-plugins/src/types/index.ts @@ -118,9 +118,12 @@ export declare type CrossProjectLinksOptions = { useBaseUrl?: boolean } -export declare type TypeListLinkFixerOptions = { +export declare type ComponentLinkFixerLinkType = "md" | "value" + +export declare type ComponentLinkFixerOptions = { filePath?: string basePath?: string + checkLinksType: ComponentLinkFixerLinkType } export declare type LocalLinkOptions = { diff --git a/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts b/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts new file mode 100644 index 0000000000..e1e56ea2fe --- /dev/null +++ b/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts @@ -0,0 +1,138 @@ +import path from "path" +import { Transformer } from "unified" +import { + ComponentLinkFixerLinkType, + ExpressionJsVar, + UnistNodeWithData, + UnistTree, +} from "../types/index.js" +import { FixLinkOptions, fixLinkUtil } from "../index.js" +import getAttribute from "../utils/get-attribute.js" +import { estreeToJs } from "../utils/estree-to-js.js" +import { + isExpressionJsVarLiteral, + isExpressionJsVarObj, +} from "../utils/expression-is-utils.js" +import { ComponentLinkFixerOptions } from "../types/index.js" + +const MD_LINK_REGEX = /\[(.*?)\]\((?.*?)\)/gm +const VALUE_LINK_REGEX = /^(![a-z]+!|\.)/gm + +function matchMdLinks( + str: string, + linkOptions: Omit +) { + let linkMatches + while ((linkMatches = MD_LINK_REGEX.exec(str)) !== null) { + if (!linkMatches.groups?.link) { + return + } + + const newUrl = fixLinkUtil({ + ...linkOptions, + linkedPath: linkMatches.groups.link, + }) + + str = str.replace(linkMatches.groups.link, newUrl) + } + + return str +} + +function matchValueLink( + str: string, + linkOptions: Omit +) { + if (!VALUE_LINK_REGEX.exec(str)) { + return str + } + + return fixLinkUtil({ + ...linkOptions, + linkedPath: str, + }) +} + +function traverseJsVar( + item: ExpressionJsVar[] | ExpressionJsVar, + linkOptions: Omit, + checkLinksType: ComponentLinkFixerLinkType +) { + const linkFn = checkLinksType === "md" ? matchMdLinks : matchValueLink + if (Array.isArray(item)) { + item.forEach((item) => traverseJsVar(item, linkOptions, checkLinksType)) + } else if (isExpressionJsVarLiteral(item)) { + item.original.value = linkFn(item.original.value as string, linkOptions) + item.original.raw = JSON.stringify(item.original.value) + } else { + Object.values(item).forEach((value) => { + if (Array.isArray(value) || isExpressionJsVarObj(value)) { + return traverseJsVar(value, linkOptions, checkLinksType) + } + + if (!isExpressionJsVarLiteral(value)) { + return + } + + value.original.value = linkFn(value.original.value as string, linkOptions) + value.original.raw = JSON.stringify(value.original.value) + }) + } +} + +export function componentLinkFixer( + componentName: string, + attributeName: string, + options?: ComponentLinkFixerOptions +): Transformer { + const { filePath, basePath, checkLinksType = "md" } = options || {} + return async (tree, file) => { + if (!file.cwd) { + return + } + + if (!file.history.length) { + if (!filePath) { + return + } + + file.history.push(filePath) + } + + const { visit } = await import("unist-util-visit") + + const currentPageFilePath = file.history[0].replace( + `/${path.basename(file.history[0])}`, + "" + ) + const appsPath = basePath || path.join(file.cwd, "app") + visit(tree as UnistTree, "mdxJsxFlowElement", (node: UnistNodeWithData) => { + if (node.name !== componentName) { + return + } + + const workflowAttribute = getAttribute(node, attributeName) + + if ( + !workflowAttribute || + typeof workflowAttribute.value === "string" || + !workflowAttribute.value.data?.estree + ) { + return + } + + const linkOptions = { + currentPageFilePath, + appsPath, + } + + const itemJsVar = estreeToJs(workflowAttribute.value.data.estree) + + if (!itemJsVar) { + return + } + + traverseJsVar(itemJsVar, linkOptions, checkLinksType) + }) + } +} diff --git a/www/packages/remark-rehype-plugins/src/workflow-diagram-link-fixer.ts b/www/packages/remark-rehype-plugins/src/workflow-diagram-link-fixer.ts new file mode 100644 index 0000000000..534822552e --- /dev/null +++ b/www/packages/remark-rehype-plugins/src/workflow-diagram-link-fixer.ts @@ -0,0 +1,9 @@ +import { Transformer } from "unified" +import { ComponentLinkFixerOptions } from "./types/index.js" +import { componentLinkFixer } from "./utils/component-link-fixer.js" + +export function workflowDiagramLinkFixerPlugin( + options?: ComponentLinkFixerOptions +): Transformer { + return componentLinkFixer("WorkflowDiagram", "workflow", options) +} diff --git a/www/packages/types/src/index.ts b/www/packages/types/src/index.ts index 8abc96e963..8d7f487fed 100644 --- a/www/packages/types/src/index.ts +++ b/www/packages/types/src/index.ts @@ -2,3 +2,4 @@ export * from "./api-testing.js" export * from "./config.js" export * from "./general.js" export * from "./sidebar.js" +export * from "./workflow.js" diff --git a/www/packages/types/src/sidebar.ts b/www/packages/types/src/sidebar.ts index c2ebc0d170..33491ca24e 100644 --- a/www/packages/types/src/sidebar.ts +++ b/www/packages/types/src/sidebar.ts @@ -26,5 +26,6 @@ export type SidebarSectionItemsType = { export type RawSidebarItemType = SidebarItemType & { autogenerate_path?: string + custom_autogenerate?: string number?: string } diff --git a/www/packages/types/src/workflow.ts b/www/packages/types/src/workflow.ts new file mode 100644 index 0000000000..23f24e62a3 --- /dev/null +++ b/www/packages/types/src/workflow.ts @@ -0,0 +1,25 @@ +export type WorkflowStep = { + type: "step" | "workflow" | "hook" + name: string + description?: string + link?: string + depth: number +} + +export type WorkflowWhenSteps = { + type: "when" + condition: string + steps: WorkflowStep[] + depth: number +} + +export type WorkflowStepUi = WorkflowStep & { + when?: WorkflowWhenSteps +} + +export type WorkflowSteps = (WorkflowStepUi | WorkflowWhenSteps)[] + +export type Workflow = { + name: string + steps: WorkflowSteps +} diff --git a/www/utils/packages/typedoc-generate-references/src/constants/custom-options.ts b/www/utils/packages/typedoc-generate-references/src/constants/custom-options.ts index 804dea5da6..8f55f6cbeb 100644 --- a/www/utils/packages/typedoc-generate-references/src/constants/custom-options.ts +++ b/www/utils/packages/typedoc-generate-references/src/constants/custom-options.ts @@ -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> = { "core-flows": getOptions({ @@ -18,20 +19,7 @@ const customOptions: Record> = { 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", diff --git a/www/utils/packages/typedoc-generate-references/src/constants/merger-custom-options/core-flows.ts b/www/utils/packages/typedoc-generate-references/src/constants/merger-custom-options/core-flows.ts new file mode 100644 index 0000000000..3e36900b75 --- /dev/null +++ b/www/utils/packages/typedoc-generate-references/src/constants/merger-custom-options/core-flows.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 diff --git a/www/utils/packages/typedoc-generate-references/src/constants/merger-custom-options/index.ts b/www/utils/packages/typedoc-generate-references/src/constants/merger-custom-options/index.ts index c708d43dd5..c618ac7f45 100644 --- a/www/utils/packages/typedoc-generate-references/src/constants/merger-custom-options/index.ts +++ b/www/utils/packages/typedoc-generate-references/src/constants/merger-custom-options/index.ts @@ -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, diff --git a/www/utils/packages/typedoc-generate-references/src/constants/merger-options.ts b/www/utils/packages/typedoc-generate-references/src/constants/merger-options.ts index a5d2303951..869c56c44d 100644 --- a/www/utils/packages/typedoc-generate-references/src/constants/merger-options.ts +++ b/www/utils/packages/typedoc-generate-references/src/constants/merger-options.ts @@ -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 = { ...baseOptions, @@ -36,7 +40,9 @@ const mergerOptions: Partial = { "helper-steps", "workflows", ], - allReflectionsHaveOwnDocumentInNamespace: ["Utilities"], + allReflectionsHaveOwnDocumentInNamespace: [ + ...getNamespaceNames(getCoreFlowNamespaces()), + ], formatting: { "*": { showCommentsAsHeader: true, diff --git a/www/utils/packages/typedoc-generate-references/src/utils/get-namespaces.ts b/www/utils/packages/typedoc-generate-references/src/utils/get-namespaces.ts new file mode 100644 index 0000000000..672f0a0952 --- /dev/null +++ b/www/utils/packages/typedoc-generate-references/src/utils/get-namespaces.ts @@ -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 +} diff --git a/www/utils/packages/typedoc-plugin-custom/src/generate-namespace.ts b/www/utils/packages/typedoc-plugin-custom/src/generate-namespace.ts index a86d86650d..ccfbe11e05 100644 --- a/www/utils/packages/typedoc-plugin-custom/src/generate-namespace.ts +++ b/www/utils/packages/typedoc-plugin-custom/src/generate-namespace.ts @@ -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 +} diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/render-utils.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/render-utils.ts index ef44c29e7a..e1a666a569 100644 --- a/www/utils/packages/typedoc-plugin-markdown-medusa/src/render-utils.ts +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/render-utils.ts @@ -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) } diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/example.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/example.ts index 93751c2e5f..8db10cf115 100644 --- a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/example.ts +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/example.ts @@ -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" ) diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/if-workflow.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/if-workflow.ts new file mode 100644 index 0000000000..1e4bbadd21 --- /dev/null +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/if-workflow.ts @@ -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) + } + ) +} diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/step-input.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/step-input.ts index c52076bb68..e1681fc248 100644 --- a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/step-input.ts +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/step-input.ts @@ -24,6 +24,7 @@ export default function (theme: MarkdownTheme) { reflectionType: inputType, project: this.project || options.data.theme.project, maxLevel, + wrapObject: true, }) if (!input.length) { diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/step-output.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/step-output.ts index b4a449c905..72ae84b005 100644 --- a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/step-output.ts +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/step-output.ts @@ -24,6 +24,7 @@ export default function (theme: MarkdownTheme) { reflectionType: outputType, project: this.project || options.data.theme.project, maxLevel, + wrapObject: true, }) if (!output.length) { diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/toc.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/toc.ts index ac178bf195..0291cc903e 100644 --- a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/toc.ts +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/toc.ts @@ -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`) } diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-diagram.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-diagram.ts new file mode 100644 index 0000000000..e38808e16e --- /dev/null +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-diagram.ts @@ -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[] = [] + + 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[], + } + + 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 +} diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-hooks.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-hooks.ts new file mode 100644 index 0000000000..4d6f2049df --- /dev/null +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-hooks.ts @@ -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 + } + ) +} diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-input.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-input.ts new file mode 100644 index 0000000000..d62a4a96ee --- /dev/null +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-input.ts @@ -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}` + } + ) +} diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-output.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-output.ts new file mode 100644 index 0000000000..9ba2ccdc8d --- /dev/null +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-output.ts @@ -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}` + } + ) +} diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.signature-wrapper.hbs b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.signature-wrapper.hbs index 07053d706c..4d947295ba 100644 --- a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.signature-wrapper.hbs +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.signature-wrapper.hbs @@ -4,6 +4,12 @@ {{else}} +{{#ifWorkflow}} + +{{> member.workflow}} + +{{else}} + {{#ifWorkflowStep}} {{> member.step}} @@ -14,4 +20,6 @@ {{/ifWorkflowStep}} +{{/ifWorkflow}} + {{/ifReactQueryType}} \ No newline at end of file diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.workflow.hbs b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.workflow.hbs new file mode 100644 index 0000000000..138d42cfb4 --- /dev/null +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.workflow.hbs @@ -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}}} \ No newline at end of file diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/utils/format-workflow-diagram-component.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/utils/format-workflow-diagram-component.ts new file mode 100644 index 0000000000..0c1438485d --- /dev/null +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/utils/format-workflow-diagram-component.ts @@ -0,0 +1,9 @@ +export function formatWorkflowDiagramComponent({ + component, + componentItem, +}: { + component: string | undefined + componentItem: Record +}): string { + return `<${component} workflow={${JSON.stringify(componentItem)}} />` +} diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/utils/workflow-utils.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/utils/workflow-utils.ts new file mode 100644 index 0000000000..b3d6ebe843 --- /dev/null +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/utils/workflow-utils.ts @@ -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 +} diff --git a/www/utils/packages/typedoc-plugin-workflows/src/plugin.ts b/www/utils/packages/typedoc-plugin-workflows/src/plugin.ts index ee861ac0ab..97d9ffa2e7 100644 --- a/www/utils/packages/typedoc-plugin-workflows/src/plugin.ts +++ b/www/utils/packages/typedoc-plugin-workflows/src/plugin.ts @@ -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 diff --git a/www/utils/packages/typedoc-plugin-workflows/src/types.ts b/www/utils/packages/typedoc-plugin-workflows/src/types.ts index 840e90bc6a..cbe2bac180 100644 --- a/www/utils/packages/typedoc-plugin-workflows/src/types.ts +++ b/www/utils/packages/typedoc-plugin-workflows/src/types.ts @@ -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" diff --git a/www/utils/packages/typedoc-plugin-workflows/src/utils/helper.ts b/www/utils/packages/typedoc-plugin-workflows/src/utils/helper.ts index 301d180cfc..32c537eeec 100644 --- a/www/utils/packages/typedoc-plugin-workflows/src/utils/helper.ts +++ b/www/utils/packages/typedoc-plugin-workflows/src/utils/helper.ts @@ -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 + } } diff --git a/www/utils/packages/types/lib/index.d.ts b/www/utils/packages/types/lib/index.d.ts index 42db2c0639..f243f69cb6 100644 --- a/www/utils/packages/types/lib/index.d.ts +++ b/www/utils/packages/types/lib/index.d.ts @@ -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[] } diff --git a/www/utils/packages/utils/src/hooks-util.ts b/www/utils/packages/utils/src/hooks-util.ts new file mode 100644 index 0000000000..47246c3e12 --- /dev/null +++ b/www/utils/packages/utils/src/hooks-util.ts @@ -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 | 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 +} diff --git a/www/utils/packages/utils/src/index.ts b/www/utils/packages/utils/src/index.ts index b4f2ce8f3f..f8fe0ea9f9 100644 --- a/www/utils/packages/utils/src/index.ts +++ b/www/utils/packages/utils/src/index.ts @@ -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" diff --git a/www/utils/packages/utils/src/step-utils.ts b/www/utils/packages/utils/src/step-utils.ts index 85eace1ead..38a0ac31b7 100644 --- a/www/utils/packages/utils/src/step-utils.ts +++ b/www/utils/packages/utils/src/step-utils.ts @@ -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 } diff --git a/www/utils/packages/utils/src/workflow-utils.ts b/www/utils/packages/utils/src/workflow-utils.ts index a5182f5701..5358cd5864 100644 --- a/www/utils/packages/utils/src/workflow-utils.ts +++ b/www/utils/packages/utils/src/workflow-utils.ts @@ -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)) + ) +} diff --git a/www/yarn.lock b/www/yarn.lock index 96e179621c..37f358da83 100644 --- a/www/yarn.lock +++ b/www/yarn.lock @@ -7250,6 +7250,7 @@ __metadata: cpy-cli: ^5.0.0 eslint: ^8 eslint-config-docs: "*" + framer-motion: ^11.3.21 mermaid: ^10.9.0 next: 14.1.0 npm-to-yarn: ^2.1.0 @@ -8606,6 +8607,26 @@ eslint-config-next@latest: languageName: node linkType: hard +"framer-motion@npm:^11.3.21": + version: 11.3.21 + resolution: "framer-motion@npm:11.3.21" + dependencies: + tslib: ^2.4.0 + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 47ea88207920800294159f20d89d217c0118cbe2f5b7c4f1204490a04ac379a60e4ba323bfb101f334155014ebf07b51ad70a4d67b0363043f9d1d00347b1b3b + languageName: node + linkType: hard + "fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0"