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.
- package/CHANGELOG.md +19 -0
- package/bin/boltdocs.js +2 -2
- package/dist/base-ui/index.d.mts +25 -0
- package/dist/base-ui/index.d.ts +25 -0
- package/dist/base-ui/index.js +1 -0
- package/dist/base-ui/index.mjs +1 -0
- package/dist/{cache-Q4T6VAUL.mjs → cache-P6WK424C.mjs} +1 -1
- package/dist/chunk-22NXDNP4.mjs +74 -0
- package/dist/chunk-2HUVMMJU.mjs +1 -0
- package/dist/chunk-2Z5T6EAU.mjs +1 -0
- package/dist/chunk-CRZGOE32.mjs +1 -0
- package/dist/chunk-HA6543SL.mjs +1 -0
- package/dist/chunk-JD3RSDE4.mjs +1 -0
- package/dist/chunk-JZXLCA2E.mjs +1 -0
- package/dist/chunk-NBCYHLAA.mjs +1 -0
- package/dist/chunk-RPUERTVC.mjs +1 -0
- package/dist/chunk-T3W44KWY.mjs +1 -0
- package/dist/chunk-URTD6E6S.mjs +1 -0
- package/dist/chunk-W2NB4T6V.mjs +1 -0
- package/dist/chunk-Y4RRHPXC.mjs +1 -0
- package/dist/client/index.d.mts +13 -115
- package/dist/client/index.d.ts +13 -115
- package/dist/client/index.js +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/client/ssr.js +1 -1
- package/dist/client/ssr.mjs +1 -1
- package/dist/client/types.d.mts +3 -0
- package/dist/client/types.d.ts +3 -0
- package/dist/client/types.js +1 -0
- package/dist/client/types.mjs +0 -0
- package/dist/copy-markdown-C-90ixSe.d.ts +15 -0
- package/dist/copy-markdown-CbS8X-qe.d.mts +15 -0
- package/dist/{client/hooks → hooks}/index.d.mts +16 -11
- package/dist/{client/hooks → hooks}/index.d.ts +16 -11
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.mjs +1 -0
- package/dist/integrations/index.d.mts +48 -0
- package/dist/integrations/index.d.ts +48 -0
- package/dist/integrations/index.js +1 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/link-DfBwCeZc.d.mts +68 -0
- package/dist/link-DfBwCeZc.d.ts +68 -0
- package/dist/loading-B7X5Wchs.d.ts +66 -0
- package/dist/loading-WuaQbsKb.d.mts +66 -0
- package/dist/{client/components/mdx → mdx}/index.d.mts +6 -38
- package/dist/{client/components/mdx → mdx}/index.d.ts +6 -38
- package/dist/mdx/index.js +1 -0
- package/dist/mdx/index.mjs +1 -0
- package/dist/node/cli-entry.js +31 -27
- package/dist/node/cli-entry.mjs +5 -1
- package/dist/node/index.d.mts +44 -14
- package/dist/node/index.d.ts +44 -14
- package/dist/node/index.js +24 -24
- package/dist/node/index.mjs +1 -1
- package/dist/primitives/index.d.mts +301 -0
- package/dist/primitives/index.d.ts +301 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/index.mjs +1 -0
- package/dist/search-dialog-ZRXBAQJ5.mjs +1 -0
- package/dist/{types-Cp21DHI6.d.mts → types-j7jvWsJj.d.mts} +63 -17
- package/dist/{types-Cp21DHI6.d.ts → types-j7jvWsJj.d.ts} +63 -17
- package/dist/{use-routes-xLhumjbV.d.ts → use-routes-Cd806kGw.d.ts} +1 -1
- package/dist/{use-routes-8Iei6jTp.d.mts → use-routes-DDL0_jkQ.d.mts} +1 -1
- package/package.json +35 -8
- package/src/client/app/index.tsx +155 -35
- package/src/client/app/mdx-component.tsx +7 -3
- package/src/client/app/theme-context.tsx +47 -23
- package/src/client/components/default-layout.tsx +16 -6
- package/src/client/components/primitives/breadcrumbs.tsx +1 -1
- package/src/client/components/primitives/navbar.tsx +8 -5
- package/src/client/components/primitives/search-dialog.tsx +15 -6
- package/src/client/components/primitives/sidebar.tsx +3 -2
- package/src/client/components/primitives/skeleton.tsx +26 -0
- package/src/client/components/ui-base/breadcrumbs.tsx +1 -1
- package/src/client/components/ui-base/index.ts +17 -0
- package/src/client/components/ui-base/loading.tsx +43 -73
- package/src/client/components/ui-base/navbar.tsx +74 -39
- package/src/client/components/ui-base/page-nav.tsx +2 -1
- package/src/client/components/ui-base/powered-by.tsx +11 -5
- package/src/client/components/ui-base/search-dialog.tsx +16 -5
- package/src/client/components/ui-base/sidebar.tsx +33 -22
- package/src/client/components/ui-base/tabs.tsx +4 -1
- package/src/client/components/ui-base/theme-toggle.tsx +35 -15
- package/src/client/hooks/use-i18n.ts +38 -7
- package/src/client/hooks/use-localized-to.ts +51 -73
- package/src/client/hooks/use-navbar.ts +10 -3
- package/src/client/hooks/use-page-nav.ts +27 -6
- package/src/client/hooks/use-routes.ts +62 -17
- package/src/client/hooks/use-search.ts +84 -46
- package/src/client/hooks/use-sidebar.ts +6 -2
- package/src/client/hooks/use-version.ts +5 -0
- package/src/client/integrations/index.ts +1 -0
- package/src/client/store/use-boltdocs-store.ts +44 -0
- package/src/client/theme/neutral.css +29 -0
- package/src/client/types.ts +4 -2
- package/src/client/utils/i18n.ts +23 -0
- package/src/node/{cli.ts → cli/build.ts} +17 -23
- package/src/node/cli/dev.ts +22 -0
- package/src/node/cli/doctor.ts +243 -0
- package/src/node/cli/index.ts +9 -0
- package/src/node/cli/ui.ts +54 -0
- package/src/node/cli-entry.ts +16 -16
- package/src/node/config.ts +54 -17
- package/src/node/index.ts +1 -1
- package/src/node/mdx/cache.ts +12 -0
- package/src/node/mdx/highlighter.ts +47 -0
- package/src/node/mdx/index.ts +114 -0
- package/src/node/mdx/rehype-shiki.ts +53 -0
- package/src/node/mdx/remark-shiki.ts +61 -0
- package/src/node/plugin/entry.ts +1 -1
- package/src/node/plugin/html.ts +8 -4
- package/src/node/plugin/index.ts +135 -72
- package/src/node/routes/index.ts +34 -13
- package/src/node/routes/parser.ts +13 -5
- package/src/node/search/index.ts +55 -0
- package/src/node/ssg/index.ts +15 -7
- package/src/node/ssg/robots.ts +7 -4
- package/src/node/utils.ts +32 -2
- package/tsup.config.ts +7 -2
- package/dist/chunk-52MVMZWS.mjs +0 -1
- package/dist/chunk-BVWWKXJH.mjs +0 -1
- package/dist/chunk-DVY3RDXD.mjs +0 -1
- package/dist/chunk-FUVYCYWC.mjs +0 -1
- package/dist/chunk-GBLMDJ2B.mjs +0 -1
- package/dist/chunk-ISPX45DF.mjs +0 -1
- package/dist/chunk-PNXZMUCO.mjs +0 -1
- package/dist/chunk-V2ZHKQSP.mjs +0 -74
- package/dist/client/components/mdx/index.js +0 -1
- package/dist/client/components/mdx/index.mjs +0 -1
- package/dist/client/hooks/index.js +0 -1
- package/dist/client/hooks/index.mjs +0 -1
- package/dist/search-dialog-TWGYKF2D.mjs +0 -1
- 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
21
|
-
|
|
22
|
-
for (const
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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"],
|
package/src/client/types.ts
CHANGED
|
@@ -119,7 +119,8 @@ export interface SandboxEmbedOptions {
|
|
|
119
119
|
*/
|
|
120
120
|
export interface BoltdocsTab {
|
|
121
121
|
id: string
|
|
122
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import { createViteConfig, resolveConfig } from '
|
|
3
|
-
import { getHtmlTemplate } from '
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
-
|
|
27
|
+
ui.success('Build completed successfully.')
|
|
39
28
|
} catch (e) {
|
|
40
|
-
|
|
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
|
-
|
|
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
|
+
}
|