boltdocs 1.0.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/dist/CodeBlock-37XMKCYY.mjs +7 -0
- package/dist/PackageManagerTabs-4NWXLXQO.mjs +314 -0
- package/dist/Playground-OE2OE6B6.mjs +7 -0
- package/dist/SearchDialog-FTOQZ763.mjs +187 -0
- package/dist/SearchDialog-ZAZXYIFX.css +2147 -0
- package/dist/Video-I6QY4X7J.mjs +7 -0
- package/dist/chunk-2YRDWM6O.mjs +56 -0
- package/dist/chunk-PN4GCTYG.mjs +67 -0
- package/dist/chunk-X2TDGMTR.mjs +64 -0
- package/dist/chunk-X6BYQHVC.mjs +12 -0
- package/dist/chunk-Z7JHYNAS.mjs +57 -0
- package/dist/chunk-ZFCOLEXN.mjs +1644 -0
- package/dist/client/index.css +2147 -0
- package/dist/client/index.d.mts +298 -0
- package/dist/client/index.d.ts +298 -0
- package/dist/client/index.js +2793 -0
- package/dist/client/index.mjs +63 -0
- package/dist/client/ssr.css +2147 -0
- package/dist/client/ssr.d.mts +25 -0
- package/dist/client/ssr.d.ts +25 -0
- package/dist/client/ssr.js +2727 -0
- package/dist/client/ssr.mjs +32 -0
- package/dist/config-D2XmHJYe.d.mts +122 -0
- package/dist/config-D2XmHJYe.d.ts +122 -0
- package/dist/index-CRQKWAeo.d.mts +82 -0
- package/dist/index-CRQKWAeo.d.ts +82 -0
- package/dist/node/cli/index.d.mts +1 -0
- package/dist/node/cli/index.d.ts +1 -0
- package/dist/node/cli/index.js +199 -0
- package/dist/node/cli/index.mjs +154 -0
- package/dist/node/index.d.mts +79 -0
- package/dist/node/index.d.ts +79 -0
- package/dist/node/index.js +797 -0
- package/dist/node/index.mjs +719 -0
- package/package.json +79 -0
- package/src/client/app/index.tsx +422 -0
- package/src/client/app/preload.tsx +56 -0
- package/src/client/index.ts +40 -0
- package/src/client/ssr.tsx +50 -0
- package/src/client/theme/components/CodeBlock/CodeBlock.tsx +76 -0
- package/src/client/theme/components/CodeBlock/index.ts +1 -0
- package/src/client/theme/components/PackageManagerTabs/PackageManagerTabs.tsx +154 -0
- package/src/client/theme/components/PackageManagerTabs/index.ts +1 -0
- package/src/client/theme/components/PackageManagerTabs/pkg-tabs.css +64 -0
- package/src/client/theme/components/Playground/Playground.tsx +86 -0
- package/src/client/theme/components/Playground/index.ts +1 -0
- package/src/client/theme/components/Playground/playground.css +168 -0
- package/src/client/theme/components/Video/Video.tsx +84 -0
- package/src/client/theme/components/Video/index.ts +1 -0
- package/src/client/theme/components/Video/video.css +41 -0
- package/src/client/theme/components/mdx/Admonition.tsx +80 -0
- package/src/client/theme/components/mdx/Badge.tsx +31 -0
- package/src/client/theme/components/mdx/Button.tsx +50 -0
- package/src/client/theme/components/mdx/Card.tsx +80 -0
- package/src/client/theme/components/mdx/List.tsx +57 -0
- package/src/client/theme/components/mdx/Tabs.tsx +94 -0
- package/src/client/theme/components/mdx/index.ts +18 -0
- package/src/client/theme/components/mdx/mdx-components.css +405 -0
- package/src/client/theme/icons/bun.tsx +62 -0
- package/src/client/theme/icons/deno.tsx +20 -0
- package/src/client/theme/icons/discord.tsx +12 -0
- package/src/client/theme/icons/github.tsx +15 -0
- package/src/client/theme/icons/npm.tsx +13 -0
- package/src/client/theme/icons/pnpm.tsx +72 -0
- package/src/client/theme/icons/twitter.tsx +12 -0
- package/src/client/theme/styles/home.css +60 -0
- package/src/client/theme/styles/markdown.css +343 -0
- package/src/client/theme/styles/variables.css +162 -0
- package/src/client/theme/styles.css +38 -0
- package/src/client/theme/ui/BackgroundGradient/BackgroundGradient.tsx +10 -0
- package/src/client/theme/ui/BackgroundGradient/index.ts +1 -0
- package/src/client/theme/ui/Breadcrumbs/Breadcrumbs.tsx +68 -0
- package/src/client/theme/ui/Breadcrumbs/index.ts +1 -0
- package/src/client/theme/ui/Footer/footer.css +32 -0
- package/src/client/theme/ui/Head/Head.tsx +69 -0
- package/src/client/theme/ui/Head/index.ts +1 -0
- package/src/client/theme/ui/LanguageSwitcher/LanguageSwitcher.tsx +125 -0
- package/src/client/theme/ui/LanguageSwitcher/index.ts +1 -0
- package/src/client/theme/ui/LanguageSwitcher/language-switcher.css +98 -0
- package/src/client/theme/ui/Layout/Layout.tsx +213 -0
- package/src/client/theme/ui/Layout/base.css +76 -0
- package/src/client/theme/ui/Layout/index.ts +2 -0
- package/src/client/theme/ui/Layout/pagination.css +72 -0
- package/src/client/theme/ui/Layout/responsive.css +40 -0
- package/src/client/theme/ui/Link/Link.tsx +202 -0
- package/src/client/theme/ui/Link/index.ts +2 -0
- package/src/client/theme/ui/Loading/Loading.tsx +10 -0
- package/src/client/theme/ui/Loading/index.ts +1 -0
- package/src/client/theme/ui/Loading/loading.css +30 -0
- package/src/client/theme/ui/Navbar/GithubStars.tsx +27 -0
- package/src/client/theme/ui/Navbar/Navbar.tsx +145 -0
- package/src/client/theme/ui/Navbar/index.ts +2 -0
- package/src/client/theme/ui/Navbar/navbar.css +233 -0
- package/src/client/theme/ui/NotFound/NotFound.tsx +20 -0
- package/src/client/theme/ui/NotFound/index.ts +1 -0
- package/src/client/theme/ui/NotFound/not-found.css +64 -0
- package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +192 -0
- package/src/client/theme/ui/OnThisPage/index.ts +1 -0
- package/src/client/theme/ui/OnThisPage/toc.css +132 -0
- package/src/client/theme/ui/PoweredBy/PoweredBy.tsx +18 -0
- package/src/client/theme/ui/PoweredBy/index.ts +1 -0
- package/src/client/theme/ui/PoweredBy/powered-by.css +76 -0
- package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +199 -0
- package/src/client/theme/ui/SearchDialog/index.ts +1 -0
- package/src/client/theme/ui/SearchDialog/search.css +152 -0
- package/src/client/theme/ui/Sidebar/Sidebar.tsx +200 -0
- package/src/client/theme/ui/Sidebar/index.ts +1 -0
- package/src/client/theme/ui/Sidebar/sidebar.css +269 -0
- package/src/client/theme/ui/ThemeToggle/ThemeToggle.tsx +69 -0
- package/src/client/theme/ui/ThemeToggle/index.ts +1 -0
- package/src/client/theme/ui/VersionSwitcher/VersionSwitcher.tsx +136 -0
- package/src/client/theme/ui/VersionSwitcher/index.ts +1 -0
- package/src/client/utils.ts +26 -0
- package/src/node/cache.ts +94 -0
- package/src/node/cli/commands/config.ts +15 -0
- package/src/node/cli/commands/generate-css.ts +24 -0
- package/src/node/cli/constants.ts +70 -0
- package/src/node/cli/index.ts +22 -0
- package/src/node/config.ts +185 -0
- package/src/node/index.ts +21 -0
- package/src/node/mdx.ts +41 -0
- package/src/node/plugin/entry.ts +58 -0
- package/src/node/plugin/html.ts +55 -0
- package/src/node/plugin/index.ts +190 -0
- package/src/node/plugin/types.ts +11 -0
- package/src/node/routes/cache.ts +24 -0
- package/src/node/routes/index.ts +152 -0
- package/src/node/routes/parser.ts +127 -0
- package/src/node/routes/sorter.ts +42 -0
- package/src/node/routes/types.ts +49 -0
- package/src/node/ssg/index.ts +110 -0
- package/src/node/ssg/meta.ts +34 -0
- package/src/node/ssg/options.ts +13 -0
- package/src/node/ssg/sitemap.ts +54 -0
- package/src/node/utils.ts +134 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +22 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { normalizePath } from "../utils";
|
|
2
|
+
import type { BoltdocsConfig } from "../config";
|
|
3
|
+
import type { BoltdocsPluginOptions } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates the raw source code for the virtual entry file (`\0virtual:boltdocs-entry`).
|
|
7
|
+
* This code initializes the client-side React application.
|
|
8
|
+
*
|
|
9
|
+
* @param options - Plugin options containing potential custom overrides (like `homePage` or `customCss`)
|
|
10
|
+
* @param config - The resolved Boltdocs configuration containing custom plugins and components
|
|
11
|
+
* @returns A string of JavaScript code to be evaluated by the browser
|
|
12
|
+
*/
|
|
13
|
+
export function generateEntryCode(
|
|
14
|
+
options: BoltdocsPluginOptions,
|
|
15
|
+
config?: BoltdocsConfig,
|
|
16
|
+
): string {
|
|
17
|
+
const homeImport = options.homePage
|
|
18
|
+
? `import HomePage from '${normalizePath(options.homePage)}';`
|
|
19
|
+
: "";
|
|
20
|
+
const homeOption = options.homePage ? "homePage: HomePage," : "";
|
|
21
|
+
const customCssImport = options.customCss
|
|
22
|
+
? `import '${normalizePath(options.customCss)}';`
|
|
23
|
+
: "";
|
|
24
|
+
|
|
25
|
+
const pluginComponents =
|
|
26
|
+
config?.plugins?.flatMap((p) => Object.entries(p.components || {})) || [];
|
|
27
|
+
|
|
28
|
+
const componentImports = pluginComponents
|
|
29
|
+
.map(
|
|
30
|
+
([
|
|
31
|
+
name,
|
|
32
|
+
path,
|
|
33
|
+
]) => `import * as _comp_${name} from '${normalizePath(path)}';
|
|
34
|
+
const ${name} = _comp_${name}.default || _comp_${name}['${name}'] || _comp_${name};`,
|
|
35
|
+
)
|
|
36
|
+
.join("\n");
|
|
37
|
+
const componentMap = pluginComponents.map(([name]) => name).join(", ");
|
|
38
|
+
|
|
39
|
+
return `
|
|
40
|
+
import { createBoltdocsApp as _createApp } from 'boltdocs/client';
|
|
41
|
+
import 'boltdocs/style.css';
|
|
42
|
+
${customCssImport}
|
|
43
|
+
import _routes from 'virtual:boltdocs-routes';
|
|
44
|
+
import _config from 'virtual:boltdocs-config';
|
|
45
|
+
${homeImport}
|
|
46
|
+
${componentImports}
|
|
47
|
+
|
|
48
|
+
_createApp({
|
|
49
|
+
target: '#root',
|
|
50
|
+
routes: _routes,
|
|
51
|
+
config: _config,
|
|
52
|
+
modules: import.meta.glob('/docs/**/*.{md,mdx}'),
|
|
53
|
+
hot: import.meta.hot,
|
|
54
|
+
${homeOption}
|
|
55
|
+
components: { ${componentMap} },
|
|
56
|
+
});
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { BoltdocsConfig } from "../config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Injects OpenGraph, Twitter, and generic SEO meta tags into the final HTML output.
|
|
5
|
+
* Also ensures the virtual entry file is injected if it's missing (e.g., standard Vite index.html).
|
|
6
|
+
*
|
|
7
|
+
* @param html - The original HTML string
|
|
8
|
+
* @param config - The resolved Boltdocs configuration containing site metadata
|
|
9
|
+
* @returns The modified HTML string with injected tags
|
|
10
|
+
*/
|
|
11
|
+
export function injectHtmlMeta(html: string, config: BoltdocsConfig): string {
|
|
12
|
+
const title = config.themeConfig?.title || "Boltdocs";
|
|
13
|
+
const description = config.themeConfig?.description || "";
|
|
14
|
+
|
|
15
|
+
const seoTags = [
|
|
16
|
+
`<meta name="description" content="${description}">`,
|
|
17
|
+
`<meta property="og:title" content="${title}">`,
|
|
18
|
+
`<meta property="og:description" content="${description}">`,
|
|
19
|
+
`<meta property="og:type" content="website">`,
|
|
20
|
+
`<meta name="twitter:card" content="summary">`,
|
|
21
|
+
`<meta name="twitter:title" content="${title}">`,
|
|
22
|
+
`<meta name="twitter:description" content="${description}">`,
|
|
23
|
+
`<meta name="generator" content="Boltdocs">`,
|
|
24
|
+
].join("\n ");
|
|
25
|
+
|
|
26
|
+
const themeScript = `
|
|
27
|
+
<script>
|
|
28
|
+
(function() {
|
|
29
|
+
try {
|
|
30
|
+
var stored = localStorage.getItem("boltdocs-theme");
|
|
31
|
+
var theme = stored || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
|
32
|
+
if (theme === "light") {
|
|
33
|
+
document.documentElement.classList.add("theme-light");
|
|
34
|
+
document.documentElement.dataset.theme = "light";
|
|
35
|
+
} else {
|
|
36
|
+
document.documentElement.classList.remove("theme-light");
|
|
37
|
+
document.documentElement.dataset.theme = "dark";
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {}
|
|
40
|
+
})();
|
|
41
|
+
</script>
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
html = html.replace(/<title>.*?<\/title>/, `<title>${title}</title>`);
|
|
45
|
+
html = html.replace("</head>", ` ${seoTags}\n${themeScript} </head>`);
|
|
46
|
+
|
|
47
|
+
if (!html.includes("src/main")) {
|
|
48
|
+
html = html.replace(
|
|
49
|
+
"</body>",
|
|
50
|
+
' <script type="module">import "virtual:boltdocs-entry";</script>\n </body>',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return html;
|
|
55
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Plugin, ResolvedConfig, loadEnv } from "vite";
|
|
2
|
+
import {
|
|
3
|
+
generateRoutes,
|
|
4
|
+
invalidateRouteCache,
|
|
5
|
+
invalidateFile,
|
|
6
|
+
} from "../routes";
|
|
7
|
+
import { ViteImageOptimizer } from "vite-plugin-image-optimizer";
|
|
8
|
+
import { resolveConfig, BoltdocsConfig, CONFIG_FILES } from "../config";
|
|
9
|
+
import { generateStaticPages } from "../ssg";
|
|
10
|
+
import { normalizePath, isDocFile } from "../utils";
|
|
11
|
+
import path from "path";
|
|
12
|
+
|
|
13
|
+
import { BoltdocsPluginOptions } from "./types";
|
|
14
|
+
import { generateEntryCode } from "./entry";
|
|
15
|
+
import { injectHtmlMeta } from "./html";
|
|
16
|
+
|
|
17
|
+
export * from "./types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The core Boltdocs Vite plugin.
|
|
21
|
+
* Handles virtual module resolution, HMR for documentation files,
|
|
22
|
+
* injecting HTML meta tags for SEO, and triggering the SSG process on build.
|
|
23
|
+
*
|
|
24
|
+
* @param options - Optional configuration for the plugin
|
|
25
|
+
* @param passedConfig - Pre-resolved configuration (internal use)
|
|
26
|
+
* @returns An array of Vite plugins
|
|
27
|
+
*/
|
|
28
|
+
export function boltdocsPlugin(
|
|
29
|
+
options: BoltdocsPluginOptions = {},
|
|
30
|
+
passedConfig?: BoltdocsConfig,
|
|
31
|
+
): Plugin[] {
|
|
32
|
+
const docsDir = path.resolve(process.cwd(), options.docsDir || "docs");
|
|
33
|
+
const normalizedDocsDir = normalizePath(docsDir);
|
|
34
|
+
let config: BoltdocsConfig = passedConfig!;
|
|
35
|
+
let viteConfig: ResolvedConfig;
|
|
36
|
+
let isBuild = false;
|
|
37
|
+
|
|
38
|
+
const extraVitePlugins =
|
|
39
|
+
config?.plugins?.flatMap((p) => p.vitePlugins || []) || [];
|
|
40
|
+
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
name: "vite-plugin-boltdocs",
|
|
44
|
+
enforce: "pre",
|
|
45
|
+
|
|
46
|
+
async config(userConfig, env) {
|
|
47
|
+
isBuild = env.command === "build";
|
|
48
|
+
|
|
49
|
+
// Load env variables and inject into process.env so they are available in boltdocs.config.js
|
|
50
|
+
const envDir = userConfig.envDir || process.cwd();
|
|
51
|
+
const envs = loadEnv(env.mode, envDir, "");
|
|
52
|
+
Object.assign(process.env, envs);
|
|
53
|
+
|
|
54
|
+
// Resolve config async if not already passed
|
|
55
|
+
if (!config) {
|
|
56
|
+
config = await resolveConfig(docsDir);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If customCss specified in user's config file, use it
|
|
60
|
+
if (!options.customCss && config.themeConfig?.customCss) {
|
|
61
|
+
options.customCss = config.themeConfig.customCss;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
optimizeDeps: { include: ["react", "react-dom"] },
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
configResolved(resolved) {
|
|
70
|
+
viteConfig = resolved;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
configureServer(server) {
|
|
74
|
+
// Explicitly watch config files to trigger server restarts
|
|
75
|
+
const configPaths = CONFIG_FILES.map((c) =>
|
|
76
|
+
path.resolve(process.cwd(), c),
|
|
77
|
+
);
|
|
78
|
+
server.watcher.add(configPaths);
|
|
79
|
+
|
|
80
|
+
const handleFileEvent = async (
|
|
81
|
+
file: string,
|
|
82
|
+
type: "add" | "unlink" | "change",
|
|
83
|
+
) => {
|
|
84
|
+
const normalized = normalizePath(file);
|
|
85
|
+
|
|
86
|
+
// Restart the Vite server if the Boltdocs config changes
|
|
87
|
+
if (CONFIG_FILES.some((c) => normalized.endsWith(c))) {
|
|
88
|
+
server.restart();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
!normalized.startsWith(normalizedDocsDir) ||
|
|
94
|
+
!isDocFile(normalized)
|
|
95
|
+
)
|
|
96
|
+
return;
|
|
97
|
+
|
|
98
|
+
// Invalidate appropriately
|
|
99
|
+
if (type === "add" || type === "unlink") {
|
|
100
|
+
invalidateRouteCache();
|
|
101
|
+
} else {
|
|
102
|
+
invalidateFile(file);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Regenerate and push to client
|
|
106
|
+
const newRoutes = await generateRoutes(docsDir, config);
|
|
107
|
+
|
|
108
|
+
const routesMod = server.moduleGraph.getModuleById(
|
|
109
|
+
"\0virtual:boltdocs-routes",
|
|
110
|
+
);
|
|
111
|
+
if (routesMod) server.moduleGraph.invalidateModule(routesMod);
|
|
112
|
+
|
|
113
|
+
server.ws.send({
|
|
114
|
+
type: "custom",
|
|
115
|
+
event: "boltdocs:routes-update",
|
|
116
|
+
data: newRoutes,
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
server.watcher.on("add", (f) => handleFileEvent(f, "add"));
|
|
121
|
+
server.watcher.on("unlink", (f) => handleFileEvent(f, "unlink"));
|
|
122
|
+
server.watcher.on("change", (f) => handleFileEvent(f, "change"));
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
resolveId(id) {
|
|
126
|
+
if (
|
|
127
|
+
id === "virtual:boltdocs-routes" ||
|
|
128
|
+
id === "virtual:boltdocs-config" ||
|
|
129
|
+
id === "virtual:boltdocs-entry"
|
|
130
|
+
) {
|
|
131
|
+
return "\0" + id;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
async load(id) {
|
|
136
|
+
if (id === "\0virtual:boltdocs-routes") {
|
|
137
|
+
const routes = await generateRoutes(docsDir, config);
|
|
138
|
+
return `export default ${JSON.stringify(routes, null, 2)};`;
|
|
139
|
+
}
|
|
140
|
+
if (id === "\0virtual:boltdocs-config") {
|
|
141
|
+
const clientConfig = {
|
|
142
|
+
themeConfig: config?.themeConfig,
|
|
143
|
+
i18n: config?.i18n,
|
|
144
|
+
versions: config?.versions,
|
|
145
|
+
siteUrl: config?.siteUrl,
|
|
146
|
+
};
|
|
147
|
+
return `export default ${JSON.stringify(clientConfig, null, 2)};`;
|
|
148
|
+
}
|
|
149
|
+
if (id === "\0virtual:boltdocs-entry") {
|
|
150
|
+
const code = generateEntryCode(options, config);
|
|
151
|
+
return code;
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
transformIndexHtml: {
|
|
156
|
+
order: "pre",
|
|
157
|
+
handler(html) {
|
|
158
|
+
return injectHtmlMeta(html, config);
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
async closeBundle() {
|
|
163
|
+
if (!isBuild) return;
|
|
164
|
+
const outDir = viteConfig?.build?.outDir
|
|
165
|
+
? path.resolve(viteConfig.root, viteConfig.build.outDir)
|
|
166
|
+
: path.resolve(process.cwd(), "dist");
|
|
167
|
+
|
|
168
|
+
await generateStaticPages({ docsDir, outDir, config });
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
ViteImageOptimizer({
|
|
172
|
+
includePublic: true,
|
|
173
|
+
png: { quality: 80 },
|
|
174
|
+
jpeg: { quality: 80 },
|
|
175
|
+
jpg: { quality: 80 },
|
|
176
|
+
webp: { quality: 80 },
|
|
177
|
+
avif: { quality: 80 },
|
|
178
|
+
svg: {
|
|
179
|
+
multipass: true,
|
|
180
|
+
plugins: [
|
|
181
|
+
{
|
|
182
|
+
name: "preset-default",
|
|
183
|
+
params: { overrides: { removeViewBox: false } },
|
|
184
|
+
},
|
|
185
|
+
] as any,
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
...extraVitePlugins.filter((p): p is Plugin => !!p),
|
|
189
|
+
];
|
|
190
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options specifically for the Boltdocs Vite plugin.
|
|
3
|
+
*/
|
|
4
|
+
export interface BoltdocsPluginOptions {
|
|
5
|
+
/** The root directory containing markdown files (default: 'docs') */
|
|
6
|
+
docsDir?: string;
|
|
7
|
+
/** Path to a custom home page component (relative to project root) to render at '/' */
|
|
8
|
+
homePage?: string;
|
|
9
|
+
/** Path to a custom CSS file to override theme variables. Can also be set in boltdocs.config.js */
|
|
10
|
+
customCss?: string;
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FileCache } from "../cache";
|
|
2
|
+
import { ParsedDocFile } from "./types";
|
|
3
|
+
|
|
4
|
+
const docCache = new FileCache<ParsedDocFile>();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Invalidate all cached routes.
|
|
8
|
+
* Typically called when a file is added or deleted, requiring a complete route rebuild.
|
|
9
|
+
*/
|
|
10
|
+
export function invalidateRouteCache(): void {
|
|
11
|
+
docCache.invalidateAll();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Invalidate a specific file from cache.
|
|
16
|
+
* Called when a specific file is modified (changed).
|
|
17
|
+
*
|
|
18
|
+
* @param filePath - The absolute path of the file to invalidate
|
|
19
|
+
*/
|
|
20
|
+
export function invalidateFile(filePath: string): void {
|
|
21
|
+
docCache.invalidate(filePath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { docCache };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import fastGlob from "fast-glob";
|
|
2
|
+
import { BoltdocsConfig } from "../config";
|
|
3
|
+
import { capitalize } from "../utils";
|
|
4
|
+
|
|
5
|
+
import { RouteMeta, ParsedDocFile } from "./types";
|
|
6
|
+
import { docCache, invalidateRouteCache, invalidateFile } from "./cache";
|
|
7
|
+
import { parseDocFile } from "./parser";
|
|
8
|
+
import { sortRoutes } from "./sorter";
|
|
9
|
+
|
|
10
|
+
// Re-export public API
|
|
11
|
+
export type { RouteMeta };
|
|
12
|
+
export { invalidateRouteCache, invalidateFile };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generates the entire route map for the documentation site.
|
|
16
|
+
* This reads all `.md` and `.mdx` files in the `docsDir`, parses them (using cache),
|
|
17
|
+
* infers group hierarchies based on directory structure and `index.md` files,
|
|
18
|
+
* and returns a sorted array of RouteMeta objects intended for the client.
|
|
19
|
+
*
|
|
20
|
+
* @param docsDir - The root directory containing markdown files
|
|
21
|
+
* @param config - Optional configuration for i18n and versioning
|
|
22
|
+
* @param basePath - The base URL path to prefix to generated routes (e.g., '/docs')
|
|
23
|
+
* @returns A promise that resolves to the final list of RouteMeta objects
|
|
24
|
+
*/
|
|
25
|
+
export async function generateRoutes(
|
|
26
|
+
docsDir: string,
|
|
27
|
+
config?: BoltdocsConfig,
|
|
28
|
+
basePath: string = "/docs",
|
|
29
|
+
): Promise<RouteMeta[]> {
|
|
30
|
+
const files = await fastGlob(["**/*.md", "**/*.mdx"], {
|
|
31
|
+
cwd: docsDir,
|
|
32
|
+
absolute: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Prune cache entries for deleted files
|
|
36
|
+
docCache.pruneStale(new Set(files));
|
|
37
|
+
|
|
38
|
+
// Invalidate all caches if config changes drastically (e.g. i18n enabled)
|
|
39
|
+
if (config?.i18n) {
|
|
40
|
+
docCache.invalidateAll();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Parse files in parallel using Promise.all for increased efficiency
|
|
44
|
+
const parsed: ParsedDocFile[] = await Promise.all(
|
|
45
|
+
files.map(async (file) => {
|
|
46
|
+
const cached = docCache.get(file);
|
|
47
|
+
if (cached) return cached;
|
|
48
|
+
|
|
49
|
+
const result = parseDocFile(file, docsDir, basePath, config);
|
|
50
|
+
docCache.set(file, result);
|
|
51
|
+
return result;
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Collect group metadata from directory names and index files
|
|
56
|
+
const groupMeta = new Map<string, { title: string; position?: number }>();
|
|
57
|
+
for (const p of parsed) {
|
|
58
|
+
if (p.relativeDir) {
|
|
59
|
+
if (!groupMeta.has(p.relativeDir)) {
|
|
60
|
+
groupMeta.set(p.relativeDir, {
|
|
61
|
+
title: capitalize(p.relativeDir),
|
|
62
|
+
position: p.inferredGroupPosition,
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
const entry = groupMeta.get(p.relativeDir)!;
|
|
66
|
+
if (
|
|
67
|
+
entry.position === undefined &&
|
|
68
|
+
p.inferredGroupPosition !== undefined
|
|
69
|
+
) {
|
|
70
|
+
entry.position = p.inferredGroupPosition;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (p.isGroupIndex && p.relativeDir && p.groupMeta) {
|
|
76
|
+
const entry = groupMeta.get(p.relativeDir)!;
|
|
77
|
+
entry.title = p.groupMeta.title;
|
|
78
|
+
if (p.groupMeta.position !== undefined) {
|
|
79
|
+
entry.position = p.groupMeta.position;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build final routes with group info
|
|
85
|
+
const routes: RouteMeta[] = parsed.map((p) => {
|
|
86
|
+
const dir = p.relativeDir;
|
|
87
|
+
const meta = dir ? groupMeta.get(dir) : undefined;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
...p.route,
|
|
91
|
+
group: dir,
|
|
92
|
+
groupTitle: meta?.title || (dir ? capitalize(dir) : undefined),
|
|
93
|
+
groupPosition: meta?.position,
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Add fallbacks if i18n is enabled
|
|
98
|
+
if (config?.i18n) {
|
|
99
|
+
const defaultLocale = config.i18n.defaultLocale;
|
|
100
|
+
const allLocales = Object.keys(config.i18n.locales);
|
|
101
|
+
|
|
102
|
+
const fallbackRoutes: RouteMeta[] = [];
|
|
103
|
+
const defaultRoutes = routes.filter(
|
|
104
|
+
(r) => (r.locale || defaultLocale) === defaultLocale,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
for (const locale of allLocales) {
|
|
108
|
+
if (locale === defaultLocale) continue;
|
|
109
|
+
|
|
110
|
+
const localeRoutePaths = new Set(
|
|
111
|
+
routes.filter((r) => r.locale === locale).map((r) => r.path),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
for (const defRoute of defaultRoutes) {
|
|
115
|
+
let prefix = basePath;
|
|
116
|
+
if (defRoute.version) {
|
|
117
|
+
prefix += "/" + defRoute.version;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let pathAfterVersion = defRoute.path.substring(prefix.length);
|
|
121
|
+
|
|
122
|
+
if (pathAfterVersion.startsWith("/" + defaultLocale + "/")) {
|
|
123
|
+
pathAfterVersion = pathAfterVersion.substring(
|
|
124
|
+
defaultLocale.length + 1,
|
|
125
|
+
);
|
|
126
|
+
} else if (pathAfterVersion === "/" + defaultLocale) {
|
|
127
|
+
pathAfterVersion = "/";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const targetPath =
|
|
131
|
+
prefix +
|
|
132
|
+
"/" +
|
|
133
|
+
locale +
|
|
134
|
+
(pathAfterVersion === "/" || pathAfterVersion === ""
|
|
135
|
+
? ""
|
|
136
|
+
: pathAfterVersion);
|
|
137
|
+
|
|
138
|
+
if (!localeRoutePaths.has(targetPath)) {
|
|
139
|
+
fallbackRoutes.push({
|
|
140
|
+
...defRoute,
|
|
141
|
+
path: targetPath,
|
|
142
|
+
locale: locale,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return sortRoutes([...routes, ...fallbackRoutes]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return sortRoutes(routes);
|
|
152
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import GithubSlugger from "github-slugger";
|
|
3
|
+
import { BoltdocsConfig } from "../config";
|
|
4
|
+
import { ParsedDocFile } from "./types";
|
|
5
|
+
import {
|
|
6
|
+
normalizePath,
|
|
7
|
+
parseFrontmatter,
|
|
8
|
+
fileToRoutePath,
|
|
9
|
+
capitalize,
|
|
10
|
+
stripNumberPrefix,
|
|
11
|
+
extractNumberPrefix,
|
|
12
|
+
} from "../utils";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parses a single Markdown/MDX file and extracts its metadata for routing.
|
|
16
|
+
* Checks frontmatter for explicit titles, descriptions, and sidebar positions.
|
|
17
|
+
*
|
|
18
|
+
* @param file - The absolute path to the file
|
|
19
|
+
* @param docsDir - The root documentation directory (e.g., 'docs')
|
|
20
|
+
* @param basePath - The base URL path for the routes (default: '/docs')
|
|
21
|
+
* @returns A parsed structure ready for route assembly and caching
|
|
22
|
+
*/
|
|
23
|
+
export function parseDocFile(
|
|
24
|
+
file: string,
|
|
25
|
+
docsDir: string,
|
|
26
|
+
basePath: string,
|
|
27
|
+
config?: BoltdocsConfig,
|
|
28
|
+
): ParsedDocFile {
|
|
29
|
+
const { data, content } = parseFrontmatter(file);
|
|
30
|
+
const relativePath = normalizePath(path.relative(docsDir, file));
|
|
31
|
+
let parts = relativePath.split("/");
|
|
32
|
+
|
|
33
|
+
let locale: string | undefined;
|
|
34
|
+
let version: string | undefined;
|
|
35
|
+
|
|
36
|
+
// Level 1: Check for version
|
|
37
|
+
if (config?.versions && parts.length > 0) {
|
|
38
|
+
const potentialVersion = parts[0];
|
|
39
|
+
if (config.versions.versions[potentialVersion]) {
|
|
40
|
+
version = potentialVersion;
|
|
41
|
+
parts = parts.slice(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Level 2: Check for locale
|
|
46
|
+
if (config?.i18n && parts.length > 0) {
|
|
47
|
+
const potentialLocale = parts[0];
|
|
48
|
+
if (config.i18n.locales[potentialLocale]) {
|
|
49
|
+
locale = potentialLocale;
|
|
50
|
+
parts = parts.slice(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const cleanRelativePath = parts.join("/");
|
|
55
|
+
const cleanRoutePath = fileToRoutePath(cleanRelativePath || "index.md");
|
|
56
|
+
|
|
57
|
+
let finalPath = basePath;
|
|
58
|
+
if (version) {
|
|
59
|
+
finalPath += "/" + version;
|
|
60
|
+
}
|
|
61
|
+
if (locale) {
|
|
62
|
+
finalPath += "/" + locale;
|
|
63
|
+
}
|
|
64
|
+
finalPath += cleanRoutePath === "/" ? "" : cleanRoutePath;
|
|
65
|
+
|
|
66
|
+
if (!finalPath || finalPath === "") finalPath = "/";
|
|
67
|
+
|
|
68
|
+
const rawFileName = parts[parts.length - 1];
|
|
69
|
+
const cleanFileName = stripNumberPrefix(rawFileName);
|
|
70
|
+
const inferredTitle = stripNumberPrefix(
|
|
71
|
+
path.basename(file, path.extname(file)),
|
|
72
|
+
);
|
|
73
|
+
const sidebarPosition =
|
|
74
|
+
data.sidebarPosition ?? extractNumberPrefix(rawFileName);
|
|
75
|
+
|
|
76
|
+
const rawDirName = parts.length >= 2 ? parts[0] : undefined;
|
|
77
|
+
const cleanDirName = rawDirName ? stripNumberPrefix(rawDirName) : undefined;
|
|
78
|
+
|
|
79
|
+
const isGroupIndex = parts.length >= 2 && /^index\.mdx?$/.test(cleanFileName);
|
|
80
|
+
|
|
81
|
+
const headings: { level: number; text: string; id: string }[] = [];
|
|
82
|
+
const slugger = new GithubSlugger();
|
|
83
|
+
const headingsRegex = /^(#{2,4})\s+(.+)$/gm;
|
|
84
|
+
let match;
|
|
85
|
+
while ((match = headingsRegex.exec(content)) !== null) {
|
|
86
|
+
const level = match[1].length;
|
|
87
|
+
// Strip simple markdown formatting specifically for the plain-text search index
|
|
88
|
+
const text = match[2]
|
|
89
|
+
.replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1")
|
|
90
|
+
.replace(/[_*`]/g, "")
|
|
91
|
+
.trim();
|
|
92
|
+
const id = slugger.slug(text);
|
|
93
|
+
headings.push({ level, text, id });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
route: {
|
|
98
|
+
path: finalPath,
|
|
99
|
+
componentPath: file,
|
|
100
|
+
filePath: relativePath,
|
|
101
|
+
title: data.title || inferredTitle,
|
|
102
|
+
description: data.description || "",
|
|
103
|
+
sidebarPosition,
|
|
104
|
+
headings,
|
|
105
|
+
locale,
|
|
106
|
+
version,
|
|
107
|
+
badge: data.badge,
|
|
108
|
+
},
|
|
109
|
+
relativeDir: cleanDirName,
|
|
110
|
+
isGroupIndex,
|
|
111
|
+
groupMeta: isGroupIndex
|
|
112
|
+
? {
|
|
113
|
+
title:
|
|
114
|
+
data.groupTitle ||
|
|
115
|
+
data.title ||
|
|
116
|
+
(cleanDirName ? capitalize(cleanDirName) : ""),
|
|
117
|
+
position:
|
|
118
|
+
data.groupPosition ??
|
|
119
|
+
data.sidebarPosition ??
|
|
120
|
+
(rawDirName ? extractNumberPrefix(rawDirName) : undefined),
|
|
121
|
+
}
|
|
122
|
+
: undefined,
|
|
123
|
+
inferredGroupPosition: rawDirName
|
|
124
|
+
? extractNumberPrefix(rawDirName)
|
|
125
|
+
: undefined,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { RouteMeta } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sorts an array of generated routes.
|
|
5
|
+
* Ungrouped items come first. Items within the same group are sorted by position, then alphabetically.
|
|
6
|
+
* Groups are sorted relative to each other by their group position, then alphabetically.
|
|
7
|
+
*
|
|
8
|
+
* @param routes - The unsorted routes
|
|
9
|
+
* @returns A new array of sorted routes suitable for sidebar generation
|
|
10
|
+
*/
|
|
11
|
+
export function sortRoutes(routes: RouteMeta[]): RouteMeta[] {
|
|
12
|
+
return routes.sort((a, b) => {
|
|
13
|
+
// Ungrouped first
|
|
14
|
+
if (!a.group && !b.group) return compareByPosition(a, b);
|
|
15
|
+
if (!a.group) return -1;
|
|
16
|
+
if (!b.group) return 1;
|
|
17
|
+
|
|
18
|
+
// Different groups: sort by group position
|
|
19
|
+
if (a.group !== b.group) {
|
|
20
|
+
return compareByGroupPosition(a, b);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Same group: sort by item position
|
|
24
|
+
return compareByPosition(a, b);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compareByPosition(a: RouteMeta, b: RouteMeta): number {
|
|
29
|
+
if (a.sidebarPosition !== undefined && b.sidebarPosition !== undefined)
|
|
30
|
+
return a.sidebarPosition - b.sidebarPosition;
|
|
31
|
+
if (a.sidebarPosition !== undefined) return -1;
|
|
32
|
+
if (b.sidebarPosition !== undefined) return 1;
|
|
33
|
+
return a.title.localeCompare(b.title);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function compareByGroupPosition(a: RouteMeta, b: RouteMeta): number {
|
|
37
|
+
if (a.groupPosition !== undefined && b.groupPosition !== undefined)
|
|
38
|
+
return a.groupPosition - b.groupPosition;
|
|
39
|
+
if (a.groupPosition !== undefined) return -1;
|
|
40
|
+
if (b.groupPosition !== undefined) return 1;
|
|
41
|
+
return (a.groupTitle || a.group!).localeCompare(b.groupTitle || b.group!);
|
|
42
|
+
}
|