@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.
- package/cli/loadNextConfig.d.mts +9 -0
- package/cli/loadNextConfig.mjs +11 -2
- package/cli/runValidate.mjs +15 -7
- package/cli/validateWorker.d.mts +1 -0
- package/cli/validateWorker.mjs +2 -1
- package/package.json +2 -2
- package/pipeline/cacheUtils/constants.d.mts +5 -0
- package/pipeline/cacheUtils/constants.mjs +5 -0
- package/pipeline/cacheUtils/hashCacheContent.d.mts +5 -0
- package/pipeline/cacheUtils/hashCacheContent.mjs +9 -0
- package/pipeline/cacheUtils/index.d.mts +9 -0
- package/pipeline/cacheUtils/index.mjs +7 -0
- package/pipeline/cacheUtils/loadFileCache.d.mts +13 -0
- package/pipeline/cacheUtils/loadFileCache.mjs +20 -0
- package/pipeline/cacheUtils/loadFileCacheEntry.d.mts +13 -0
- package/pipeline/cacheUtils/loadFileCacheEntry.mjs +47 -0
- package/pipeline/cacheUtils/resolveCachePath.d.mts +16 -0
- package/pipeline/cacheUtils/resolveCachePath.mjs +24 -0
- package/pipeline/cacheUtils/saveFileCache.d.mts +17 -0
- package/pipeline/cacheUtils/saveFileCache.mjs +54 -0
- package/pipeline/cacheUtils/types.d.mts +23 -0
- package/pipeline/cacheUtils/types.mjs +1 -0
- package/pipeline/cacheUtils/withFileCache.d.mts +40 -0
- package/pipeline/cacheUtils/withFileCache.mjs +81 -0
- package/pipeline/loadPrecomputedSitemap/loadPrecomputedSitemap.d.mts +6 -0
- package/pipeline/loadPrecomputedSitemap/loadPrecomputedSitemap.mjs +2 -1
- package/pipeline/loadPrecomputedTypes/loadPrecomputedTypes.d.mts +6 -0
- package/pipeline/loadPrecomputedTypes/loadPrecomputedTypes.mjs +5 -1
- package/pipeline/loadServerPageIndex/enrichPageIndex.d.mts +15 -0
- package/pipeline/loadServerPageIndex/enrichPageIndex.mjs +50 -0
- package/pipeline/loadServerPageIndex/extractPrefixAndTitle.d.mts +19 -0
- package/pipeline/loadServerPageIndex/extractPrefixAndTitle.mjs +80 -0
- package/pipeline/loadServerPageIndex/index.d.mts +5 -2
- package/pipeline/loadServerPageIndex/index.mjs +4 -1
- package/pipeline/loadServerPageIndex/loadServerPageIndex.d.mts +13 -22
- package/pipeline/loadServerPageIndex/loadServerPageIndex.mjs +32 -124
- package/pipeline/loadServerPageIndex/resolvePageIndexCacheKey.d.mts +13 -0
- package/pipeline/loadServerPageIndex/resolvePageIndexCacheKey.mjs +23 -0
- package/pipeline/loadServerTypes/loadServerTypes.mjs +67 -7
- package/pipeline/loadServerTypesText/loadServerTypesText.d.mts +15 -1
- package/pipeline/loadServerTypesText/loadServerTypesText.mjs +30 -8
- package/pipeline/loadServerTypesText/normalizeTypesSourceDataForCache.d.mts +2 -0
- package/pipeline/loadServerTypesText/normalizeTypesSourceDataForCache.mjs +144 -0
- package/pipeline/loadServerTypesText/resolveTypesCacheKey.d.mts +19 -0
- package/pipeline/loadServerTypesText/resolveTypesCacheKey.mjs +29 -0
- package/pipeline/syncPageIndex/mergeMetadataMarkdown.d.mts +24 -0
- package/pipeline/syncPageIndex/mergeMetadataMarkdown.mjs +79 -49
- package/pipeline/syncPageIndex/metadataToMarkdown.d.mts +14 -0
- package/pipeline/syncPageIndex/metadataToMarkdown.mjs +32 -10
- package/pipeline/syncPageIndex/syncPageIndex.d.mts +12 -0
- package/pipeline/syncPageIndex/syncPageIndex.mjs +72 -12
- package/pipeline/syncTypes/syncTypes.d.mts +7 -0
- package/pipeline/syncTypes/syncTypes.mjs +31 -1
- package/pipeline/transformMarkdownMetadata/transformMarkdownMetadata.mjs +3 -0
- package/pipeline/transformMarkdownMetadata/types.d.mts +7 -0
- package/withDocsInfra/withDocsInfra.d.mts +14 -0
- package/withDocsInfra/withDocsInfra.mjs +25 -11
package/cli/loadNextConfig.d.mts
CHANGED
|
@@ -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.
|
package/cli/loadNextConfig.mjs
CHANGED
|
@@ -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
|
};
|
package/cli/runValidate.mjs
CHANGED
|
@@ -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 = '
|
|
193
|
-
const markerDirPath = path.
|
|
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 = '
|
|
267
|
-
const typesMarkerDirPath = path.
|
|
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
|
}));
|
package/cli/validateWorker.d.mts
CHANGED
package/cli/validateWorker.mjs
CHANGED
|
@@ -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.
|
|
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": "
|
|
807
|
+
"gitSha": "0ffea436eb8e1aa519727d55d2e9a4daa0ec6ac2"
|
|
808
808
|
}
|
|
@@ -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.
|