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

* add template for workflows

* initial changes

* added support for parallel steps

* added support for when

* added merge options

* fix merge options

* fix to tooltip

* clean up

* remove redirects

* fixes

* theme fixes + added merge options

* generate hook examples + fixes

* changed namespaces

* add custom autogenerator

* change type of additional data
This commit is contained in:
Shahed Nasser authored and GitHub committed 2024-08-09 16:35:52 +03:00
1 parent 79b49c1288
commit a19c562bec
58 files changed
+2098 -328

No files matched your search

@@ -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<string, () => Promise<ItemsToAdd[]>> = {
"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))
)
+2
View File
@@ -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"
@@ -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
}
@@ -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<ItemsToAdd | undefined> {
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,
}
}
+1
View File
@@ -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",
@@ -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 && <Sidebar className="text-medusa-fg-muted" />}
{!mobileSidebarOpen && <SidebarLeft className="text-medusa-fg-muted" />}
{mobileSidebarOpen && <XMark className="text-medusa-fg-muted" />}
</NavbarIconButton>
)
@@ -0,0 +1,19 @@
import React from "react"
export const WorkflowDiagramArrowEnd = () => {
return (
<svg
width="22"
height="38"
viewBox="0 0 22 38"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="-mt-[6px]"
>
<path
d="M21.5284 32.5303C21.8213 32.2374 21.8213 31.7626 21.5284 31.4697L16.7554 26.6967C16.4625 26.4038 15.9876 26.4038 15.6947 26.6967C15.4019 26.9896 15.4019 27.4645 15.6947 27.7574L19.9374 32L15.6947 36.2426C15.4019 36.5355 15.4019 37.0104 15.6947 37.3033C15.9876 37.5962 16.4625 37.5962 16.7554 37.3033L21.5284 32.5303ZM0.249878 0L0.249878 28H1.74988L1.74988 0H0.249878ZM4.99988 32.75L20.998 32.75V31.25L4.99988 31.25V32.75ZM0.249878 28C0.249878 30.6234 2.37653 32.75 4.99988 32.75V31.25C3.20495 31.25 1.74988 29.7949 1.74988 28H0.249878Z"
fill="var(--docs-border-strong)"
/>
</svg>
)
}
@@ -0,0 +1,18 @@
import React from "react"
export const WorkflowDiagramArrowHorizontal = () => {
return (
<svg
width="42"
height="12"
viewBox="0 0 42 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M41.5303 6.53033C41.8232 6.23744 41.8232 5.76256 41.5303 5.46967L36.7574 0.696699C36.4645 0.403806 35.9896 0.403806 35.6967 0.696699C35.4038 0.989593 35.4038 1.46447 35.6967 1.75736L39.9393 6L35.6967 10.2426C35.4038 10.5355 35.4038 11.0104 35.6967 11.3033C35.9896 11.5962 36.4645 11.5962 36.7574 11.3033L41.5303 6.53033ZM0.999996 5.25C0.585785 5.25 0.249996 5.58579 0.249996 6C0.249996 6.41421 0.585785 6.75 0.999996 6.75V5.25ZM41 5.25L0.999996 5.25V6.75L41 6.75V5.25Z"
fill="var(--docs-border-strong)"
/>
</svg>
)
}
@@ -0,0 +1,19 @@
import React from "react"
export const WorkflowDiagramArrowMiddle = () => {
return (
<svg
width="22"
height="38"
viewBox="0 0 22 38"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="-mt-[6px]"
>
<path
d="M0.999878 32H0.249878V32.75H0.999878V32ZM21.5284 32.5303C21.8213 32.2374 21.8213 31.7626 21.5284 31.4697L16.7554 26.6967C16.4625 26.4038 15.9876 26.4038 15.6947 26.6967C15.4019 26.9896 15.4019 27.4645 15.6947 27.7574L19.9374 32L15.6947 36.2426C15.4019 36.5355 15.4019 37.0104 15.6947 37.3033C15.9876 37.5962 16.4625 37.5962 16.7554 37.3033L21.5284 32.5303ZM0.249878 0L0.249878 32H1.74988L1.74988 0H0.249878ZM0.999878 32.75L20.998 32.75V31.25L0.999878 31.25V32.75Z"
fill="var(--docs-border-strong)"
/>
</svg>
)
}
@@ -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 <WorkflowDiagramArrowHorizontal />
}
if (depth === 2) {
return (
<div className="flex flex-col items-end">
<WorkflowDiagramArrowHorizontal />
<WorkflowDiagramArrowEnd />
</div>
)
}
const inbetween = Array.from({ length: depth - 2 }).map((_, index) => (
<WorkflowDiagramArrowMiddle key={index} />
))
return (
<div className="flex flex-col items-end">
<WorkflowDiagramArrowHorizontal />
{inbetween}
<WorkflowDiagramArrowEnd />
</div>
)
}
@@ -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 (
<div className="flex items-start">
<div className="flex flex-col justify-center gap-y-docs_0.5">
{cluster.map((step) => (
<WorkflowDiagramStepNode key={step.name} step={step} />
))}
</div>
<WorkflowDiagramLine step={next} />
</div>
)
}
@@ -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 (
<div className="ml-0 -mr-[7px] w-[60px] pr-[7px]">
<div className="flex min-h-[24px] w-full items-start">
<div className="flex h-docs_1.5 w-[10px] items-center justify-center">
<div className="bg-medusa-button-neutral shadow-borders-base size-[10px] shrink-0 rounded-full" />
</div>
<div className="pt-[6px]">
<WorkflowDiagramArrow depth={step.length} />
</div>
</div>
</div>
)
}
@@ -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 (
<Tooltip
tooltipClassName="!text-left max-w-full text-pretty overflow-scroll"
tooltipChildren={
<>
<h4 className="text-compact-x-small-plus">{step.name}</h4>
{step.when && (
<span className="block py-docs_0.25">
Runs only if a <InlineCode>when</InlineCode> condition is
satisfied.
</span>
)}
{step.description && (
<MarkdownContent
allowedElements={["a", "strong", "code"]}
unwrapDisallowed={true}
>
{step.description}
</MarkdownContent>
)}
</>
}
clickable={true}
>
<Link
href={step.link || `#${step.name}`}
className="focus-visible:shadow-borders-focus transition-fg rounded-docs_sm outline-none"
>
<div
className={clsx(
"shadow-borders-base flex min-w-[120px] bg-medusa-bg-base",
"items-center rounded-docs_sm py-docs_0.125 px-docs_0.5",
(step.type === "hook" || step.when) && "gap-x-docs_0.125"
)}
data-step-id={step.name}
>
{step.type === "hook" && (
<div className="flex size-[20px] items-center justify-center text-medusa-tag-orange-icon">
<Bolt />
</div>
)}
{step.when && (
<div className="flex size-[20px] items-center justify-center text-medusa-tag-green-icon">
<InformationCircle />
</div>
)}
<Text
size="xsmall"
leading="compact"
weight="plus"
className="select-none"
>
{stepId}
</Text>
</div>
</Link>
</Tooltip>
)
}
@@ -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<number>(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<HTMLDivElement>(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 (
<div className="h-[200px] w-full rounded-docs_DEFAULT">
<div
ref={dragConstraints}
className="relative size-full rounded-docs_DEFAULT"
>
<div className="relative size-full overflow-hidden object-contain rounded-docs_DEFAULT shadow-elevation-card-rest">
<div>
<motion.div
onMouseDown={() => 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"
)}
>
<main className="size-full">
<div className="absolute left-[1100px] top-[1100px] flex select-none items-start">
{Object.entries(clusters).map(([depth, cluster]) => {
const next = getNextCluster(clusters, Number(depth))
return (
<WorkflowDiagramDepth
cluster={cluster}
next={next}
key={depth}
/>
)
})}
</div>
</main>
</motion.div>
</div>
</div>
<div className="bg-medusa-bg-base shadow-borders-base text-medusa-fg-subtle absolute bottom-docs_1 left-docs_1.5 flex h-[28px] items-center overflow-hidden rounded-docs_sm">
<div className="flex items-center">
<button
onClick={zoomIn}
type="button"
disabled={!canZoomIn}
aria-label="Zoom in"
className="disabled:text-medusa-fg-disabled transition-fg hover:bg-medusa-bg-base-hover active:bg-medusa-bg-base-pressed focus-visible:bg-medusa-bg-base-pressed border-r p-docs_0.25 outline-none"
>
<PlusMini />
</button>
<div>
<DropdownMenu>
<DropdownMenu.Trigger className="disabled:text-medusa-fg-disabled transition-fg hover:bg-medusa-bg-base-hover active:bg-medusa-bg-base-pressed focus-visible:bg-medusa-bg-base-pressed flex w-[50px] items-center justify-center border-r p-docs_0.25 outline-none">
<Text
as="span"
size="xsmall"
leading="compact"
className="select-none tabular-nums"
>
{Math.round(zoom * 100)}%
</Text>
</DropdownMenu.Trigger>
<DropdownMenu.Content className="bg-medusa-bg-base">
{[50, 75, 100, 125, 150].map((value) => (
<DropdownMenu.Item
key={value}
onClick={() => changeZoom(value / 100)}
className="px-docs_0.5 py-[6px]"
>
{value}%
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu>
</div>
<button
onClick={zoomOut}
type="button"
disabled={!canZoomOut}
aria-label="Zoom out"
className="disabled:text-medusa-fg-disabled transition-fg hover:bg-medusa-bg-base-hover active:bg-medusa-bg-base-pressed focus-visible:bg-medusa-bg-base-pressed border-r p-docs_0.25 outline-none"
>
<MinusMini />
</button>
</div>
<button
onClick={resetCanvas}
type="button"
aria-label="Reset canvas"
className="disabled:text-medusa-fg-disabled transition-fg hover:bg-medusa-bg-base-hover active:bg-medusa-bg-base-pressed focus-visible:bg-medusa-bg-base-pressed p-docs_0.25 outline-none"
>
<ArrowPathMini />
</button>
</div>
</div>
</div>
)
}
export const WorkflowDiagram = (props: WorkflowDiagramProps) => {
return (
<Suspense fallback={<Loading />}>
<WorkflowDiagramContent {...props} />
</Suspense>
)
}
@@ -74,4 +74,5 @@ export * from "./ThemeImage"
export * from "./Toggle"
export * from "./Tooltip"
export * from "./TypeList"
export * from "./WorkflowDiagram"
export * from "./ZoomImg"
+1
View File
@@ -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"
@@ -0,0 +1,32 @@
import { WorkflowSteps, WorkflowStepUi } from "types"
export const createNodeClusters = (steps: WorkflowSteps) => {
const clusters: Record<number, WorkflowStepUi[]> = {}
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<number, WorkflowStepUi[]>,
depth: number
) => {
const nextDepth = depth + 1
return clusters[nextDepth]
}
@@ -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"
@@ -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 = /\[(.*?)\]\((?<link>.*?)\)/gm
function matchLinks(
str: string,
linkOptions: Omit<FixLinkOptions, "linkedPath">
) {
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<FixLinkOptions, "linkedPath">
) {
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<string, unknown>[]
const typesJsVar = estreeToJs(typesAttribute.value.data.estree)
if (!typesJsVar) {
return
}
traverseTypes(typesJsVar, linkOptions)
})
}
return componentLinkFixer("TypeList", "types", options)
}
@@ -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 = {
@@ -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 = /\[(.*?)\]\((?<link>.*?)\)/gm
const VALUE_LINK_REGEX = /^(![a-z]+!|\.)/gm
function matchMdLinks(
str: string,
linkOptions: Omit<FixLinkOptions, "linkedPath">
) {
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<FixLinkOptions, "linkedPath">
) {
if (!VALUE_LINK_REGEX.exec(str)) {
return str
}
return fixLinkUtil({
...linkOptions,
linkedPath: str,
})
}
function traverseJsVar(
item: ExpressionJsVar[] | ExpressionJsVar,
linkOptions: Omit<FixLinkOptions, "linkedPath">,
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)
})
}
}
@@ -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)
}
+1
View File
@@ -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"
+1
View File
@@ -26,5 +26,6 @@ export type SidebarSectionItemsType = {
export type RawSidebarItemType = SidebarItemType & {
autogenerate_path?: string
custom_autogenerate?: string
number?: string
}
+25
View File
@@ -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
}