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:
@@ -205,40 +205,40 @@ You can now build your plugin's customizations. The following guide explains how
|
||||
<CardList
|
||||
items={[
|
||||
{
|
||||
text: "Create a module",
|
||||
link: "/learn/fundamentals/modules",
|
||||
title: "Create a module",
|
||||
href: "/learn/fundamentals/modules",
|
||||
},
|
||||
{
|
||||
text: "Create a module link",
|
||||
link: "/learn/fundamentals/module-links",
|
||||
title: "Create a module link",
|
||||
href: "/learn/fundamentals/module-links",
|
||||
},
|
||||
{
|
||||
text: "Create a workflow",
|
||||
link: "/learn/fundamentals/workflows",
|
||||
title: "Create a workflow",
|
||||
href: "/learn/fundamentals/workflows",
|
||||
},
|
||||
{
|
||||
text: "Add a workflow hook",
|
||||
link: "/learn/fundamentals/workflows/add-workflow-hook",
|
||||
title: "Add a workflow hook",
|
||||
href: "/learn/fundamentals/workflows/add-workflow-hook",
|
||||
},
|
||||
{
|
||||
text: "Create an API route",
|
||||
link: "/learn/fundamentals/api-routes",
|
||||
title: "Create an API route",
|
||||
href: "/learn/fundamentals/api-routes",
|
||||
},
|
||||
{
|
||||
text: "Add a subscriber",
|
||||
link: "/learn/fundamentals/events-and-subscribers",
|
||||
title: "Add a subscriber",
|
||||
href: "/learn/fundamentals/events-and-subscribers",
|
||||
},
|
||||
{
|
||||
text: "Add a scheduled job",
|
||||
link: "/learn/fundamentals/scheduled-jobs",
|
||||
title: "Add a scheduled job",
|
||||
href: "/learn/fundamentals/scheduled-jobs",
|
||||
},
|
||||
{
|
||||
text: "Add an admin widget",
|
||||
link: "/learn/fundamentals/admin/widgets",
|
||||
title: "Add an admin widget",
|
||||
href: "/learn/fundamentals/admin/widgets",
|
||||
},
|
||||
{
|
||||
text: "Add an admin UI route",
|
||||
link: "/learn/fundamentals/admin/ui-routes",
|
||||
title: "Add an admin UI route",
|
||||
href: "/learn/fundamentals/admin/ui-routes",
|
||||
}
|
||||
]}
|
||||
className="mb-1.5"
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ You can build your storefront from scratch with your preferred tech stack, or st
|
||||
<CardList
|
||||
items={[
|
||||
{
|
||||
text: "Install Next.js Starter Storefront",
|
||||
title: "Install Next.js Starter Storefront",
|
||||
href: "!resources!/nextjs-starter"
|
||||
},
|
||||
{
|
||||
text: "Build Custom Storefront",
|
||||
title: "Build Custom Storefront",
|
||||
href: "!resources!/storefront-development"
|
||||
}
|
||||
]}
|
||||
|
||||
75
www/apps/book/app/md-content/[...slug]/route.ts
Normal file
75
www/apps/book/app/md-content/[...slug]/route.ts
Normal file
@@ -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
www/apps/book/middleware.ts
Normal file
15
www/apps/book/middleware.ts
Normal 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",
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"@next/mdx": "15.0.4",
|
||||
"clsx": "^2.1.0",
|
||||
"docs-ui": "*",
|
||||
"docs-utils": "*",
|
||||
"next": "15.0.4",
|
||||
"react": "rc",
|
||||
"react-dom": "rc",
|
||||
|
||||
98
www/apps/resources/app/md-content/[[...slug]]/route.ts
Normal file
98
www/apps/resources/app/md-content/[[...slug]]/route.ts
Normal file
@@ -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
www/apps/resources/middleware.ts
Normal file
15
www/apps/resources/middleware.ts
Normal 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,8 +1,9 @@
|
||||
import "dotenv/config"
|
||||
|
||||
import { defineDocumentType, makeSource } from "contentlayer/source-files"
|
||||
import { rehypeComponent } from "./src/lib/rehype-component"
|
||||
import rehypeSlug from "rehype-slug"
|
||||
import { uiRehypePlugin } from "../../packages/remark-rehype-plugins/src"
|
||||
import { ExampleRegistry } from "./src/registries/example-registry"
|
||||
|
||||
export const Doc = defineDocumentType(() => ({
|
||||
name: "Doc",
|
||||
@@ -29,7 +30,15 @@ export default makeSource({
|
||||
contentDirPath: "./src/content",
|
||||
documentTypes: [Doc],
|
||||
mdx: {
|
||||
rehypePlugins: [[rehypeComponent], [rehypeSlug]],
|
||||
rehypePlugins: [
|
||||
[
|
||||
uiRehypePlugin,
|
||||
{
|
||||
exampleRegistry: ExampleRegistry,
|
||||
},
|
||||
],
|
||||
[rehypeSlug],
|
||||
],
|
||||
mdxOptions: (options) => {
|
||||
return {
|
||||
...options,
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"contentlayer": "^0.3.4",
|
||||
"date-fns": "^3.3.1",
|
||||
"docs-ui": "*",
|
||||
"docs-utils": "*",
|
||||
"mdast-util-toc": "^7.0.0",
|
||||
"next": "15.0.4",
|
||||
"next-contentlayer": "^0.3.4",
|
||||
@@ -47,6 +48,7 @@
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"react-docgen": "^7.1.0",
|
||||
"remark-rehype-plugins": "*",
|
||||
"ts-node": "^10.9.1",
|
||||
"types": "*"
|
||||
},
|
||||
|
||||
86
www/apps/ui/src/app/md-content/[[...slug]]/route.ts
Normal file
86
www/apps/ui/src/app/md-content/[[...slug]]/route.ts
Normal file
@@ -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
www/apps/ui/src/middleware.ts
Normal file
15
www/apps/ui/src/middleware.ts
Normal 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,19 +1,8 @@
|
||||
import { HookTable } from "@/components/hook-table"
|
||||
import { HookDataMap } from "@/types/hooks"
|
||||
|
||||
const useToastValues: HookDataMap = [
|
||||
{
|
||||
value: "dialog",
|
||||
type: {
|
||||
type: "function",
|
||||
signature: `async (props: PromptProps): Promise<boolean>`,
|
||||
},
|
||||
description: "Async function used to display a new confirmation dialog.",
|
||||
},
|
||||
]
|
||||
import { usePrompt } from "../../registries/hook-values"
|
||||
|
||||
const Props = () => {
|
||||
return <HookTable props={useToastValues} />
|
||||
return <HookTable props={usePrompt} />
|
||||
}
|
||||
|
||||
export default Props
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
import { HookTable } from "@/components/hook-table"
|
||||
import { HookDataMap } from "@/types/hooks"
|
||||
|
||||
const useToggleStateValuesArray: HookDataMap = [
|
||||
{
|
||||
value: "state",
|
||||
type: {
|
||||
type: "object",
|
||||
name: "StateData",
|
||||
shape:
|
||||
"[\n state: boolean,\n open: () => void,\n close: () => void,\n toggle: () => void\n]",
|
||||
},
|
||||
},
|
||||
]
|
||||
import { useToggleState } from "../../registries/hook-values"
|
||||
|
||||
const Props = () => {
|
||||
return <HookTable props={useToggleStateValuesArray} />
|
||||
return <HookTable props={useToggleState} />
|
||||
}
|
||||
|
||||
export default Props
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import * as React from "react"
|
||||
import { ExampleRegistry as ExampleRegistryType } from "types"
|
||||
|
||||
type ExampleType = {
|
||||
name: string
|
||||
component: React.LazyExoticComponent<() => React.JSX.Element>
|
||||
file: string
|
||||
}
|
||||
|
||||
export const ExampleRegistry: Record<string, ExampleType> = {
|
||||
export const ExampleRegistry: ExampleRegistryType = {
|
||||
"alert-demo": {
|
||||
name: "alert-demo",
|
||||
component: React.lazy(async () => import("@/examples/alert-demo")),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { HookRegistryItem } from "@/types/hooks"
|
||||
import * as React from "react"
|
||||
|
||||
export type HookRegistryItem = {
|
||||
table: React.LazyExoticComponent<React.ComponentType>
|
||||
}
|
||||
|
||||
export const HookRegistry: Record<string, HookRegistryItem> = {
|
||||
usePrompt: {
|
||||
table: React.lazy(async () => import("../props/hooks/usePrompt")),
|
||||
|
||||
24
www/apps/ui/src/registries/hook-values.ts
Normal file
24
www/apps/ui/src/registries/hook-values.ts
Normal 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.",
|
||||
},
|
||||
]
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ComponentType, LazyExoticComponent } from "react"
|
||||
import { PropType } from "./props"
|
||||
|
||||
export type HookData = {
|
||||
@@ -8,7 +7,3 @@ export type HookData = {
|
||||
}
|
||||
|
||||
export type HookDataMap = HookData[]
|
||||
|
||||
export type HookRegistryItem = {
|
||||
table: LazyExoticComponent<ComponentType>
|
||||
}
|
||||
|
||||
76
www/apps/user-guide/app/md-content/[...slug]/route.ts
Normal file
76
www/apps/user-guide/app/md-content/[...slug]/route.ts
Normal file
@@ -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
www/apps/user-guide/middleware.ts
Normal file
15
www/apps/user-guide/middleware.ts
Normal 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",
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdx-js/mdx": "^3.1.0",
|
||||
"react-docgen": "^7.1.0",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-mdx": "^3.1.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
|
||||
@@ -2,25 +2,62 @@ import remarkMdx from "remark-mdx"
|
||||
import remarkParse from "remark-parse"
|
||||
import remarkStringify from "remark-stringify"
|
||||
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 { SKIP } from "unist-util-visit"
|
||||
import type { VFile } from "vfile"
|
||||
import {
|
||||
ComponentParser,
|
||||
parseCard,
|
||||
parseCardList,
|
||||
parseCodeTabs,
|
||||
parseColors,
|
||||
parseComponentExample,
|
||||
parseComponentReference,
|
||||
parseDetails,
|
||||
parseHookValues,
|
||||
parseIconSearch,
|
||||
parseNote,
|
||||
parsePackageInstall,
|
||||
parsePrerequisites,
|
||||
parseSourceCodeLink,
|
||||
parseTable,
|
||||
parseTabs,
|
||||
parseTypeList,
|
||||
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) => {
|
||||
const { visit } = await import("unist-util-visit")
|
||||
|
||||
@@ -50,81 +87,110 @@ const parseComponentsPlugin = (): Transformer => {
|
||||
}
|
||||
}
|
||||
if (node.type === "heading") {
|
||||
if (
|
||||
node.depth === 1 &&
|
||||
node.children?.length &&
|
||||
node.children[0].value === "metadata.title"
|
||||
) {
|
||||
node.children[0] = {
|
||||
type: "text",
|
||||
value: pageTitle,
|
||||
if (node.depth === 1 && node.children?.length) {
|
||||
if (node.children[0].value === "metadata.title") {
|
||||
node.children[0] = {
|
||||
type: "text",
|
||||
value: pageTitle,
|
||||
}
|
||||
} else {
|
||||
node.children = node.children
|
||||
.filter((child) => child.type === "text")
|
||||
.map((child) => ({
|
||||
...child,
|
||||
value: child.value?.trim(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if (
|
||||
node.type === "mdxjsEsm" ||
|
||||
node.name === "Feedback" ||
|
||||
node.name === "ChildDocs" ||
|
||||
node.name === "DetailsList"
|
||||
!isComponentAllowed(node.name as string)
|
||||
) {
|
||||
parent?.children.splice(index, 1)
|
||||
return [SKIP, index]
|
||||
}
|
||||
switch (node.name) {
|
||||
case "Card":
|
||||
return parseCard(node, index, parent)
|
||||
case "CardList":
|
||||
return parseCardList(node as UnistNodeWithData, index, parent)
|
||||
case "CodeTabs":
|
||||
return parseCodeTabs(node as UnistNodeWithData, index, parent)
|
||||
case "Details":
|
||||
return parseDetails(node as UnistNodeWithData, index, parent)
|
||||
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
|
||||
)
|
||||
|
||||
if (!node.name) {
|
||||
return
|
||||
}
|
||||
|
||||
const parser = parsers[node.name]
|
||||
if (parser) {
|
||||
const parserOptions = options[node.name] || {}
|
||||
return parser(node as UnistNodeWithData, index, parent, parserOptions)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const getParsedAsString = (file: VFile): string => {
|
||||
return file.toString().replaceAll(/^([\s]*)\* /gm, "$1- ")
|
||||
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]
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const getCleanMd = async (
|
||||
filePath: string,
|
||||
const getParsedAsString = (file: VFile): string => {
|
||||
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
|
||||
}
|
||||
|
||||
type Options = {
|
||||
filePath: string
|
||||
plugins?: {
|
||||
before?: Plugin[]
|
||||
after?: Plugin[]
|
||||
}
|
||||
): Promise<string> => {
|
||||
parserOptions?: ParserPluginOptions
|
||||
}
|
||||
|
||||
export const getCleanMd = async ({
|
||||
filePath,
|
||||
plugins,
|
||||
parserOptions,
|
||||
}: Options): Promise<string> => {
|
||||
if (!filePath.endsWith(".md") && !filePath.endsWith(".mdx")) {
|
||||
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) => {
|
||||
unifier.use(...(Array.isArray(plugin) ? plugin : [plugin]))
|
||||
})
|
||||
|
||||
unifier.use(parseComponentsPlugin)
|
||||
unifier
|
||||
.use(parseComponentsPlugin, parserOptions || {})
|
||||
.use(removeFrontmatterPlugin)
|
||||
|
||||
plugins?.after?.forEach((plugin) => {
|
||||
unifier.use(...(Array.isArray(plugin) ? plugin : [plugin]))
|
||||
|
||||
@@ -5,9 +5,19 @@ import {
|
||||
isExpressionJsVarLiteral,
|
||||
isExpressionJsVarObj,
|
||||
} from "../expression-is-utils.js"
|
||||
import path from "path"
|
||||
import { readFileSync } from "fs"
|
||||
import type { Documentation } from "react-docgen"
|
||||
|
||||
export const parseCard = (
|
||||
node: UnistNode,
|
||||
export type ComponentParser<TOptions = any> = (
|
||||
node: UnistNodeWithData,
|
||||
index: number,
|
||||
parent: UnistTree,
|
||||
options?: TOptions
|
||||
) => VisitorResult
|
||||
|
||||
export const parseCard: ComponentParser = (
|
||||
node: UnistNodeWithData,
|
||||
index: number,
|
||||
parent: UnistTree
|
||||
): VisitorResult => {
|
||||
@@ -52,7 +62,7 @@ export const parseCard = (
|
||||
return [SKIP, index]
|
||||
}
|
||||
|
||||
export const parseCardList = (
|
||||
export const parseCardList: ComponentParser = (
|
||||
node: UnistNodeWithData,
|
||||
index: number,
|
||||
parent: UnistTree
|
||||
@@ -72,30 +82,40 @@ export const parseCardList = (
|
||||
.map((item) => {
|
||||
if (
|
||||
!isExpressionJsVarObj(item) ||
|
||||
!("text" in item) ||
|
||||
!("link" in item) ||
|
||||
!isExpressionJsVarLiteral(item.text) ||
|
||||
!isExpressionJsVarLiteral(item.link)
|
||||
!("title" in item) ||
|
||||
!("href" in item) ||
|
||||
!isExpressionJsVarLiteral(item.title) ||
|
||||
!isExpressionJsVarLiteral(item.href)
|
||||
) {
|
||||
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 {
|
||||
type: "listItem",
|
||||
children: [
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [
|
||||
{
|
||||
type: "link",
|
||||
url: `#${item.link.data}`,
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
value: item.text.data,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
children,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -111,7 +131,7 @@ export const parseCardList = (
|
||||
return [SKIP, index]
|
||||
}
|
||||
|
||||
export const parseCodeTabs = (
|
||||
export const parseCodeTabs: ComponentParser = (
|
||||
node: UnistNodeWithData,
|
||||
index: number,
|
||||
parent: UnistTree
|
||||
@@ -131,30 +151,26 @@ export const parseCodeTabs = (
|
||||
return
|
||||
}
|
||||
|
||||
children.push({
|
||||
type: "mdxJsxFlowElement",
|
||||
name: "details",
|
||||
children: [
|
||||
{
|
||||
type: "mdxJsxFlowElement",
|
||||
name: "summary",
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
value: (label.value as string) || "summary",
|
||||
},
|
||||
],
|
||||
},
|
||||
code,
|
||||
],
|
||||
})
|
||||
children.push(
|
||||
{
|
||||
type: "heading",
|
||||
depth: 3,
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
value: label.value as string,
|
||||
},
|
||||
],
|
||||
},
|
||||
code
|
||||
)
|
||||
})
|
||||
|
||||
parent?.children.splice(index, 1, ...children)
|
||||
return [SKIP, index]
|
||||
}
|
||||
|
||||
export const parseDetails = (
|
||||
export const parseDetails: ComponentParser = (
|
||||
node: UnistNodeWithData,
|
||||
index: number,
|
||||
parent: UnistTree
|
||||
@@ -163,28 +179,29 @@ export const parseDetails = (
|
||||
(attr) => attr.name === "summaryContent"
|
||||
)
|
||||
|
||||
parent?.children.splice(index, 1, {
|
||||
type: "mdxJsxFlowElement",
|
||||
name: "details",
|
||||
children: [
|
||||
{
|
||||
type: "mdxJsxFlowElement",
|
||||
name: "summary",
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
value: (summary?.value as string) || "Details",
|
||||
},
|
||||
],
|
||||
},
|
||||
...(node.children || []),
|
||||
],
|
||||
})
|
||||
const children: UnistNode[] = []
|
||||
|
||||
if (summary?.value) {
|
||||
children.push({
|
||||
type: "heading",
|
||||
depth: 3,
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
value: (summary?.value as string) || "Details",
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
children.push(...(node.children || []))
|
||||
|
||||
parent?.children.splice(index, 1, ...children)
|
||||
return [SKIP, index]
|
||||
}
|
||||
|
||||
export const parseNote = (
|
||||
node: UnistNode,
|
||||
export const parseNote: ComponentParser = (
|
||||
node: UnistNodeWithData,
|
||||
index: number,
|
||||
parent: UnistTree
|
||||
): VisitorResult => {
|
||||
@@ -192,7 +209,7 @@ export const parseNote = (
|
||||
return [SKIP, index]
|
||||
}
|
||||
|
||||
export const parsePrerequisites = (
|
||||
export const parsePrerequisites: ComponentParser = (
|
||||
node: UnistNodeWithData,
|
||||
index: number,
|
||||
parent: UnistTree
|
||||
@@ -265,7 +282,7 @@ export const parsePrerequisites = (
|
||||
return [SKIP, index]
|
||||
}
|
||||
|
||||
export const parseSourceCodeLink = (
|
||||
export const parseSourceCodeLink: ComponentParser = (
|
||||
node: UnistNodeWithData,
|
||||
index: number,
|
||||
parent: UnistTree
|
||||
@@ -293,7 +310,7 @@ export const parseSourceCodeLink = (
|
||||
return [SKIP, index]
|
||||
}
|
||||
|
||||
export const parseTable = (
|
||||
export const parseTable: ComponentParser = (
|
||||
node: UnistNodeWithData,
|
||||
index: number,
|
||||
parent: UnistTree
|
||||
@@ -348,7 +365,7 @@ export const parseTable = (
|
||||
})
|
||||
}
|
||||
|
||||
export const parseTabs = (
|
||||
export const parseTabs: ComponentParser = (
|
||||
node: UnistNodeWithData,
|
||||
index: number,
|
||||
parent: UnistTree
|
||||
@@ -368,23 +385,19 @@ export const parseTabs = (
|
||||
return
|
||||
}
|
||||
|
||||
tabs.push({
|
||||
type: "mdxJsxFlowElement",
|
||||
name: "details",
|
||||
children: [
|
||||
{
|
||||
type: "mdxJsxFlowElement",
|
||||
name: "summary",
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
value: tabLabel,
|
||||
},
|
||||
],
|
||||
},
|
||||
...tabContent,
|
||||
],
|
||||
})
|
||||
tabs.push(
|
||||
{
|
||||
type: "heading",
|
||||
depth: 3,
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
value: tabLabel,
|
||||
},
|
||||
],
|
||||
},
|
||||
...tabContent
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -392,7 +405,7 @@ export const parseTabs = (
|
||||
return [SKIP, index]
|
||||
}
|
||||
|
||||
export const parseTypeList = (
|
||||
export const parseTypeList: ComponentParser = (
|
||||
node: UnistNodeWithData,
|
||||
index: number,
|
||||
parent: UnistTree
|
||||
@@ -435,7 +448,7 @@ export const parseTypeList = (
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
value: `${typeName}: (${itemType}) ${itemDescription}`,
|
||||
value: `${typeName}: (${itemType}) ${itemDescription}`.trim(),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -465,7 +478,7 @@ export const parseTypeList = (
|
||||
return [SKIP, index]
|
||||
}
|
||||
|
||||
export const parseWorkflowDiagram = (
|
||||
export const parseWorkflowDiagram: ComponentParser = (
|
||||
node: UnistNodeWithData,
|
||||
index: number,
|
||||
parent: UnistTree
|
||||
@@ -558,6 +571,321 @@ export const parseWorkflowDiagram = (
|
||||
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
|
||||
*/
|
||||
@@ -7,6 +7,7 @@ export * from "./page-number.js"
|
||||
export * from "./prerequisites-link-fixer.js"
|
||||
export * from "./resolve-admonitions.js"
|
||||
export * from "./type-list-link-fixer.js"
|
||||
export * from "./ui-rehype-plugin.js"
|
||||
export * from "./workflow-diagram-link-fixer.js"
|
||||
|
||||
export * from "./utils/fix-link.js"
|
||||
|
||||
82
www/packages/remark-rehype-plugins/src/ui-rehype-plugin.ts
Normal file
82
www/packages/remark-rehype-plugins/src/ui-rehype-plugin.ts
Normal file
@@ -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)
|
||||
}
|
||||
@@ -7,4 +7,6 @@ export declare type FrontMatter = {
|
||||
sidebar_autogenerate_exclude?: boolean
|
||||
sidebar_description?: string
|
||||
tags?: string[]
|
||||
title?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
@@ -10,4 +10,5 @@ export * from "./navigation-dropdown.js"
|
||||
export * from "./sidebar.js"
|
||||
export * from "./tags.js"
|
||||
export * from "./toc.js"
|
||||
export * from "./ui.js"
|
||||
export * from "./workflow.js"
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface UnistNode extends Node {
|
||||
url?: string
|
||||
spread?: boolean
|
||||
depth?: number
|
||||
lang?: string
|
||||
}
|
||||
|
||||
export type ArrayExpression = {
|
||||
|
||||
7
www/packages/types/src/ui.ts
Normal file
7
www/packages/types/src/ui.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type ExampleType = {
|
||||
name: string
|
||||
component: React.LazyExoticComponent<() => React.JSX.Element>
|
||||
file: string
|
||||
}
|
||||
|
||||
export type ExampleRegistry = Record<string, ExampleType>
|
||||
@@ -5868,6 +5868,7 @@ __metadata:
|
||||
build-scripts: "*"
|
||||
clsx: ^2.1.0
|
||||
docs-ui: "*"
|
||||
docs-utils: "*"
|
||||
eslint: ^9.13.0
|
||||
eslint-plugin-prettier: ^5.2.1
|
||||
eslint-plugin-react-hooks: ^5.0.0
|
||||
@@ -7198,6 +7199,7 @@ __metadata:
|
||||
dependencies:
|
||||
"@mdx-js/mdx": ^3.1.0
|
||||
"@types/node": ^20.11.20
|
||||
react-docgen: ^7.1.0
|
||||
remark-frontmatter: ^5.0.0
|
||||
remark-mdx: ^3.1.0
|
||||
remark-parse: ^11.0.0
|
||||
@@ -15044,6 +15046,7 @@ turbo@latest:
|
||||
contentlayer: ^0.3.4
|
||||
date-fns: ^3.3.1
|
||||
docs-ui: "*"
|
||||
docs-utils: "*"
|
||||
eslint: ^9.13.0
|
||||
eslint-plugin-prettier: ^5.2.1
|
||||
eslint-plugin-react-hooks: ^5.0.0
|
||||
@@ -15057,6 +15060,7 @@ turbo@latest:
|
||||
react-dom: rc
|
||||
rehype-slug: ^6.0.0
|
||||
remark: ^14.0.3
|
||||
remark-rehype-plugins: "*"
|
||||
tailwind: "*"
|
||||
tailwindcss: 3.3.3
|
||||
ts-node: ^10.9.1
|
||||
|
||||
Reference in New Issue
Block a user