boltdocs 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/boltdocs.js +2 -2
  3. package/dist/base-ui/index.d.mts +25 -0
  4. package/dist/base-ui/index.d.ts +25 -0
  5. package/dist/base-ui/index.js +1 -0
  6. package/dist/base-ui/index.mjs +1 -0
  7. package/dist/{cache-Q4T6VAUL.mjs → cache-P6WK424C.mjs} +1 -1
  8. package/dist/chunk-22NXDNP4.mjs +74 -0
  9. package/dist/chunk-2HUVMMJU.mjs +1 -0
  10. package/dist/chunk-2Z5T6EAU.mjs +1 -0
  11. package/dist/chunk-CRZGOE32.mjs +1 -0
  12. package/dist/chunk-HA6543SL.mjs +1 -0
  13. package/dist/chunk-JD3RSDE4.mjs +1 -0
  14. package/dist/chunk-JZXLCA2E.mjs +1 -0
  15. package/dist/chunk-NBCYHLAA.mjs +1 -0
  16. package/dist/chunk-RPUERTVC.mjs +1 -0
  17. package/dist/chunk-T3W44KWY.mjs +1 -0
  18. package/dist/chunk-URTD6E6S.mjs +1 -0
  19. package/dist/chunk-W2NB4T6V.mjs +1 -0
  20. package/dist/chunk-Y4RRHPXC.mjs +1 -0
  21. package/dist/client/index.d.mts +13 -115
  22. package/dist/client/index.d.ts +13 -115
  23. package/dist/client/index.js +1 -1
  24. package/dist/client/index.mjs +1 -1
  25. package/dist/client/ssr.js +1 -1
  26. package/dist/client/ssr.mjs +1 -1
  27. package/dist/client/types.d.mts +3 -0
  28. package/dist/client/types.d.ts +3 -0
  29. package/dist/client/types.js +1 -0
  30. package/dist/client/types.mjs +0 -0
  31. package/dist/copy-markdown-C-90ixSe.d.ts +15 -0
  32. package/dist/copy-markdown-CbS8X-qe.d.mts +15 -0
  33. package/dist/{client/hooks → hooks}/index.d.mts +16 -11
  34. package/dist/{client/hooks → hooks}/index.d.ts +16 -11
  35. package/dist/hooks/index.js +1 -0
  36. package/dist/hooks/index.mjs +1 -0
  37. package/dist/integrations/index.d.mts +48 -0
  38. package/dist/integrations/index.d.ts +48 -0
  39. package/dist/integrations/index.js +1 -0
  40. package/dist/integrations/index.mjs +1 -0
  41. package/dist/link-DfBwCeZc.d.mts +68 -0
  42. package/dist/link-DfBwCeZc.d.ts +68 -0
  43. package/dist/loading-B7X5Wchs.d.ts +66 -0
  44. package/dist/loading-WuaQbsKb.d.mts +66 -0
  45. package/dist/{client/components/mdx → mdx}/index.d.mts +6 -38
  46. package/dist/{client/components/mdx → mdx}/index.d.ts +6 -38
  47. package/dist/mdx/index.js +1 -0
  48. package/dist/mdx/index.mjs +1 -0
  49. package/dist/node/cli-entry.js +31 -27
  50. package/dist/node/cli-entry.mjs +5 -1
  51. package/dist/node/index.d.mts +44 -14
  52. package/dist/node/index.d.ts +44 -14
  53. package/dist/node/index.js +24 -24
  54. package/dist/node/index.mjs +1 -1
  55. package/dist/primitives/index.d.mts +301 -0
  56. package/dist/primitives/index.d.ts +301 -0
  57. package/dist/primitives/index.js +1 -0
  58. package/dist/primitives/index.mjs +1 -0
  59. package/dist/search-dialog-ZRXBAQJ5.mjs +1 -0
  60. package/dist/{types-Cp21DHI6.d.mts → types-j7jvWsJj.d.mts} +63 -17
  61. package/dist/{types-Cp21DHI6.d.ts → types-j7jvWsJj.d.ts} +63 -17
  62. package/dist/{use-routes-xLhumjbV.d.ts → use-routes-Cd806kGw.d.ts} +1 -1
  63. package/dist/{use-routes-8Iei6jTp.d.mts → use-routes-DDL0_jkQ.d.mts} +1 -1
  64. package/package.json +35 -8
  65. package/src/client/app/index.tsx +155 -35
  66. package/src/client/app/mdx-component.tsx +7 -3
  67. package/src/client/app/theme-context.tsx +47 -23
  68. package/src/client/components/default-layout.tsx +16 -6
  69. package/src/client/components/primitives/breadcrumbs.tsx +1 -1
  70. package/src/client/components/primitives/navbar.tsx +8 -5
  71. package/src/client/components/primitives/search-dialog.tsx +15 -6
  72. package/src/client/components/primitives/sidebar.tsx +3 -2
  73. package/src/client/components/primitives/skeleton.tsx +26 -0
  74. package/src/client/components/ui-base/breadcrumbs.tsx +1 -1
  75. package/src/client/components/ui-base/index.ts +17 -0
  76. package/src/client/components/ui-base/loading.tsx +43 -73
  77. package/src/client/components/ui-base/navbar.tsx +74 -39
  78. package/src/client/components/ui-base/page-nav.tsx +2 -1
  79. package/src/client/components/ui-base/powered-by.tsx +11 -5
  80. package/src/client/components/ui-base/search-dialog.tsx +16 -5
  81. package/src/client/components/ui-base/sidebar.tsx +33 -22
  82. package/src/client/components/ui-base/tabs.tsx +4 -1
  83. package/src/client/components/ui-base/theme-toggle.tsx +35 -15
  84. package/src/client/hooks/use-i18n.ts +38 -7
  85. package/src/client/hooks/use-localized-to.ts +51 -73
  86. package/src/client/hooks/use-navbar.ts +10 -3
  87. package/src/client/hooks/use-page-nav.ts +27 -6
  88. package/src/client/hooks/use-routes.ts +62 -17
  89. package/src/client/hooks/use-search.ts +84 -46
  90. package/src/client/hooks/use-sidebar.ts +6 -2
  91. package/src/client/hooks/use-version.ts +5 -0
  92. package/src/client/integrations/index.ts +1 -0
  93. package/src/client/store/use-boltdocs-store.ts +44 -0
  94. package/src/client/theme/neutral.css +29 -0
  95. package/src/client/types.ts +4 -2
  96. package/src/client/utils/i18n.ts +23 -0
  97. package/src/node/{cli.ts → cli/build.ts} +17 -23
  98. package/src/node/cli/dev.ts +22 -0
  99. package/src/node/cli/doctor.ts +243 -0
  100. package/src/node/cli/index.ts +9 -0
  101. package/src/node/cli/ui.ts +54 -0
  102. package/src/node/cli-entry.ts +16 -16
  103. package/src/node/config.ts +54 -17
  104. package/src/node/index.ts +1 -1
  105. package/src/node/mdx/cache.ts +12 -0
  106. package/src/node/mdx/highlighter.ts +47 -0
  107. package/src/node/mdx/index.ts +114 -0
  108. package/src/node/mdx/rehype-shiki.ts +53 -0
  109. package/src/node/mdx/remark-shiki.ts +61 -0
  110. package/src/node/plugin/entry.ts +1 -1
  111. package/src/node/plugin/html.ts +8 -4
  112. package/src/node/plugin/index.ts +135 -72
  113. package/src/node/routes/index.ts +34 -13
  114. package/src/node/routes/parser.ts +13 -5
  115. package/src/node/search/index.ts +55 -0
  116. package/src/node/ssg/index.ts +15 -7
  117. package/src/node/ssg/robots.ts +7 -4
  118. package/src/node/utils.ts +32 -2
  119. package/tsup.config.ts +7 -2
  120. package/dist/chunk-52MVMZWS.mjs +0 -1
  121. package/dist/chunk-BVWWKXJH.mjs +0 -1
  122. package/dist/chunk-DVY3RDXD.mjs +0 -1
  123. package/dist/chunk-FUVYCYWC.mjs +0 -1
  124. package/dist/chunk-GBLMDJ2B.mjs +0 -1
  125. package/dist/chunk-ISPX45DF.mjs +0 -1
  126. package/dist/chunk-PNXZMUCO.mjs +0 -1
  127. package/dist/chunk-V2ZHKQSP.mjs +0 -74
  128. package/dist/client/components/mdx/index.js +0 -1
  129. package/dist/client/components/mdx/index.mjs +0 -1
  130. package/dist/client/hooks/index.js +0 -1
  131. package/dist/client/hooks/index.mjs +0 -1
  132. package/dist/search-dialog-TWGYKF2D.mjs +0 -1
  133. package/src/node/mdx.ts +0 -279
@@ -1,61 +1,98 @@
1
- import { useState, useMemo } from 'react'
1
+ import { useState, useMemo, useEffect } from 'react'
2
+ import { Index } from 'flexsearch'
3
+ import { useRoutes } from './use-routes'
2
4
  import type { ComponentRoute } from '@client/types'
5
+ // @ts-ignore
6
+ import searchData from 'virtual:boltdocs-search'
7
+
8
+ interface SearchDataItem {
9
+ id: string
10
+ title: string
11
+ content: string
12
+ url: string
13
+ display: string
14
+ locale?: string
15
+ version?: string
16
+ }
3
17
 
4
18
  export function useSearch(routes: ComponentRoute[]) {
19
+ const { currentLocale, currentVersion } = useRoutes()
5
20
  const [isOpen, setIsOpen] = useState(false)
6
21
  const [query, setQuery] = useState('')
22
+ const [index, setIndex] = useState<Index | null>(null)
23
+
24
+ // Initialize FlexSearch index once
25
+ useEffect(() => {
26
+ if (!isOpen || index) return
27
+
28
+ const newIndex = new Index({
29
+ preset: 'match',
30
+ tokenize: 'full',
31
+ resolution: 9,
32
+ cache: true,
33
+ })
34
+
35
+ // Index all documents
36
+ for (const doc of searchData as SearchDataItem[]) {
37
+ newIndex.add(doc.id, `${doc.title} ${doc.content}`)
38
+ }
39
+
40
+ setIndex(newIndex)
41
+ }, [isOpen, index])
7
42
 
8
43
  const list = useMemo(() => {
9
44
  if (!query) {
10
- return routes.slice(0, 10).map((r) => ({
11
- id: r.path,
12
- title: r.title,
13
- path: r.path,
14
- bio: r.description || '',
15
- groupTitle: r.groupTitle,
16
- }))
45
+ // Default results: just active routes
46
+ return routes
47
+ .filter((r) => {
48
+ const localeMatch = !currentLocale || r.locale === currentLocale
49
+ const versionMatch = !currentVersion || r.version === currentVersion
50
+ return localeMatch && versionMatch
51
+ })
52
+ .slice(0, 10)
53
+ .map((r) => ({
54
+ id: r.path,
55
+ title: r.title,
56
+ path: r.path,
57
+ bio: r.description || '',
58
+ groupTitle: r.groupTitle,
59
+ }))
17
60
  }
18
61
 
62
+ if (!index) return []
63
+
64
+ const searchResults = index.search(query, {
65
+ limit: 20,
66
+ suggest: true,
67
+ })
68
+
19
69
  const results: any[] = []
20
- const lowerQuery = query.toLowerCase()
21
-
22
- for (const route of routes) {
23
- if (route.title?.toLowerCase().includes(lowerQuery)) {
24
- results.push({
25
- id: route.path,
26
- title: route.title,
27
- path: route.path,
28
- bio: route.description || '',
29
- groupTitle: route.groupTitle,
30
- })
31
- }
32
-
33
- if (route.headings) {
34
- for (const heading of route.headings) {
35
- if (heading.text.toLowerCase().includes(lowerQuery)) {
36
- results.push({
37
- id: `${route.path}#${heading.id}`,
38
- title: heading.text,
39
- path: `${route.path}#${heading.id}`,
40
- bio: `Heading in ${route.title}`,
41
- groupTitle: route.title,
42
- isHeading: true,
43
- })
44
- }
45
- }
46
- }
47
- }
70
+ const seen = new Set<string>()
71
+
72
+ for (const id of searchResults) {
73
+ const doc = (searchData as SearchDataItem[]).find((d) => d.id === id)
74
+ if (!doc) continue
48
75
 
49
- // Deduplicate by path
50
- const seen = new Set()
51
- return results
52
- .filter((r) => {
53
- if (seen.has(r.path)) return false
54
- seen.add(r.path)
55
- return true
76
+ // Filter by locale and version
77
+ const localeMatch = !currentLocale || doc.locale === currentLocale
78
+ const versionMatch = !currentVersion || doc.version === currentVersion
79
+ if (!localeMatch || !versionMatch) continue
80
+
81
+ if (seen.has(doc.url)) continue
82
+ seen.add(doc.url)
83
+
84
+ results.push({
85
+ id: doc.url,
86
+ title: doc.title,
87
+ path: doc.url,
88
+ bio: doc.display,
89
+ groupTitle: doc.display.split(' > ')[0],
90
+ isHeading: doc.url.includes('#'),
56
91
  })
57
- .slice(0, 10)
58
- }, [routes, query])
92
+ }
93
+
94
+ return results.slice(0, 10)
95
+ }, [query, index, currentLocale, currentVersion, routes])
59
96
 
60
97
  return {
61
98
  isOpen,
@@ -65,7 +102,8 @@ export function useSearch(routes: ComponentRoute[]) {
65
102
  list,
66
103
  input: {
67
104
  value: query,
68
- onChange: (e: any) => setQuery(e.target.value),
105
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
106
+ setQuery(e.target.value),
69
107
  },
70
108
  }
71
109
  }
@@ -7,7 +7,11 @@ export function useSidebar(routes: ComponentRoute[]) {
7
7
  const location = useLocation()
8
8
 
9
9
  // Find active route and tab
10
- const activeRoute = routes.find((r) => r.path === location.pathname)
10
+ const normalize = (p: string) =>
11
+ p.endsWith('/') && p.length > 1 ? p.slice(0, -1) : p
12
+ const currentPath = normalize(location.pathname)
13
+
14
+ const activeRoute = routes.find((r) => normalize(r.path) === currentPath)
11
15
  const activeTabId = activeRoute?.tab?.toLowerCase()
12
16
 
13
17
  // Filter routes by active tab if any
@@ -43,7 +47,7 @@ export function useSidebar(routes: ComponentRoute[]) {
43
47
  groups,
44
48
  ungrouped,
45
49
  activeRoute,
46
- activePath: location.pathname,
50
+ activePath: currentPath,
47
51
  config,
48
52
  }
49
53
  }
@@ -1,6 +1,7 @@
1
1
  import { useNavigate } from 'react-router-dom'
2
2
  import { getBaseFilePath } from '@client/utils/get-base-file-path'
3
3
  import { useRoutes } from './use-routes'
4
+ import { useBoltdocsStore } from '../store/use-boltdocs-store'
4
5
 
5
6
  export interface VersionOption {
6
7
  key: string
@@ -25,10 +26,14 @@ export function useVersion(): UseVersionReturn {
25
26
  const { allRoutes, currentRoute, currentVersion, currentLocale, config } =
26
27
  routeContext
27
28
  const versions = config.versions
29
+ const setVersion = useBoltdocsStore((s) => s.setVersion)
28
30
 
29
31
  const handleVersionChange = (version: string) => {
30
32
  if (!versions || version === currentVersion) return
31
33
 
34
+ // Update store
35
+ setVersion(version)
36
+
32
37
  let targetPath = `/docs/${version}`
33
38
 
34
39
  if (currentRoute) {
@@ -0,0 +1 @@
1
+ export * from './codesandbox'
@@ -0,0 +1,44 @@
1
+ import { create } from 'zustand'
2
+ import { persist, createJSONStorage } from 'zustand/middleware'
3
+
4
+ interface BoltdocsState {
5
+ currentLocale: string | undefined
6
+ currentVersion: string | undefined
7
+ hasHydrated: boolean
8
+
9
+ // Actions
10
+ setLocale: (locale: string | undefined) => void
11
+ setVersion: (version: string | undefined) => void
12
+ setHasHydrated: (val: boolean) => void
13
+ }
14
+
15
+ /**
16
+ * Global store for Boltdocs documentation state.
17
+ * Uses localStorage persistence to remember user preferences across sessions.
18
+ */
19
+ export const useBoltdocsStore = create<BoltdocsState>()(
20
+ persist(
21
+ (set) => ({
22
+ currentLocale: undefined,
23
+ currentVersion: undefined,
24
+ hasHydrated: false,
25
+
26
+ setLocale: (locale: string | undefined) => set({ currentLocale: locale }),
27
+ setVersion: (version: string | undefined) =>
28
+ set({ currentVersion: version }),
29
+ setHasHydrated: (val: boolean) => set({ hasHydrated: val }),
30
+ }),
31
+ {
32
+ name: 'boltdocs-storage',
33
+ storage: createJSONStorage(() => localStorage),
34
+ // Only persist identifying state
35
+ partialize: (state: BoltdocsState) => ({
36
+ currentLocale: state.currentLocale,
37
+ currentVersion: state.currentVersion,
38
+ }),
39
+ onRehydrateStorage: () => (state?: BoltdocsState) => {
40
+ state?.setHasHydrated(true)
41
+ },
42
+ },
43
+ ),
44
+ )
@@ -67,6 +67,35 @@
67
67
  --spacing-sidebar: 16rem;
68
68
  --spacing-toc: 14rem;
69
69
  --spacing-content-max: 48rem;
70
+
71
+ @keyframes pulse {
72
+ 0%,
73
+ 100% {
74
+ opacity: 1;
75
+ }
76
+ 50% {
77
+ opacity: 0.5;
78
+ }
79
+ }
80
+
81
+ @keyframes fade-in {
82
+ from {
83
+ opacity: 0;
84
+ transform: translateY(10px);
85
+ }
86
+ to {
87
+ opacity: 1;
88
+ transform: translateY(0);
89
+ }
90
+ }
91
+ }
92
+
93
+ .animate-pulse {
94
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
95
+ }
96
+
97
+ .animate-fade-in {
98
+ animation: fade-in 0.5s ease-out forwards;
70
99
  }
71
100
 
72
101
  :root[data-theme="dark"],
@@ -119,7 +119,8 @@ export interface SandboxEmbedOptions {
119
119
  */
120
120
  export interface BoltdocsTab {
121
121
  id: string
122
- text: string
122
+ /** Text to display (can be a string or a map of translations) */
123
+ text: string | Record<string, string>
123
124
  icon?: string
124
125
  }
125
126
 
@@ -160,7 +161,8 @@ export interface LayoutProps {
160
161
  * Unified type for navbar links.
161
162
  */
162
163
  export interface NavbarLink {
163
- label: string
164
+ /** Label to display (can be a string or a map of translations) */
165
+ label: string | Record<string, string>
164
166
  href: string
165
167
  active: boolean
166
168
  /** Optional icon or string for external link indication */
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Retrieves the correct translation from a value that can be either
3
+ * a simple string or a map of locale-specific strings.
4
+ *
5
+ * @param value - The text to translate
6
+ * @param locale - The current active locale (e.g., 'en', 'es')
7
+ * @returns The translated string
8
+ */
9
+ export function getTranslated(
10
+ value: string | Record<string, string> | undefined,
11
+ locale?: string,
12
+ ): string {
13
+ if (!value) return ''
14
+ if (typeof value === 'string') return value
15
+
16
+ if (locale && value[locale]) {
17
+ return value[locale]
18
+ }
19
+
20
+ // Fallback: Use the first available translation or an empty string
21
+ const firstValue = Object.values(value)[0]
22
+ return firstValue || ''
23
+ }
@@ -1,27 +1,16 @@
1
- import { createServer, build, preview } from 'vite'
2
- import { createViteConfig, resolveConfig } from './index'
3
- import { getHtmlTemplate } from './plugin/html'
1
+ import { build, preview } from 'vite'
2
+ import { createViteConfig, resolveConfig } from '../index'
3
+ import { getHtmlTemplate } from '../plugin/html'
4
4
  import path from 'path'
5
5
  import fs from 'fs'
6
+ import * as ui from './ui'
6
7
 
7
8
  /**
8
- * Core logic for the Boltdocs CLI commands.
9
- * These functions wrap Vite's JS API to provide a seamless experience.
9
+ * Logic for the `boltdocs build` command.
10
+ * Prepares the production bundle and handles dynamic index.html generation.
11
+ *
12
+ * @param root - The project root directory
10
13
  */
11
-
12
- export async function devAction(root: string = process.cwd()) {
13
- try {
14
- const viteConfig = await createViteConfig(root, 'development')
15
- const server = await createServer(viteConfig)
16
- await server.listen()
17
- server.printUrls()
18
- server.bindCLIShortcuts({ print: true })
19
- } catch (e) {
20
- console.error('[boltdocs] Failed to start dev server:', e)
21
- process.exit(1)
22
- }
23
- }
24
-
25
14
  export async function buildAction(root: string = process.cwd()) {
26
15
  let createdIndexHtml = false
27
16
  const indexPath = path.resolve(root, 'index.html')
@@ -35,9 +24,9 @@ export async function buildAction(root: string = process.cwd()) {
35
24
 
36
25
  const viteConfig = await createViteConfig(root, 'production')
37
26
  await build(viteConfig)
38
- console.log('[boltdocs] Build completed successfully.')
27
+ ui.success('Build completed successfully.')
39
28
  } catch (e) {
40
- console.error('[boltdocs] Build failed:', e)
29
+ ui.error('Build failed:', e)
41
30
  process.exit(1)
42
31
  } finally {
43
32
  if (createdIndexHtml && fs.existsSync(indexPath)) {
@@ -46,14 +35,19 @@ export async function buildAction(root: string = process.cwd()) {
46
35
  }
47
36
  }
48
37
 
38
+ /**
39
+ * Logic for the `boltdocs preview` command.
40
+ * Serves the production build from the disk.
41
+ *
42
+ * @param root - The project root directory
43
+ */
49
44
  export async function previewAction(root: string = process.cwd()) {
50
45
  try {
51
46
  const viteConfig = await createViteConfig(root, 'production')
52
47
  const previewServer = await preview(viteConfig)
53
48
  previewServer.printUrls()
54
49
  } catch (e) {
55
- console.error('[boltdocs] Failed to start preview server:', e)
50
+ ui.error('Failed to start preview server:', e)
56
51
  process.exit(1)
57
52
  }
58
53
  }
59
-
@@ -0,0 +1,22 @@
1
+ import { createServer } from 'vite'
2
+ import { createViteConfig } from '../index'
3
+ import * as ui from './ui'
4
+
5
+ /**
6
+ * Logic for the `boltdocs dev` command.
7
+ * Starts a Vite development server and sets up HMR.
8
+ *
9
+ * @param root - The project root directory
10
+ */
11
+ export async function devAction(root: string = process.cwd()) {
12
+ try {
13
+ const viteConfig = await createViteConfig(root, 'development')
14
+ const server = await createServer(viteConfig)
15
+ await server.listen()
16
+ server.printUrls()
17
+ server.bindCLIShortcuts({ print: true })
18
+ } catch (e) {
19
+ ui.error('Failed to start dev server:', e)
20
+ process.exit(1)
21
+ }
22
+ }
@@ -0,0 +1,243 @@
1
+ import path from 'path'
2
+ import fs from 'fs'
3
+ import fastGlob from 'fast-glob'
4
+ import { resolveConfig } from '../config'
5
+ import { parseFrontmatter, normalizePath } from '../utils'
6
+ import * as ui from './ui'
7
+
8
+ /**
9
+ * Interface representing a documentation hygiene issue.
10
+ */
11
+ interface Issue {
12
+ level: 'high' | 'warning' | 'low'
13
+ message: string
14
+ suggestion?: string
15
+ }
16
+
17
+ /**
18
+ * Logic for the `boltdocs doctor` command.
19
+ * Scans the documentation directory for broken links, missing frontmatter,
20
+ * and orphaned translations.
21
+ *
22
+ * @param root - The project root directory
23
+ */
24
+ export async function doctorAction(root: string = process.cwd()) {
25
+ const { colors } = ui
26
+ ui.info(
27
+ `${colors.bold}Running documentation health check...${colors.reset}\n`,
28
+ )
29
+ const start = performance.now()
30
+
31
+ try {
32
+ const config = await resolveConfig('docs', root)
33
+ const docsDir = path.resolve(root, 'docs')
34
+
35
+ if (!fs.existsSync(docsDir)) {
36
+ ui.error(`Documentation directory not found at ${docsDir}`)
37
+ process.exit(1)
38
+ }
39
+
40
+ const files = await fastGlob(['**/*.md', '**/*.mdx'], {
41
+ cwd: docsDir,
42
+ absolute: true,
43
+ suppressErrors: true,
44
+ })
45
+
46
+ let highCount = 0
47
+ let warningCount = 0
48
+ let lowCount = 0
49
+ const issuesMap = new Map<string, Issue[]>()
50
+
51
+ const addIssue = (file: string, issue: Issue) => {
52
+ const relPath = path.relative(docsDir, file)
53
+ let issues = issuesMap.get(relPath)
54
+ if (!issues) {
55
+ issues = []
56
+ issuesMap.set(relPath, issues)
57
+ }
58
+ issues.push(issue)
59
+ if (issue.level === 'high') highCount++
60
+ else if (issue.level === 'warning') warningCount++
61
+ else if (issue.level === 'low') lowCount++
62
+ }
63
+
64
+ const basePath = '/docs'
65
+
66
+ // 1. Scan for Frontmatter, Links, and Content Issues
67
+ for (const file of files) {
68
+ const { data, content } = parseFrontmatter(file)
69
+
70
+ // Frontmatter Validation
71
+ if (!data.title) {
72
+ addIssue(file, {
73
+ level: 'warning',
74
+ message: 'Missing "title" in frontmatter.',
75
+ suggestion:
76
+ 'Add `title: Your Title` to the YAML frontmatter at the top of the file.',
77
+ })
78
+ }
79
+
80
+ if (!data.description) {
81
+ addIssue(file, {
82
+ level: 'low',
83
+ message: 'Missing "description" in frontmatter.',
84
+ suggestion:
85
+ 'Adding a description helps with SEO and search previews.',
86
+ })
87
+ }
88
+
89
+ // Link Validation
90
+ const linkRegex = /\[.*?\]\((.*?)\)/g
91
+ const htmlLinkRegex = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>/g
92
+ const links = [
93
+ ...content.matchAll(linkRegex),
94
+ ...content.matchAll(htmlLinkRegex),
95
+ ]
96
+
97
+ for (const match of links) {
98
+ let link = match[1]
99
+ if (
100
+ !link ||
101
+ link.startsWith('http') ||
102
+ link.startsWith('https') ||
103
+ link.startsWith('#') ||
104
+ link.startsWith('mailto:') ||
105
+ link.startsWith('tel:')
106
+ ) {
107
+ continue
108
+ }
109
+
110
+ link = link.split('#')[0]
111
+ if (!link) continue
112
+
113
+ let targetPath: string
114
+ if (link.startsWith('/')) {
115
+ let pathAfterBase = link
116
+ if (link.startsWith(basePath + '/') || link === basePath) {
117
+ pathAfterBase = link.substring(basePath.length)
118
+ }
119
+ targetPath = path.join(docsDir, pathAfterBase)
120
+ } else {
121
+ targetPath = path.resolve(path.dirname(file), link)
122
+ }
123
+
124
+ const extensions = ['', '.md', '.mdx', '/index.md', '/index.mdx']
125
+ let exists = false
126
+ for (const ext of extensions) {
127
+ const finalPath = targetPath + ext
128
+ if (fs.existsSync(finalPath) && fs.statSync(finalPath).isFile()) {
129
+ exists = true
130
+ break
131
+ }
132
+ }
133
+
134
+ if (!exists) {
135
+ addIssue(file, {
136
+ level: 'high',
137
+ message: `Broken internal link: "${link}"`,
138
+ suggestion: `Ensure the file exists at "${targetPath}". If it's a directory, ensure it has an "index.md" or "index.mdx".`,
139
+ })
140
+ }
141
+ }
142
+ }
143
+
144
+ // 2. Scan for Orphaned Translations
145
+ if (config.i18n) {
146
+ const { defaultLocale, locales } = config.i18n
147
+ const otherLocales = Object.keys(locales).filter(
148
+ (l) => l !== defaultLocale,
149
+ )
150
+
151
+ for (const file of files) {
152
+ const relPath = normalizePath(path.relative(docsDir, file))
153
+ const parts = relPath.split('/')
154
+
155
+ if (parts[0] === defaultLocale) {
156
+ const pathAfterLocale = parts.slice(1).join('/')
157
+ for (const locale of otherLocales) {
158
+ const localeParts = [locale, ...parts.slice(1)]
159
+ const targetLocaleFile = path.join(docsDir, ...localeParts)
160
+
161
+ if (!fs.existsSync(targetLocaleFile)) {
162
+ addIssue(file, {
163
+ level: 'warning',
164
+ message: `Missing translation for locale "${locale}"`,
165
+ suggestion: `Create a translated version of this file at "${locale}/${pathAfterLocale}".`,
166
+ })
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ // Final Reporting
174
+ if (issuesMap.size === 0) {
175
+ ui.success('All documentation files are healthy!\n')
176
+ } else {
177
+ for (const [file, issues] of issuesMap.entries()) {
178
+ console.log(`📄 ${colors.bold}${file}${colors.reset}`)
179
+
180
+ const sortedIssues = issues.sort((a, b) => {
181
+ const order = { high: 1, warning: 2, low: 3 }
182
+ return order[a.level] - order[b.level]
183
+ })
184
+
185
+ for (const issue of sortedIssues) {
186
+ let prefix = ''
187
+ let color = ''
188
+ if (issue.level === 'high') {
189
+ prefix = '❌'
190
+ color = colors.red
191
+ } else if (issue.level === 'warning') {
192
+ prefix = '⚠️'
193
+ color = colors.yellow
194
+ } else {
195
+ prefix = 'ℹ️'
196
+ color = colors.cyan
197
+ }
198
+
199
+ console.log(
200
+ ` ${color}${prefix} ${issue.level.toUpperCase()}:${colors.reset} ${issue.message}`,
201
+ )
202
+ if (issue.suggestion) {
203
+ console.log(
204
+ ` ${colors.gray}💡 Suggestion: ${issue.suggestion}${colors.reset}`,
205
+ )
206
+ }
207
+ }
208
+ console.log('')
209
+ }
210
+
211
+ console.log(`${colors.bold}Summary:${colors.reset}`)
212
+ console.log(
213
+ ` ${colors.red}${highCount} high-level errors${colors.reset}`,
214
+ )
215
+ console.log(` ${colors.yellow}${warningCount} warnings${colors.reset}`)
216
+ console.log(
217
+ ` ${colors.cyan}${lowCount} minor improvements${colors.reset}\n`,
218
+ )
219
+
220
+ if (highCount > 0) {
221
+ ui.error(
222
+ 'HIGH ERROR: Fix these to ensure your documentation builds correctly.',
223
+ )
224
+ }
225
+ if (warningCount > 0 || lowCount > 0) {
226
+ ui.info(
227
+ 'TIP: Address warnings and suggestions for premium quality docs.',
228
+ )
229
+ }
230
+ console.log('')
231
+ }
232
+
233
+ const duration = performance.now() - start
234
+ ui.info(`Finished in ${duration.toFixed(2)}ms\n`)
235
+
236
+ if (highCount > 0) {
237
+ process.exit(1)
238
+ }
239
+ } catch (e) {
240
+ ui.error('Failed to run doctor check:', e)
241
+ process.exit(1)
242
+ }
243
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Boltdocs CLI Actions Module.
3
+ * This module exports all command-line actions used by the Boltdocs tool.
4
+ */
5
+
6
+ export * from './dev'
7
+ export * from './build'
8
+ export * from './doctor'
9
+ export * from './ui'