docs: add prepare script to generate sidebar (#11894)

This commit is contained in:
Shahed Nasser authored and GitHub committed 2025-03-18 17:37:51 +02:00
1 parent eb2aa8da3c
commit 9ead47c51e
72 files changed
+1709 -295

No files matched your search

+2
View File
@@ -28,8 +28,10 @@
"watch": "tsc --watch"
},
"dependencies": {
"@readme/openapi-parser": "^3.0.1",
"docs-utils": "*",
"fdir": "^6.4.3",
"pluralize": "^8.0.0",
"slugify": "^1.6.6",
"tags": "*",
"yaml": "^2.7.0"
@@ -7,6 +7,7 @@ import numberSidebarItems from "./utils/number-sidebar-items.js"
import { sortSidebarItems } from "./utils/sidebar-sorting.js"
import { Sidebar } from "types"
import { validateSidebarUniqueIds } from "./utils/validate-sidebar-unique-ids.js"
import getApiRefSidebarChildren from "./utils/get-api-ref-sidebar-children.js"
export type ItemsToAdd = Sidebar.SidebarItem & {
sidebar_position?: number
@@ -17,8 +18,12 @@ export type GenerateSidebarOptions = {
writeToFile?: boolean
}
const customGenerators: Record<string, () => Promise<ItemsToAdd[]>> = {
const customGenerators: Record<
string,
(sidebar?: Sidebar.RawSidebar) => Promise<ItemsToAdd[]>
> = {
"core-flows": getCoreFlowsRefSidebarChildren,
"api-ref": getApiRefSidebarChildren,
}
function sortItems(itemA: ItemsToAdd, itemB: ItemsToAdd): number {
@@ -241,9 +246,14 @@ export async function generateSidebar(
validateSidebarUniqueIds(sidebars)
for (const sidebarItem of sidebars) {
const hasCustomAutogenerator =
sidebarItem.custom_autogenerate &&
Object.hasOwn(customGenerators, sidebarItem.custom_autogenerate)
normalizedSidebars.push({
...sidebarItem,
items: await checkItems(sidebarItem.items, restOptions),
items: !hasCustomAutogenerator
? await checkItems(sidebarItem.items, restOptions)
: await customGenerators[sidebarItem.custom_autogenerate!](sidebarItem),
})
}
@@ -0,0 +1,85 @@
import { OpenAPI, Sidebar } from "types"
import { ItemsToAdd } from "../index.js"
import path from "path"
import { findAllPageHeadings, getSectionId } from "docs-utils"
import { readFile } from "fs/promises"
import { parse } from "@readme/openapi-parser"
import pkg from "pluralize"
const { singular } = pkg
export default async function getApiRefSidebarChildren(
sidebar?: Sidebar.RawSidebar
): Promise<ItemsToAdd[]> {
if (!sidebar) {
return []
}
const projPath = path.resolve()
const area = sidebar.sidebar_id
const items: ItemsToAdd[] = [
{
type: "link",
title: "Introduction",
path: "introduction",
loaded: true,
},
]
// get sidebar items from markdown files
const markdownPath = path.join(projPath, "markdown", `${area}.mdx`)
const headings = findAllPageHeadings({
content: await readFile(markdownPath, "utf-8"),
level: 2,
})
headings.forEach((heading) => {
items.push({
type: "link",
title: heading,
path: getSectionId([heading]),
loaded: true,
})
})
// read base specs
const baseSpecs = (await parse(
path.join(projPath, "specs", area, "openapi.yaml")
)) as OpenAPI.ExpandedDocument
if (baseSpecs.tags?.length) {
items.push({
type: "separator",
})
}
// add tags to sidebar
baseSpecs.tags?.forEach((tag: OpenAPI.TagObject) => {
const item: ItemsToAdd = {
type: "category",
title: tag.name,
children: [],
loaded: false,
showLoadingIfEmpty: true,
}
if (tag["x-associatedSchema"]) {
const formattedName = singular(tag.name).replaceAll(" ", "")
const schemaSlug = getSectionId([tag.name, formattedName, "schema"])
item.children!.push({
type: "link",
path: schemaSlug,
title: `${formattedName} Object`,
loaded: true,
badge: {
variant: "neutral",
text: "Schema",
},
})
}
items.push(item)
})
return items
}
@@ -4,7 +4,7 @@
import React, { useEffect, useMemo, useState } from "react"
import { Sidebar } from "types"
import { Loading, SidebarItem, useSidebar } from "../../../.."
import { Badge, Loading, SidebarItem, useSidebar } from "../../../.."
import clsx from "clsx"
import { MinusMini, PlusMini } from "@medusajs/icons"
@@ -106,6 +106,9 @@ export const SidebarItemCategory = ({
{item.title}
</span>
{item.additionalElms}
{item.badge && (
<Badge variant={item.badge.variant}>{item.badge.text}</Badge>
)}
{!item.additionalElms && (
<>
{open && <MinusMini />}
@@ -123,13 +126,6 @@ export const SidebarItemCategory = ({
!open && "overflow-hidden m-0 h-0"
)}
>
{showLoading && (
<Loading
count={3}
className="!mb-0 !px-docs_0.5"
barClassName="h-[20px]"
/>
)}
{item.children?.map((childItem, index) => (
<SidebarItem
item={childItem}
@@ -137,6 +133,13 @@ export const SidebarItemCategory = ({
isParentCategoryOpen={open}
/>
))}
{showLoading && (
<Loading
count={3}
className="!mb-0 !px-docs_0.5"
barClassName="h-[20px]"
/>
)}
</ul>
)}
</div>
@@ -5,6 +5,7 @@
import React, { useCallback, useEffect, useMemo, useRef } from "react"
import { Sidebar } from "types"
import {
Badge,
checkSidebarItemVisibility,
SidebarItem,
useMobile,
@@ -147,6 +148,9 @@ export const SidebarItemLink = ({
{item.title}
</span>
{item.additionalElms}
{item.badge && (
<Badge variant={item.badge.variant}>{item.badge.text}</Badge>
)}
</Link>
</span>
{hasChildren && (
@@ -4,7 +4,7 @@
import React, { useMemo } from "react"
import { Sidebar } from "types"
import { useSidebar } from "../../../.."
import { Badge, useSidebar } from "../../../.."
import clsx from "clsx"
import Link from "next/link"
@@ -56,6 +56,9 @@ export const SidebarItemSidebar = ({
{item.title}
</span>
{item.additionalElms}
{item.badge && (
<Badge variant={item.badge.variant}>{item.badge.text}</Badge>
)}
</Link>
</span>
</li>
@@ -4,7 +4,7 @@
import React, { useMemo, useRef } from "react"
import { Sidebar } from "types"
import { SidebarItem } from "../../../.."
import { Badge, SidebarItem } from "../../../.."
import clsx from "clsx"
export type SidebarItemSubCategoryProps = {
@@ -53,6 +53,9 @@ export const SidebarItemSubCategory = ({
{item.title}
</span>
{item.additionalElms}
{item.badge && (
<Badge variant={item.badge.variant}>{item.badge.text}</Badge>
)}
</span>
</span>
{hasChildren && (
@@ -21,6 +21,7 @@ import { useSiteConfig } from "../SiteConifg"
import { useIsBrowser } from "../BrowserProvider"
import { getScrolledTop } from "../../utils"
import { usePathname, useRouter } from "next/navigation"
import { SidebarItemCategory, SidebarItemSidebar } from "types/dist/sidebar"
export type SidebarActionOptions = {
sidebar_id: string
@@ -53,6 +54,24 @@ export type SidebarStyleOptions = {
disableActiveTransition?: boolean
}
export type UpdateSidebarItemTypes =
| Partial<Pick<Sidebar.SidebarItemLink, "path" | "title" | "additionalElms">>
| Partial<
Pick<SidebarItemCategory, "title" | "loaded" | "onOpen" | "children">
>
| Partial<Pick<SidebarItemSidebar, "title" | "children">>
export type UpdateActionType = {
sidebar_id: string
items: {
existingItem: Sidebar.SidebarItem
newItem: UpdateSidebarItemTypes
options?: {
setChildrenBehavior: "replace" | "merge"
}
}[]
}
export type SidebarContextType = {
sidebars: Sidebar.Sidebar[]
/**
@@ -75,6 +94,7 @@ export type SidebarContextType = {
items: Sidebar.SidebarItem[],
options?: SidebarActionOptions
) => void
updateItems: (options: UpdateActionType) => void
removeItems: (options: {
items: Sidebar.SidebarItem[]
sidebar_id: string
@@ -125,7 +145,7 @@ export const SidebarContext = createContext<SidebarContextType | null>(null)
export type ActionType =
| {
type: "add" | "update"
type: "add" | "update-child"
items: Sidebar.SidebarItem[]
options?: SidebarActionOptions
}
@@ -138,32 +158,73 @@ export type ActionType =
items: Sidebar.SidebarItem[]
sidebar_id: string
}
| ({
type: "update"
} & UpdateActionType)
export const reducer = (
state: Sidebar.Sidebar[],
actionData: ActionType
): Sidebar.Sidebar[] => {
if (actionData.type === "replace") {
return actionData.sidebars
}
if (actionData.type === "remove") {
const { sidebar_id, items: itemsToRemove } = actionData
return state.map((sidebar) => {
if (sidebar.sidebar_id === sidebar_id) {
return {
...sidebar,
items: sidebar.items.filter((item) => {
return !itemsToRemove.some((itemToRemove) =>
areSidebarItemsEqual({
itemA: item,
itemB: itemToRemove,
})
)
}),
switch (actionData.type) {
case "replace":
return actionData.sidebars
case "remove": {
const { sidebar_id, items: itemsToRemove } = actionData
return state.map((sidebar) => {
if (sidebar.sidebar_id === sidebar_id) {
return {
...sidebar,
items: sidebar.items.filter((item) => {
return !itemsToRemove.some((itemToRemove) =>
areSidebarItemsEqual({
itemA: item,
itemB: itemToRemove,
})
)
}),
}
}
}
return sidebar
})
return sidebar
})
}
case "update":
return state.map((sidebar) => {
if (sidebar.sidebar_id === actionData.sidebar_id) {
return {
...sidebar,
items: sidebar.items.map((item) => {
const itemToUpdate = actionData.items.find((i) =>
areSidebarItemsEqual({
itemA: item,
itemB: i.existingItem,
})
)
if (itemToUpdate) {
const updatedItem = {
...item,
...itemToUpdate.newItem,
} as Sidebar.SidebarItem
if ("children" in updatedItem) {
updatedItem.children =
itemToUpdate.options?.setChildrenBehavior === "merge"
? [
...((item as Sidebar.InteractiveSidebarItem)
.children || []),
...(updatedItem.children || []),
]
: updatedItem.children
}
return updatedItem
}
return item
}),
}
}
return sidebar
})
}
const { type, options } = actionData
@@ -207,7 +268,7 @@ export const reducer = (
},
...state.slice(sidebarIndex + 1),
]
case "update":
case "update-child":
// find item index
return [
...state.slice(0, sidebarIndex),
@@ -406,12 +467,20 @@ export const SidebarProvider = ({
options?: SidebarActionOptions
) => {
dispatch({
type: options?.parent ? "update" : "add",
type: options?.parent ? "update-child" : "add",
items: newItems,
options,
})
}
const updateItems = ({ sidebar_id, items }: UpdateActionType) => {
dispatch({
type: "update",
items,
sidebar_id,
})
}
const removeItems = ({
items,
sidebar_id,
@@ -452,30 +521,32 @@ export const SidebarProvider = ({
}
}, [shouldHandleHashChange])
const handleScroll = useCallback(() => {
const scrolledTop = getScrolledTop(resolvedScrollableElement)
// account for navbar height
if (scrolledTop >= 0 && scrolledTop <= 56) {
const firstChild = getFirstLinkChild(activeMainSidebar.items)
if (firstChild) {
setActivePath(firstChild.path)
router.push(`#${firstChild.path}`, {
scroll: false,
})
}
}
}, [activeMainSidebar, resolvedScrollableElement])
useEffect(() => {
if (!shouldHandleHashChange || !resolvedScrollableElement) {
return
}
const handleScroll = () => {
if (getScrolledTop(resolvedScrollableElement) === 0) {
const firstChild = getFirstLinkChild(activeMainSidebar.items)
if (firstChild) {
setActivePath(firstChild.path)
router.push(`#${firstChild.path}`, {
scroll: false,
})
}
}
}
resolvedScrollableElement.addEventListener("scroll", handleScroll)
return () => {
resolvedScrollableElement.removeEventListener("scroll", handleScroll)
}
}, [shouldHandleHashChange, resolvedScrollableElement])
}, [shouldHandleHashChange, resolvedScrollableElement, handleScroll])
useEffect(() => {
if (!shouldHandleHashChange || !isBrowser) {
@@ -635,6 +706,7 @@ export const SidebarProvider = ({
setActivePath,
isItemActive,
addItems,
updateItems,
removeItems,
mobileSidebarOpen,
setMobileSidebarOpen,
+1
View File
@@ -34,6 +34,7 @@
"remark-mdx": "^3.1.0",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"slugify": "^1.6.6",
"to-vfile": "^8.0.0",
"unified": "^11.0.4",
"vfile-matter": "^5.0.0"
+16 -3
View File
@@ -8,14 +8,27 @@ export function findMetadataTitle(content: string): string | undefined {
return headingMatch?.groups?.title
}
const HEADING_REGEX = /# (?<title>.*)/
export function findPageHeading(content: string): string | undefined {
const headingMatch = HEADING_REGEX.exec(content)
const headingMatch = /# (?<title>.*)/.exec(content)
return headingMatch?.groups?.title
}
export function findAllPageHeadings({
content,
level = 1,
}: {
content: string
level?: number
}): string[] {
const regex = new RegExp(
`^${"#".repeat(level)}(?!#) (?<title>.*?)(?:\n|$)`,
"gm"
)
const matches = [...content.matchAll(regex)]
return matches.map((match) => match.groups?.title).filter(Boolean) as string[]
}
export function findPageTitle(filePath: string): string | undefined {
const content = readFileSync(filePath, "utf-8")
+1 -1
View File
@@ -1,7 +1,6 @@
import remarkMdx from "remark-mdx"
import remarkParse from "remark-parse"
import remarkStringify from "remark-stringify"
import { read } from "to-vfile"
import { FrontMatter, UnistNode, UnistNodeWithData, UnistTree } from "types"
import { Plugin, Transformer, unified } from "unified"
import { SKIP } from "unist-util-visit"
@@ -172,6 +171,7 @@ export const getCleanMd = async ({
parserOptions,
type = "file",
}: GetCleanMdOptions): Promise<string> => {
const { read } = await import("to-vfile")
if (type === "file" && !file.endsWith(".md") && !file.endsWith(".mdx")) {
return ""
}
+1
View File
@@ -6,3 +6,4 @@ export * from "./get-file-slug-sync.js"
export * from "./get-file-slug.js"
export * from "./get-front-matter.js"
export * from "./oas-file-to-path.js"
export * from "./sidebar-utils.js"
@@ -0,0 +1,7 @@
import pkg from "slugify"
const { default: slugify } = pkg
export function getSectionId(path: string[]) {
path = path.map((p) => slugify(p.trim().toLowerCase()))
return path.join("_")
}
+3
View File
@@ -40,5 +40,8 @@
},
"engines": {
"node": ">=18.17.0"
},
"dependencies": {
"openapi-types": "^12.1.3"
}
}
+1
View File
@@ -5,6 +5,7 @@ export * from "./frontmatter.js"
export * from "./general.js"
export * from "./menu.js"
export * from "./navigation.js"
export * as OpenAPI from "./openapi.js"
export * from "./remark-rehype.js"
export * from "./navigation-dropdown.js"
export * as Sidebar from "./sidebar.js"
+136
View File
@@ -0,0 +1,136 @@
import type { OpenAPIV3 } from "openapi-types"
export type { OpenAPIV3 }
export type Area = "admin" | "store"
export type Code = {
lang: string
label: string
source: string
}
export type Operation = OpenAPIV3.OperationObject<{
operationId: string
summary: string
description: string
"x-authenticated": boolean
"x-codeSamples": Code[]
requestBody: RequestObject
responses: ResponsesObject
parameters: Parameter[]
"x-featureFlag"?: string
"x-workflow"?: string
"x-sidebar-summary"?: string
}>
export type RequestObject = OpenAPIV3.RequestBodyObject & {
content: {
[media: string]: OpenAPIV3.MediaTypeObject & {
schema: SchemaObject
}
}
}
export type ResponseObject = OpenAPIV3.ResponseObject & {
content: {
[media: string]: Omit<OpenAPIV3.MediaTypeObject, "examples"> & {
schema: SchemaObject
}
}
contentSample?: string
}
export type ResponsesObject = {
[code: string]: ResponseObject
}
export type ExampleObject = {
title: string
value: string
content: string
contentDetailed?: string
contentSchema?: string
}
export type PathsObject = {
[pattern: string]: Path
}
export type Path = OpenAPIV3.PathItemObject & {
[method in OpenAPIV3.HttpMethods]?: Operation
}
export type ArraySchemaObject = Omit<
OpenAPIV3.ArraySchemaObject,
"properties" | "anyOf" | "allOf" | "oneOf" | "examples"
> & {
items: SchemaObject
properties: PropertiesObject
anyOf?: SchemaObject[]
allOf?: SchemaObject[]
oneOf?: SchemaObject[]
}
export type NonArraySchemaObject = Omit<
OpenAPIV3.NonArraySchemaObject,
| "properties"
| "anyOf"
| "allOf"
| "oneOf"
| "examples"
| "additionalProperties"
> & {
properties: PropertiesObject
additionalProperties?: SchemaObject
anyOf?: SchemaObject[]
allOf?: SchemaObject[]
oneOf?: SchemaObject[]
}
export type SchemaObject = (ArraySchemaObject | NonArraySchemaObject) & {
parameterName?: string
resolvedRef?: SchemaObject
examples?: {
[media: string]: OpenAPIV3.ExampleObject
}
isRequired?: boolean
"x-featureFlag"?: string
"x-expandable"?: string
"x-schemaName"?: string
additionalProperties?: SchemaObject
}
export type PropertiesObject = {
[name: string]: SchemaObject
}
export type SecuritySchemeObject = OpenAPIV3.SecuritySchemeObject & {
"x-displayName"?: string
"x-is-auth"?: boolean
}
export type Parameter = OpenAPIV3.ParameterObject & {
examples: {
[media: string]: OpenAPIV3.ExampleObject
}
schema: SchemaObject
}
export type Document = Omit<OpenAPIV3.Document, "paths"> & {
paths: PathsObject
}
export type ExpandedDocument = Document & {
expandedTags?: {
[k: string]: PathsObject
}
}
export type TagObject = OpenAPIV3.TagObject & {
"x-associatedSchema"?: OpenAPIV3.ReferenceObject
}
export type ParsedPathItemObject = OpenAPIV3.PathItemObject<Operation> & {
operationPath?: string
}
+5
View File
@@ -9,6 +9,10 @@ export type SidebarItemCommon = {
children?: SidebarItem[]
loaded?: boolean
additionalElms?: React.ReactNode
badge?: {
variant: "purple" | "orange" | "green" | "blue" | "red" | "neutral"
text: string
}
chapterTitle?: string
childSidebarTitle?: string
hideChildren?: boolean
@@ -88,6 +92,7 @@ export type RawSidebarItem = SidebarItem & {
export type RawSidebar = Omit<Sidebar, "items"> & {
items: RawSidebarItem[]
custom_autogenerate?: string
}
export type PersistedSidebarCategoryState = {