import { execSync } from "child_process"; import fs from "fs"; import path from "path"; import { ALL_APPS } from "../packages/stack-shared/src/apps/apps-config"; import { buildDashboardReferenceApps, DASHBOARD_REFERENCE_PLACEHOLDER_BODY, getDashboardReferenceDocPath, listDashboardReferencePages, type DiscoveredNavigableApp, } from "./lib/dashboard-reference-docs"; import { extractDashboardReferenceJSDocs } from "./lib/extract-dashboard-reference-jsdoc"; import { writeFileSyncIfChanged } from "./utils"; const repoRoot = path.resolve(__dirname, ".."); const appsFrontendPath = path.join(repoRoot, "apps/dashboard/src/lib/apps-frontend.tsx"); const generatedMdxComment = "This file is auto-generated by scripts/generate-dashboard-reference-docs.ts. Do not edit it manually; update the @dashboardReference JSDoc on the dashboard page component."; const mdxRoot = path.join(repoRoot, "docs-mintlify/guides/dashboard-references"); const docsJsonPath = path.join(repoRoot, "docs-mintlify/docs.json"); type DocsJson = { navigation: { tabs: Array<{ tab: string, pages: unknown[], }>, }, redirects: Array<{ source: string, destination: string }>, }; type NavigationItem = { displayName: string, external: boolean, }; function readBody( jsdocDocs: ReturnType, appId: string, slug: string, ): string { const fromJsdoc = jsdocDocs.get(`${appId}/${slug}`); if (fromJsdoc != null) { return fromJsdoc.body.replace(/\r\n/g, "\n").replace(/\n+$/, "") + "\n"; } return `${DASHBOARD_REFERENCE_PLACEHOLDER_BODY}\n`; } function mergePageMetadataFromJsdoc( appId: string, slug: string, page: T, jsdocDocs: ReturnType, ): T { const fromJsdoc = jsdocDocs.get(`${appId}/${slug}`); if (fromJsdoc == null) { return page; } return { ...page, title: fromJsdoc.title ?? page.title, description: fromJsdoc.description ?? page.description, sidebarTitle: fromJsdoc.title != null ? (page.sidebarTitle ?? page.title) : page.sidebarTitle, }; } function renderMdx( page: { title: string, description: string, sidebarTitle?: string }, body: string, ): string { const sidebarTitle = page.sidebarTitle ?? page.title; return [ "---", "hidden: true", `title: ${JSON.stringify(page.title)}`, `description: ${JSON.stringify(page.description)}`, `sidebarTitle: ${JSON.stringify(sidebarTitle)}`, "---", "", `{/* ${generatedMdxComment} */}`, "", body.endsWith("\n") ? body : `${body}\n`, ].join("\n"); } function buildDashboardReferenceNavGroup(dashboardReferenceApps: ReturnType) { return { group: "Dashboard reference", hidden: true, searchable: true, pages: dashboardReferenceApps.map((app) => ({ group: app.groupLabel, icon: app.icon, pages: app.pages.map((page) => getDashboardReferenceDocPath(app.appId, page.slug)), })), }; } function patchDocsJson( dashboardReferenceNavGroup: ReturnType, dashboardReferenceApps: ReturnType, ) { const docsJson = JSON.parse(fs.readFileSync(docsJsonPath, "utf-8")) as DocsJson; const documentationTab = docsJson.navigation.tabs.find((tab) => tab.tab === "Documentation"); if (documentationTab == null) { throw new Error('docs.json: "Documentation" tab not found'); } const pages = documentationTab.pages; const integrationsIndex = pages.findIndex( (entry) => typeof entry === "object" && entry != null && "group" in entry && (entry as { group: string }).group === "Integrations", ); if (integrationsIndex === -1) { throw new Error('docs.json: "Integrations" group not found under Documentation'); } const dashboardRefIndex = pages.findIndex( (entry) => typeof entry === "object" && entry != null && "group" in entry && (entry as { group: string }).group === "Dashboard reference", ); const nextPages = [...pages]; if (dashboardRefIndex === -1) { nextPages.splice(integrationsIndex, 0, dashboardReferenceNavGroup); } else { nextPages[dashboardRefIndex] = dashboardReferenceNavGroup; } documentationTab.pages = nextPages; const legacyRedirects: Array<{ source: string, destination: string }> = []; for (const { app, page } of listDashboardReferencePages(dashboardReferenceApps)) { const legacySource = `/guides/apps/${app.appId}/${page.slug}`; const destination = `/${getDashboardReferenceDocPath(app.appId, page.slug)}`; legacyRedirects.push({ source: legacySource, destination }); } const redirectSources = new Set(docsJson.redirects.map((r) => r.source)); for (const redirect of legacyRedirects) { if (!redirectSources.has(redirect.source)) { docsJson.redirects.push(redirect); redirectSources.add(redirect.source); } } writeFileSyncIfChanged(docsJsonPath, `${JSON.stringify(docsJson, null, 2)}\n`); } function sliceAppObject(content: string, appKeyMatchIndex: number): string { const objectStart = content.indexOf("{", appKeyMatchIndex); let depth = 0; for (let i = objectStart; i < content.length; i++) { if (content[i] === "{") { depth++; } else if (content[i] === "}") { depth--; if (depth === 0) { return content.slice(objectStart, i + 1); } } } throw new Error("Could not parse app object in apps-frontend.tsx"); } function extractNavigationItemsFromAppsFrontend(appId: string): NavigationItem[] { const content = fs.readFileSync(appsFrontendPath, "utf-8"); const escapedAppId = appId.replace(/-/g, "\\-"); const appKeyPattern = new RegExp(`\\n\\s*(?:"${escapedAppId}"|${escapedAppId}):\\s*\\{`); const appMatch = appKeyPattern.exec(content); if (appMatch == null) { throw new Error(`Could not find app "${appId}" in apps-frontend.tsx`); } const appObject = sliceAppObject(content, appMatch.index); const navItemsKeywordIndex = appObject.indexOf("navigationItems:"); if (navItemsKeywordIndex === -1) { throw new Error(`App "${appId}" has no navigationItems in apps-frontend.tsx`); } const arrayStart = appObject.indexOf("[", navItemsKeywordIndex); let depth = 0; let arrayEnd = -1; for (let i = arrayStart; i < appObject.length; i++) { if (appObject[i] === "[") { depth++; } else if (appObject[i] === "]") { depth--; if (depth === 0) { arrayEnd = i; break; } } } if (arrayEnd === -1) { throw new Error(`Could not parse navigationItems for app "${appId}" in apps-frontend.tsx`); } const navBlock = appObject.slice(arrayStart, arrayEnd + 1); const items: NavigationItem[] = []; const displayNamePattern = /displayName:\s*"([^"]+)"/g; let labelMatch = displayNamePattern.exec(navBlock); while (labelMatch != null) { const objectStart = labelMatch.index; const objectEnd = navBlock.indexOf("},", objectStart); const objectSlice = navBlock.slice(objectStart, objectEnd === -1 ? navBlock.length : objectEnd + 2); items.push({ displayName: labelMatch[1], external: /external:\s*true/.test(objectSlice), }); labelMatch = displayNamePattern.exec(navBlock); } return items; } function discoverNavigableAppsFromAppsFrontend(): DiscoveredNavigableApp[] { const discovered: DiscoveredNavigableApp[] = []; for (const appId of Object.keys(ALL_APPS)) { let navItems: NavigationItem[]; try { navItems = extractNavigationItemsFromAppsFrontend(appId); } catch (error) { const message = error instanceof Error ? error.message : String(error); if (message.includes("has no navigationItems") || message.includes("Could not find app")) { continue; } throw error; } const internalNavLabels = navItems .filter((item) => !item.external) .map((item) => item.displayName); if (internalNavLabels.length === 0) { continue; } discovered.push({ appId, navLabels: internalNavLabels }); } discovered.sort((a, b) => a.appId.localeCompare(b.appId)); return discovered; } function removeOrphanGeneratedMdx(dashboardReferenceApps: ReturnType) { const expectedPaths = new Set( listDashboardReferencePages(dashboardReferenceApps).map(({ app, page }) => path.join(mdxRoot, app.appId, `${page.slug}.mdx`)), ); for (const app of dashboardReferenceApps) { const appDir = path.join(mdxRoot, app.appId); if (!fs.existsSync(appDir)) { continue; } for (const entry of fs.readdirSync(appDir)) { if (!entry.endsWith(".mdx")) { continue; } const fullPath = path.join(appDir, entry); if (!expectedPaths.has(fullPath)) { fs.unlinkSync(fullPath); console.log(`Removed orphan generated file: ${fullPath}`); } } } } function main() { const jsdocDocs = extractDashboardReferenceJSDocs(repoRoot); const discoveredApps = discoverNavigableAppsFromAppsFrontend(); const dashboardReferenceApps = buildDashboardReferenceApps(discoveredApps); const allPages = listDashboardReferencePages(dashboardReferenceApps); for (const { app, page } of allPages) { const body = readBody(jsdocDocs, app.appId, page.slug); const pageMetadata = mergePageMetadataFromJsdoc(app.appId, page.slug, page, jsdocDocs); const mdxPath = path.join(mdxRoot, app.appId, `${page.slug}.mdx`); fs.mkdirSync(path.dirname(mdxPath), { recursive: true }); writeFileSyncIfChanged(mdxPath, renderMdx(pageMetadata, body)); } removeOrphanGeneratedMdx(dashboardReferenceApps); patchDocsJson(buildDashboardReferenceNavGroup(dashboardReferenceApps), dashboardReferenceApps); execSync("pnpm run generate-setup-prompt-docs", { cwd: repoRoot, stdio: "inherit", }); const documentedCount = allPages.filter(({ app, page }) => jsdocDocs.has(`${app.appId}/${page.slug}`)).length; console.log( `Generated dashboard reference docs for ${dashboardReferenceApps.length} apps, ` + `${allPages.length} pages (${documentedCount} with JSDoc, ${allPages.length - documentedCount} placeholder).`, ); } main();