import type { RawSidebarItem, SidebarItem } from "types" import { existsSync, mkdirSync, readdirSync, statSync } from "fs" import path from "path" import { getSidebarItemLink, sidebarAttachHrefCommonOptions } from "./index.js" import getCoreFlowsRefSidebarChildren from "./utils/get-core-flows-ref-sidebar-children.js" import { parseTags } from "./utils/parse-tags.js" import numberSidebarItems from "./utils/number-sidebar-items.js" export type ItemsToAdd = SidebarItem & { sidebar_position?: number } type GenerateSidebarOptions = { addNumbering?: boolean } const customGenerators: Record Promise> = { "core-flows": getCoreFlowsRefSidebarChildren, } async function getAutogeneratedSidebarItems( dir: string, nested = false ): Promise { const isRefDir = dir.startsWith("/references") const basePath = isRefDir ? path.resolve() : path.resolve("app") const fullPath = path.resolve(basePath, dir.replace(/^\//, "")) const items: ItemsToAdd[] = [] const files = readdirSync(fullPath) let mainPageIndex = -1 for (const file of files) { const filePath = path.join(fullPath, file) const fileBasename = path.basename(file) if (fileBasename.startsWith("_")) { continue } if (fileBasename !== "page.mdx" && statSync(filePath).isDirectory()) { const newItems = await getAutogeneratedSidebarItems( filePath.replace(basePath, ""), true ) if (nested && newItems.length > 1) { items.push({ type: "sub-category", title: fileBasename.charAt(0).toUpperCase() + fileBasename.substring(1), children: newItems, loaded: true, }) } else { items.push( ...(sidebarAttachHrefCommonOptions(newItems) as ItemsToAdd[]) ) } continue } const newItem = await getSidebarItemLink({ filePath, basePath, fileBasename, }) if (!newItem) { continue } items.push(newItem) mainPageIndex = items.length - 1 } if ( mainPageIndex !== -1 && items.length > 1 && items[0].type !== "separator" ) { // push all other items to be children of that page. const mainPageChildren = [ ...items.splice(0, mainPageIndex), ...items.splice(1), ] mainPageChildren.sort(sortItems) items[0].children = mainPageChildren } else if (items.length > 1) { items.sort(sortItems) } return items } async function getAutogeneratedTagSidebarItems( tags: string, type: "link" | "ref", existingChildren?: RawSidebarItem[] ): Promise { const items: ItemsToAdd[] = [] const parsedTags = parseTags(tags) items.push( ...parsedTags .filter((tagItem) => { return existingChildren?.every((existingItem) => { if (existingItem.type !== "link" && existingItem.type !== "ref") { return true } return existingItem.path !== tagItem.path }) }) .map( (tagItem) => ({ type, ...tagItem, }) as ItemsToAdd ) ) return sidebarAttachHrefCommonOptions([...(existingChildren || []), ...items]) } async function checkItem(item: RawSidebarItem): Promise { if (!item.type) { throw new Error( `ERROR: The following item doesn't have a type: ${JSON.stringify( item, undefined, 2 )}` ) } if (item.type === "separator") { return item } if (item.autogenerate_path) { item.children = ( await getAutogeneratedSidebarItems(item.autogenerate_path) ).map((child) => { delete child.sidebar_position return child }) } else if (item.autogenerate_tags) { item.children = await getAutogeneratedTagSidebarItems( item.autogenerate_tags, item.autogenerate_as_ref ? "ref" : "link", item.children ) } else if ( item.custom_autogenerate && Object.hasOwn(customGenerators, item.custom_autogenerate) ) { item.children = await customGenerators[item.custom_autogenerate]() } else if (item.children) { item.children = await checkItems(item.children) } return item } async function checkItems( items: RawSidebarItem[], options?: GenerateSidebarOptions ): Promise { const updatedItems = ( await Promise.all(items.map(async (item) => await checkItem(item))) ).filter((item) => { if (item.type !== "category" && item.type !== "sub-category") { return true } return (item.children?.length || 0) > 0 }) if (options?.addNumbering) { return numberSidebarItems(updatedItems) } return updatedItems } export async function generateSidebar( sidebar: RawSidebarItem[], options?: GenerateSidebarOptions ): Promise { const path = await import("path") const { writeFileSync } = await import("fs") const normalizedSidebar = await checkItems(sidebar, options) const generatedDirPath = path.resolve("generated") if (!existsSync(generatedDirPath)) { mkdirSync(generatedDirPath) } // write normalized sidebar writeFileSync( path.resolve(generatedDirPath, "sidebar.mjs"), `export const generatedSidebar = ${JSON.stringify( normalizedSidebar, null, 2 )}`, { flag: "w", } ) } function sortItems(itemA: ItemsToAdd, itemB: ItemsToAdd): number { const itemASidebarPosition = itemA.sidebar_position || 0 const itemBSidebarPosition = itemB.sidebar_position || 0 if (itemASidebarPosition > itemBSidebarPosition) { return 1 } return itemASidebarPosition < itemBSidebarPosition ? -1 : 0 }