boltdocs 1.0.4 → 1.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 (41) hide show
  1. package/dist/{SearchDialog-R36WKAQ7.mjs → SearchDialog-5EDRACEG.mjs} +1 -1
  2. package/dist/{SearchDialog-PYF3QMYG.css → SearchDialog-X57WPTNN.css} +54 -126
  3. package/dist/cache-EHR7SXRU.mjs +12 -0
  4. package/dist/chunk-GSYECEZY.mjs +381 -0
  5. package/dist/{chunk-TWSRXUFF.mjs → chunk-NS7WHDYA.mjs} +229 -418
  6. package/dist/client/index.css +54 -126
  7. package/dist/client/index.d.mts +5 -4
  8. package/dist/client/index.d.ts +5 -4
  9. package/dist/client/index.js +555 -580
  10. package/dist/client/index.mjs +304 -16
  11. package/dist/client/ssr.css +54 -126
  12. package/dist/client/ssr.js +257 -580
  13. package/dist/client/ssr.mjs +1 -1
  14. package/dist/{config-D2XmHJYe.d.mts → config-BD5ZHz15.d.mts} +7 -0
  15. package/dist/{config-D2XmHJYe.d.ts → config-BD5ZHz15.d.ts} +7 -0
  16. package/dist/node/index.d.mts +2 -2
  17. package/dist/node/index.d.ts +2 -2
  18. package/dist/node/index.js +457 -118
  19. package/dist/node/index.mjs +93 -136
  20. package/package.json +2 -2
  21. package/src/client/app/index.tsx +25 -54
  22. package/src/client/theme/components/mdx/mdx-components.css +39 -20
  23. package/src/client/theme/styles/markdown.css +1 -1
  24. package/src/client/theme/styles.css +0 -1
  25. package/src/client/theme/ui/Layout/Layout.tsx +2 -13
  26. package/src/client/theme/ui/Layout/responsive.css +0 -4
  27. package/src/client/theme/ui/Link/Link.tsx +52 -0
  28. package/src/client/theme/ui/NotFound/NotFound.tsx +0 -1
  29. package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +45 -2
  30. package/src/client/theme/ui/Sidebar/Sidebar.tsx +44 -40
  31. package/src/client/theme/ui/Sidebar/sidebar.css +25 -58
  32. package/src/node/cache.ts +360 -46
  33. package/src/node/config.ts +7 -0
  34. package/src/node/mdx.ts +83 -4
  35. package/src/node/plugin/index.ts +3 -0
  36. package/src/node/routes/cache.ts +5 -1
  37. package/src/node/routes/index.ts +17 -2
  38. package/src/node/ssg/index.ts +4 -0
  39. package/dist/Playground-B2FA34BC.mjs +0 -6
  40. package/dist/chunk-WPT4MWTQ.mjs +0 -89
  41. package/src/client/theme/styles/home.css +0 -60
package/src/node/cache.ts CHANGED
@@ -1,43 +1,167 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+ import zlib from "zlib";
5
+ import { promisify } from "util";
1
6
  import { getFileMtime } from "./utils";
2
7
 
8
+ const writeFile = promisify(fs.writeFile);
9
+ const readFile = promisify(fs.readFile);
10
+ const mkdir = promisify(fs.mkdir);
11
+ const rename = promisify(fs.rename);
12
+ const unlink = promisify(fs.unlink);
13
+
14
+ /**
15
+ * Configuration constants for the caching system.
16
+ */
17
+ const CACHE_DIR = process.env.BOLTDOCS_CACHE_DIR || ".boltdocs";
18
+ const ASSETS_DIR = "assets";
19
+ const SHARDS_DIR = "shards";
20
+
21
+ /**
22
+ * Default limits for the caching system.
23
+ */
24
+ const DEFAULT_LRU_LIMIT = parseInt(
25
+ process.env.BOLTDOCS_CACHE_LRU_LIMIT || "2000",
26
+ 10,
27
+ );
28
+ const DEFAULT_COMPRESS = process.env.BOLTDOCS_CACHE_COMPRESS !== "0";
29
+
30
+ /**
31
+ * Simple LRU cache implementation to prevent memory leaks.
32
+ */
33
+ class LRUCache<K, V> {
34
+ private cache = new Map<K, V>();
35
+ constructor(private limit: number) {}
36
+
37
+ get(key: K): V | undefined {
38
+ const val = this.cache.get(key);
39
+ if (val !== undefined) {
40
+ this.cache.delete(key);
41
+ this.cache.set(key, val);
42
+ }
43
+ return val;
44
+ }
45
+
46
+ set(key: K, value: V): void {
47
+ if (this.cache.has(key)) {
48
+ this.cache.delete(key);
49
+ } else if (this.cache.size >= this.limit) {
50
+ const firstKey = this.cache.keys().next().value;
51
+ if (firstKey !== undefined) {
52
+ this.cache.delete(firstKey);
53
+ }
54
+ }
55
+ this.cache.set(key, value);
56
+ }
57
+
58
+ get size() {
59
+ return this.cache.size;
60
+ }
61
+ clear() {
62
+ this.cache.clear();
63
+ }
64
+ }
65
+
3
66
  /**
4
- * Per-file cache entry. Stores parsed data + the mtime at parse time.
67
+ * Simple background task queue to prevent blocking the main thread during IO.
5
68
  */
6
- interface FileCacheEntry<T> {
7
- data: T;
8
- mtime: number;
69
+ class BackgroundQueue {
70
+ private queue: Promise<any> = Promise.resolve();
71
+ private pendingCount = 0;
72
+
73
+ add(task: () => Promise<any>) {
74
+ this.pendingCount++;
75
+ this.queue = this.queue.then(task).finally(() => {
76
+ this.pendingCount--;
77
+ });
78
+ }
79
+
80
+ async flush() {
81
+ await this.queue;
82
+ }
83
+
84
+ get pending() {
85
+ return this.pendingCount;
86
+ }
9
87
  }
10
88
 
89
+ const backgroundQueue = new BackgroundQueue();
90
+
11
91
  /**
12
- * Generic file-based cache with per-file granularity.
13
- * Only re-parses files whose mtime has changed.
92
+ * Generic file-based cache with per-file granularity and asynchronous persistence.
14
93
  */
15
94
  export class FileCache<T> {
16
- private entries = new Map<string, FileCacheEntry<T>>();
95
+ private entries = new Map<string, { data: T; mtime: number }>();
96
+ private readonly cachePath: string | null = null;
97
+ private readonly compress: boolean;
98
+
99
+ constructor(
100
+ options: { name?: string; root?: string; compress?: boolean } = {},
101
+ ) {
102
+ this.compress =
103
+ options.compress !== undefined ? options.compress : DEFAULT_COMPRESS;
104
+ if (options.name) {
105
+ const root = options.root || process.cwd();
106
+ const ext = this.compress ? "json.gz" : "json";
107
+ this.cachePath = path.resolve(root, CACHE_DIR, `${options.name}.${ext}`);
108
+ }
109
+ }
17
110
 
18
111
  /**
19
- * Retrieves parsed data for a file from the cache.
20
- * Compares the current filesystem mtime with the cached mtime.
21
- *
22
- * @param filePath - The absolute path of the file
23
- * @returns The cached data if valid, or `null` if the file has changed or doesn't exist
112
+ * Loads the cache. Synchronous for startup simplicity but uses fast I/O.
24
113
  */
114
+ load(): void {
115
+ if (process.env.BOLTDOCS_NO_CACHE === "1") return;
116
+ if (!this.cachePath || !fs.existsSync(this.cachePath)) return;
117
+
118
+ try {
119
+ let raw = fs.readFileSync(this.cachePath);
120
+ if (this.cachePath.endsWith(".gz")) {
121
+ raw = zlib.gunzipSync(raw);
122
+ }
123
+ const data = JSON.parse(raw.toString("utf-8"));
124
+ this.entries = new Map(Object.entries(data));
125
+ } catch (e) {
126
+ // Fallback: ignore cache errors
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Saves the cache in the background.
132
+ */
133
+ save(): void {
134
+ if (process.env.BOLTDOCS_NO_CACHE === "1") return;
135
+ if (!this.cachePath) return;
136
+
137
+ const data = Object.fromEntries(this.entries);
138
+ const content = JSON.stringify(data);
139
+ const target = this.cachePath;
140
+ const useCompress = this.compress;
141
+
142
+ backgroundQueue.add(async () => {
143
+ try {
144
+ await mkdir(path.dirname(target), { recursive: true });
145
+ let buffer = Buffer.from(content);
146
+ if (useCompress) {
147
+ buffer = zlib.gzipSync(buffer);
148
+ }
149
+ const tempPath = `${target}.${crypto.randomBytes(4).toString("hex")}.tmp`;
150
+ await writeFile(tempPath, buffer);
151
+ await rename(tempPath, target);
152
+ } catch (e) {
153
+ // Fallback: critical error logging skipped for performance
154
+ }
155
+ });
156
+ }
157
+
25
158
  get(filePath: string): T | null {
26
159
  const entry = this.entries.get(filePath);
27
160
  if (!entry) return null;
28
-
29
- const currentMtime = getFileMtime(filePath);
30
- if (currentMtime !== entry.mtime) return null;
31
-
161
+ if (getFileMtime(filePath) !== entry.mtime) return null;
32
162
  return entry.data;
33
163
  }
34
164
 
35
- /**
36
- * Stores parsed data for a file in the cache, recording its current mtime.
37
- *
38
- * @param filePath - The absolute path to the file
39
- * @param data - The parsed data to store
40
- */
41
165
  set(filePath: string, data: T): void {
42
166
  this.entries.set(filePath, {
43
167
  data,
@@ -45,40 +169,20 @@ export class FileCache<T> {
45
169
  });
46
170
  }
47
171
 
48
- /**
49
- * Checks if a specific file's cache is still valid (based on its mtime).
50
- *
51
- * @param filePath - The absolute path to the file
52
- * @returns `true` if the cache is valid, `false` otherwise
53
- */
54
172
  isValid(filePath: string): boolean {
55
- return this.get(filePath) !== null;
173
+ const entry = this.entries.get(filePath);
174
+ if (!entry) return false;
175
+ return getFileMtime(filePath) === entry.mtime;
56
176
  }
57
177
 
58
- /**
59
- * Manually removes a specific file from the cache.
60
- * Useful when forcefully invalidating a single updated file.
61
- *
62
- * @param filePath - The absolute path to the file
63
- */
64
178
  invalidate(filePath: string): void {
65
179
  this.entries.delete(filePath);
66
180
  }
67
181
 
68
- /**
69
- * Clears the entire cache, forcing all files to be re-parsed on the next request.
70
- * Useful when global dependencies (like config) change.
71
- */
72
182
  invalidateAll(): void {
73
183
  this.entries.clear();
74
184
  }
75
185
 
76
- /**
77
- * Removes cached entries for files that no longer exist on the filesystem.
78
- * Prevents memory leaks from deleted files.
79
- *
80
- * @param currentFiles - A Set of absolute file paths currently discovered on the disk
81
- */
82
186
  pruneStale(currentFiles: Set<string>): void {
83
187
  for (const key of this.entries.keys()) {
84
188
  if (!currentFiles.has(key)) {
@@ -87,8 +191,218 @@ export class FileCache<T> {
87
191
  }
88
192
  }
89
193
 
90
- /** Number of cached entries */
91
194
  get size(): number {
92
195
  return this.entries.size;
93
196
  }
197
+
198
+ async flush() {
199
+ await backgroundQueue.flush();
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Sharded Cache: Optimized for large-scale data (like MDX transformations).
205
+ * Uses a memory index and individual files for each entry to avoid massive JSON parsing.
206
+ */
207
+ export class TransformCache {
208
+ private index = new Map<string, string>(); // key -> hash
209
+ private memoryCache = new LRUCache<string, string>(DEFAULT_LRU_LIMIT);
210
+ private readonly baseDir: string;
211
+ private readonly shardsDir: string;
212
+ private readonly indexPath: string;
213
+
214
+ constructor(name: string, root: string = process.cwd()) {
215
+ this.baseDir = path.resolve(root, CACHE_DIR, `transform-${name}`);
216
+ this.shardsDir = path.resolve(this.baseDir, SHARDS_DIR);
217
+ this.indexPath = path.resolve(this.baseDir, "index.json");
218
+ }
219
+
220
+ /**
221
+ * Loads the index into memory.
222
+ */
223
+ load(): void {
224
+ if (process.env.BOLTDOCS_NO_CACHE === "1") return;
225
+ if (!fs.existsSync(this.indexPath)) return;
226
+
227
+ try {
228
+ const data = fs.readFileSync(this.indexPath, "utf-8");
229
+ this.index = new Map(Object.entries(JSON.parse(data)));
230
+ } catch (e) {
231
+ // Index might be corrupt, ignore
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Persists the index in background.
237
+ */
238
+ save(): void {
239
+ if (process.env.BOLTDOCS_NO_CACHE === "1") return;
240
+ const data = JSON.stringify(Object.fromEntries(this.index));
241
+ const target = this.indexPath;
242
+
243
+ backgroundQueue.add(async () => {
244
+ await mkdir(path.dirname(target), { recursive: true });
245
+ await writeFile(target, data);
246
+ });
247
+ }
248
+
249
+ /**
250
+ * Batch Read: Retrieves multiple transformation results concurrently.
251
+ */
252
+ async getMany(keys: string[]): Promise<Map<string, string>> {
253
+ const results = new Map<string, string>();
254
+ const toLoad: string[] = [];
255
+
256
+ for (const key of keys) {
257
+ const mem = this.memoryCache.get(key);
258
+ if (mem) results.set(key, mem);
259
+ else if (this.index.has(key)) toLoad.push(key);
260
+ }
261
+
262
+ if (toLoad.length > 0) {
263
+ const shards = await Promise.all(
264
+ toLoad.map(async (key) => {
265
+ const hash = this.index.get(key)!;
266
+ const shardPath = path.resolve(this.shardsDir, `${hash}.gz`);
267
+ try {
268
+ const compressed = await readFile(shardPath);
269
+ const decompressed = zlib.gunzipSync(compressed).toString("utf-8");
270
+ this.memoryCache.set(key, decompressed);
271
+ return { key, val: decompressed };
272
+ } catch (e) {
273
+ return null;
274
+ }
275
+ }),
276
+ );
277
+
278
+ for (const s of shards) {
279
+ if (s) results.set(s.key, s.val);
280
+ }
281
+ }
282
+
283
+ return results;
284
+ }
285
+
286
+ /**
287
+ * Retrieves a cached transformation. Fast lookup via index, lazy loading from disk.
288
+ */
289
+ get(key: string): string | null {
290
+ // 1. Check memory first (LRU)
291
+ const mem = this.memoryCache.get(key);
292
+ if (mem) return mem;
293
+
294
+ // 2. Check index
295
+ const hash = this.index.get(key);
296
+ if (!hash) return null;
297
+
298
+ // 3. Load from shard (synchronous read for Vite's transform hook compatibility)
299
+ const shardPath = path.resolve(this.shardsDir, `${hash}.gz`);
300
+ if (!fs.existsSync(shardPath)) return null;
301
+
302
+ try {
303
+ const compressed = fs.readFileSync(shardPath);
304
+ const decompressed = zlib.gunzipSync(compressed).toString("utf-8");
305
+ this.memoryCache.set(key, decompressed);
306
+ return decompressed;
307
+ } catch (e) {
308
+ return null;
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Stores a transformation result.
314
+ */
315
+ set(key: string, result: string): void {
316
+ const hash = crypto.createHash("md5").update(result).digest("hex");
317
+ this.index.set(key, hash);
318
+ this.memoryCache.set(key, result);
319
+
320
+ const shardPath = path.resolve(this.shardsDir, `${hash}.gz`);
321
+
322
+ // Background write shard
323
+ backgroundQueue.add(async () => {
324
+ if (fs.existsSync(shardPath)) return; // Already exists
325
+ await mkdir(this.shardsDir, { recursive: true });
326
+
327
+ const compressed = zlib.gzipSync(Buffer.from(result));
328
+ const tempPath = `${shardPath}.${crypto.randomBytes(4).toString("hex")}.tmp`;
329
+ await writeFile(tempPath, compressed);
330
+ await rename(tempPath, shardPath);
331
+ });
332
+ }
333
+
334
+ get size() {
335
+ return this.index.size;
336
+ }
337
+
338
+ async flush() {
339
+ await backgroundQueue.flush();
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Specialized cache for processed assets (e.g., optimized images).
345
+ */
346
+ export class AssetCache {
347
+ private readonly assetsDir: string;
348
+
349
+ constructor(root: string = process.cwd()) {
350
+ this.assetsDir = path.resolve(root, CACHE_DIR, ASSETS_DIR);
351
+ }
352
+
353
+ private getFileHash(filePath: string): string {
354
+ return crypto
355
+ .createHash("md5")
356
+ .update(fs.readFileSync(filePath))
357
+ .digest("hex");
358
+ }
359
+
360
+ get(sourcePath: string, cacheKey: string): string | null {
361
+ if (!fs.existsSync(sourcePath)) return null;
362
+ const sourceHash = this.getFileHash(sourcePath);
363
+ const cachedPath = this.getCachedPath(
364
+ sourcePath,
365
+ `${cacheKey}-${sourceHash}`,
366
+ );
367
+ return fs.existsSync(cachedPath) ? cachedPath : null;
368
+ }
369
+
370
+ set(sourcePath: string, cacheKey: string, content: Buffer | string): void {
371
+ const sourceHash = this.getFileHash(sourcePath);
372
+ const cachedPath = this.getCachedPath(
373
+ sourcePath,
374
+ `${cacheKey}-${sourceHash}`,
375
+ );
376
+
377
+ backgroundQueue.add(async () => {
378
+ await mkdir(this.assetsDir, { recursive: true });
379
+ const tempPath = `${cachedPath}.${crypto.randomBytes(4).toString("hex")}.tmp`;
380
+ await writeFile(tempPath, content);
381
+ await rename(tempPath, cachedPath);
382
+ });
383
+ }
384
+
385
+ private getCachedPath(sourcePath: string, cacheKey: string): string {
386
+ const ext = path.extname(sourcePath);
387
+ const name = path.basename(sourcePath, ext);
388
+ const safeKey = cacheKey.replace(/[^a-z0-9]/gi, "-").toLowerCase();
389
+ return path.join(this.assetsDir, `${name}.${safeKey}${ext}`);
390
+ }
391
+
392
+ clear(): void {
393
+ if (fs.existsSync(this.assetsDir)) {
394
+ fs.rmSync(this.assetsDir, { recursive: true, force: true });
395
+ }
396
+ }
397
+
398
+ async flush() {
399
+ await backgroundQueue.flush();
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Flushes all pending background cache operations.
405
+ */
406
+ export async function flushCache() {
407
+ await backgroundQueue.flush();
94
408
  }
@@ -64,6 +64,13 @@ export interface BoltdocsThemeConfig {
64
64
  className?: string;
65
65
  style?: any;
66
66
  };
67
+ /**
68
+ * The syntax highlighting theme for code blocks.
69
+ * Supports any Shiki theme name (e.g., 'github-dark', 'one-dark-pro', 'aurora-x').
70
+ * Can also be an object for multiple themes (e.g., { light: 'github-light', dark: 'github-dark' }).
71
+ * Default: 'one-dark-pro'
72
+ */
73
+ codeTheme?: string | Record<string, string>;
67
74
  }
68
75
 
69
76
  /**
package/src/node/mdx.ts CHANGED
@@ -4,16 +4,27 @@ import remarkFrontmatter from "remark-frontmatter";
4
4
  import rehypeSlug from "rehype-slug";
5
5
  import rehypePrettyCode from "rehype-pretty-code";
6
6
  import type { Plugin } from "vite";
7
+ import crypto from "crypto";
7
8
 
8
9
  import type { BoltdocsConfig } from "./config";
10
+ import { TransformCache } from "./cache";
11
+
12
+ /**
13
+ * Persistent cache for MDX transformations.
14
+ * Saves results to `.boltdocs/transform-mdx.json.gz`.
15
+ */
16
+ const mdxCache = new TransformCache("mdx");
17
+ let mdxCacheLoaded = false;
9
18
 
10
19
  /**
11
20
  * Configures the MDX compiler for Vite using `@mdx-js/rollup`.
12
21
  * Includes standard remark and rehype plugins for GitHub Flavored Markdown (GFM),
13
22
  * frontmatter extraction, auto-linking headers, and syntax highlighting via `rehype-pretty-code`.
14
23
  *
24
+ * Also wraps the plugin with a persistent cache to avoid re-compiling unchanged MDX files.
25
+ *
15
26
  * @param config - The Boltdocs configuration containing custom plugins
16
- * @returns A Vite plugin configured for MDX parsing
27
+ * @returns A Vite plugin configured for MDX parsing with caching
17
28
  */
18
29
  export function boltdocsMdxPlugin(config?: BoltdocsConfig): Plugin {
19
30
  const extraRemarkPlugins =
@@ -21,7 +32,7 @@ export function boltdocsMdxPlugin(config?: BoltdocsConfig): Plugin {
21
32
  const extraRehypePlugins =
22
33
  config?.plugins?.flatMap((p) => p.rehypePlugins || []) || [];
23
34
 
24
- return mdxPlugin({
35
+ const baseMdxPlugin = mdxPlugin({
25
36
  remarkPlugins: [remarkGfm, remarkFrontmatter, ...extraRemarkPlugins],
26
37
  rehypePlugins: [
27
38
  rehypeSlug,
@@ -29,13 +40,81 @@ export function boltdocsMdxPlugin(config?: BoltdocsConfig): Plugin {
29
40
  [
30
41
  rehypePrettyCode,
31
42
  {
32
- theme: "one-dark-pro",
43
+ theme: config?.themeConfig?.codeTheme || "one-dark-pro",
33
44
  keepBackground: false,
34
45
  },
35
46
  ],
36
47
  ],
37
- // Provide React as default for JSX
38
48
  jsxRuntime: "automatic",
39
49
  providerImportSource: "@mdx-js/react",
40
50
  }) as Plugin;
51
+
52
+ return {
53
+ ...baseMdxPlugin,
54
+ name: "vite-plugin-boltdocs-mdx",
55
+
56
+ async buildStart() {
57
+ hits = 0;
58
+ total = 0;
59
+ if (!mdxCacheLoaded) {
60
+ mdxCache.load();
61
+ mdxCacheLoaded = true;
62
+ }
63
+ if (baseMdxPlugin.buildStart) {
64
+ await (baseMdxPlugin.buildStart as any).call(this);
65
+ }
66
+ },
67
+
68
+ async transform(code, id, options) {
69
+ if (!id.endsWith(".md") && !id.endsWith(".mdx")) {
70
+ return (baseMdxPlugin.transform as any)?.call(this, code, id, options);
71
+ }
72
+
73
+ total++;
74
+ // Create a cache key based on path, content, and config (simplified)
75
+ const contentHash = crypto.createHash("md5").update(code).digest("hex");
76
+ const cacheKey = `${id}:${contentHash}`;
77
+
78
+ const cached = mdxCache.get(cacheKey);
79
+ if (cached) {
80
+ hits++;
81
+ return { code: cached, map: null };
82
+ }
83
+
84
+ const result = await (baseMdxPlugin.transform as any).call(
85
+ this,
86
+ code,
87
+ id,
88
+ options,
89
+ );
90
+
91
+ if (result && typeof result === "object" && result.code) {
92
+ mdxCache.set(cacheKey, result.code);
93
+ } else if (typeof result === "string") {
94
+ mdxCache.set(cacheKey, result);
95
+ }
96
+
97
+ return result;
98
+ },
99
+
100
+ async buildEnd() {
101
+ mdxCache.save();
102
+ await mdxCache.flush(); // Use instance flush or global flushCache
103
+ if (baseMdxPlugin.buildEnd) {
104
+ await (baseMdxPlugin.buildEnd as any).call(this);
105
+ }
106
+ },
107
+ };
41
108
  }
109
+
110
+ /**
111
+ * Returns the current MDX cache statistics.
112
+ * @returns An object with total and hit counts
113
+ * @deprecated Removed for performance
114
+ */
115
+ export function getMdxCacheStats() {
116
+ return { hits: 0, total: 0 };
117
+ }
118
+
119
+ let hits = 0;
120
+ let total = 0;
@@ -166,6 +166,9 @@ export function boltdocsPlugin(
166
166
  : path.resolve(process.cwd(), "dist");
167
167
 
168
168
  await generateStaticPages({ docsDir, outDir, config });
169
+
170
+ const { flushCache } = await import("../cache");
171
+ await flushCache();
169
172
  },
170
173
  },
171
174
  ViteImageOptimizer({
@@ -1,7 +1,11 @@
1
1
  import { FileCache } from "../cache";
2
2
  import { ParsedDocFile } from "./types";
3
3
 
4
- const docCache = new FileCache<ParsedDocFile>();
4
+ /**
5
+ * Persistent cache for parsed documentation files.
6
+ * Saves data to `.boltdocs/routes.json`.
7
+ */
8
+ const docCache = new FileCache<ParsedDocFile>({ name: "routes" });
5
9
 
6
10
  /**
7
11
  * Invalidate all cached routes.
@@ -27,6 +27,9 @@ export async function generateRoutes(
27
27
  config?: BoltdocsConfig,
28
28
  basePath: string = "/docs",
29
29
  ): Promise<RouteMeta[]> {
30
+ // Load persistent cache on first call
31
+ docCache.load();
32
+
30
33
  const files = await fastGlob(["**/*.md", "**/*.mdx"], {
31
34
  cwd: docsDir,
32
35
  absolute: true,
@@ -41,10 +44,14 @@ export async function generateRoutes(
41
44
  }
42
45
 
43
46
  // Parse files in parallel using Promise.all for increased efficiency
47
+ let cacheHits = 0;
44
48
  const parsed: ParsedDocFile[] = await Promise.all(
45
49
  files.map(async (file) => {
46
50
  const cached = docCache.get(file);
47
- if (cached) return cached;
51
+ if (cached) {
52
+ cacheHits++;
53
+ return cached;
54
+ }
48
55
 
49
56
  const result = parseDocFile(file, docsDir, basePath, config);
50
57
  docCache.set(file, result);
@@ -52,6 +59,15 @@ export async function generateRoutes(
52
59
  }),
53
60
  );
54
61
 
62
+ if (files.length > 0) {
63
+ console.log(
64
+ `[boltdocs] Routes generated: ${files.length} files (${cacheHits} from cache, ${files.length - cacheHits} parsed)`,
65
+ );
66
+ }
67
+
68
+ // Save cache after batch processing
69
+ docCache.save();
70
+
55
71
  // Collect group metadata from directory names and index files
56
72
  const groupMeta = new Map<string, { title: string; position?: number }>();
57
73
  for (const p of parsed) {
@@ -126,7 +142,6 @@ export async function generateRoutes(
126
142
  } else if (pathAfterVersion === "/" + defaultLocale) {
127
143
  pathAfterVersion = "/";
128
144
  }
129
-
130
145
  const targetPath =
131
146
  prefix +
132
147
  "/" +
@@ -107,4 +107,8 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
107
107
  console.log(
108
108
  `[boltdocs] Generated ${routes.length} static pages + sitemap.xml`,
109
109
  );
110
+
111
+ // Ensure all cache operations (like index persistence) are finished
112
+ const { flushCache } = await import("../cache");
113
+ await flushCache();
110
114
  }
@@ -1,6 +0,0 @@
1
- import {
2
- Playground
3
- } from "./chunk-WPT4MWTQ.mjs";
4
- export {
5
- Playground
6
- };