@mui/internal-docs-infra 0.12.1-canary.6 → 0.12.1-canary.7

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 (57) hide show
  1. package/cli/loadNextConfig.d.mts +9 -0
  2. package/cli/loadNextConfig.mjs +11 -2
  3. package/cli/runValidate.mjs +15 -7
  4. package/cli/validateWorker.d.mts +1 -0
  5. package/cli/validateWorker.mjs +2 -1
  6. package/package.json +2 -2
  7. package/pipeline/cacheUtils/constants.d.mts +5 -0
  8. package/pipeline/cacheUtils/constants.mjs +5 -0
  9. package/pipeline/cacheUtils/hashCacheContent.d.mts +5 -0
  10. package/pipeline/cacheUtils/hashCacheContent.mjs +9 -0
  11. package/pipeline/cacheUtils/index.d.mts +9 -0
  12. package/pipeline/cacheUtils/index.mjs +7 -0
  13. package/pipeline/cacheUtils/loadFileCache.d.mts +13 -0
  14. package/pipeline/cacheUtils/loadFileCache.mjs +20 -0
  15. package/pipeline/cacheUtils/loadFileCacheEntry.d.mts +13 -0
  16. package/pipeline/cacheUtils/loadFileCacheEntry.mjs +47 -0
  17. package/pipeline/cacheUtils/resolveCachePath.d.mts +16 -0
  18. package/pipeline/cacheUtils/resolveCachePath.mjs +24 -0
  19. package/pipeline/cacheUtils/saveFileCache.d.mts +17 -0
  20. package/pipeline/cacheUtils/saveFileCache.mjs +54 -0
  21. package/pipeline/cacheUtils/types.d.mts +23 -0
  22. package/pipeline/cacheUtils/types.mjs +1 -0
  23. package/pipeline/cacheUtils/withFileCache.d.mts +40 -0
  24. package/pipeline/cacheUtils/withFileCache.mjs +81 -0
  25. package/pipeline/loadPrecomputedSitemap/loadPrecomputedSitemap.d.mts +6 -0
  26. package/pipeline/loadPrecomputedSitemap/loadPrecomputedSitemap.mjs +2 -1
  27. package/pipeline/loadPrecomputedTypes/loadPrecomputedTypes.d.mts +6 -0
  28. package/pipeline/loadPrecomputedTypes/loadPrecomputedTypes.mjs +5 -1
  29. package/pipeline/loadServerPageIndex/enrichPageIndex.d.mts +15 -0
  30. package/pipeline/loadServerPageIndex/enrichPageIndex.mjs +50 -0
  31. package/pipeline/loadServerPageIndex/extractPrefixAndTitle.d.mts +19 -0
  32. package/pipeline/loadServerPageIndex/extractPrefixAndTitle.mjs +80 -0
  33. package/pipeline/loadServerPageIndex/index.d.mts +5 -2
  34. package/pipeline/loadServerPageIndex/index.mjs +4 -1
  35. package/pipeline/loadServerPageIndex/loadServerPageIndex.d.mts +13 -22
  36. package/pipeline/loadServerPageIndex/loadServerPageIndex.mjs +32 -124
  37. package/pipeline/loadServerPageIndex/resolvePageIndexCacheKey.d.mts +13 -0
  38. package/pipeline/loadServerPageIndex/resolvePageIndexCacheKey.mjs +23 -0
  39. package/pipeline/loadServerTypes/loadServerTypes.mjs +67 -7
  40. package/pipeline/loadServerTypesText/loadServerTypesText.d.mts +15 -1
  41. package/pipeline/loadServerTypesText/loadServerTypesText.mjs +30 -8
  42. package/pipeline/loadServerTypesText/normalizeTypesSourceDataForCache.d.mts +2 -0
  43. package/pipeline/loadServerTypesText/normalizeTypesSourceDataForCache.mjs +144 -0
  44. package/pipeline/loadServerTypesText/resolveTypesCacheKey.d.mts +19 -0
  45. package/pipeline/loadServerTypesText/resolveTypesCacheKey.mjs +29 -0
  46. package/pipeline/syncPageIndex/mergeMetadataMarkdown.d.mts +24 -0
  47. package/pipeline/syncPageIndex/mergeMetadataMarkdown.mjs +79 -49
  48. package/pipeline/syncPageIndex/metadataToMarkdown.d.mts +14 -0
  49. package/pipeline/syncPageIndex/metadataToMarkdown.mjs +32 -10
  50. package/pipeline/syncPageIndex/syncPageIndex.d.mts +12 -0
  51. package/pipeline/syncPageIndex/syncPageIndex.mjs +72 -12
  52. package/pipeline/syncTypes/syncTypes.d.mts +7 -0
  53. package/pipeline/syncTypes/syncTypes.mjs +31 -1
  54. package/pipeline/transformMarkdownMetadata/transformMarkdownMetadata.mjs +3 -0
  55. package/pipeline/transformMarkdownMetadata/types.d.mts +7 -0
  56. package/withDocsInfra/withDocsInfra.d.mts +14 -0
  57. package/withDocsInfra/withDocsInfra.mjs +25 -11
@@ -23,11 +23,20 @@ export type ExtractedNextConfigOptions = {
23
23
  descriptionReplacements?: DescriptionReplacement[];
24
24
  useVisibleDescription?: boolean;
25
25
  socketDir?: string;
26
+ /** Page-index cache directory configured on the sitemap loader. */
27
+ cacheDir?: string;
26
28
  /** Demo index patterns that opted into automatic `client.ts` generation. */
27
29
  demoClientRequirements?: DemoClientRequirement[];
28
30
  /** Demo index patterns that opted into automatic `page.tsx` generation. */
29
31
  demoPageRequirements?: DemoPageRequirement[];
30
32
  };
33
+ /**
34
+ * Searches turbopack rules for docs-infra options (ordering,
35
+ * descriptionReplacements, socketDir, useVisibleDescription, cacheDir).
36
+ *
37
+ * Exported for tests.
38
+ */
39
+ export declare function extractOptionsFromTurbopack(config: any): ExtractedNextConfigOptions;
31
40
  /**
32
41
  * Walks Turbopack rules to collect demo patterns that opted into automatic
33
42
  * `page.tsx` generation via the `requirePage` option.
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { createJiti } from 'jiti';
5
5
  const TYPES_LOADER = '@mui/internal-docs-infra/pipeline/loadPrecomputedTypes';
6
+ const SITEMAP_LOADER = '@mui/internal-docs-infra/pipeline/loadPrecomputedSitemap';
6
7
  const CODE_HIGHLIGHTER_LOADER = '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter';
7
8
  const TRANSFORM_METADATA_PLUGIN = '@mui/internal-docs-infra/pipeline/transformMarkdownMetadata';
8
9
  const TRANSFORM_METADATA_PLUGIN_FUNCTION_NAME = 'transformMarkdownMetadata';
@@ -49,6 +50,9 @@ function extractOptionsFromLoaderEntries(loaders) {
49
50
  if (!result.socketDir && loader.loader === TYPES_LOADER && typeof loader.options?.socketDir === 'string') {
50
51
  result.socketDir = loader.options.socketDir;
51
52
  }
53
+ if (!result.cacheDir && loader.loader === SITEMAP_LOADER && typeof loader.options?.cacheDir === 'string') {
54
+ result.cacheDir = loader.options.cacheDir;
55
+ }
52
56
  if (result.useVisibleDescription === undefined && loader.options?.remarkPlugins) {
53
57
  const extracted = extractUseVisibleDescriptionFromRemarkPlugins(loader.options.remarkPlugins);
54
58
  if (typeof extracted === 'boolean') {
@@ -61,9 +65,11 @@ function extractOptionsFromLoaderEntries(loaders) {
61
65
 
62
66
  /**
63
67
  * Searches turbopack rules for docs-infra options (ordering,
64
- * descriptionReplacements, socketDir, useVisibleDescription).
68
+ * descriptionReplacements, socketDir, useVisibleDescription, cacheDir).
69
+ *
70
+ * Exported for tests.
65
71
  */
66
- function extractOptionsFromTurbopack(config) {
72
+ export function extractOptionsFromTurbopack(config) {
67
73
  const rules = config?.turbopack?.rules;
68
74
  if (!rules) {
69
75
  return {};
@@ -79,6 +85,7 @@ function extractOptionsFromTurbopack(config) {
79
85
  merged.descriptionReplacements ??= extracted.descriptionReplacements;
80
86
  merged.useVisibleDescription ??= extracted.useVisibleDescription;
81
87
  merged.socketDir ??= extracted.socketDir;
88
+ merged.cacheDir ??= extracted.cacheDir;
82
89
  }
83
90
  return merged;
84
91
  }
@@ -166,6 +173,7 @@ function extractOptionsFromWebpackResult(result) {
166
173
  merged.descriptionReplacements ??= extracted.descriptionReplacements;
167
174
  merged.useVisibleDescription ??= extracted.useVisibleDescription;
168
175
  merged.socketDir ??= extracted.socketDir;
176
+ merged.cacheDir ??= extracted.cacheDir;
169
177
  }
170
178
  return merged;
171
179
  }
@@ -323,6 +331,7 @@ export async function extractDocsInfraOptionsFromNextConfig(dir) {
323
331
  descriptionReplacements: turbopack.descriptionReplacements ?? webpack.descriptionReplacements,
324
332
  useVisibleDescription: turbopack.useVisibleDescription ?? webpack.useVisibleDescription,
325
333
  socketDir: turbopack.socketDir ?? webpack.socketDir,
334
+ cacheDir: turbopack.cacheDir ?? webpack.cacheDir,
326
335
  demoClientRequirements: demoClientRequirements.length > 0 ? demoClientRequirements : undefined,
327
336
  demoPageRequirements: demoPageRequirements.length > 0 ? demoPageRequirements : undefined
328
337
  };
@@ -10,6 +10,7 @@ import { fileURLToPath } from 'url';
10
10
  import { Worker } from 'node:worker_threads';
11
11
  import { createPerformanceLogger, logPerformance, nameMark, performanceMeasure } from "../pipeline/loadPrecomputedCodeHighlighter/performanceLogger.mjs";
12
12
  import { terminateWorkerManager } from "../pipeline/loadServerTypesMeta/workerManager.mjs";
13
+ import { DEFAULT_CACHE_DIR } from "../pipeline/cacheUtils/index.mjs";
13
14
  import { extractDocsInfraOptionsFromNextConfig } from "./loadNextConfig.mjs";
14
15
  import { ensureDemoClients } from "./ensureDemoClients.mjs";
15
16
  import { ensureDemoPages } from "./ensureDemoPages.mjs";
@@ -89,10 +90,14 @@ const runValidate = {
89
90
  descriptionReplacements,
90
91
  useVisibleDescription = false,
91
92
  socketDir: configSocketDir,
93
+ cacheDir: configCacheDir,
92
94
  demoClientRequirements = [],
93
95
  demoPageRequirements = []
94
96
  } = await extractDocsInfraOptionsFromNextConfig(cwd);
95
97
  const socketDir = configSocketDir ? path.resolve(cwd, configSocketDir) : undefined;
98
+ // Use the same page-index cache directory that the sitemap loader reads at build time.
99
+ // The index validation pass writes cache entries even when committed indexes are current.
100
+ const cacheDir = configCacheDir ?? DEFAULT_CACHE_DIR;
96
101
 
97
102
  // If neither flag is set, run both. If one is set, run only that one.
98
103
  const runIndexes = !typesOnly || indexesOnly;
@@ -189,8 +194,8 @@ const runValidate = {
189
194
  try {
190
195
  // === Validate page.mdx index files ===
191
196
  if (runIndexes) {
192
- const markerDir = '.next/cache/docs-infra/index-updates';
193
- const markerDirPath = path.join(cwd, markerDir);
197
+ const markerDir = path.posix.join(cacheDir, 'index-updates');
198
+ const markerDirPath = path.resolve(cwd, markerDir);
194
199
  try {
195
200
  await rm(markerDirPath, {
196
201
  recursive: true,
@@ -222,7 +227,8 @@ const runValidate = {
222
227
  baseDir: cwd,
223
228
  onlyUpdateIndexes: true,
224
229
  markerDir,
225
- useVisibleDescription
230
+ useVisibleDescription,
231
+ cacheDir
226
232
  };
227
233
  const indexResults = await Promise.all(pageMdxFiles.map(filePath => {
228
234
  const taskId = taskIdCounter;
@@ -263,8 +269,8 @@ const runValidate = {
263
269
  // === Validate types.ts files ===
264
270
  if (runTypes) {
265
271
  // Use same marker directory structure for index updates from types
266
- const typesMarkerDir = '.next/cache/docs-infra/types-index-updates';
267
- const typesMarkerDirPath = path.join(cwd, typesMarkerDir);
272
+ const typesMarkerDir = path.posix.join(cacheDir, 'types-index-updates');
273
+ const typesMarkerDirPath = path.resolve(cwd, typesMarkerDir);
268
274
  try {
269
275
  await rm(typesMarkerDirPath, {
270
276
  recursive: true,
@@ -289,11 +295,13 @@ const runValidate = {
289
295
  syncTypesOptions: {
290
296
  updateParentIndex: {
291
297
  baseDir: cwd,
292
- markerDir: typesMarkerDir
298
+ markerDir: typesMarkerDir,
299
+ cacheDir
293
300
  },
294
301
  ordering,
295
302
  descriptionReplacements,
296
- socketDir
303
+ socketDir,
304
+ cacheDir
297
305
  }
298
306
  });
299
307
  }));
@@ -11,6 +11,7 @@ interface IndexTask {
11
11
  onlyUpdateIndexes: boolean;
12
12
  markerDir: string;
13
13
  useVisibleDescription: boolean;
14
+ cacheDir?: string;
14
15
  };
15
16
  }
16
17
  interface TypesTask {
@@ -74,7 +74,8 @@ if (parentPort) {
74
74
  updateParentIndex: excludeFromIndex ? undefined : task.syncTypesOptions.updateParentIndex,
75
75
  ordering: task.syncTypesOptions.ordering,
76
76
  descriptionReplacements: task.syncTypesOptions.descriptionReplacements,
77
- socketDir: task.syncTypesOptions.socketDir
77
+ socketDir: task.syncTypesOptions.socketDir,
78
+ cacheDir: task.syncTypesOptions.cacheDir
78
79
  });
79
80
  parentPort.postMessage({
80
81
  type: 'types',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-docs-infra",
3
- "version": "0.12.1-canary.6",
3
+ "version": "0.12.1-canary.7",
4
4
  "author": "MUI Team",
5
5
  "description": "MUI Infra - internal documentation creation tools.",
6
6
  "license": "MIT",
@@ -804,5 +804,5 @@
804
804
  "bin": {
805
805
  "docs-infra": "./cli/index.mjs"
806
806
  },
807
- "gitSha": "8d7deee2ae1eb5157dadfae77c6e82cc8e574687"
807
+ "gitSha": "0ffea436eb8e1aa519727d55d2e9a4daa0ec6ac2"
808
808
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Default root directory for docs-infra build caches. Sits alongside the marker
3
+ * directories already written under `.next/cache/docs-infra`.
4
+ */
5
+ export declare const DEFAULT_CACHE_DIR = ".next/cache/docs-infra";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Default root directory for docs-infra build caches. Sits alongside the marker
3
+ * directories already written under `.next/cache/docs-infra`.
4
+ */
5
+ export const DEFAULT_CACHE_DIR = '.next/cache/docs-infra';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Computes the sha256 hex digest of cache source content. Used to validate that a
3
+ * cached value still matches the file it was derived from.
4
+ */
5
+ export declare function hashCacheContent(content: string): string;
@@ -0,0 +1,9 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ /**
4
+ * Computes the sha256 hex digest of cache source content. Used to validate that a
5
+ * cached value still matches the file it was derived from.
6
+ */
7
+ export function hashCacheContent(content) {
8
+ return createHash('sha256').update(content).digest('hex');
9
+ }
@@ -0,0 +1,9 @@
1
+ export { hashCacheContent } from "./hashCacheContent.mjs";
2
+ export { loadFileCache } from "./loadFileCache.mjs";
3
+ export { loadFileCacheEntry } from "./loadFileCacheEntry.mjs";
4
+ export { saveFileCache } from "./saveFileCache.mjs";
5
+ export { resolveCachePath } from "./resolveCachePath.mjs";
6
+ export { withFileCache } from "./withFileCache.mjs";
7
+ export type { FileCacheTask } from "./withFileCache.mjs";
8
+ export { DEFAULT_CACHE_DIR } from "./constants.mjs";
9
+ export type { FileCacheRef, FileCacheEntry } from "./types.mjs";
@@ -0,0 +1,7 @@
1
+ export { hashCacheContent } from "./hashCacheContent.mjs";
2
+ export { loadFileCache } from "./loadFileCache.mjs";
3
+ export { loadFileCacheEntry } from "./loadFileCacheEntry.mjs";
4
+ export { saveFileCache } from "./saveFileCache.mjs";
5
+ export { resolveCachePath } from "./resolveCachePath.mjs";
6
+ export { withFileCache } from "./withFileCache.mjs";
7
+ export { DEFAULT_CACHE_DIR } from "./constants.mjs";
@@ -0,0 +1,13 @@
1
+ import type { FileCacheRef } from "./types.mjs";
2
+ /**
3
+ * Reads a cache entry and returns its data only when the stored hash matches the
4
+ * current content.
5
+ *
6
+ * Best-effort by design: a missing file, an unreadable file, malformed JSON, or a
7
+ * hash mismatch all resolve to `null` (a cache miss) rather than throwing, so a
8
+ * stale or corrupt cache can never break a build — it is simply recomputed.
9
+ *
10
+ * To overlap the cache read with reading the source file, use {@link loadFileCacheEntry}
11
+ * directly and compare the hash yourself.
12
+ */
13
+ export declare function loadFileCache<T>(ref: FileCacheRef, content: string): Promise<T | null>;
@@ -0,0 +1,20 @@
1
+ import { hashCacheContent } from "./hashCacheContent.mjs";
2
+ import { loadFileCacheEntry } from "./loadFileCacheEntry.mjs";
3
+ /**
4
+ * Reads a cache entry and returns its data only when the stored hash matches the
5
+ * current content.
6
+ *
7
+ * Best-effort by design: a missing file, an unreadable file, malformed JSON, or a
8
+ * hash mismatch all resolve to `null` (a cache miss) rather than throwing, so a
9
+ * stale or corrupt cache can never break a build — it is simply recomputed.
10
+ *
11
+ * To overlap the cache read with reading the source file, use {@link loadFileCacheEntry}
12
+ * directly and compare the hash yourself.
13
+ */
14
+ export async function loadFileCache(ref, content) {
15
+ const entry = await loadFileCacheEntry(ref);
16
+ if (!entry || entry.hash !== hashCacheContent(content)) {
17
+ return null;
18
+ }
19
+ return entry.data;
20
+ }
@@ -0,0 +1,13 @@
1
+ import type { FileCacheEntry, FileCacheRef } from "./types.mjs";
2
+ /**
3
+ * Reads and parses a cache entry without validating it against any content.
4
+ *
5
+ * Best-effort: a missing file, an unreadable file, malformed JSON, or a path-containment failure
6
+ * all resolve to `null` (a cache miss) rather than throwing — so a stale or corrupt cache never
7
+ * breaks a build. Use this to read the cache in parallel with the source file, then compare
8
+ * `entry.hash` against `hashCacheContent(content)` once both reads complete.
9
+ *
10
+ * A non-missing read failure (e.g. a permissions error silently disabling the cache) is logged
11
+ * under DEBUG rather than masquerading as a cold cache.
12
+ */
13
+ export declare function loadFileCacheEntry<T>(ref: FileCacheRef): Promise<FileCacheEntry<T> | null>;
@@ -0,0 +1,47 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolveCachePath } from "./resolveCachePath.mjs";
3
+ // Set DOCS_INFRA_CACHE_DEBUG=1 to surface non-missing read failures and corrupt entries.
4
+ const DEBUG = Boolean(process.env.DOCS_INFRA_CACHE_DEBUG);
5
+ function isFileCacheEntry(value) {
6
+ return typeof value === 'object' && value !== null && 'hash' in value && 'data' in value && typeof value.hash === 'string';
7
+ }
8
+
9
+ /**
10
+ * Reads and parses a cache entry without validating it against any content.
11
+ *
12
+ * Best-effort: a missing file, an unreadable file, malformed JSON, or a path-containment failure
13
+ * all resolve to `null` (a cache miss) rather than throwing — so a stale or corrupt cache never
14
+ * breaks a build. Use this to read the cache in parallel with the source file, then compare
15
+ * `entry.hash` against `hashCacheContent(content)` once both reads complete.
16
+ *
17
+ * A non-missing read failure (e.g. a permissions error silently disabling the cache) is logged
18
+ * under DEBUG rather than masquerading as a cold cache.
19
+ */
20
+ export async function loadFileCacheEntry(ref) {
21
+ let raw;
22
+ try {
23
+ raw = await readFile(resolveCachePath(ref), 'utf-8');
24
+ } catch (error) {
25
+ const isMissing = error instanceof Error && error.code === 'ENOENT';
26
+ if (DEBUG && !isMissing) {
27
+ console.warn(`[docs-infra cache] read failed for ${ref.namespace}/${ref.cacheKey}:`, error);
28
+ }
29
+ return null;
30
+ }
31
+ let parsed;
32
+ try {
33
+ parsed = JSON.parse(raw);
34
+ } catch {
35
+ if (DEBUG) {
36
+ console.warn(`[docs-infra cache] ignoring corrupt entry ${ref.namespace}/${ref.cacheKey}`);
37
+ }
38
+ return null;
39
+ }
40
+ if (!isFileCacheEntry(parsed)) {
41
+ if (DEBUG) {
42
+ console.warn(`[docs-infra cache] ignoring malformed entry ${ref.namespace}/${ref.cacheKey}`);
43
+ }
44
+ return null;
45
+ }
46
+ return parsed;
47
+ }
@@ -0,0 +1,16 @@
1
+ import type { FileCacheRef } from "./types.mjs";
2
+ /**
3
+ * Resolves the absolute path of a cache entry: `{cacheDir}/{namespace}/{cacheKey}.json`.
4
+ *
5
+ * Relative `cacheDir` values resolve against the current working directory so that readers and
6
+ * writers running from the same project root agree on the location. `cacheKey` may contain `/`
7
+ * separators, which become nested directories.
8
+ *
9
+ * Fails fast if a `namespace`/`cacheKey` segment (e.g. a `..`) would escape the cache directory,
10
+ * so a malformed route can never read or write outside `{cacheDir}`.
11
+ */
12
+ export declare function resolveCachePath({
13
+ cacheDir,
14
+ namespace,
15
+ cacheKey
16
+ }: FileCacheRef): string;
@@ -0,0 +1,24 @@
1
+ import { resolve, sep } from 'node:path';
2
+ /**
3
+ * Resolves the absolute path of a cache entry: `{cacheDir}/{namespace}/{cacheKey}.json`.
4
+ *
5
+ * Relative `cacheDir` values resolve against the current working directory so that readers and
6
+ * writers running from the same project root agree on the location. `cacheKey` may contain `/`
7
+ * separators, which become nested directories.
8
+ *
9
+ * Fails fast if a `namespace`/`cacheKey` segment (e.g. a `..`) would escape the cache directory,
10
+ * so a malformed route can never read or write outside `{cacheDir}`.
11
+ */
12
+ export function resolveCachePath({
13
+ cacheDir,
14
+ namespace,
15
+ cacheKey
16
+ }) {
17
+ const base = resolve(cacheDir);
18
+ const full = resolve(base, namespace, `${cacheKey}.json`);
19
+ const baseWithSep = base.endsWith(sep) ? base : `${base}${sep}`;
20
+ if (!full.startsWith(baseWithSep)) {
21
+ throw new Error(`Cache path for "${namespace}/${cacheKey}" escapes the cache directory "${cacheDir}".`);
22
+ }
23
+ return full;
24
+ }
@@ -0,0 +1,17 @@
1
+ import type { FileCacheEntry, FileCacheRef } from "./types.mjs";
2
+ /**
3
+ * Writes a precomputed cache entry (`{ hash, data }`) atomically (temp file + rename) so a
4
+ * concurrent reader never observes a half-written file. Creates the cache directory tree as needed.
5
+ *
6
+ * Fails fast: any filesystem error propagates so a misconfigured cache directory surfaces loudly at
7
+ * build time. Callers that treat the write as best-effort (e.g. {@link withFileCache}) catch it.
8
+ *
9
+ * Use this (rather than {@link saveFileCache}) when the hash is already known, so the (possibly
10
+ * large) hash-input string does not have to be retained just to be re-hashed here.
11
+ */
12
+ export declare function saveFileCacheEntry<T>(ref: FileCacheRef, entry: FileCacheEntry<T>): Promise<void>;
13
+ /**
14
+ * Writes a cache entry as `{ hash: sha256(content), data }`. Convenience over
15
+ * {@link saveFileCacheEntry} for callers that hold the source `content` and want it hashed here.
16
+ */
17
+ export declare function saveFileCache<T>(ref: FileCacheRef, content: string, data: T): Promise<void>;
@@ -0,0 +1,54 @@
1
+ import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { threadId } from 'node:worker_threads';
4
+ import { hashCacheContent } from "./hashCacheContent.mjs";
5
+ import { resolveCachePath } from "./resolveCachePath.mjs";
6
+ let tempCounter = 0;
7
+
8
+ /**
9
+ * Writes a precomputed cache entry (`{ hash, data }`) atomically (temp file + rename) so a
10
+ * concurrent reader never observes a half-written file. Creates the cache directory tree as needed.
11
+ *
12
+ * Fails fast: any filesystem error propagates so a misconfigured cache directory surfaces loudly at
13
+ * build time. Callers that treat the write as best-effort (e.g. {@link withFileCache}) catch it.
14
+ *
15
+ * Use this (rather than {@link saveFileCache}) when the hash is already known, so the (possibly
16
+ * large) hash-input string does not have to be retained just to be re-hashed here.
17
+ */
18
+ export async function saveFileCacheEntry(ref, entry) {
19
+ const cachePath = resolveCachePath(ref);
20
+ await mkdir(dirname(cachePath), {
21
+ recursive: true
22
+ });
23
+
24
+ // Write to a temp file then atomically rename over the target, so a torn write can never corrupt
25
+ // the cache file an in-flight reader is parsing. The temp name includes pid + threadId + a counter
26
+ // so two concurrent writers (including across worker_threads, which share a pid) never collide on
27
+ // it. The serialized string is transient.
28
+ tempCounter += 1;
29
+ const tempPath = `${cachePath}.${process.pid}.${threadId}.${tempCounter}.tmp`;
30
+ try {
31
+ await writeFile(tempPath, JSON.stringify(entry), 'utf-8');
32
+ await rename(tempPath, cachePath);
33
+ } catch (error) {
34
+ try {
35
+ await rm(tempPath, {
36
+ force: true
37
+ });
38
+ } catch {
39
+ // Best-effort cleanup of the temp file; ignore.
40
+ }
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Writes a cache entry as `{ hash: sha256(content), data }`. Convenience over
47
+ * {@link saveFileCacheEntry} for callers that hold the source `content` and want it hashed here.
48
+ */
49
+ export async function saveFileCache(ref, content, data) {
50
+ return saveFileCacheEntry(ref, {
51
+ hash: hashCacheContent(content),
52
+ data
53
+ });
54
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Reference to a single cache entry on disk.
3
+ *
4
+ * The entry is stored at `{cacheDir}/{namespace}/{cacheKey}.json`. `cacheKey` may
5
+ * contain `/` separators to nest entries (e.g. `utilities/parsing`).
6
+ */
7
+ export interface FileCacheRef {
8
+ /** Root cache directory. Relative paths resolve against the current working directory. */
9
+ cacheDir: string;
10
+ /** Subdirectory grouping related entries (e.g. `pages-index`). */
11
+ namespace: string;
12
+ /** Entry key, used as the file name without extension. May contain `/` for nesting. */
13
+ cacheKey: string;
14
+ }
15
+ /**
16
+ * On-disk shape of a cache entry: the validating content hash plus the cached value.
17
+ */
18
+ export interface FileCacheEntry<T> {
19
+ /** sha256 hex digest of the source content the cached value was derived from. */
20
+ hash: string;
21
+ /** The cached value. */
22
+ data: T;
23
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import type { FileCacheRef } from "./types.mjs";
2
+ /**
3
+ * A read-through file cache task: fetch an origin, derive its validating content, and either
4
+ * return the cached result or compute a fresh one.
5
+ */
6
+ export interface FileCacheTask<TOrigin, TData> {
7
+ /** The cache entry to use, or `undefined` to bypass the cache entirely. */
8
+ ref: FileCacheRef | undefined;
9
+ /** Fetches the origin (e.g. reads the source file). Runs concurrently with the cache read. */
10
+ readOrigin: () => Promise<TOrigin> | TOrigin;
11
+ /**
12
+ * Derives the string whose sha256 validates the cache from the origin — e.g. the file content,
13
+ * or the content combined with the options that affect the result. May differ from what the
14
+ * processor consumes.
15
+ */
16
+ getCacheContent: (origin: TOrigin) => string;
17
+ /** Computes the result from the origin on a cache miss. */
18
+ processor: (origin: TOrigin) => Promise<TData> | TData;
19
+ }
20
+ /**
21
+ * Standardizes the read-through cache flow shared by the page-index and types caches:
22
+ *
23
+ * 1. Read the cache entry while fetching and hashing the origin, so the two reads overlap and the
24
+ * hash is ready before the cache read resolves.
25
+ * 2. On a hash match, return the cached data and skip the processor.
26
+ * 3. On a miss (or with no `ref`), run the processor and return its result.
27
+ *
28
+ * A rejection from `readOrigin`/`getCacheContent` surfaces to the caller; the in-flight cache read
29
+ * is abandoned safely because {@link loadFileCacheEntry} never rejects. Populating the cache is
30
+ * **best-effort** — a write failure (e.g. an unwritable cache dir) is swallowed (logged under
31
+ * DEBUG) and the freshly computed result is returned regardless, so a cache hiccup never fails the
32
+ * caller or drops its output. The explicit build/validate writers (syncPageIndex/syncTypes) call
33
+ * {@link saveFileCache} directly and stay fail-fast.
34
+ */
35
+ export declare function withFileCache<TOrigin, TData>({
36
+ ref,
37
+ readOrigin,
38
+ getCacheContent,
39
+ processor
40
+ }: FileCacheTask<TOrigin, TData>): Promise<TData>;
@@ -0,0 +1,81 @@
1
+ import { hashCacheContent } from "./hashCacheContent.mjs";
2
+ import { loadFileCacheEntry } from "./loadFileCacheEntry.mjs";
3
+ import { saveFileCacheEntry } from "./saveFileCache.mjs";
4
+ // Set DOCS_INFRA_CACHE_DEBUG=1 to log cache hit/miss and best-effort write failures.
5
+ const DEBUG = Boolean(process.env.DOCS_INFRA_CACHE_DEBUG);
6
+
7
+ /**
8
+ * A read-through file cache task: fetch an origin, derive its validating content, and either
9
+ * return the cached result or compute a fresh one.
10
+ */
11
+
12
+ /**
13
+ * Standardizes the read-through cache flow shared by the page-index and types caches:
14
+ *
15
+ * 1. Read the cache entry while fetching and hashing the origin, so the two reads overlap and the
16
+ * hash is ready before the cache read resolves.
17
+ * 2. On a hash match, return the cached data and skip the processor.
18
+ * 3. On a miss (or with no `ref`), run the processor and return its result.
19
+ *
20
+ * A rejection from `readOrigin`/`getCacheContent` surfaces to the caller; the in-flight cache read
21
+ * is abandoned safely because {@link loadFileCacheEntry} never rejects. Populating the cache is
22
+ * **best-effort** — a write failure (e.g. an unwritable cache dir) is swallowed (logged under
23
+ * DEBUG) and the freshly computed result is returned regardless, so a cache hiccup never fails the
24
+ * caller or drops its output. The explicit build/validate writers (syncPageIndex/syncTypes) call
25
+ * {@link saveFileCache} directly and stay fail-fast.
26
+ */
27
+ export async function withFileCache({
28
+ ref,
29
+ readOrigin,
30
+ getCacheContent,
31
+ processor
32
+ }) {
33
+ if (!ref) {
34
+ return processor(await readOrigin());
35
+ }
36
+
37
+ // Start the cache read so it overlaps fetching and hashing the origin.
38
+ const cacheEntryPromise = loadFileCacheEntry(ref);
39
+ const origin = await readOrigin();
40
+ // Hash the cache content without binding it to a variable: the hash-input string can be large
41
+ // (e.g. a multi-MB JSON of the loaded types), and it is only needed to compute the hash — so we
42
+ // let it be collected immediately rather than retaining it through the cache read, the processor,
43
+ // and the write. saveFileCacheEntry below reuses the hash, so the content is never needed again.
44
+ const contentHash = hashCacheContent(getCacheContent(origin));
45
+ let cacheEntry = await cacheEntryPromise;
46
+ if (cacheEntry && cacheEntry.hash === contentHash) {
47
+ if (DEBUG) {
48
+ console.warn(`[docs-infra cache] hit ${ref.namespace}/${ref.cacheKey}`);
49
+ }
50
+ return cacheEntry.data;
51
+ }
52
+
53
+ // Distinguish a stale (warm-but-mismatched) miss from a cold one — a perpetual writer/reader hash
54
+ // divergence surfaces here, and is exactly the failure DEBUG exists to make visible. Drop the
55
+ // stale entry before the processor runs so its (possibly multi-MB) data is not retained alongside
56
+ // the freshly computed result.
57
+ const wasStale = Boolean(cacheEntry);
58
+ cacheEntry = null;
59
+ if (DEBUG && wasStale) {
60
+ console.warn(`[docs-infra cache] stale ${ref.namespace}/${ref.cacheKey} (hash mismatch, recomputing)`);
61
+ }
62
+ const data = await processor(origin);
63
+ try {
64
+ await saveFileCacheEntry(ref, {
65
+ hash: contentHash,
66
+ data
67
+ });
68
+ if (DEBUG) {
69
+ console.warn(`[docs-infra cache] miss → wrote ${ref.namespace}/${ref.cacheKey}`);
70
+ }
71
+ } catch (error) {
72
+ // Best-effort: any save failure — IO, the resolveCachePath traversal guard, or a serialization
73
+ // error on non-JSON-safe data — is swallowed so a misconfigured cache can never drop a page or
74
+ // fail a build; the computed result is returned regardless. The fail-fast writers (syncPageIndex
75
+ // and syncTypes, via saveFileCache) surface these instead. DEBUG logs them here.
76
+ if (DEBUG) {
77
+ console.warn(`[docs-infra cache] write failed for ${ref.namespace}/${ref.cacheKey}:`, error);
78
+ }
79
+ }
80
+ return data;
81
+ }
@@ -6,5 +6,11 @@ export type LoaderOptions = {
6
6
  showWrapperMeasures?: boolean;
7
7
  significantDependencyCountThreshold?: number;
8
8
  };
9
+ /**
10
+ * Directory for the sha256-validated JSON cache of parsed page indexes. When set,
11
+ * unchanged indexes are read from `{cacheDir}/pages-index/{route}.json` instead of
12
+ * being re-parsed.
13
+ */
14
+ cacheDir?: string;
9
15
  };
10
16
  export declare function loadPrecomputedSitemap(this: LoaderContext<LoaderOptions>, source: string): Promise<void>;
@@ -72,7 +72,8 @@ export async function loadPrecomputedSitemap(source) {
72
72
  // Create page index loader with root context
73
73
  const rootContext = this.rootContext || process.cwd();
74
74
  const loadPageIndex = createLoadServerPageIndex({
75
- rootContext
75
+ rootContext,
76
+ cacheDir: options.cacheDir
76
77
  });
77
78
 
78
79
  // Process all markdown files in parallel using shared logic
@@ -56,6 +56,12 @@ export type LoaderOptions = {
56
56
  descriptionReplacements?: DescriptionReplacement[];
57
57
  /** Options for code blocks highlighted inside generated type metadata */
58
58
  codeBlockEmphasisOptions?: TransformHtmlCodeBlockOptions;
59
+ /**
60
+ * Directory for the sha256-validated JSON cache of the types pipeline. When set, the
61
+ * parsed types.md (`types-text`) and the enhanced/highlighted result (`types-enhanced`)
62
+ * are cached under it, keyed by content hash.
63
+ */
64
+ cacheDir?: string;
59
65
  };
60
66
  /**
61
67
  * Webpack loader that processes types and precomputes meta.