@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.
@@ -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
- export { initializeFrontmatterHashCache, initializeImportCache };
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
- regenerateNav = await shouldRegenerateNavForPage(filename, contentStr, frontmatterHashes, handleParseError);
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 (regenerateNav) {
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) => Promise<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
- export const updateGeneratedNav = async (onError) => {
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
  };