docs: add clean markdown version of all documentation pages (#11308)

* added route to book

* added to resources

* added route to ui

* added to user guide
This commit is contained in:
Shahed Nasser authored and GitHub committed 2025-02-05 11:23:13 +02:00
1 parent 87db3f0c45
commit 98236c8262
30 files changed
+1059 -165

No files matched your search

@@ -205,40 +205,40 @@ You can now build your plugin's customizations. The following guide explains how
<CardList <CardList
items={[ items={[
{ {
text: "Create a module", title: "Create a module",
link: "/learn/fundamentals/modules", href: "/learn/fundamentals/modules",
}, },
{ {
text: "Create a module link", title: "Create a module link",
link: "/learn/fundamentals/module-links", href: "/learn/fundamentals/module-links",
}, },
{ {
text: "Create a workflow", title: "Create a workflow",
link: "/learn/fundamentals/workflows", href: "/learn/fundamentals/workflows",
}, },
{ {
text: "Add a workflow hook", title: "Add a workflow hook",
link: "/learn/fundamentals/workflows/add-workflow-hook", href: "/learn/fundamentals/workflows/add-workflow-hook",
}, },
{ {
text: "Create an API route", title: "Create an API route",
link: "/learn/fundamentals/api-routes", href: "/learn/fundamentals/api-routes",
}, },
{ {
text: "Add a subscriber", title: "Add a subscriber",
link: "/learn/fundamentals/events-and-subscribers", href: "/learn/fundamentals/events-and-subscribers",
}, },
{ {
text: "Add a scheduled job", title: "Add a scheduled job",
link: "/learn/fundamentals/scheduled-jobs", href: "/learn/fundamentals/scheduled-jobs",
}, },
{ {
text: "Add an admin widget", title: "Add an admin widget",
link: "/learn/fundamentals/admin/widgets", href: "/learn/fundamentals/admin/widgets",
}, },
{ {
text: "Add an admin UI route", title: "Add an admin UI route",
link: "/learn/fundamentals/admin/ui-routes", href: "/learn/fundamentals/admin/ui-routes",
} }
]} ]}
className="mb-1.5" className="mb-1.5"
+1 -1
View File
@@ -72,7 +72,7 @@ Below are some stories from companies that use Medusa:
This documentation introduces you to Medusa's concepts and how they help you build your business use case. The documentation is structured to gradually introduce Medusa's concepts, with easy-to-follow examples along the way. This documentation introduces you to Medusa's concepts and how they help you build your business use case. The documentation is structured to gradually introduce Medusa's concepts, with easy-to-follow examples along the way.
By following this documentation, you’ll be able to create custom commerce experiences that would otherwise take large engineering teams months to build. By following this documentation, you'll be able to create custom commerce experiences that would otherwise take large engineering teams months to build.
### How to use the documentation ### How to use the documentation
@@ -13,11 +13,11 @@ You can build your storefront from scratch with your preferred tech stack, or st
<CardList <CardList
items={[ items={[
{ {
text: "Install Next.js Starter Storefront", title: "Install Next.js Starter Storefront",
href: "!resources!/nextjs-starter" href: "!resources!/nextjs-starter"
}, },
{ {
text: "Build Custom Storefront", title: "Build Custom Storefront",
href: "!resources!/storefront-development" href: "!resources!/storefront-development"
} }
]} ]}
@@ -0,0 +1,75 @@
import { getCleanMd } from "docs-utils"
import { existsSync } from "fs"
import { unstable_cache } from "next/cache"
import { notFound } from "next/navigation"
import { NextRequest, NextResponse } from "next/server"
import path from "path"
import {
addUrlToRelativeLink,
crossProjectLinksPlugin,
localLinksRehypePlugin,
} from "remark-rehype-plugins"
import type { Plugin } from "unified"
type Params = {
params: Promise<{ slug: string[] }>
}
export async function GET(req: NextRequest, { params }: Params) {
const { slug } = await params
// keep this so that Vercel keeps the files in deployment
const basePath = path.join(process.cwd(), "app")
const filePath = path.join(basePath, ...slug, "page.mdx")
if (!existsSync(filePath)) {
return notFound()
}
const cleanMdContent = await getCleanMd_(filePath, {
before: [
[
crossProjectLinksPlugin,
{
baseUrl: process.env.NEXT_PUBLIC_BASE_URL,
projectUrls: {
resources: {
url: process.env.NEXT_PUBLIC_RESOURCES_URL,
},
"user-guide": {
url: process.env.NEXT_PUBLIC_RESOURCES_URL,
},
ui: {
url: process.env.NEXT_PUBLIC_RESOURCES_URL,
},
api: {
url: process.env.NEXT_PUBLIC_RESOURCES_URL,
},
},
useBaseUrl:
process.env.NODE_ENV === "production" ||
process.env.VERCEL_ENV === "production",
},
],
[localLinksRehypePlugin],
] as unknown as Plugin[],
after: [
[addUrlToRelativeLink, { url: process.env.NEXT_PUBLIC_BASE_URL }],
] as unknown as Plugin[],
})
return new NextResponse(cleanMdContent, {
headers: {
"Content-Type": "text/markdown",
},
})
}
const getCleanMd_ = unstable_cache(
async (filePath: string, plugins?: { before?: Plugin[]; after?: Plugin[] }) =>
getCleanMd({ filePath, plugins }),
["clean-md"],
{
revalidate: 3600,
}
)
+15
View File
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export function middleware(request: NextRequest) {
return NextResponse.rewrite(
new URL(
`/md-content${request.nextUrl.pathname.replace("/index.html.md", "")}`,
request.url
)
)
}
export const config = {
matcher: "/:path*/index.html.md",
}
+1
View File
@@ -20,6 +20,7 @@
"@next/mdx": "15.0.4", "@next/mdx": "15.0.4",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"docs-ui": "*", "docs-ui": "*",
"docs-utils": "*",
"next": "15.0.4", "next": "15.0.4",
"react": "rc", "react": "rc",
"react-dom": "rc", "react-dom": "rc",
@@ -0,0 +1,98 @@
import { getCleanMd } from "docs-utils"
import { existsSync } from "fs"
import { unstable_cache } from "next/cache"
import { notFound } from "next/navigation"
import { NextRequest, NextResponse } from "next/server"
import path from "path"
import {
addUrlToRelativeLink,
crossProjectLinksPlugin,
localLinksRehypePlugin,
} from "remark-rehype-plugins"
import type { Plugin } from "unified"
import { filesMap } from "../../../generated/files-map.mjs"
import { slugChanges } from "../../../generated/slug-changes.mjs"
type Params = {
params: Promise<{ slug: string[] }>
}
export async function GET(req: NextRequest, { params }: Params) {
const { slug = ["/"] } = await params
// keep this so that Vercel keeps the files in deployment
path.join(process.cwd(), "app")
path.join(process.cwd(), "references")
const filePathFromMap = await getFileFromMaps(`/${slug.join("/")}`)
if (!filePathFromMap) {
return notFound()
}
const filePath = path.join(path.resolve("..", "..", ".."), filePathFromMap)
if (!existsSync(filePath)) {
return notFound()
}
const cleanMdContent = await getCleanMd_(filePath, {
before: [
[
crossProjectLinksPlugin,
{
baseUrl: process.env.NEXT_PUBLIC_BASE_URL,
projectUrls: {
docs: {
url: process.env.NEXT_PUBLIC_DOCS_URL,
path: "",
},
"user-guide": {
url: process.env.NEXT_PUBLIC_USER_GUIDE_URL,
},
ui: {
url: process.env.NEXT_PUBLIC_UI_URL,
},
api: {
url: process.env.NEXT_PUBLIC_API_URL,
},
},
useBaseUrl:
process.env.NODE_ENV === "production" ||
process.env.VERCEL_ENV === "production",
},
],
[localLinksRehypePlugin],
] as unknown as Plugin[],
after: [
[addUrlToRelativeLink, { url: process.env.NEXT_PUBLIC_BASE_URL }],
] as unknown as Plugin[],
})
return new NextResponse(cleanMdContent, {
headers: {
"Content-Type": "text/markdown",
},
})
}
const getCleanMd_ = unstable_cache(
async (filePath: string, plugins?: { before?: Plugin[]; after?: Plugin[] }) =>
getCleanMd({ filePath, plugins }),
["clean-md"],
{
revalidate: 3600,
}
)
const getFileFromMaps = unstable_cache(
async (path: string) => {
return (
slugChanges.find((slugChange) => slugChange.newSlug === path)?.filePath ||
filesMap.find((file) => file.pathname === path)?.filePath
)
},
["file-map"],
{
revalidate: 3600,
}
)
+15
View File
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export function middleware(request: NextRequest) {
return NextResponse.rewrite(
new URL(
`${request.nextUrl.basePath}/md-content${request.nextUrl.pathname.replace("/index.html.md", "")}`,
request.url
)
)
}
export const config = {
matcher: "/:path*/index.html.md",
}
+11 -2
View File
@@ -1,8 +1,9 @@
import "dotenv/config" import "dotenv/config"
import { defineDocumentType, makeSource } from "contentlayer/source-files" import { defineDocumentType, makeSource } from "contentlayer/source-files"
import { rehypeComponent } from "./src/lib/rehype-component"
import rehypeSlug from "rehype-slug" import rehypeSlug from "rehype-slug"
import { uiRehypePlugin } from "../../packages/remark-rehype-plugins/src"
import { ExampleRegistry } from "./src/registries/example-registry"
export const Doc = defineDocumentType(() => ({ export const Doc = defineDocumentType(() => ({
name: "Doc", name: "Doc",
@@ -29,7 +30,15 @@ export default makeSource({
contentDirPath: "./src/content", contentDirPath: "./src/content",
documentTypes: [Doc], documentTypes: [Doc],
mdx: { mdx: {
rehypePlugins: [[rehypeComponent], [rehypeSlug]], rehypePlugins: [
[
uiRehypePlugin,
{
exampleRegistry: ExampleRegistry,
},
],
[rehypeSlug],
],
mdxOptions: (options) => { mdxOptions: (options) => {
return { return {
...options, ...options,
+2
View File
@@ -24,6 +24,7 @@
"contentlayer": "^0.3.4", "contentlayer": "^0.3.4",
"date-fns": "^3.3.1", "date-fns": "^3.3.1",
"docs-ui": "*", "docs-ui": "*",
"docs-utils": "*",
"mdast-util-toc": "^7.0.0", "mdast-util-toc": "^7.0.0",
"next": "15.0.4", "next": "15.0.4",
"next-contentlayer": "^0.3.4", "next-contentlayer": "^0.3.4",
@@ -47,6 +48,7 @@
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"react-docgen": "^7.1.0", "react-docgen": "^7.1.0",
"remark-rehype-plugins": "*",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"types": "*" "types": "*"
}, },
@@ -0,0 +1,86 @@
import { getCleanMd } from "docs-utils"
import { existsSync } from "fs"
import { unstable_cache } from "next/cache"
import { notFound } from "next/navigation"
import { NextRequest, NextResponse } from "next/server"
import path from "path"
import { addUrlToRelativeLink } from "remark-rehype-plugins"
import type { Plugin } from "unified"
import * as Icons from "@medusajs/icons"
import * as HookValues from "@/registries/hook-values"
import { colors as allColors } from "@/config/colors"
type Params = {
params: Promise<{ slug: string[] }>
}
export async function GET(req: NextRequest, { params }: Params) {
const { slug = ["/"] } = await params
// keep this so that Vercel keeps the files in deployment
const basePath = path.join(process.cwd(), "src", "content", "docs")
const examplesPath = path.join(process.cwd(), "src", "examples")
const specsPath = path.join(process.cwd(), "src", "specs")
const fileName = slug.length === 1 ? "index" : slug.pop() || "index"
const filePath = path.join(basePath, ...slug, `${fileName}.mdx`)
if (!existsSync(filePath)) {
return notFound()
}
const cleanMdContent = await getCleanMd_(
filePath,
{ examplesPath, specsPath },
{
after: [
[addUrlToRelativeLink, { url: process.env.NEXT_PUBLIC_BASE_URL }],
] as unknown as Plugin[],
}
)
return new NextResponse(cleanMdContent, {
headers: {
"Content-Type": "text/markdown",
},
})
}
const getCleanMd_ = unstable_cache(
async (
filePath: string,
parserOptions: {
examplesPath: string
specsPath: string
},
plugins?: { before?: Plugin[]; after?: Plugin[] }
) => {
const iconNames = Object.keys(Icons).filter((name) => name !== "default")
return getCleanMd({
filePath,
plugins,
parserOptions: {
ComponentExample: {
examplesBasePath: parserOptions.examplesPath,
},
ComponentReference: {
specsPath: parserOptions.specsPath,
},
IconSearch: {
iconNames,
},
HookValues: {
hooksData: HookValues,
},
Colors: {
colors: allColors,
},
},
})
},
["clean-md"],
{
revalidate: 3600,
}
)
+15
View File
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export function middleware(request: NextRequest) {
return NextResponse.rewrite(
new URL(
`${request.nextUrl.basePath}/md-content${request.nextUrl.pathname.replace("/index.html.md", "")}`,
request.url
)
)
}
export const config = {
matcher: "/:path*/index.html.md",
}
+2 -13
View File
@@ -1,19 +1,8 @@
import { HookTable } from "@/components/hook-table" import { HookTable } from "@/components/hook-table"
import { HookDataMap } from "@/types/hooks" import { usePrompt } from "../../registries/hook-values"
const useToastValues: HookDataMap = [
{
value: "dialog",
type: {
type: "function",
signature: `async (props: PromptProps): Promise<boolean>`,
},
description: "Async function used to display a new confirmation dialog.",
},
]
const Props = () => { const Props = () => {
return <HookTable props={useToastValues} /> return <HookTable props={usePrompt} />
} }
export default Props export default Props
+2 -14
View File
@@ -1,20 +1,8 @@
import { HookTable } from "@/components/hook-table" import { HookTable } from "@/components/hook-table"
import { HookDataMap } from "@/types/hooks" import { useToggleState } from "../../registries/hook-values"
const useToggleStateValuesArray: HookDataMap = [
{
value: "state",
type: {
type: "object",
name: "StateData",
shape:
"[\n state: boolean,\n open: () => void,\n close: () => void,\n toggle: () => void\n]",
},
},
]
const Props = () => { const Props = () => {
return <HookTable props={useToggleStateValuesArray} /> return <HookTable props={useToggleState} />
} }
export default Props export default Props
@@ -1,12 +1,7 @@
import * as React from "react" import * as React from "react"
import { ExampleRegistry as ExampleRegistryType } from "types"
type ExampleType = { export const ExampleRegistry: ExampleRegistryType = {
name: string
component: React.LazyExoticComponent<() => React.JSX.Element>
file: string
}
export const ExampleRegistry: Record<string, ExampleType> = {
"alert-demo": { "alert-demo": {
name: "alert-demo", name: "alert-demo",
component: React.lazy(async () => import("@/examples/alert-demo")), component: React.lazy(async () => import("@/examples/alert-demo")),
+4 -1
View File
@@ -1,6 +1,9 @@
import { HookRegistryItem } from "@/types/hooks"
import * as React from "react" import * as React from "react"
export type HookRegistryItem = {
table: React.LazyExoticComponent<React.ComponentType>
}
export const HookRegistry: Record<string, HookRegistryItem> = { export const HookRegistry: Record<string, HookRegistryItem> = {
usePrompt: { usePrompt: {
table: React.lazy(async () => import("../props/hooks/usePrompt")), table: React.lazy(async () => import("../props/hooks/usePrompt")),
+24
View File
@@ -0,0 +1,24 @@
import { HookDataMap } from "../types/hooks"
export const useToggleState: HookDataMap = [
{
value: "state",
type: {
type: "object",
name: "StateData",
shape:
"[\n state: boolean,\n open: () => void,\n close: () => void,\n toggle: () => void\n]",
},
},
]
export const usePrompt: HookDataMap = [
{
value: "dialog",
type: {
type: "function",
signature: `async (props: PromptProps): Promise<boolean>`,
},
description: "Async function used to display a new confirmation dialog.",
},
]
-5
View File
@@ -1,4 +1,3 @@
import { ComponentType, LazyExoticComponent } from "react"
import { PropType } from "./props" import { PropType } from "./props"
export type HookData = { export type HookData = {
@@ -8,7 +7,3 @@ export type HookData = {
} }
export type HookDataMap = HookData[] export type HookDataMap = HookData[]
export type HookRegistryItem = {
table: LazyExoticComponent<ComponentType>
}
@@ -0,0 +1,76 @@
import { getCleanMd } from "docs-utils"
import { existsSync } from "fs"
import { unstable_cache } from "next/cache"
import { notFound } from "next/navigation"
import { NextRequest, NextResponse } from "next/server"
import path from "path"
import {
addUrlToRelativeLink,
crossProjectLinksPlugin,
localLinksRehypePlugin,
} from "remark-rehype-plugins"
import type { Plugin } from "unified"
type Params = {
params: Promise<{ slug: string[] }>
}
export async function GET(req: NextRequest, { params }: Params) {
const { slug } = await params
// keep this so that Vercel keeps the files in deployment
const basePath = path.join(process.cwd(), "app")
const filePath = path.join(basePath, ...slug, "page.mdx")
if (!existsSync(filePath)) {
return notFound()
}
const cleanMdContent = await getCleanMd_(filePath, {
before: [
[
crossProjectLinksPlugin,
{
baseUrl: process.env.NEXT_PUBLIC_BASE_URL,
projectUrls: {
docs: {
url: process.env.NEXT_PUBLIC_DOCS_URL,
path: "",
},
resources: {
url: process.env.NEXT_PUBLIC_RESOURCES_URL,
},
ui: {
url: process.env.NEXT_PUBLIC_UI_URL,
},
api: {
url: process.env.NEXT_PUBLIC_API_URL,
},
},
useBaseUrl:
process.env.NODE_ENV === "production" ||
process.env.VERCEL_ENV === "production",
},
],
[localLinksRehypePlugin],
] as unknown as Plugin[],
after: [
[addUrlToRelativeLink, { url: process.env.NEXT_PUBLIC_BASE_URL }],
] as unknown as Plugin[],
})
return new NextResponse(cleanMdContent, {
headers: {
"Content-Type": "text/markdown",
},
})
}
const getCleanMd_ = unstable_cache(
async (filePath: string, plugins?: { before?: Plugin[]; after?: Plugin[] }) =>
getCleanMd({ filePath, plugins }),
["clean-md"],
{
revalidate: 3600,
}
)
+15
View File
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export function middleware(request: NextRequest) {
return NextResponse.rewrite(
new URL(
`${request.nextUrl.basePath}/md-content${request.nextUrl.pathname.replace("/index.html.md", "")}`,
request.url
)
)
}
export const config = {
matcher: "/:path*/index.html.md",
}
+1
View File
@@ -29,6 +29,7 @@
}, },
"dependencies": { "dependencies": {
"@mdx-js/mdx": "^3.1.0", "@mdx-js/mdx": "^3.1.0",
"react-docgen": "^7.1.0",
"remark-frontmatter": "^5.0.0", "remark-frontmatter": "^5.0.0",
"remark-mdx": "^3.1.0", "remark-mdx": "^3.1.0",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
+109 -43
View File
@@ -2,25 +2,62 @@ import remarkMdx from "remark-mdx"
import remarkParse from "remark-parse" import remarkParse from "remark-parse"
import remarkStringify from "remark-stringify" import remarkStringify from "remark-stringify"
import { read } from "to-vfile" import { read } from "to-vfile"
import { UnistNode, UnistNodeWithData, UnistTree } from "types" import { FrontMatter, UnistNode, UnistNodeWithData, UnistTree } from "types"
import { Plugin, Transformer, unified } from "unified" import { Plugin, Transformer, unified } from "unified"
import { SKIP } from "unist-util-visit" import { SKIP } from "unist-util-visit"
import type { VFile } from "vfile" import type { VFile } from "vfile"
import { import {
ComponentParser,
parseCard, parseCard,
parseCardList, parseCardList,
parseCodeTabs, parseCodeTabs,
parseColors,
parseComponentExample,
parseComponentReference,
parseDetails, parseDetails,
parseHookValues,
parseIconSearch,
parseNote, parseNote,
parsePackageInstall,
parsePrerequisites, parsePrerequisites,
parseSourceCodeLink, parseSourceCodeLink,
parseTable, parseTable,
parseTabs, parseTabs,
parseTypeList, parseTypeList,
parseWorkflowDiagram, parseWorkflowDiagram,
} from "./utils/parse-elms.js" } from "./utils/parsers.js"
import remarkFrontmatter from "remark-frontmatter"
import { matter } from "vfile-matter"
const parseComponentsPlugin = (): Transformer => { const parsers: Record<string, ComponentParser> = {
Card: parseCard,
CardList: parseCardList,
CodeTabs: parseCodeTabs,
Details: parseDetails,
Note: parseNote,
Prerequisites: parsePrerequisites,
SourceCodeLink: parseSourceCodeLink,
Table: parseTable,
Tabs: parseTabs,
TypeList: parseTypeList,
WorkflowDiagram: parseWorkflowDiagram,
ComponentExample: parseComponentExample,
ComponentReference: parseComponentReference,
PackageInstall: parsePackageInstall,
IconSearch: parseIconSearch,
HookValues: parseHookValues,
Colors: parseColors,
}
const isComponentAllowed = (nodeName: string): boolean => {
return Object.keys(parsers).includes(nodeName)
}
type ParserPluginOptions = {
[key: string]: unknown
}
const parseComponentsPlugin = (options: ParserPluginOptions): Transformer => {
return async (tree) => { return async (tree) => {
const { visit } = await import("unist-util-visit") const { visit } = await import("unist-util-visit")
@@ -50,81 +87,110 @@ const parseComponentsPlugin = (): Transformer => {
} }
} }
if (node.type === "heading") { if (node.type === "heading") {
if ( if (node.depth === 1 && node.children?.length) {
node.depth === 1 && if (node.children[0].value === "metadata.title") {
node.children?.length &&
node.children[0].value === "metadata.title"
) {
node.children[0] = { node.children[0] = {
type: "text", type: "text",
value: pageTitle, value: pageTitle,
} }
} else {
node.children = node.children
.filter((child) => child.type === "text")
.map((child) => ({
...child,
value: child.value?.trim(),
}))
}
} }
return return
} }
if ( if (
node.type === "mdxjsEsm" || node.type === "mdxjsEsm" ||
node.name === "Feedback" || !isComponentAllowed(node.name as string)
node.name === "ChildDocs" ||
node.name === "DetailsList"
) { ) {
parent?.children.splice(index, 1) parent?.children.splice(index, 1)
return [SKIP, index] return [SKIP, index]
} }
switch (node.name) {
case "Card": if (!node.name) {
return parseCard(node, index, parent) return
case "CardList": }
return parseCardList(node as UnistNodeWithData, index, parent)
case "CodeTabs": const parser = parsers[node.name]
return parseCodeTabs(node as UnistNodeWithData, index, parent) if (parser) {
case "Details": const parserOptions = options[node.name] || {}
return parseDetails(node as UnistNodeWithData, index, parent) return parser(node as UnistNodeWithData, index, parent, parserOptions)
case "Note": }
return parseNote(node, index, parent) }
case "Prerequisites":
return parsePrerequisites(node as UnistNodeWithData, index, parent)
case "SourceCodeLink":
return parseSourceCodeLink(node as UnistNodeWithData, index, parent)
case "Table":
return parseTable(node as UnistNodeWithData, index, parent)
case "Tabs":
return parseTabs(node as UnistNodeWithData, index, parent)
case "TypeList":
return parseTypeList(node as UnistNodeWithData, index, parent)
case "WorkflowDiagram":
return parseWorkflowDiagram(
node as UnistNodeWithData,
index,
parent
) )
} }
}
const removeFrontmatterPlugin = (): Transformer => {
return async (tree) => {
const { visit } = await import("unist-util-visit")
visit(
tree as UnistTree,
["yaml", "toml"],
(node: UnistNode, index, parent) => {
if (typeof index !== "number" || parent?.type !== "root") {
return
}
parent.children.splice(index, 1)
return [SKIP, index]
} }
) )
} }
} }
const getParsedAsString = (file: VFile): string => { const getParsedAsString = (file: VFile): string => {
return file.toString().replaceAll(/^([\s]*)\* /gm, "$1- ") let content = file.toString().replaceAll(/^([\s]*)\* /gm, "$1- ")
const frontmatter = file.data.matter as FrontMatter | undefined
if (frontmatter?.title) {
content = `# ${frontmatter.title}\n\n${frontmatter.description ? `${frontmatter.description}\n\n` : ""}${content}`
}
return content
} }
export const getCleanMd = async ( type Options = {
filePath: string, filePath: string
plugins?: { plugins?: {
before?: Plugin[] before?: Plugin[]
after?: Plugin[] after?: Plugin[]
} }
): Promise<string> => { parserOptions?: ParserPluginOptions
}
export const getCleanMd = async ({
filePath,
plugins,
parserOptions,
}: Options): Promise<string> => {
if (!filePath.endsWith(".md") && !filePath.endsWith(".mdx")) { if (!filePath.endsWith(".md") && !filePath.endsWith(".mdx")) {
return "" return ""
} }
const unifier = unified().use(remarkParse).use(remarkMdx).use(remarkStringify) const unifier = unified()
.use(remarkParse)
.use(remarkMdx)
.use(remarkStringify)
.use(remarkFrontmatter, ["yaml"])
.use(() => {
return (tree, file) => {
matter(file)
}
})
plugins?.before?.forEach((plugin) => { plugins?.before?.forEach((plugin) => {
unifier.use(...(Array.isArray(plugin) ? plugin : [plugin])) unifier.use(...(Array.isArray(plugin) ? plugin : [plugin]))
}) })
unifier.use(parseComponentsPlugin) unifier
.use(parseComponentsPlugin, parserOptions || {})
.use(removeFrontmatterPlugin)
plugins?.after?.forEach((plugin) => { plugins?.after?.forEach((plugin) => {
unifier.use(...(Array.isArray(plugin) ? plugin : [plugin])) unifier.use(...(Array.isArray(plugin) ? plugin : [plugin]))
@@ -5,9 +5,19 @@ import {
isExpressionJsVarLiteral, isExpressionJsVarLiteral,
isExpressionJsVarObj, isExpressionJsVarObj,
} from "../expression-is-utils.js" } from "../expression-is-utils.js"
import path from "path"
import { readFileSync } from "fs"
import type { Documentation } from "react-docgen"
export const parseCard = ( export type ComponentParser<TOptions = any> = (
node: UnistNode, node: UnistNodeWithData,
index: number,
parent: UnistTree,
options?: TOptions
) => VisitorResult
export const parseCard: ComponentParser = (
node: UnistNodeWithData,
index: number, index: number,
parent: UnistTree parent: UnistTree
): VisitorResult => { ): VisitorResult => {
@@ -52,7 +62,7 @@ export const parseCard = (
return [SKIP, index] return [SKIP, index]
} }
export const parseCardList = ( export const parseCardList: ComponentParser = (
node: UnistNodeWithData, node: UnistNodeWithData,
index: number, index: number,
parent: UnistTree parent: UnistTree
@@ -72,30 +82,40 @@ export const parseCardList = (
.map((item) => { .map((item) => {
if ( if (
!isExpressionJsVarObj(item) || !isExpressionJsVarObj(item) ||
!("text" in item) || !("title" in item) ||
!("link" in item) || !("href" in item) ||
!isExpressionJsVarLiteral(item.text) || !isExpressionJsVarLiteral(item.title) ||
!isExpressionJsVarLiteral(item.link) !isExpressionJsVarLiteral(item.href)
) { ) {
return null return null
} }
const description = isExpressionJsVarLiteral(item.text)
? (item.text.data as string)
: ""
const children: UnistNode[] = [
{
type: "link",
url: `${item.href.data}`,
children: [
{
type: "text",
value: item.title.data as string,
},
],
},
]
if (description.length) {
children.push({
type: "text",
value: `: ${description}`,
})
}
return { return {
type: "listItem", type: "listItem",
children: [ children: [
{ {
type: "paragraph", type: "paragraph",
children: [ children,
{
type: "link",
url: `#${item.link.data}`,
children: [
{
type: "text",
value: item.text.data,
},
],
},
],
}, },
], ],
} }
@@ -111,7 +131,7 @@ export const parseCardList = (
return [SKIP, index] return [SKIP, index]
} }
export const parseCodeTabs = ( export const parseCodeTabs: ComponentParser = (
node: UnistNodeWithData, node: UnistNodeWithData,
index: number, index: number,
parent: UnistTree parent: UnistTree
@@ -131,30 +151,26 @@ export const parseCodeTabs = (
return return
} }
children.push({ children.push(
type: "mdxJsxFlowElement",
name: "details",
children: [
{ {
type: "mdxJsxFlowElement", type: "heading",
name: "summary", depth: 3,
children: [ children: [
{ {
type: "text", type: "text",
value: (label.value as string) || "summary", value: label.value as string,
}, },
], ],
}, },
code, code
], )
})
}) })
parent?.children.splice(index, 1, ...children) parent?.children.splice(index, 1, ...children)
return [SKIP, index] return [SKIP, index]
} }
export const parseDetails = ( export const parseDetails: ComponentParser = (
node: UnistNodeWithData, node: UnistNodeWithData,
index: number, index: number,
parent: UnistTree parent: UnistTree
@@ -163,28 +179,29 @@ export const parseDetails = (
(attr) => attr.name === "summaryContent" (attr) => attr.name === "summaryContent"
) )
parent?.children.splice(index, 1, { const children: UnistNode[] = []
type: "mdxJsxFlowElement",
name: "details", if (summary?.value) {
children: [ children.push({
{ type: "heading",
type: "mdxJsxFlowElement", depth: 3,
name: "summary",
children: [ children: [
{ {
type: "text", type: "text",
value: (summary?.value as string) || "Details", value: (summary?.value as string) || "Details",
}, },
], ],
},
...(node.children || []),
],
}) })
}
children.push(...(node.children || []))
parent?.children.splice(index, 1, ...children)
return [SKIP, index] return [SKIP, index]
} }
export const parseNote = ( export const parseNote: ComponentParser = (
node: UnistNode, node: UnistNodeWithData,
index: number, index: number,
parent: UnistTree parent: UnistTree
): VisitorResult => { ): VisitorResult => {
@@ -192,7 +209,7 @@ export const parseNote = (
return [SKIP, index] return [SKIP, index]
} }
export const parsePrerequisites = ( export const parsePrerequisites: ComponentParser = (
node: UnistNodeWithData, node: UnistNodeWithData,
index: number, index: number,
parent: UnistTree parent: UnistTree
@@ -265,7 +282,7 @@ export const parsePrerequisites = (
return [SKIP, index] return [SKIP, index]
} }
export const parseSourceCodeLink = ( export const parseSourceCodeLink: ComponentParser = (
node: UnistNodeWithData, node: UnistNodeWithData,
index: number, index: number,
parent: UnistTree parent: UnistTree
@@ -293,7 +310,7 @@ export const parseSourceCodeLink = (
return [SKIP, index] return [SKIP, index]
} }
export const parseTable = ( export const parseTable: ComponentParser = (
node: UnistNodeWithData, node: UnistNodeWithData,
index: number, index: number,
parent: UnistTree parent: UnistTree
@@ -348,7 +365,7 @@ export const parseTable = (
}) })
} }
export const parseTabs = ( export const parseTabs: ComponentParser = (
node: UnistNodeWithData, node: UnistNodeWithData,
index: number, index: number,
parent: UnistTree parent: UnistTree
@@ -368,13 +385,10 @@ export const parseTabs = (
return return
} }
tabs.push({ tabs.push(
type: "mdxJsxFlowElement",
name: "details",
children: [
{ {
type: "mdxJsxFlowElement", type: "heading",
name: "summary", depth: 3,
children: [ children: [
{ {
type: "text", type: "text",
@@ -382,9 +396,8 @@ export const parseTabs = (
}, },
], ],
}, },
...tabContent, ...tabContent
], )
})
}) })
}) })
@@ -392,7 +405,7 @@ export const parseTabs = (
return [SKIP, index] return [SKIP, index]
} }
export const parseTypeList = ( export const parseTypeList: ComponentParser = (
node: UnistNodeWithData, node: UnistNodeWithData,
index: number, index: number,
parent: UnistTree parent: UnistTree
@@ -435,7 +448,7 @@ export const parseTypeList = (
children: [ children: [
{ {
type: "text", type: "text",
value: `${typeName}: (${itemType}) ${itemDescription}`, value: `${typeName}: (${itemType}) ${itemDescription}`.trim(),
}, },
], ],
}, },
@@ -465,7 +478,7 @@ export const parseTypeList = (
return [SKIP, index] return [SKIP, index]
} }
export const parseWorkflowDiagram = ( export const parseWorkflowDiagram: ComponentParser = (
node: UnistNodeWithData, node: UnistNodeWithData,
index: number, index: number,
parent: UnistTree parent: UnistTree
@@ -558,6 +571,321 @@ export const parseWorkflowDiagram = (
return [SKIP, index] return [SKIP, index]
} }
export const parseComponentExample: ComponentParser<{
examplesBasePath: string
}> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.examplesBasePath) {
return
}
const exampleName = node.attributes?.find((attr) => attr.name === "name")
if (!exampleName) {
return
}
const fileContent = readFileSync(
path.join(options.examplesBasePath, `${exampleName.value as string}.tsx`),
"utf-8"
)
parent.children?.splice(index, 1, {
type: "code",
lang: "tsx",
value: fileContent,
})
return [SKIP, index]
}
export const parseComponentReference: ComponentParser<{ specsPath: string }> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.specsPath) {
return
}
const mainComponent = node.attributes?.find(
(attr) => attr.name === "mainComponent"
)?.value as string
if (!mainComponent) {
return
}
const componentNames: string[] = []
const componentsToShowAttr = node.attributes?.find(
(attr) => attr.name === "componentsToShow"
)
if (
componentsToShowAttr &&
typeof componentsToShowAttr.value !== "string" &&
componentsToShowAttr.value.data?.estree
) {
const componentsToShowJsVar = estreeToJs(
componentsToShowAttr.value.data.estree
)
if (componentsToShowAttr && Array.isArray(componentsToShowJsVar)) {
componentNames.push(
...componentsToShowJsVar
.map((item) => {
return isExpressionJsVarLiteral(item) ? (item.data as string) : ""
})
.filter((name) => name.length > 0)
)
}
}
if (!componentNames.length) {
componentNames.push(mainComponent)
}
const getComponentNodes = (componentName: string): UnistNode[] => {
const componentSpecsFile = path.join(
options.specsPath,
mainComponent,
`${componentName}.json`
)
const componentSpecs: Documentation = JSON.parse(
readFileSync(componentSpecsFile, "utf-8")
)
const componentNodes: UnistNode[] = [
{
type: "heading",
depth: 3,
children: [
{
type: "text",
value: `${componentName} Props`,
},
],
},
]
if (componentSpecs.description) {
componentNodes.push({
type: "paragraph",
children: [
{
type: "text",
value: componentSpecs.description,
},
],
})
}
if (componentSpecs.props) {
const listNode: UnistNode = {
type: "list",
ordered: false,
spread: false,
children: [],
}
Object.entries(componentSpecs.props).forEach(([propName, propData]) => {
listNode.children?.push({
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "text",
value:
`${propName}: (${propData.type?.name || propData.tsType?.name}) ${propData.description || ""}${propData.defaultValue ? ` Default: ${propData.defaultValue.value}` : ""}`.trim(),
},
],
},
],
})
})
componentNodes.push(listNode)
}
return componentNodes
}
parent.children?.splice(
index,
1,
...componentNames.flatMap(getComponentNodes)
)
}
export const parsePackageInstall: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
const packageName = node.attributes?.find(
(attr) => attr.name === "packageName"
)
if (!packageName) {
return
}
parent.children?.splice(index, 1, {
type: "code",
lang: "bash",
value: `npm install ${packageName.value}`,
})
return [SKIP, index]
}
export const parseIconSearch: ComponentParser<{ iconNames: string[] }> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.iconNames) {
return
}
parent.children?.splice(index, 1, {
type: "list",
ordered: false,
spread: false,
children: options.iconNames.map((iconName) => ({
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "text",
value: iconName,
},
],
},
],
})),
})
return [SKIP, index]
}
export const parseHookValues: ComponentParser<{
hooksData: {
[k: string]: {
value: string
type?: {
type: string
}
description?: string
}[]
}
}> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.hooksData) {
return
}
const hookName = node.attributes?.find((attr) => attr.name === "hook")
if (
!hookName ||
!hookName.value ||
typeof hookName.value !== "string" ||
!options.hooksData[hookName.value]
) {
return
}
const hookData = options.hooksData[hookName.value]
const listItems = hookData.map((item) => {
return {
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "text",
value:
`${item.value}: (${item.type?.type}) ${item.description || ""}`.trim(),
},
],
},
],
}
})
parent.children?.splice(index, 1, {
type: "list",
ordered: false,
spread: false,
children: listItems,
})
return [SKIP, index]
}
export const parseColors: ComponentParser<{
colors: {
[k: string]: Record<string, string>
}
}> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.colors) {
return
}
parent.children?.splice(index, 1, {
type: "list",
ordered: false,
spread: false,
children: Object.entries(options.colors).flatMap(([section, colors]) => [
{
type: "heading",
depth: 3,
children: [
{
type: "text",
value: section,
},
],
},
...Object.entries(colors).map(([name, value]) => ({
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "text",
value: name,
},
{
type: "text",
value: `: ${value}`,
},
],
},
],
})),
]),
})
}
/** /**
* Helpers * Helpers
*/ */
@@ -7,6 +7,7 @@ export * from "./page-number.js"
export * from "./prerequisites-link-fixer.js" export * from "./prerequisites-link-fixer.js"
export * from "./resolve-admonitions.js" export * from "./resolve-admonitions.js"
export * from "./type-list-link-fixer.js" export * from "./type-list-link-fixer.js"
export * from "./ui-rehype-plugin.js"
export * from "./workflow-diagram-link-fixer.js" export * from "./workflow-diagram-link-fixer.js"
export * from "./utils/fix-link.js" export * from "./utils/fix-link.js"
@@ -0,0 +1,82 @@
import fs from "fs"
import path from "path"
import { u } from "unist-builder"
import { visit } from "unist-util-visit"
import { Documentation } from "react-docgen"
import { ExampleRegistry, UnistNode, UnistTree } from "types"
type Options = {
exampleRegistry: ExampleRegistry
}
export function uiRehypePlugin({ exampleRegistry }: Options) {
return async (tree: UnistTree) => {
visit(tree, (node: UnistNode) => {
if (node.name === "ComponentExample") {
const name = getNodeAttributeByName(node, "name")?.value as string
if (!name) {
return null
}
try {
const component = exampleRegistry[name]
const src = component.file
const filePath = path.join(process.cwd(), src)
let source = fs.readFileSync(filePath, "utf8")
source = source.replaceAll("export default", "export")
// Trim newline at the end of file. It's correct, but it makes source display look off
if (source.endsWith("\n")) {
source = source.substring(0, source.length - 1)
}
node.children?.push(
u("element", {
tagName: "span",
properties: {
__src__: src,
code: source,
},
})
)
} catch (error) {
console.error(error)
}
} else if (node.name === "ComponentReference") {
const mainComponent = getNodeAttributeByName(node, "mainComponent")
?.value as string
if (!mainComponent) {
return null
}
const mainSpecsDir = path.join(process.cwd(), "src/specs")
const componentSpecsDir = path.join(mainSpecsDir, mainComponent)
const specs: Documentation[] = []
const specFiles = fs.readdirSync(componentSpecsDir)
specFiles.map((specFileName) => {
// read spec file
const specFile = fs.readFileSync(
path.join(componentSpecsDir, specFileName),
"utf-8"
)
specs.push(JSON.parse(specFile) as Documentation)
})
node.attributes?.push({
name: "specsSrc",
value: JSON.stringify(specs),
type: "mdxJsxAttribute",
})
}
})
}
}
function getNodeAttributeByName(node: UnistNode, name: string) {
return node.attributes?.find((attribute) => attribute.name === name)
}
+2
View File
@@ -7,4 +7,6 @@ export declare type FrontMatter = {
sidebar_autogenerate_exclude?: boolean sidebar_autogenerate_exclude?: boolean
sidebar_description?: string sidebar_description?: string
tags?: string[] tags?: string[]
title?: string
description?: string
} }
+1
View File
@@ -10,4 +10,5 @@ export * from "./navigation-dropdown.js"
export * from "./sidebar.js" export * from "./sidebar.js"
export * from "./tags.js" export * from "./tags.js"
export * from "./toc.js" export * from "./toc.js"
export * from "./ui.js"
export * from "./workflow.js" export * from "./workflow.js"
+1
View File
@@ -18,6 +18,7 @@ export interface UnistNode extends Node {
url?: string url?: string
spread?: boolean spread?: boolean
depth?: number depth?: number
lang?: string
} }
export type ArrayExpression = { export type ArrayExpression = {
+7
View File
@@ -0,0 +1,7 @@
export type ExampleType = {
name: string
component: React.LazyExoticComponent<() => React.JSX.Element>
file: string
}
export type ExampleRegistry = Record<string, ExampleType>
+4
View File
@@ -5868,6 +5868,7 @@ __metadata:
build-scripts: "*" build-scripts: "*"
clsx: ^2.1.0 clsx: ^2.1.0
docs-ui: "*" docs-ui: "*"
docs-utils: "*"
eslint: ^9.13.0 eslint: ^9.13.0
eslint-plugin-prettier: ^5.2.1 eslint-plugin-prettier: ^5.2.1
eslint-plugin-react-hooks: ^5.0.0 eslint-plugin-react-hooks: ^5.0.0
@@ -7198,6 +7199,7 @@ __metadata:
dependencies: dependencies:
"@mdx-js/mdx": ^3.1.0 "@mdx-js/mdx": ^3.1.0
"@types/node": ^20.11.20 "@types/node": ^20.11.20
react-docgen: ^7.1.0
remark-frontmatter: ^5.0.0 remark-frontmatter: ^5.0.0
remark-mdx: ^3.1.0 remark-mdx: ^3.1.0
remark-parse: ^11.0.0 remark-parse: ^11.0.0
@@ -15044,6 +15046,7 @@ turbo@latest:
contentlayer: ^0.3.4 contentlayer: ^0.3.4
date-fns: ^3.3.1 date-fns: ^3.3.1
docs-ui: "*" docs-ui: "*"
docs-utils: "*"
eslint: ^9.13.0 eslint: ^9.13.0
eslint-plugin-prettier: ^5.2.1 eslint-plugin-prettier: ^5.2.1
eslint-plugin-react-hooks: ^5.0.0 eslint-plugin-react-hooks: ^5.0.0
@@ -15057,6 +15060,7 @@ turbo@latest:
react-dom: rc react-dom: rc
rehype-slug: ^6.0.0 rehype-slug: ^6.0.0
remark: ^14.0.3 remark: ^14.0.3
remark-rehype-plugins: "*"
tailwind: "*" tailwind: "*"
tailwindcss: 3.3.3 tailwindcss: 3.3.3
ts-node: ^10.9.1 ts-node: ^10.9.1