@mintlify/previewing 4.0.1159 → 4.0.1161
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/__tests__/docs-config-cache.test.d.ts +1 -0
- package/dist/__tests__/docs-config-cache.test.js +83 -0
- package/dist/__tests__/page-metadata-cache.test.d.ts +1 -0
- package/dist/__tests__/page-metadata-cache.test.js +51 -0
- package/dist/local-preview/listener/docs-config-cache.d.ts +10 -0
- package/dist/local-preview/listener/docs-config-cache.js +44 -0
- package/dist/local-preview/listener/index.d.ts +2 -1
- package/dist/local-preview/listener/index.js +145 -13
- package/dist/local-preview/listener/page-metadata-cache.d.ts +9 -0
- package/dist/local-preview/listener/page-metadata-cache.js +55 -0
- package/dist/local-preview/listener/update.d.ts +6 -1
- package/dist/local-preview/listener/update.js +20 -3
- package/dist/local-preview/run.js +2 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { clearDocsConfigCaches, getDocsConfigApiHash, getDocsConfigNavigationHash, getVariablesHash, hasDocsConfigApiChanged, hasDocsConfigNavigationChanged, hasNavigationApiReference, hasDocsConfigVariablesChanged, updateDocsConfigHashCache, } from '../local-preview/listener/docs-config-cache.js';
|
|
3
|
+
const createDocsConfig = (page) => ({
|
|
4
|
+
$schema: 'https://mintlify.com/docs.json',
|
|
5
|
+
name: 'Mintlify',
|
|
6
|
+
theme: 'mint',
|
|
7
|
+
colors: {
|
|
8
|
+
primary: '#16A34A',
|
|
9
|
+
},
|
|
10
|
+
navigation: {
|
|
11
|
+
pages: [page],
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
describe('docsConfigCache', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
clearDocsConfigCaches();
|
|
17
|
+
});
|
|
18
|
+
it('tracks api, navigation, and variables hash changes independently', () => {
|
|
19
|
+
const docsConfig = {
|
|
20
|
+
...createDocsConfig('raw-page'),
|
|
21
|
+
api: {
|
|
22
|
+
openapi: 'openapi.json',
|
|
23
|
+
},
|
|
24
|
+
variables: {
|
|
25
|
+
product: 'Mintlify',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
updateDocsConfigHashCache(docsConfig);
|
|
29
|
+
expect(hasDocsConfigApiChanged(getDocsConfigApiHash(docsConfig))).toBe(false);
|
|
30
|
+
expect(hasDocsConfigNavigationChanged(getDocsConfigNavigationHash(docsConfig))).toBe(false);
|
|
31
|
+
expect(hasDocsConfigVariablesChanged(getVariablesHash(docsConfig.variables))).toBe(false);
|
|
32
|
+
expect(hasDocsConfigApiChanged(getDocsConfigApiHash(createDocsConfig('raw-page')))).toBe(true);
|
|
33
|
+
expect(hasDocsConfigNavigationChanged(getDocsConfigNavigationHash(createDocsConfig('new-page')))).toBe(true);
|
|
34
|
+
expect(hasDocsConfigVariablesChanged(getVariablesHash({ product: 'Mint' }))).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
it('detects openapi and asyncapi references anywhere in navigation', () => {
|
|
37
|
+
expect(hasNavigationApiReference({
|
|
38
|
+
groups: [
|
|
39
|
+
{
|
|
40
|
+
group: 'API',
|
|
41
|
+
openapi: 'openapi.json',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
})).toBe(true);
|
|
45
|
+
expect(hasNavigationApiReference({
|
|
46
|
+
tabs: [
|
|
47
|
+
{
|
|
48
|
+
tab: 'Realtime',
|
|
49
|
+
groups: [
|
|
50
|
+
{
|
|
51
|
+
group: 'Events',
|
|
52
|
+
asyncapi: 'asyncapi.json',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
})).toBe(true);
|
|
58
|
+
expect(hasNavigationApiReference({
|
|
59
|
+
tabs: [
|
|
60
|
+
{
|
|
61
|
+
tab: 'Realtime',
|
|
62
|
+
asyncapi: 'asyncapi.json',
|
|
63
|
+
groups: [
|
|
64
|
+
{
|
|
65
|
+
group: 'Events',
|
|
66
|
+
asyncapi: 'asyncapi.json',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
})).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('allows plain MDX navigation through the navigation fast path', () => {
|
|
74
|
+
expect(hasNavigationApiReference({
|
|
75
|
+
groups: [
|
|
76
|
+
{
|
|
77
|
+
group: 'Guides',
|
|
78
|
+
pages: ['quickstart', 'install'],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
})).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { clearPageMetadataCache, generateDocsNavFromPageMetadataCache, upsertPageMetadataCacheEntryForFile, } from '../local-preview/listener/page-metadata-cache.js';
|
|
3
|
+
const createDocsConfig = (pages) => ({
|
|
4
|
+
$schema: 'https://mintlify.com/docs.json',
|
|
5
|
+
name: 'Mintlify',
|
|
6
|
+
theme: 'mint',
|
|
7
|
+
colors: {
|
|
8
|
+
primary: '#16A34A',
|
|
9
|
+
},
|
|
10
|
+
navigation: {
|
|
11
|
+
pages,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
describe('pageMetadataCache', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
clearPageMetadataCache();
|
|
17
|
+
});
|
|
18
|
+
it('regenerates docs nav with updated frontmatter metadata from the cache', () => {
|
|
19
|
+
upsertPageMetadataCacheEntryForFile('intro.mdx', '---\ntitle: Updated Intro\ndescription: Updated description\n---\n# Intro');
|
|
20
|
+
const generatedNav = generateDocsNavFromPageMetadataCache(createDocsConfig(['intro']));
|
|
21
|
+
expect(generatedNav).toMatchObject({
|
|
22
|
+
pages: [
|
|
23
|
+
{
|
|
24
|
+
href: '/intro',
|
|
25
|
+
title: 'Updated Intro',
|
|
26
|
+
description: 'Updated description',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
it('reports when a page references an OpenAPI spec via frontmatter', () => {
|
|
32
|
+
expect(upsertPageMetadataCacheEntryForFile('plain.mdx', '---\ntitle: Plain\n---\n# Plain')).toBe(false);
|
|
33
|
+
expect(upsertPageMetadataCacheEntryForFile('endpoint.mdx', '---\nopenapi: GET /pets\n---')).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
it('reports when a page references an AsyncAPI spec via frontmatter', () => {
|
|
36
|
+
expect(upsertPageMetadataCacheEntryForFile('channel.mdx', '---\nasyncapi: subscribe /events\n---')).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it('regenerates docs nav without pages removed from docs config navigation', () => {
|
|
39
|
+
upsertPageMetadataCacheEntryForFile('intro.mdx', '---\ntitle: Intro\n---\n# Intro');
|
|
40
|
+
upsertPageMetadataCacheEntryForFile('removed.mdx', '---\ntitle: Removed\n---\n# Removed');
|
|
41
|
+
const generatedNav = generateDocsNavFromPageMetadataCache(createDocsConfig(['intro']));
|
|
42
|
+
expect(generatedNav).toMatchObject({
|
|
43
|
+
pages: [
|
|
44
|
+
{
|
|
45
|
+
href: '/intro',
|
|
46
|
+
title: 'Intro',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DocsConfig } from '@mintlify/validation';
|
|
2
|
+
export declare const getVariablesHash: (variables: Record<string, string> | undefined) => string;
|
|
3
|
+
export declare const getDocsConfigApiHash: (docsConfig: DocsConfig) => string;
|
|
4
|
+
export declare const getDocsConfigNavigationHash: (docsConfig: DocsConfig) => string;
|
|
5
|
+
export declare const updateDocsConfigHashCache: (docsConfig: DocsConfig) => void;
|
|
6
|
+
export declare const hasNavigationApiReference: (value: unknown) => boolean;
|
|
7
|
+
export declare const hasDocsConfigApiChanged: (newApiHash: string) => boolean;
|
|
8
|
+
export declare const hasDocsConfigNavigationChanged: (newNavigationHash: string) => boolean;
|
|
9
|
+
export declare const hasDocsConfigVariablesChanged: (newVariablesHash: string) => boolean;
|
|
10
|
+
export declare const clearDocsConfigCaches: () => void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
let docsConfigVariablesHash;
|
|
2
|
+
let docsConfigApiHash;
|
|
3
|
+
let docsConfigNavigationHash;
|
|
4
|
+
const getJsonHash = (value) => {
|
|
5
|
+
return JSON.stringify(value ?? {});
|
|
6
|
+
};
|
|
7
|
+
export const getVariablesHash = (variables) => {
|
|
8
|
+
return getJsonHash(variables);
|
|
9
|
+
};
|
|
10
|
+
export const getDocsConfigApiHash = (docsConfig) => {
|
|
11
|
+
return getJsonHash(docsConfig.api);
|
|
12
|
+
};
|
|
13
|
+
export const getDocsConfigNavigationHash = (docsConfig) => {
|
|
14
|
+
return getJsonHash(docsConfig.navigation);
|
|
15
|
+
};
|
|
16
|
+
export const updateDocsConfigHashCache = (docsConfig) => {
|
|
17
|
+
docsConfigVariablesHash = getVariablesHash(docsConfig.variables);
|
|
18
|
+
docsConfigApiHash = getDocsConfigApiHash(docsConfig);
|
|
19
|
+
docsConfigNavigationHash = getDocsConfigNavigationHash(docsConfig);
|
|
20
|
+
};
|
|
21
|
+
export const hasNavigationApiReference = (value) => {
|
|
22
|
+
if (value == null || typeof value !== 'object') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (Object.prototype.hasOwnProperty.call(value, 'openapi') ||
|
|
26
|
+
Object.prototype.hasOwnProperty.call(value, 'asyncapi')) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return Object.values(value).some(hasNavigationApiReference);
|
|
30
|
+
};
|
|
31
|
+
export const hasDocsConfigApiChanged = (newApiHash) => {
|
|
32
|
+
return docsConfigApiHash == null || docsConfigApiHash !== newApiHash;
|
|
33
|
+
};
|
|
34
|
+
export const hasDocsConfigNavigationChanged = (newNavigationHash) => {
|
|
35
|
+
return docsConfigNavigationHash == null || docsConfigNavigationHash !== newNavigationHash;
|
|
36
|
+
};
|
|
37
|
+
export const hasDocsConfigVariablesChanged = (newVariablesHash) => {
|
|
38
|
+
return docsConfigVariablesHash == null || docsConfigVariablesHash !== newVariablesHash;
|
|
39
|
+
};
|
|
40
|
+
export const clearDocsConfigCaches = () => {
|
|
41
|
+
docsConfigVariablesHash = undefined;
|
|
42
|
+
docsConfigApiHash = undefined;
|
|
43
|
+
docsConfigNavigationHash = undefined;
|
|
44
|
+
};
|
|
@@ -4,5 +4,6 @@ declare const listener: (triggerRefresh: () => void, options?: {
|
|
|
4
4
|
groups?: string[];
|
|
5
5
|
}) => void;
|
|
6
6
|
declare const initializeFrontmatterHashCache: () => Promise<void>;
|
|
7
|
-
|
|
7
|
+
declare const initializeDocsConfigHashCache: () => Promise<void>;
|
|
8
|
+
export { initializeDocsConfigHashCache, initializeFrontmatterHashCache, initializeImportCache };
|
|
8
9
|
export default listener;
|
|
@@ -11,19 +11,56 @@ import pathUtil from 'path';
|
|
|
11
11
|
import { CMD_EXEC_PATH, NEXT_PROPS_PATH, NEXT_PUBLIC_PATH, CLIENT_PATH } from '../../constants.js';
|
|
12
12
|
import { addChangeLog, addErrorLog, clearErrorLogs, getCurrentErrorLogs, } from '../../logging-state.js';
|
|
13
13
|
import { AddedLog, DeletedLog, EditedLog, WarningLog, InfoLog, ErrorLog } from '../../logs.js';
|
|
14
|
+
import { clearDocsConfigCaches, getDocsConfigApiHash, getDocsConfigNavigationHash, getVariablesHash, hasDocsConfigApiChanged, hasDocsConfigNavigationChanged, hasNavigationApiReference, hasDocsConfigVariablesChanged, updateDocsConfigHashCache, } from './docs-config-cache.js';
|
|
14
15
|
import { generateDependentSnippets } from './generateDependentSnippets.js';
|
|
15
16
|
import { generatePagesWithImports } from './generatePagesWithImports.js';
|
|
16
17
|
import { getDocsState } from './getDocsState.js';
|
|
17
18
|
import { initializeImportCache, updateImportCacheForFile, removeFromImportCache, getImportedFilesFromCache, syncImportedFileLocations, } from './importCache.js';
|
|
19
|
+
import { clearPageMetadataCache, removePageMetadataCacheEntryForFile, upsertPageMetadataCacheEntry, upsertPageMetadataCacheEntryForFile, } from './page-metadata-cache.js';
|
|
18
20
|
import { hasTrackedReferencedFile, refreshTrackedReferencedFiles } from './referencedFiles.js';
|
|
19
21
|
import { regenerateAllSnippets } from './regenerateAllSnippets.js';
|
|
20
22
|
import { resolveImportsFromImportClosure } from './resolve-imports-from-import-closure.js';
|
|
21
23
|
import { resolvePageImports } from './resolve-page-imports.js';
|
|
22
|
-
import { updateCustomLanguages, updateGeneratedNav, updateOpenApiFiles, upsertOpenApiFile, } from './update.js';
|
|
23
|
-
import { getCurrentVariables, getMintIgnoreGlobs, handleParseError, isFileSizeValid, isJsonValid, seedFrontmatterHashForPage, shouldRegenerateNavForPage, suppressParseError, } from './utils.js';
|
|
24
|
+
import { updateCustomLanguages, updateGeneratedNav, updateGeneratedNavFromPageMetadataCache, updateOpenApiFiles, upsertOpenApiFile, } from './update.js';
|
|
25
|
+
import { getCurrentVariables, getMintIgnoreGlobs, handleParseError, isFileSizeValid, isJsonValid, readJsonFile, seedFrontmatterHashForPage, shouldRegenerateNavForPage, suppressParseError, } from './utils.js';
|
|
24
26
|
const { readFile } = _promises;
|
|
25
27
|
const frontmatterHashes = new Map();
|
|
26
28
|
const SLOW_UPDATE_LOG_THRESHOLD_MS = 1000;
|
|
29
|
+
const getCurrentDocsConfig = async () => {
|
|
30
|
+
const propsDocsJsonPath = pathUtil.join(NEXT_PROPS_PATH, 'docs.json');
|
|
31
|
+
try {
|
|
32
|
+
const propsDocsConfig = await readJsonFile(propsDocsJsonPath);
|
|
33
|
+
return propsDocsConfig;
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
const docsJsonPath = pathUtil.join(CMD_EXEC_PATH, 'docs.json');
|
|
37
|
+
return await DocsConfigUpdater.getConfig(docsJsonPath, false, CMD_EXEC_PATH, suppressParseError);
|
|
38
|
+
};
|
|
39
|
+
const writeDocsConfigWithCurrentNavigation = async (docsConfig) => {
|
|
40
|
+
const propsDocsJsonPath = pathUtil.join(NEXT_PROPS_PATH, 'docs.json');
|
|
41
|
+
try {
|
|
42
|
+
const currentDocsConfig = await readJsonFile(propsDocsJsonPath);
|
|
43
|
+
await DocsConfigUpdater.writeConfigFile({ ...docsConfig, navigation: currentDocsConfig.navigation }, CLIENT_PATH);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
await DocsConfigUpdater.writeConfigFile(docsConfig, CLIENT_PATH);
|
|
48
|
+
};
|
|
49
|
+
const getCurrentRawDocsConfig = async () => {
|
|
50
|
+
const docsJsonPath = pathUtil.join(CMD_EXEC_PATH, 'docs.json');
|
|
51
|
+
if (!(await fse.pathExists(docsJsonPath))) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return await DocsConfigUpdater.getConfig(docsJsonPath, false, CMD_EXEC_PATH, suppressParseError);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const hasDocsConfigApiRoutes = (docsConfig) => {
|
|
62
|
+
return docsConfig.api != null || hasNavigationApiReference(docsConfig.navigation);
|
|
63
|
+
};
|
|
27
64
|
const getElapsedMs = (startTime) => {
|
|
28
65
|
return Number((process.hrtime.bigint() - startTime) / 1000000n);
|
|
29
66
|
};
|
|
@@ -100,6 +137,7 @@ const onUnlinkEvent = async (filename, triggerRefresh, options) => {
|
|
|
100
137
|
const importedFiles = getImportedFilesFromCache();
|
|
101
138
|
const potentialCategory = getFileCategory(filename, { importedFiles });
|
|
102
139
|
frontmatterHashes.delete(filename);
|
|
140
|
+
removePageMetadataCacheEntryForFile(filename);
|
|
103
141
|
if (hasTrackedReferencedFile(filename)) {
|
|
104
142
|
try {
|
|
105
143
|
const { mintConfig, openApiFiles, docsConfig } = await getDocsState(handleParseError);
|
|
@@ -219,6 +257,7 @@ const validateConfigFiles = async () => {
|
|
|
219
257
|
};
|
|
220
258
|
const initializeFrontmatterHashCache = async () => {
|
|
221
259
|
frontmatterHashes.clear();
|
|
260
|
+
clearPageMetadataCache();
|
|
222
261
|
const importedFiles = getImportedFilesFromCache();
|
|
223
262
|
const pageFilenames = getFileListSync(CMD_EXEC_PATH).filter((file) => {
|
|
224
263
|
return getFileCategory(file, { importedFiles }) === 'page';
|
|
@@ -228,10 +267,25 @@ const initializeFrontmatterHashCache = async () => {
|
|
|
228
267
|
const filePath = pathUtil.join(CMD_EXEC_PATH, filename);
|
|
229
268
|
const contentStr = (await readFile(filePath)).toString();
|
|
230
269
|
seedFrontmatterHashForPage(filename, contentStr, frontmatterHashes, suppressParseError);
|
|
270
|
+
upsertPageMetadataCacheEntryForFile(filename, contentStr);
|
|
231
271
|
}
|
|
232
272
|
catch { }
|
|
233
273
|
}));
|
|
234
274
|
};
|
|
275
|
+
const initializeDocsConfigHashCache = async () => {
|
|
276
|
+
clearDocsConfigCaches();
|
|
277
|
+
// Seed the cache from the raw config (no OpenAPI generation) so it stays
|
|
278
|
+
// consistent with the comparison done on each docs.json save.
|
|
279
|
+
const docsJsonPath = pathUtil.join(CMD_EXEC_PATH, 'docs.json');
|
|
280
|
+
try {
|
|
281
|
+
if (await fse.pathExists(docsJsonPath)) {
|
|
282
|
+
const docsConfig = await DocsConfigUpdater.getConfig(docsJsonPath, false, CMD_EXEC_PATH, suppressParseError);
|
|
283
|
+
updateDocsConfigHashCache(docsConfig);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch { }
|
|
288
|
+
};
|
|
235
289
|
/**
|
|
236
290
|
* This function is called when a file is added or changed
|
|
237
291
|
* @param filename
|
|
@@ -246,7 +300,7 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
|
|
|
246
300
|
const potentialCategory = getFileCategory(filename, { importedFiles });
|
|
247
301
|
if (hasTrackedReferencedFile(filename)) {
|
|
248
302
|
try {
|
|
249
|
-
const { mintConfig, openApiFiles, docsConfig } = await getDocsState(handleParseError);
|
|
303
|
+
const { mintConfig, openApiFiles, docsConfig, pagesAcc } = await getDocsState(handleParseError);
|
|
250
304
|
await refreshTrackedReferencedFiles();
|
|
251
305
|
if (mintConfig) {
|
|
252
306
|
await MintConfigUpdater.writeConfigFile(mintConfig, CLIENT_PATH);
|
|
@@ -254,7 +308,7 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
|
|
|
254
308
|
await DocsConfigUpdater.writeConfigFile(docsConfig, CLIENT_PATH);
|
|
255
309
|
await updateOpenApiFiles(openApiFiles, suppressParseError);
|
|
256
310
|
await updateCustomLanguages(docsConfig, suppressParseError);
|
|
257
|
-
await updateGeneratedNav(suppressParseError);
|
|
311
|
+
await updateGeneratedNav(suppressParseError, { pagesAcc, docsConfig });
|
|
258
312
|
}
|
|
259
313
|
catch (err) {
|
|
260
314
|
if (getCurrentErrorLogs().length === 0) {
|
|
@@ -263,6 +317,12 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
|
|
|
263
317
|
}
|
|
264
318
|
const updatedSnippets = await regenerateAllSnippets();
|
|
265
319
|
await generatePagesWithImports(new Set(updatedSnippets));
|
|
320
|
+
// A tracked $ref change rebuilds config-derived artifacts; keep every docs
|
|
321
|
+
// config hash in sync with the same raw docs.json source used by save checks.
|
|
322
|
+
const rawDocsConfig = await getCurrentRawDocsConfig();
|
|
323
|
+
if (rawDocsConfig) {
|
|
324
|
+
updateDocsConfigHashCache(rawDocsConfig);
|
|
325
|
+
}
|
|
266
326
|
triggerRefresh();
|
|
267
327
|
return null;
|
|
268
328
|
}
|
|
@@ -271,6 +331,7 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
|
|
|
271
331
|
return undefined;
|
|
272
332
|
}
|
|
273
333
|
let regenerateNav = false;
|
|
334
|
+
let regenerateNavFromPageMetadataCache = false;
|
|
274
335
|
let category = potentialCategory === 'potentialYamlOpenApiSpec' ||
|
|
275
336
|
potentialCategory === 'potentialJsonOpenApiSpec'
|
|
276
337
|
? 'staticFile'
|
|
@@ -279,16 +340,17 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
|
|
|
279
340
|
switch (potentialCategory) {
|
|
280
341
|
case 'page': {
|
|
281
342
|
let contentStr = (await readFile(filePath)).toString();
|
|
282
|
-
|
|
343
|
+
regenerateNavFromPageMetadataCache = await shouldRegenerateNavForPage(filename, contentStr, frontmatterHashes, handleParseError);
|
|
283
344
|
const tree = await preparseMdxTree(contentStr, CMD_EXEC_PATH, filePath, suppressParseError);
|
|
284
345
|
const importsResponse = await findAndRemoveImports(tree);
|
|
285
346
|
if (hasImports(importsResponse)) {
|
|
286
347
|
contentStr = stringifyTree(await resolvePageImports({ ...importsResponse, filename }));
|
|
287
348
|
}
|
|
288
|
-
const { pageContent } = await createPage(filename, contentStr, CMD_EXEC_PATH, [], [], handleParseError);
|
|
349
|
+
const { pageContent, pageMetadata, slug } = await createPage(filename, contentStr, CMD_EXEC_PATH, [], [], handleParseError);
|
|
289
350
|
await fse.outputFile(targetPath, pageContent, {
|
|
290
351
|
flag: 'w',
|
|
291
352
|
});
|
|
353
|
+
upsertPageMetadataCacheEntry(slug, pageMetadata);
|
|
292
354
|
break;
|
|
293
355
|
}
|
|
294
356
|
case 'snippet': {
|
|
@@ -317,6 +379,68 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
|
|
|
317
379
|
addErrorLog(_jsx(ErrorLog, { message: `Syntax error in ${filename}: ${error}` }));
|
|
318
380
|
return undefined;
|
|
319
381
|
}
|
|
382
|
+
if (potentialCategory === 'docsConfig') {
|
|
383
|
+
try {
|
|
384
|
+
// Parse the raw config only (cheap) so we can decide what actually
|
|
385
|
+
// needs rebuilding before paying for the expensive OpenAPI/nav work.
|
|
386
|
+
const docsConfig = await DocsConfigUpdater.getConfig(filePath, false, CMD_EXEC_PATH, handleParseError);
|
|
387
|
+
const newApiHash = getDocsConfigApiHash(docsConfig);
|
|
388
|
+
const apiChanged = hasDocsConfigApiChanged(newApiHash);
|
|
389
|
+
const newNavigationHash = getDocsConfigNavigationHash(docsConfig);
|
|
390
|
+
const navigationChanged = hasDocsConfigNavigationChanged(newNavigationHash);
|
|
391
|
+
const newVariablesHash = getVariablesHash(docsConfig.variables);
|
|
392
|
+
const variablesChanged = hasDocsConfigVariablesChanged(newVariablesHash);
|
|
393
|
+
const shouldUseNavigationFastPath = navigationChanged && !apiChanged && !hasDocsConfigApiRoutes(docsConfig);
|
|
394
|
+
const shouldRegenerateGeneratedData = apiChanged || (navigationChanged && hasDocsConfigApiRoutes(docsConfig));
|
|
395
|
+
await refreshTrackedReferencedFiles();
|
|
396
|
+
if (shouldUseNavigationFastPath) {
|
|
397
|
+
// Plain MDX navigation changed without API changes: regenerate
|
|
398
|
+
// generatedDocsNav from cached page metadata. If a page references a
|
|
399
|
+
// spec via frontmatter, this falls back to the full rebuild and
|
|
400
|
+
// returns the API-expanded config, which we then persist.
|
|
401
|
+
await updateCustomLanguages(docsConfig, suppressParseError);
|
|
402
|
+
const generatedDocsConfig = await updateGeneratedNavFromPageMetadataCache(docsConfig, suppressParseError);
|
|
403
|
+
await DocsConfigUpdater.writeConfigFile(generatedDocsConfig, CLIENT_PATH);
|
|
404
|
+
}
|
|
405
|
+
else if (shouldRegenerateGeneratedData) {
|
|
406
|
+
// Preserve the old full-rebuild route for top-level api, API-derived
|
|
407
|
+
// navigation, and top-level api removals.
|
|
408
|
+
const docsState = await getDocsState(handleParseError);
|
|
409
|
+
await DocsConfigUpdater.writeConfigFile(docsState.docsConfig, CLIENT_PATH);
|
|
410
|
+
await updateOpenApiFiles(docsState.openApiFiles, suppressParseError);
|
|
411
|
+
await updateCustomLanguages(docsState.docsConfig, suppressParseError);
|
|
412
|
+
await updateGeneratedNav(suppressParseError, {
|
|
413
|
+
pagesAcc: docsState.pagesAcc,
|
|
414
|
+
docsConfig: docsState.docsConfig,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
// Non-generated docs.json changes (theme, styling, metadata, etc.) do
|
|
419
|
+
// not need OpenAPI/nav regeneration. Preserve the current generated
|
|
420
|
+
// navigation so API-expanded docs config does not collapse to raw nav.
|
|
421
|
+
await updateCustomLanguages(docsConfig, suppressParseError);
|
|
422
|
+
await writeDocsConfigWithCurrentNavigation(docsConfig);
|
|
423
|
+
}
|
|
424
|
+
if (variablesChanged) {
|
|
425
|
+
const updatedSnippets = await regenerateAllSnippets();
|
|
426
|
+
await generatePagesWithImports(new Set(updatedSnippets));
|
|
427
|
+
}
|
|
428
|
+
// Update the cache only after every step above succeeded, so a failure
|
|
429
|
+
// mid-update doesn't leave the cache claiming work that never happened.
|
|
430
|
+
updateDocsConfigHashCache(docsConfig);
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
if (getCurrentErrorLogs().length > 0) {
|
|
434
|
+
// no-op to suppress duplicate error logging
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
console.error(err);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
// mint.json: preserve the original full-rebuild behavior.
|
|
320
444
|
regenerateNav = true;
|
|
321
445
|
try {
|
|
322
446
|
const { mintConfig, openApiFiles, docsConfig } = await getDocsState(handleParseError);
|
|
@@ -337,10 +461,6 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
|
|
|
337
461
|
console.error(err);
|
|
338
462
|
}
|
|
339
463
|
}
|
|
340
|
-
if (potentialCategory === 'docsConfig') {
|
|
341
|
-
const updatedSnippets = await regenerateAllSnippets();
|
|
342
|
-
await generatePagesWithImports(new Set(updatedSnippets));
|
|
343
|
-
}
|
|
344
464
|
break;
|
|
345
465
|
}
|
|
346
466
|
case 'mintIgnore': {
|
|
@@ -410,12 +530,24 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
|
|
|
410
530
|
break;
|
|
411
531
|
}
|
|
412
532
|
}
|
|
413
|
-
if (
|
|
533
|
+
if (regenerateNavFromPageMetadataCache) {
|
|
534
|
+
const rawDocsConfig = await getCurrentRawDocsConfig();
|
|
535
|
+
const docsConfig = rawDocsConfig ?? (await getCurrentDocsConfig());
|
|
536
|
+
if (hasDocsConfigApiRoutes(docsConfig)) {
|
|
537
|
+
const generatedDocsConfig = await updateGeneratedNav(suppressParseError);
|
|
538
|
+
await DocsConfigUpdater.writeConfigFile(generatedDocsConfig, CLIENT_PATH);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
await updateGeneratedNavFromPageMetadataCache(docsConfig, suppressParseError);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else if (regenerateNav) {
|
|
414
545
|
// TODO: Instead of re-generating the entire nav, optimize by just updating the specific page that changed.
|
|
415
|
-
await updateGeneratedNav(suppressParseError);
|
|
546
|
+
const docsConfig = await updateGeneratedNav(suppressParseError);
|
|
547
|
+
await DocsConfigUpdater.writeConfigFile(docsConfig, CLIENT_PATH);
|
|
416
548
|
}
|
|
417
549
|
triggerRefresh();
|
|
418
550
|
return category;
|
|
419
551
|
};
|
|
420
|
-
export { initializeFrontmatterHashCache, initializeImportCache };
|
|
552
|
+
export { initializeDocsConfigHashCache, initializeFrontmatterHashCache, initializeImportCache };
|
|
421
553
|
export default listener;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DecoratedNavigationPage } from '@mintlify/models';
|
|
2
|
+
import type { DecoratedNavigationConfig, DocsConfig } from '@mintlify/validation';
|
|
3
|
+
export declare const clearPageMetadataCache: () => void;
|
|
4
|
+
export declare const upsertPageMetadataCacheEntry: (slug: string, pageMetadata: DecoratedNavigationPage) => void;
|
|
5
|
+
export declare const removePageMetadataCacheEntry: (slug: string) => void;
|
|
6
|
+
export declare const removePageMetadataCacheEntryForFile: (filename: string) => void;
|
|
7
|
+
export declare const upsertPageMetadataCacheEntryForFile: (filename: string, contentStr: string) => boolean;
|
|
8
|
+
export declare const ensurePageMetadataCacheForDocsConfig: (docsConfig: DocsConfig) => Promise<boolean>;
|
|
9
|
+
export declare const generateDocsNavFromPageMetadataCache: (docsConfig: DocsConfig) => DecoratedNavigationConfig;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getAllPathsInDocsNav, getDecoratedNavPageAndSlug } from '@mintlify/common';
|
|
2
|
+
import { generateDecoratedDocsNavigationFromPages } from '@mintlify/prebuild';
|
|
3
|
+
import fse from 'fs-extra';
|
|
4
|
+
import pathUtil from 'path';
|
|
5
|
+
import { CMD_EXEC_PATH } from '../../constants.js';
|
|
6
|
+
const pageMetadataBySlug = {};
|
|
7
|
+
const getPossiblePageFilenames = (pagePath) => {
|
|
8
|
+
const normalizedPath = pagePath.startsWith('/') ? pagePath.slice(1) : pagePath;
|
|
9
|
+
if (normalizedPath.endsWith('.mdx') || normalizedPath.endsWith('.md')) {
|
|
10
|
+
return [normalizedPath];
|
|
11
|
+
}
|
|
12
|
+
return [`${normalizedPath}.mdx`, `${normalizedPath}.md`];
|
|
13
|
+
};
|
|
14
|
+
export const clearPageMetadataCache = () => {
|
|
15
|
+
for (const slug of Object.keys(pageMetadataBySlug)) {
|
|
16
|
+
delete pageMetadataBySlug[slug];
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
export const upsertPageMetadataCacheEntry = (slug, pageMetadata) => {
|
|
20
|
+
pageMetadataBySlug[slug] = pageMetadata;
|
|
21
|
+
};
|
|
22
|
+
export const removePageMetadataCacheEntry = (slug) => {
|
|
23
|
+
delete pageMetadataBySlug[slug];
|
|
24
|
+
};
|
|
25
|
+
export const removePageMetadataCacheEntryForFile = (filename) => {
|
|
26
|
+
removePageMetadataCacheEntry(filename.replace(/\.mdx?$/, ''));
|
|
27
|
+
};
|
|
28
|
+
// Returns true when the page references an OpenAPI/AsyncAPI spec via frontmatter.
|
|
29
|
+
// Those titles are derived from the schema, which the cache (built without spec
|
|
30
|
+
// files) cannot reproduce, so callers route such pages through the full rebuild.
|
|
31
|
+
export const upsertPageMetadataCacheEntryForFile = (filename, contentStr) => {
|
|
32
|
+
const { slug, pageMetadata } = getDecoratedNavPageAndSlug(filename, contentStr, [], []);
|
|
33
|
+
upsertPageMetadataCacheEntry(slug, pageMetadata);
|
|
34
|
+
return pageMetadata.openapi != null || pageMetadata.asyncapi != null;
|
|
35
|
+
};
|
|
36
|
+
// Returns true when any page in the navigation references an OpenAPI/AsyncAPI
|
|
37
|
+
// spec via frontmatter.
|
|
38
|
+
export const ensurePageMetadataCacheForDocsConfig = async (docsConfig) => {
|
|
39
|
+
const navPaths = getAllPathsInDocsNav(docsConfig.navigation);
|
|
40
|
+
const results = await Promise.all(navPaths.map(async (navPath) => {
|
|
41
|
+
for (const filename of getPossiblePageFilenames(navPath)) {
|
|
42
|
+
const filePath = pathUtil.join(CMD_EXEC_PATH, filename);
|
|
43
|
+
if (!(await fse.pathExists(filePath))) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const contentStr = (await fse.readFile(filePath)).toString();
|
|
47
|
+
return upsertPageMetadataCacheEntryForFile(filename, contentStr);
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}));
|
|
51
|
+
return results.some((hasApiSpecPage) => hasApiSpecPage);
|
|
52
|
+
};
|
|
53
|
+
export const generateDocsNavFromPageMetadataCache = (docsConfig) => {
|
|
54
|
+
return generateDecoratedDocsNavigationFromPages(pageMetadataBySlug, docsConfig.navigation);
|
|
55
|
+
};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { OpenApiFile } from '@mintlify/models';
|
|
2
|
+
import type { DecoratedNavigationPage } from '@mintlify/models';
|
|
2
3
|
import { DocsConfig } from '@mintlify/validation';
|
|
3
|
-
export declare const updateGeneratedNav: (onError?: (message: string) => void
|
|
4
|
+
export declare const updateGeneratedNav: (onError?: (message: string) => void, providedState?: {
|
|
5
|
+
pagesAcc: Record<string, DecoratedNavigationPage>;
|
|
6
|
+
docsConfig: DocsConfig;
|
|
7
|
+
}) => Promise<DocsConfig>;
|
|
8
|
+
export declare const updateGeneratedNavFromPageMetadataCache: (docsConfig: DocsConfig, onError?: (message: string) => void) => Promise<DocsConfig>;
|
|
4
9
|
export declare const updateOpenApiFiles: (providedOpenApiFiles?: OpenApiFile[], onError?: (message: string) => void) => Promise<void>;
|
|
5
10
|
export declare const upsertOpenApiFile: (openApiFile: OpenApiFile) => Promise<void>;
|
|
6
11
|
export declare const updateCustomLanguages: (docsConfig?: DocsConfig, onError?: (message: string) => void) => Promise<void>;
|
|
@@ -4,15 +4,32 @@ import { join } from 'path';
|
|
|
4
4
|
import { CMD_EXEC_PATH, NEXT_PROPS_PATH } from '../../constants.js';
|
|
5
5
|
import { generateNav } from './generate.js';
|
|
6
6
|
import { getDocsState } from './getDocsState.js';
|
|
7
|
+
import { ensurePageMetadataCacheForDocsConfig, generateDocsNavFromPageMetadataCache, } from './page-metadata-cache.js';
|
|
7
8
|
import { readJsonFile } from './utils.js';
|
|
8
|
-
|
|
9
|
-
const { pagesAcc, docsConfig } = await getDocsState(onError);
|
|
10
|
-
const generatedDocsNav = await generateNav(pagesAcc, docsConfig);
|
|
9
|
+
const writeGeneratedDocsNav = async (generatedDocsNav) => {
|
|
11
10
|
const targetDocsPath = join(NEXT_PROPS_PATH, 'generatedDocsNav.json');
|
|
12
11
|
await fse.outputFile(targetDocsPath, JSON.stringify(generatedDocsNav, null, 2), {
|
|
13
12
|
flag: 'w',
|
|
14
13
|
});
|
|
15
14
|
};
|
|
15
|
+
export const updateGeneratedNav = async (onError, providedState) => {
|
|
16
|
+
const { pagesAcc, docsConfig } = providedState ?? (await getDocsState(onError));
|
|
17
|
+
const generatedDocsNav = await generateNav(pagesAcc, docsConfig);
|
|
18
|
+
await writeGeneratedDocsNav(generatedDocsNav);
|
|
19
|
+
return docsConfig;
|
|
20
|
+
};
|
|
21
|
+
export const updateGeneratedNavFromPageMetadataCache = async (docsConfig, onError) => {
|
|
22
|
+
const hasApiSpecPage = await ensurePageMetadataCacheForDocsConfig(docsConfig);
|
|
23
|
+
if (hasApiSpecPage) {
|
|
24
|
+
// A navigation page references an OpenAPI/AsyncAPI spec via frontmatter, so
|
|
25
|
+
// its title is derived from the schema. The page metadata cache is built
|
|
26
|
+
// without spec files and can't reproduce those titles, so fall back to the
|
|
27
|
+
// full rebuild to match the non-fast-path (main branch) behavior.
|
|
28
|
+
return await updateGeneratedNav(onError);
|
|
29
|
+
}
|
|
30
|
+
await writeGeneratedDocsNav(generateDocsNavFromPageMetadataCache(docsConfig));
|
|
31
|
+
return docsConfig;
|
|
32
|
+
};
|
|
16
33
|
export const updateOpenApiFiles = async (providedOpenApiFiles, onError) => {
|
|
17
34
|
if (providedOpenApiFiles == undefined) {
|
|
18
35
|
const { openApiFiles } = await getDocsState(onError);
|
|
@@ -7,7 +7,7 @@ import { CMD_EXEC_PATH, NEXT_PUBLIC_PATH } from '../constants.js';
|
|
|
7
7
|
import { addLog, removeLastLog } from '../logging-state.js';
|
|
8
8
|
import { LaunchLog, UpdateLog } from '../logs.js';
|
|
9
9
|
import { maybeFixMissingWindowsEnvVar } from '../util.js';
|
|
10
|
-
import listener, { initializeFrontmatterHashCache, initializeImportCache } from './listener/index.js';
|
|
10
|
+
import listener, { initializeDocsConfigHashCache, initializeFrontmatterHashCache, initializeImportCache, } from './listener/index.js';
|
|
11
11
|
import { refreshTrackedReferencedFiles } from './listener/referencedFiles.js';
|
|
12
12
|
import { getLocalNetworkIp } from './network.js';
|
|
13
13
|
import { setupNext } from './setupNext.js';
|
|
@@ -146,6 +146,7 @@ export const run = async (argv) => {
|
|
|
146
146
|
});
|
|
147
147
|
await initializeImportCache(CMD_EXEC_PATH, argv.fileImportsMap);
|
|
148
148
|
await initializeFrontmatterHashCache();
|
|
149
|
+
await initializeDocsConfigHashCache();
|
|
149
150
|
await refreshTrackedReferencedFiles();
|
|
150
151
|
listener(onChange);
|
|
151
152
|
};
|