boltdocs 1.3.0 → 1.3.1
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/dist/node/index.js +21 -6
- package/dist/node/index.mjs +21 -6
- package/package.json +1 -1
- package/src/client/app/index.tsx +344 -344
- package/src/client/app/preload.tsx +56 -56
- package/src/client/index.ts +40 -40
- package/src/client/ssr.tsx +51 -51
- package/src/client/theme/components/CodeBlock/CodeBlock.tsx +76 -76
- package/src/client/theme/components/CodeBlock/index.ts +1 -1
- package/src/client/theme/components/PackageManagerTabs/PackageManagerTabs.tsx +154 -154
- package/src/client/theme/components/PackageManagerTabs/index.ts +1 -1
- package/src/client/theme/components/PackageManagerTabs/pkg-tabs.css +64 -64
- package/src/client/theme/components/Playground/Playground.tsx +124 -124
- package/src/client/theme/components/Playground/index.ts +1 -1
- package/src/client/theme/components/Playground/playground.css +168 -168
- package/src/client/theme/components/Video/Video.tsx +84 -84
- package/src/client/theme/components/Video/index.ts +1 -1
- package/src/client/theme/components/Video/video.css +41 -41
- package/src/client/theme/components/mdx/Admonition.tsx +80 -80
- package/src/client/theme/components/mdx/Badge.tsx +31 -31
- package/src/client/theme/components/mdx/Button.tsx +50 -50
- package/src/client/theme/components/mdx/Card.tsx +80 -80
- package/src/client/theme/components/mdx/List.tsx +57 -57
- package/src/client/theme/components/mdx/Tabs.tsx +94 -94
- package/src/client/theme/components/mdx/index.ts +18 -18
- package/src/client/theme/components/mdx/mdx-components.css +424 -424
- package/src/client/theme/icons/bun.tsx +62 -62
- package/src/client/theme/icons/deno.tsx +20 -20
- package/src/client/theme/icons/discord.tsx +12 -12
- package/src/client/theme/icons/github.tsx +15 -15
- package/src/client/theme/icons/npm.tsx +13 -13
- package/src/client/theme/icons/pnpm.tsx +72 -72
- package/src/client/theme/icons/twitter.tsx +12 -12
- package/src/client/theme/styles/markdown.css +343 -343
- package/src/client/theme/styles/variables.css +162 -162
- package/src/client/theme/styles.css +37 -37
- package/src/client/theme/ui/BackgroundGradient/BackgroundGradient.tsx +10 -10
- package/src/client/theme/ui/BackgroundGradient/index.ts +1 -1
- package/src/client/theme/ui/Breadcrumbs/Breadcrumbs.tsx +68 -68
- package/src/client/theme/ui/Breadcrumbs/index.ts +1 -1
- package/src/client/theme/ui/Footer/footer.css +32 -32
- package/src/client/theme/ui/Head/Head.tsx +69 -69
- package/src/client/theme/ui/Head/index.ts +1 -1
- package/src/client/theme/ui/LanguageSwitcher/LanguageSwitcher.tsx +125 -125
- package/src/client/theme/ui/LanguageSwitcher/index.ts +1 -1
- package/src/client/theme/ui/LanguageSwitcher/language-switcher.css +98 -98
- package/src/client/theme/ui/Layout/Layout.tsx +202 -202
- package/src/client/theme/ui/Layout/base.css +76 -76
- package/src/client/theme/ui/Layout/index.ts +2 -2
- package/src/client/theme/ui/Layout/pagination.css +72 -72
- package/src/client/theme/ui/Layout/responsive.css +36 -36
- package/src/client/theme/ui/Link/Link.tsx +254 -254
- package/src/client/theme/ui/Link/index.ts +2 -2
- package/src/client/theme/ui/Loading/Loading.tsx +10 -10
- package/src/client/theme/ui/Loading/index.ts +1 -1
- package/src/client/theme/ui/Loading/loading.css +30 -30
- package/src/client/theme/ui/Navbar/GithubStars.tsx +27 -27
- package/src/client/theme/ui/Navbar/Navbar.tsx +145 -145
- package/src/client/theme/ui/Navbar/index.ts +2 -2
- package/src/client/theme/ui/Navbar/navbar.css +233 -233
- package/src/client/theme/ui/NotFound/NotFound.tsx +19 -19
- package/src/client/theme/ui/NotFound/index.ts +1 -1
- package/src/client/theme/ui/NotFound/not-found.css +64 -64
- package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +235 -235
- package/src/client/theme/ui/OnThisPage/index.ts +1 -1
- package/src/client/theme/ui/OnThisPage/toc.css +132 -132
- package/src/client/theme/ui/PoweredBy/PoweredBy.tsx +18 -18
- package/src/client/theme/ui/PoweredBy/index.ts +1 -1
- package/src/client/theme/ui/PoweredBy/powered-by.css +76 -76
- package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +199 -199
- package/src/client/theme/ui/SearchDialog/index.ts +1 -1
- package/src/client/theme/ui/SearchDialog/search.css +152 -152
- package/src/client/theme/ui/Sidebar/Sidebar.tsx +204 -204
- package/src/client/theme/ui/Sidebar/index.ts +1 -1
- package/src/client/theme/ui/Sidebar/sidebar.css +236 -236
- package/src/client/theme/ui/ThemeToggle/ThemeToggle.tsx +69 -69
- package/src/client/theme/ui/ThemeToggle/index.ts +1 -1
- package/src/client/theme/ui/VersionSwitcher/VersionSwitcher.tsx +136 -136
- package/src/client/theme/ui/VersionSwitcher/index.ts +1 -1
- package/src/client/types.ts +50 -50
- package/src/client/utils.ts +26 -26
- package/src/node/cache.ts +408 -408
- package/src/node/config.ts +192 -192
- package/src/node/index.ts +21 -21
- package/src/node/mdx.ts +120 -120
- package/src/node/plugin/entry.ts +58 -58
- package/src/node/plugin/html.ts +55 -55
- package/src/node/plugin/index.ts +193 -193
- package/src/node/plugin/types.ts +11 -11
- package/src/node/routes/cache.ts +28 -28
- package/src/node/routes/index.ts +167 -167
- package/src/node/routes/parser.ts +153 -127
- package/src/node/routes/sorter.ts +42 -42
- package/src/node/routes/types.ts +49 -49
- package/src/node/ssg/index.ts +114 -114
- package/src/node/ssg/meta.ts +34 -34
- package/src/node/ssg/options.ts +13 -13
- package/src/node/ssg/sitemap.ts +54 -54
- package/src/node/utils.ts +134 -134
- package/tsconfig.json +20 -20
- package/tsup.config.ts +22 -22
package/src/client/app/index.tsx
CHANGED
|
@@ -1,344 +1,344 @@
|
|
|
1
|
-
import React, { useEffect, useState } from "react";
|
|
2
|
-
import ReactDOM from "react-dom/client";
|
|
3
|
-
import {
|
|
4
|
-
BrowserRouter,
|
|
5
|
-
Routes,
|
|
6
|
-
Route,
|
|
7
|
-
Outlet,
|
|
8
|
-
useLocation,
|
|
9
|
-
} from "react-router-dom";
|
|
10
|
-
import { ThemeLayout } from "../theme/ui/Layout";
|
|
11
|
-
import { NotFound } from "../theme/ui/NotFound";
|
|
12
|
-
import { Loading } from "../theme/ui/Loading";
|
|
13
|
-
import { MDXProvider } from "@mdx-js/react";
|
|
14
|
-
import { ComponentRoute, CreateBoltdocsAppOptions } from "../types";
|
|
15
|
-
import {
|
|
16
|
-
createContext,
|
|
17
|
-
useContext,
|
|
18
|
-
Suspense,
|
|
19
|
-
lazy,
|
|
20
|
-
useLayoutEffect,
|
|
21
|
-
} from "react";
|
|
22
|
-
import { Link as LucideLink } from "lucide-react";
|
|
23
|
-
|
|
24
|
-
export const ConfigContext = createContext<any>(null);
|
|
25
|
-
|
|
26
|
-
export function useConfig() {
|
|
27
|
-
return useContext(ConfigContext);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const CodeBlock = lazy(() =>
|
|
31
|
-
import("../theme/components/CodeBlock").then((m) => ({
|
|
32
|
-
default: m.CodeBlock,
|
|
33
|
-
})),
|
|
34
|
-
);
|
|
35
|
-
const Video = lazy(() =>
|
|
36
|
-
import("../theme/components/Video").then((m) => ({ default: m.Video })),
|
|
37
|
-
);
|
|
38
|
-
const PackageManagerTabs = lazy(() =>
|
|
39
|
-
import("../theme/components/PackageManagerTabs").then((m) => ({
|
|
40
|
-
default: m.PackageManagerTabs,
|
|
41
|
-
})),
|
|
42
|
-
);
|
|
43
|
-
declare global {
|
|
44
|
-
interface ImportMeta {
|
|
45
|
-
env: Record<string, any>;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
import { PreloadProvider } from "./preload";
|
|
50
|
-
|
|
51
|
-
const Heading = ({
|
|
52
|
-
level,
|
|
53
|
-
id,
|
|
54
|
-
children,
|
|
55
|
-
}: {
|
|
56
|
-
level: number;
|
|
57
|
-
id?: string;
|
|
58
|
-
children: React.ReactNode;
|
|
59
|
-
}) => {
|
|
60
|
-
const Tag = `h${level}` as keyof JSX.IntrinsicElements;
|
|
61
|
-
return (
|
|
62
|
-
<Tag id={id} className="boltdocs-heading">
|
|
63
|
-
{children}
|
|
64
|
-
{id && (
|
|
65
|
-
<a href={`#${id}`} className="header-anchor" aria-label="Anchor">
|
|
66
|
-
<LucideLink size={16} />
|
|
67
|
-
</a>
|
|
68
|
-
)}
|
|
69
|
-
</Tag>
|
|
70
|
-
);
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const mdxComponents = {
|
|
74
|
-
h1: (props: any) => <Heading level={1} {...props} />,
|
|
75
|
-
h2: (props: any) => <Heading level={2} {...props} />,
|
|
76
|
-
h3: (props: any) => <Heading level={3} {...props} />,
|
|
77
|
-
h4: (props: any) => <Heading level={4} {...props} />,
|
|
78
|
-
h5: (props: any) => <Heading level={5} {...props} />,
|
|
79
|
-
h6: (props: any) => <Heading level={6} {...props} />,
|
|
80
|
-
pre: (props: any) => {
|
|
81
|
-
return (
|
|
82
|
-
<Suspense fallback={<div className="code-block-skeleton" />}>
|
|
83
|
-
<CodeBlock {...props}>{props.children}</CodeBlock>
|
|
84
|
-
</Suspense>
|
|
85
|
-
);
|
|
86
|
-
},
|
|
87
|
-
video: (props: any) => (
|
|
88
|
-
<Suspense fallback={<div className="video-skeleton" />}>
|
|
89
|
-
<Video {...props} />
|
|
90
|
-
</Suspense>
|
|
91
|
-
),
|
|
92
|
-
PackageManagerTabs: (props: any) => (
|
|
93
|
-
<Suspense fallback={<div className="pkg-tabs-skeleton" />}>
|
|
94
|
-
<PackageManagerTabs {...props} />
|
|
95
|
-
</Suspense>
|
|
96
|
-
),
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export function AppShell({
|
|
100
|
-
initialRoutes,
|
|
101
|
-
initialConfig,
|
|
102
|
-
modules,
|
|
103
|
-
hot,
|
|
104
|
-
homePage: HomePage,
|
|
105
|
-
components: customComponents = {},
|
|
106
|
-
}: {
|
|
107
|
-
initialRoutes: ComponentRoute[];
|
|
108
|
-
initialConfig: any;
|
|
109
|
-
modules: Record<string, () => Promise<any>>;
|
|
110
|
-
hot?: any;
|
|
111
|
-
homePage?: React.ComponentType;
|
|
112
|
-
components?: Record<string, React.ComponentType<any>>;
|
|
113
|
-
}) {
|
|
114
|
-
const [routesInfo, setRoutesInfo] = useState<ComponentRoute[]>(initialRoutes);
|
|
115
|
-
const [config] = useState(initialConfig);
|
|
116
|
-
|
|
117
|
-
const resolveRoutes = (infos: ComponentRoute[]) => {
|
|
118
|
-
return infos
|
|
119
|
-
.filter(
|
|
120
|
-
(route) => !(HomePage && (route.path === "/" || route.path === "")),
|
|
121
|
-
)
|
|
122
|
-
.map((route) => {
|
|
123
|
-
const loaderKey = Object.keys(modules).find((k) =>
|
|
124
|
-
k.endsWith("/" + route.filePath),
|
|
125
|
-
);
|
|
126
|
-
const loader = loaderKey ? modules[loaderKey] : null;
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
...route,
|
|
130
|
-
Component: React.lazy(() => {
|
|
131
|
-
if (!loader)
|
|
132
|
-
return Promise.resolve({ default: () => <NotFound /> });
|
|
133
|
-
return loader() as any;
|
|
134
|
-
}),
|
|
135
|
-
};
|
|
136
|
-
});
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const [resolvedRoutes, setResolvedRoutes] = useState<any[]>(() =>
|
|
140
|
-
resolveRoutes(initialRoutes),
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
// Subscribe to HMR events
|
|
144
|
-
useEffect(() => {
|
|
145
|
-
if (hot) {
|
|
146
|
-
hot.on("boltdocs:routes-update", (newRoutes: ComponentRoute[]) => {
|
|
147
|
-
setRoutesInfo(newRoutes);
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
}, [hot]);
|
|
151
|
-
|
|
152
|
-
// Sync resolved routes when info or modules change
|
|
153
|
-
useEffect(() => {
|
|
154
|
-
setResolvedRoutes(resolveRoutes(routesInfo));
|
|
155
|
-
}, [routesInfo, modules]);
|
|
156
|
-
|
|
157
|
-
return (
|
|
158
|
-
<ConfigContext.Provider value={config}>
|
|
159
|
-
<PreloadProvider routes={routesInfo} modules={modules}>
|
|
160
|
-
<ScrollHandler />
|
|
161
|
-
<Routes>
|
|
162
|
-
{/* Custom home page WITHOUT docs layout */}
|
|
163
|
-
{HomePage && (
|
|
164
|
-
<Route
|
|
165
|
-
path="/"
|
|
166
|
-
element={
|
|
167
|
-
<ThemeLayout
|
|
168
|
-
config={config}
|
|
169
|
-
routes={routesInfo}
|
|
170
|
-
sidebar={null}
|
|
171
|
-
toc={null}
|
|
172
|
-
breadcrumbs={null}
|
|
173
|
-
{...config.themeConfig?.layoutProps}
|
|
174
|
-
>
|
|
175
|
-
<HomePage />
|
|
176
|
-
</ThemeLayout>
|
|
177
|
-
}
|
|
178
|
-
/>
|
|
179
|
-
)}
|
|
180
|
-
|
|
181
|
-
{/* Documentation pages WITH sidebar + TOC layout */}
|
|
182
|
-
<Route
|
|
183
|
-
key="docs-layout"
|
|
184
|
-
element={<DocsLayout config={config} routes={routesInfo} />}
|
|
185
|
-
>
|
|
186
|
-
{resolvedRoutes.map((route: any) => (
|
|
187
|
-
<Route
|
|
188
|
-
key={route.path}
|
|
189
|
-
path={route.path === "" ? "/" : route.path}
|
|
190
|
-
element={
|
|
191
|
-
<React.Suspense fallback={<Loading />}>
|
|
192
|
-
<MdxPage
|
|
193
|
-
Component={route.Component}
|
|
194
|
-
customComponents={customComponents}
|
|
195
|
-
/>
|
|
196
|
-
</React.Suspense>
|
|
197
|
-
}
|
|
198
|
-
/>
|
|
199
|
-
))}
|
|
200
|
-
</Route>
|
|
201
|
-
|
|
202
|
-
<Route
|
|
203
|
-
path="*"
|
|
204
|
-
element={
|
|
205
|
-
<ThemeLayout
|
|
206
|
-
config={config}
|
|
207
|
-
routes={routesInfo}
|
|
208
|
-
{...config.themeConfig?.layoutProps}
|
|
209
|
-
>
|
|
210
|
-
<NotFound />
|
|
211
|
-
</ThemeLayout>
|
|
212
|
-
}
|
|
213
|
-
/>
|
|
214
|
-
</Routes>
|
|
215
|
-
</PreloadProvider>
|
|
216
|
-
</ConfigContext.Provider>
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Handles scroll restoration and hash scrolling on navigation.
|
|
222
|
-
*/
|
|
223
|
-
function ScrollHandler() {
|
|
224
|
-
const { pathname, hash } = useLocation();
|
|
225
|
-
|
|
226
|
-
useLayoutEffect(() => {
|
|
227
|
-
// Only scroll if we are not in a pending transition state (if we were using useTransition)
|
|
228
|
-
// For now, we ensure the scroll happens.
|
|
229
|
-
if (hash) {
|
|
230
|
-
const id = hash.replace("#", "");
|
|
231
|
-
const element = document.getElementById(id);
|
|
232
|
-
if (element) {
|
|
233
|
-
const offset = 80;
|
|
234
|
-
const bodyRect = document.body.getBoundingClientRect().top;
|
|
235
|
-
const elementRect = element.getBoundingClientRect().top;
|
|
236
|
-
const elementPosition = elementRect - bodyRect;
|
|
237
|
-
const offsetPosition = elementPosition - offset;
|
|
238
|
-
|
|
239
|
-
window.scrollTo({
|
|
240
|
-
top: offsetPosition,
|
|
241
|
-
behavior: "smooth",
|
|
242
|
-
});
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
window.scrollTo(0, 0);
|
|
247
|
-
}, [pathname, hash]);
|
|
248
|
-
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** Wrapper layout for doc pages (sidebar + content + TOC) */
|
|
253
|
-
function DocsLayout({
|
|
254
|
-
config,
|
|
255
|
-
routes,
|
|
256
|
-
}: {
|
|
257
|
-
config: any;
|
|
258
|
-
routes: ComponentRoute[];
|
|
259
|
-
}) {
|
|
260
|
-
return (
|
|
261
|
-
<ThemeLayout
|
|
262
|
-
config={config}
|
|
263
|
-
routes={routes}
|
|
264
|
-
{...config.themeConfig?.layoutProps}
|
|
265
|
-
>
|
|
266
|
-
<Outlet />
|
|
267
|
-
</ThemeLayout>
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Renders an MDX page securely, injecting required custom components.
|
|
273
|
-
* For example, this overrides the default `<pre>` HTML tags emitted by MDX
|
|
274
|
-
* with the Boltdocs `CodeBlock` component for syntax highlighting.
|
|
275
|
-
*
|
|
276
|
-
* @param props - Contains the dynamically loaded React component representing the MDX page
|
|
277
|
-
*/
|
|
278
|
-
function MdxPage({
|
|
279
|
-
Component,
|
|
280
|
-
customComponents = {},
|
|
281
|
-
}: {
|
|
282
|
-
Component: React.LazyExoticComponent<any>;
|
|
283
|
-
customComponents?: Record<string, React.ComponentType<any>>;
|
|
284
|
-
}) {
|
|
285
|
-
const allComponents = { ...mdxComponents, ...customComponents };
|
|
286
|
-
return (
|
|
287
|
-
<MDXProvider components={allComponents}>
|
|
288
|
-
<Component />
|
|
289
|
-
</MDXProvider>
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Creates and mounts the Boltdocs documentation app.
|
|
295
|
-
*
|
|
296
|
-
* Usage:
|
|
297
|
-
* ```tsx
|
|
298
|
-
* import { createBoltdocsApp } from 'boltdocs/client'
|
|
299
|
-
* import routes from 'virtual:boltdocs-routes'
|
|
300
|
-
* import config from 'virtual:boltdocs-config'
|
|
301
|
-
* import 'boltdocs/style.css'
|
|
302
|
-
* import HomePage from './HomePage'
|
|
303
|
-
*
|
|
304
|
-
* createBoltdocsApp({
|
|
305
|
-
* target: '#root',
|
|
306
|
-
* routes,
|
|
307
|
-
* config,
|
|
308
|
-
* modules: import.meta.glob('/docs/**\/*.{md,mdx}'),
|
|
309
|
-
* hot: import.meta.hot,
|
|
310
|
-
* homePage: HomePage,
|
|
311
|
-
* })
|
|
312
|
-
* ```
|
|
313
|
-
*/
|
|
314
|
-
export function createBoltdocsApp(options: CreateBoltdocsAppOptions) {
|
|
315
|
-
const { target, routes, config, modules, hot, homePage } = options;
|
|
316
|
-
const container = document.querySelector(target);
|
|
317
|
-
if (!container) {
|
|
318
|
-
throw new Error(
|
|
319
|
-
`[boltdocs] Mount target "${target}" not found in document.`,
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const app = (
|
|
324
|
-
<React.StrictMode>
|
|
325
|
-
<BrowserRouter>
|
|
326
|
-
<AppShell
|
|
327
|
-
initialRoutes={routes}
|
|
328
|
-
initialConfig={config}
|
|
329
|
-
modules={modules}
|
|
330
|
-
hot={hot}
|
|
331
|
-
homePage={homePage}
|
|
332
|
-
components={options.components}
|
|
333
|
-
/>
|
|
334
|
-
</BrowserRouter>
|
|
335
|
-
</React.StrictMode>
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
// SSG pre-renders a shell with mock components for SEO crawlers.
|
|
339
|
-
// We always use createRoot because the SSG output doesn't match the
|
|
340
|
-
// real client-side component tree (components are lazy/dynamic).
|
|
341
|
-
// Clear any SSG placeholder content before mounting.
|
|
342
|
-
container.innerHTML = "";
|
|
343
|
-
ReactDOM.createRoot(container as HTMLElement).render(app);
|
|
344
|
-
}
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import ReactDOM from "react-dom/client";
|
|
3
|
+
import {
|
|
4
|
+
BrowserRouter,
|
|
5
|
+
Routes,
|
|
6
|
+
Route,
|
|
7
|
+
Outlet,
|
|
8
|
+
useLocation,
|
|
9
|
+
} from "react-router-dom";
|
|
10
|
+
import { ThemeLayout } from "../theme/ui/Layout";
|
|
11
|
+
import { NotFound } from "../theme/ui/NotFound";
|
|
12
|
+
import { Loading } from "../theme/ui/Loading";
|
|
13
|
+
import { MDXProvider } from "@mdx-js/react";
|
|
14
|
+
import { ComponentRoute, CreateBoltdocsAppOptions } from "../types";
|
|
15
|
+
import {
|
|
16
|
+
createContext,
|
|
17
|
+
useContext,
|
|
18
|
+
Suspense,
|
|
19
|
+
lazy,
|
|
20
|
+
useLayoutEffect,
|
|
21
|
+
} from "react";
|
|
22
|
+
import { Link as LucideLink } from "lucide-react";
|
|
23
|
+
|
|
24
|
+
export const ConfigContext = createContext<any>(null);
|
|
25
|
+
|
|
26
|
+
export function useConfig() {
|
|
27
|
+
return useContext(ConfigContext);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const CodeBlock = lazy(() =>
|
|
31
|
+
import("../theme/components/CodeBlock").then((m) => ({
|
|
32
|
+
default: m.CodeBlock,
|
|
33
|
+
})),
|
|
34
|
+
);
|
|
35
|
+
const Video = lazy(() =>
|
|
36
|
+
import("../theme/components/Video").then((m) => ({ default: m.Video })),
|
|
37
|
+
);
|
|
38
|
+
const PackageManagerTabs = lazy(() =>
|
|
39
|
+
import("../theme/components/PackageManagerTabs").then((m) => ({
|
|
40
|
+
default: m.PackageManagerTabs,
|
|
41
|
+
})),
|
|
42
|
+
);
|
|
43
|
+
declare global {
|
|
44
|
+
interface ImportMeta {
|
|
45
|
+
env: Record<string, any>;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
import { PreloadProvider } from "./preload";
|
|
50
|
+
|
|
51
|
+
const Heading = ({
|
|
52
|
+
level,
|
|
53
|
+
id,
|
|
54
|
+
children,
|
|
55
|
+
}: {
|
|
56
|
+
level: number;
|
|
57
|
+
id?: string;
|
|
58
|
+
children: React.ReactNode;
|
|
59
|
+
}) => {
|
|
60
|
+
const Tag = `h${level}` as keyof JSX.IntrinsicElements;
|
|
61
|
+
return (
|
|
62
|
+
<Tag id={id} className="boltdocs-heading">
|
|
63
|
+
{children}
|
|
64
|
+
{id && (
|
|
65
|
+
<a href={`#${id}`} className="header-anchor" aria-label="Anchor">
|
|
66
|
+
<LucideLink size={16} />
|
|
67
|
+
</a>
|
|
68
|
+
)}
|
|
69
|
+
</Tag>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const mdxComponents = {
|
|
74
|
+
h1: (props: any) => <Heading level={1} {...props} />,
|
|
75
|
+
h2: (props: any) => <Heading level={2} {...props} />,
|
|
76
|
+
h3: (props: any) => <Heading level={3} {...props} />,
|
|
77
|
+
h4: (props: any) => <Heading level={4} {...props} />,
|
|
78
|
+
h5: (props: any) => <Heading level={5} {...props} />,
|
|
79
|
+
h6: (props: any) => <Heading level={6} {...props} />,
|
|
80
|
+
pre: (props: any) => {
|
|
81
|
+
return (
|
|
82
|
+
<Suspense fallback={<div className="code-block-skeleton" />}>
|
|
83
|
+
<CodeBlock {...props}>{props.children}</CodeBlock>
|
|
84
|
+
</Suspense>
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
video: (props: any) => (
|
|
88
|
+
<Suspense fallback={<div className="video-skeleton" />}>
|
|
89
|
+
<Video {...props} />
|
|
90
|
+
</Suspense>
|
|
91
|
+
),
|
|
92
|
+
PackageManagerTabs: (props: any) => (
|
|
93
|
+
<Suspense fallback={<div className="pkg-tabs-skeleton" />}>
|
|
94
|
+
<PackageManagerTabs {...props} />
|
|
95
|
+
</Suspense>
|
|
96
|
+
),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export function AppShell({
|
|
100
|
+
initialRoutes,
|
|
101
|
+
initialConfig,
|
|
102
|
+
modules,
|
|
103
|
+
hot,
|
|
104
|
+
homePage: HomePage,
|
|
105
|
+
components: customComponents = {},
|
|
106
|
+
}: {
|
|
107
|
+
initialRoutes: ComponentRoute[];
|
|
108
|
+
initialConfig: any;
|
|
109
|
+
modules: Record<string, () => Promise<any>>;
|
|
110
|
+
hot?: any;
|
|
111
|
+
homePage?: React.ComponentType;
|
|
112
|
+
components?: Record<string, React.ComponentType<any>>;
|
|
113
|
+
}) {
|
|
114
|
+
const [routesInfo, setRoutesInfo] = useState<ComponentRoute[]>(initialRoutes);
|
|
115
|
+
const [config] = useState(initialConfig);
|
|
116
|
+
|
|
117
|
+
const resolveRoutes = (infos: ComponentRoute[]) => {
|
|
118
|
+
return infos
|
|
119
|
+
.filter(
|
|
120
|
+
(route) => !(HomePage && (route.path === "/" || route.path === "")),
|
|
121
|
+
)
|
|
122
|
+
.map((route) => {
|
|
123
|
+
const loaderKey = Object.keys(modules).find((k) =>
|
|
124
|
+
k.endsWith("/" + route.filePath),
|
|
125
|
+
);
|
|
126
|
+
const loader = loaderKey ? modules[loaderKey] : null;
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
...route,
|
|
130
|
+
Component: React.lazy(() => {
|
|
131
|
+
if (!loader)
|
|
132
|
+
return Promise.resolve({ default: () => <NotFound /> });
|
|
133
|
+
return loader() as any;
|
|
134
|
+
}),
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const [resolvedRoutes, setResolvedRoutes] = useState<any[]>(() =>
|
|
140
|
+
resolveRoutes(initialRoutes),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Subscribe to HMR events
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (hot) {
|
|
146
|
+
hot.on("boltdocs:routes-update", (newRoutes: ComponentRoute[]) => {
|
|
147
|
+
setRoutesInfo(newRoutes);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}, [hot]);
|
|
151
|
+
|
|
152
|
+
// Sync resolved routes when info or modules change
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
setResolvedRoutes(resolveRoutes(routesInfo));
|
|
155
|
+
}, [routesInfo, modules]);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<ConfigContext.Provider value={config}>
|
|
159
|
+
<PreloadProvider routes={routesInfo} modules={modules}>
|
|
160
|
+
<ScrollHandler />
|
|
161
|
+
<Routes>
|
|
162
|
+
{/* Custom home page WITHOUT docs layout */}
|
|
163
|
+
{HomePage && (
|
|
164
|
+
<Route
|
|
165
|
+
path="/"
|
|
166
|
+
element={
|
|
167
|
+
<ThemeLayout
|
|
168
|
+
config={config}
|
|
169
|
+
routes={routesInfo}
|
|
170
|
+
sidebar={null}
|
|
171
|
+
toc={null}
|
|
172
|
+
breadcrumbs={null}
|
|
173
|
+
{...config.themeConfig?.layoutProps}
|
|
174
|
+
>
|
|
175
|
+
<HomePage />
|
|
176
|
+
</ThemeLayout>
|
|
177
|
+
}
|
|
178
|
+
/>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
{/* Documentation pages WITH sidebar + TOC layout */}
|
|
182
|
+
<Route
|
|
183
|
+
key="docs-layout"
|
|
184
|
+
element={<DocsLayout config={config} routes={routesInfo} />}
|
|
185
|
+
>
|
|
186
|
+
{resolvedRoutes.map((route: any) => (
|
|
187
|
+
<Route
|
|
188
|
+
key={route.path}
|
|
189
|
+
path={route.path === "" ? "/" : route.path}
|
|
190
|
+
element={
|
|
191
|
+
<React.Suspense fallback={<Loading />}>
|
|
192
|
+
<MdxPage
|
|
193
|
+
Component={route.Component}
|
|
194
|
+
customComponents={customComponents}
|
|
195
|
+
/>
|
|
196
|
+
</React.Suspense>
|
|
197
|
+
}
|
|
198
|
+
/>
|
|
199
|
+
))}
|
|
200
|
+
</Route>
|
|
201
|
+
|
|
202
|
+
<Route
|
|
203
|
+
path="*"
|
|
204
|
+
element={
|
|
205
|
+
<ThemeLayout
|
|
206
|
+
config={config}
|
|
207
|
+
routes={routesInfo}
|
|
208
|
+
{...config.themeConfig?.layoutProps}
|
|
209
|
+
>
|
|
210
|
+
<NotFound />
|
|
211
|
+
</ThemeLayout>
|
|
212
|
+
}
|
|
213
|
+
/>
|
|
214
|
+
</Routes>
|
|
215
|
+
</PreloadProvider>
|
|
216
|
+
</ConfigContext.Provider>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Handles scroll restoration and hash scrolling on navigation.
|
|
222
|
+
*/
|
|
223
|
+
function ScrollHandler() {
|
|
224
|
+
const { pathname, hash } = useLocation();
|
|
225
|
+
|
|
226
|
+
useLayoutEffect(() => {
|
|
227
|
+
// Only scroll if we are not in a pending transition state (if we were using useTransition)
|
|
228
|
+
// For now, we ensure the scroll happens.
|
|
229
|
+
if (hash) {
|
|
230
|
+
const id = hash.replace("#", "");
|
|
231
|
+
const element = document.getElementById(id);
|
|
232
|
+
if (element) {
|
|
233
|
+
const offset = 80;
|
|
234
|
+
const bodyRect = document.body.getBoundingClientRect().top;
|
|
235
|
+
const elementRect = element.getBoundingClientRect().top;
|
|
236
|
+
const elementPosition = elementRect - bodyRect;
|
|
237
|
+
const offsetPosition = elementPosition - offset;
|
|
238
|
+
|
|
239
|
+
window.scrollTo({
|
|
240
|
+
top: offsetPosition,
|
|
241
|
+
behavior: "smooth",
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
window.scrollTo(0, 0);
|
|
247
|
+
}, [pathname, hash]);
|
|
248
|
+
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Wrapper layout for doc pages (sidebar + content + TOC) */
|
|
253
|
+
function DocsLayout({
|
|
254
|
+
config,
|
|
255
|
+
routes,
|
|
256
|
+
}: {
|
|
257
|
+
config: any;
|
|
258
|
+
routes: ComponentRoute[];
|
|
259
|
+
}) {
|
|
260
|
+
return (
|
|
261
|
+
<ThemeLayout
|
|
262
|
+
config={config}
|
|
263
|
+
routes={routes}
|
|
264
|
+
{...config.themeConfig?.layoutProps}
|
|
265
|
+
>
|
|
266
|
+
<Outlet />
|
|
267
|
+
</ThemeLayout>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Renders an MDX page securely, injecting required custom components.
|
|
273
|
+
* For example, this overrides the default `<pre>` HTML tags emitted by MDX
|
|
274
|
+
* with the Boltdocs `CodeBlock` component for syntax highlighting.
|
|
275
|
+
*
|
|
276
|
+
* @param props - Contains the dynamically loaded React component representing the MDX page
|
|
277
|
+
*/
|
|
278
|
+
function MdxPage({
|
|
279
|
+
Component,
|
|
280
|
+
customComponents = {},
|
|
281
|
+
}: {
|
|
282
|
+
Component: React.LazyExoticComponent<any>;
|
|
283
|
+
customComponents?: Record<string, React.ComponentType<any>>;
|
|
284
|
+
}) {
|
|
285
|
+
const allComponents = { ...mdxComponents, ...customComponents };
|
|
286
|
+
return (
|
|
287
|
+
<MDXProvider components={allComponents}>
|
|
288
|
+
<Component />
|
|
289
|
+
</MDXProvider>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Creates and mounts the Boltdocs documentation app.
|
|
295
|
+
*
|
|
296
|
+
* Usage:
|
|
297
|
+
* ```tsx
|
|
298
|
+
* import { createBoltdocsApp } from 'boltdocs/client'
|
|
299
|
+
* import routes from 'virtual:boltdocs-routes'
|
|
300
|
+
* import config from 'virtual:boltdocs-config'
|
|
301
|
+
* import 'boltdocs/style.css'
|
|
302
|
+
* import HomePage from './HomePage'
|
|
303
|
+
*
|
|
304
|
+
* createBoltdocsApp({
|
|
305
|
+
* target: '#root',
|
|
306
|
+
* routes,
|
|
307
|
+
* config,
|
|
308
|
+
* modules: import.meta.glob('/docs/**\/*.{md,mdx}'),
|
|
309
|
+
* hot: import.meta.hot,
|
|
310
|
+
* homePage: HomePage,
|
|
311
|
+
* })
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
314
|
+
export function createBoltdocsApp(options: CreateBoltdocsAppOptions) {
|
|
315
|
+
const { target, routes, config, modules, hot, homePage } = options;
|
|
316
|
+
const container = document.querySelector(target);
|
|
317
|
+
if (!container) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`[boltdocs] Mount target "${target}" not found in document.`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const app = (
|
|
324
|
+
<React.StrictMode>
|
|
325
|
+
<BrowserRouter>
|
|
326
|
+
<AppShell
|
|
327
|
+
initialRoutes={routes}
|
|
328
|
+
initialConfig={config}
|
|
329
|
+
modules={modules}
|
|
330
|
+
hot={hot}
|
|
331
|
+
homePage={homePage}
|
|
332
|
+
components={options.components}
|
|
333
|
+
/>
|
|
334
|
+
</BrowserRouter>
|
|
335
|
+
</React.StrictMode>
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// SSG pre-renders a shell with mock components for SEO crawlers.
|
|
339
|
+
// We always use createRoot because the SSG output doesn't match the
|
|
340
|
+
// real client-side component tree (components are lazy/dynamic).
|
|
341
|
+
// Clear any SSG placeholder content before mounting.
|
|
342
|
+
container.innerHTML = "";
|
|
343
|
+
ReactDOM.createRoot(container as HTMLElement).render(app);
|
|
344
|
+
}
|