@mintlify/previewing 4.0.1227 → 4.0.1229
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__/page-metadata-cache.test.js +48 -1
- package/dist/local-preview/listener/generate.js +9 -10
- package/dist/local-preview/listener/generatePagesWithImports.js +26 -20
- package/dist/local-preview/listener/index.js +7 -10
- package/dist/local-preview/listener/page-metadata-cache.d.ts +2 -1
- package/dist/local-preview/listener/page-metadata-cache.js +60 -6
- package/dist/local-preview/listener/update.d.ts +4 -2
- package/dist/local-preview/listener/update.js +14 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { clearPageMetadataCache, generateDocsNavFromPageMetadataCache, upsertPageMetadataCacheEntryForFile, } from '../local-preview/listener/page-metadata-cache.js';
|
|
2
|
+
import { clearPageMetadataCache, ensurePageMetadataCacheForDocsConfig, generateDocsNavFromPageMetadataCache, hydratePageMetadataCacheFromGeneratedNav, upsertPageMetadataCacheEntryForFile, } from '../local-preview/listener/page-metadata-cache.js';
|
|
3
3
|
const createDocsConfig = (pages) => ({
|
|
4
4
|
$schema: 'https://mintlify.com/docs.json',
|
|
5
5
|
name: 'Mintlify',
|
|
@@ -48,4 +48,51 @@ describe('pageMetadataCache', () => {
|
|
|
48
48
|
],
|
|
49
49
|
});
|
|
50
50
|
});
|
|
51
|
+
it('reuses generated metadata for API pages during navigation updates', async () => {
|
|
52
|
+
hydratePageMetadataCacheFromGeneratedNav({
|
|
53
|
+
pages: [
|
|
54
|
+
{
|
|
55
|
+
href: '/endpoint',
|
|
56
|
+
title: 'List pets',
|
|
57
|
+
openapi: 'GET /pets',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
const docsConfig = createDocsConfig(['endpoint']);
|
|
62
|
+
expect(await ensurePageMetadataCacheForDocsConfig(docsConfig)).toBe(false);
|
|
63
|
+
expect(generateDocsNavFromPageMetadataCache(docsConfig)).toMatchObject({
|
|
64
|
+
pages: [
|
|
65
|
+
{
|
|
66
|
+
href: '/endpoint',
|
|
67
|
+
title: 'List pets',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
it('invalidates generated metadata when API frontmatter changes', async () => {
|
|
73
|
+
hydratePageMetadataCacheFromGeneratedNav({
|
|
74
|
+
pages: [
|
|
75
|
+
{
|
|
76
|
+
href: '/endpoint',
|
|
77
|
+
title: 'List pets',
|
|
78
|
+
openapi: 'GET /pets',
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
upsertPageMetadataCacheEntryForFile('endpoint.mdx', '---\nopenapi: GET /animals\n---');
|
|
83
|
+
expect(await ensurePageMetadataCacheForDocsConfig(createDocsConfig(['endpoint']))).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
it('removes generated metadata missing from the latest navigation', async () => {
|
|
86
|
+
hydratePageMetadataCacheFromGeneratedNav({
|
|
87
|
+
pages: [
|
|
88
|
+
{
|
|
89
|
+
href: '/removed',
|
|
90
|
+
title: 'Removed title',
|
|
91
|
+
openapi: 'GET /removed',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
hydratePageMetadataCacheFromGeneratedNav({ pages: [] });
|
|
96
|
+
expect(await ensurePageMetadataCacheForDocsConfig(createDocsConfig(['removed']))).toBe(false);
|
|
97
|
+
});
|
|
51
98
|
});
|
|
@@ -4,20 +4,19 @@ import { join } from 'path';
|
|
|
4
4
|
import { CMD_EXEC_PATH } from '../../constants.js';
|
|
5
5
|
import { handleParseError } from './utils.js';
|
|
6
6
|
const { readFile } = _promises;
|
|
7
|
+
const PAGE_METADATA_CONCURRENCY = 8;
|
|
7
8
|
const createFilenamePageMetadataMap = async ({ contentDirectoryPath, contentFilenames, openApiFiles, asyncApiFiles, pagesAcc = {}, }) => {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const filenames = contentFilenames.values();
|
|
10
|
+
const processNext = async () => {
|
|
11
|
+
for (const filename of filenames) {
|
|
11
12
|
const sourcePath = join(contentDirectoryPath, filename);
|
|
12
13
|
const contentStr = (await readFile(sourcePath)).toString();
|
|
13
14
|
const { slug, pageMetadata } = await createPage(filename, contentStr, contentDirectoryPath, openApiFiles, asyncApiFiles, handleParseError);
|
|
14
|
-
pagesAcc =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
});
|
|
20
|
-
await Promise.all(contentPromises);
|
|
15
|
+
pagesAcc[slug] = pageMetadata;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const workerCount = Math.min(PAGE_METADATA_CONCURRENCY, contentFilenames.length);
|
|
19
|
+
await Promise.all(Array.from({ length: workerCount }, processNext));
|
|
21
20
|
return pagesAcc;
|
|
22
21
|
};
|
|
23
22
|
export const generateNav = async (pagesAcc, docsConfig) => {
|
|
@@ -8,6 +8,7 @@ import { getFilesImportingPathFromCache, getImportedFilesFromCache } from './imp
|
|
|
8
8
|
import { resolvePageImports } from './resolve-page-imports.js';
|
|
9
9
|
import { handleParseError } from './utils.js';
|
|
10
10
|
const { readFile } = _promises;
|
|
11
|
+
const PAGE_REGENERATION_CONCURRENCY = 8;
|
|
11
12
|
const getAffectedPageFilenames = (updatedSnippets) => {
|
|
12
13
|
const importedFiles = getImportedFilesFromCache();
|
|
13
14
|
const pageFilenames = new Set();
|
|
@@ -23,25 +24,30 @@ const getAffectedPageFilenames = (updatedSnippets) => {
|
|
|
23
24
|
};
|
|
24
25
|
export const generatePagesWithImports = async (updatedSnippets) => {
|
|
25
26
|
const pageFilenames = getAffectedPageFilenames(updatedSnippets);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
27
|
+
const filenames = pageFilenames.values();
|
|
28
|
+
const processNext = async () => {
|
|
29
|
+
for (const pageFilename of filenames) {
|
|
30
|
+
const sourcePath = join(CMD_EXEC_PATH, pageFilename);
|
|
31
|
+
const contentStr = (await readFile(sourcePath)).toString();
|
|
32
|
+
try {
|
|
33
|
+
const tree = await preparseMdxTree(contentStr, CMD_EXEC_PATH, sourcePath, handleParseError);
|
|
34
|
+
const importsResponse = await findAndRemoveImports(tree);
|
|
35
|
+
const content = await resolvePageImports({
|
|
36
|
+
...importsResponse,
|
|
37
|
+
filename: pageFilename,
|
|
38
|
+
});
|
|
39
|
+
const targetPath = join(NEXT_PROPS_PATH, pageFilename);
|
|
40
|
+
await outputFile(targetPath, stringifyTree(content), {
|
|
41
|
+
flag: 'w',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.log('Error generating pages with imports');
|
|
46
|
+
console.log(pageFilename);
|
|
47
|
+
console.log(err);
|
|
48
|
+
}
|
|
45
49
|
}
|
|
46
|
-
}
|
|
50
|
+
};
|
|
51
|
+
const workerCount = Math.min(PAGE_REGENERATION_CONCURRENCY, pageFilenames.size);
|
|
52
|
+
await Promise.all(Array.from({ length: workerCount }, processNext));
|
|
47
53
|
};
|
|
@@ -63,9 +63,9 @@ const getCurrentRawDocsConfig = async () => {
|
|
|
63
63
|
return undefined;
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
|
-
const hasDocsConfigApiRoutes = (docsConfig) =>
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
const hasDocsConfigApiRoutes = (docsConfig) => docsConfig.api?.openapi != null ||
|
|
67
|
+
docsConfig.api?.asyncapi != null ||
|
|
68
|
+
hasNavigationApiReference(docsConfig.navigation);
|
|
69
69
|
const getElapsedMs = (startTime) => {
|
|
70
70
|
return Number((process.hrtime.bigint() - startTime) / 1000000n);
|
|
71
71
|
};
|
|
@@ -321,6 +321,9 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
|
|
|
321
321
|
const importedFiles = getImportedFilesFromCache();
|
|
322
322
|
const potentialCategory = getFileCategory(filename, { importedFiles });
|
|
323
323
|
if (hasTrackedReferencedFile(filename)) {
|
|
324
|
+
if (await fse.pathExists(pathUtil.join(CMD_EXEC_PATH, 'docs.json'))) {
|
|
325
|
+
return onUpdateEvent('docs.json', triggerRefresh, options);
|
|
326
|
+
}
|
|
324
327
|
try {
|
|
325
328
|
const { mintConfig, openApiFiles, docsConfig, pagesAcc } = await getDocsState(handleParseError);
|
|
326
329
|
await refreshTrackedReferencedFiles();
|
|
@@ -339,12 +342,6 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
|
|
|
339
342
|
}
|
|
340
343
|
const updatedSnippets = await regenerateAllSnippets();
|
|
341
344
|
await generatePagesWithImports(new Set(updatedSnippets));
|
|
342
|
-
// A tracked $ref change rebuilds config-derived artifacts; keep every docs
|
|
343
|
-
// config hash in sync with the same raw docs.json source used by save checks.
|
|
344
|
-
const rawDocsConfig = await getCurrentRawDocsConfig();
|
|
345
|
-
if (rawDocsConfig) {
|
|
346
|
-
updateDocsConfigHashCache(rawDocsConfig);
|
|
347
|
-
}
|
|
348
345
|
triggerRefresh();
|
|
349
346
|
return null;
|
|
350
347
|
}
|
|
@@ -443,7 +440,7 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
|
|
|
443
440
|
// spec via frontmatter, this falls back to the full rebuild and
|
|
444
441
|
// returns the API-expanded config, which we then persist.
|
|
445
442
|
await updateCustomLanguages(docsConfig, suppressParseError);
|
|
446
|
-
const generatedDocsConfig = await updateGeneratedNavFromPageMetadataCache(docsConfig, suppressParseError);
|
|
443
|
+
const generatedDocsConfig = await updateGeneratedNavFromPageMetadataCache(docsConfig, suppressParseError, { reuseGeneratedMetadata: true });
|
|
447
444
|
await DocsConfigUpdater.writeConfigFile(generatedDocsConfig, CLIENT_PATH);
|
|
448
445
|
}
|
|
449
446
|
else if (shouldRegenerateGeneratedData) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DecoratedNavigationPage } from '@mintlify/models';
|
|
2
|
-
import type
|
|
2
|
+
import { type DecoratedNavigationConfig, type DocsConfig } from '@mintlify/validation';
|
|
3
|
+
export declare const hydratePageMetadataCacheFromGeneratedNav: (generatedDocsNav: DecoratedNavigationConfig) => void;
|
|
3
4
|
export declare const clearPageMetadataCache: () => void;
|
|
4
5
|
export declare const upsertPageMetadataCacheEntry: (slug: string, pageMetadata: DecoratedNavigationPage) => void;
|
|
5
6
|
export declare const removePageMetadataCacheEntry: (slug: string) => void;
|
|
@@ -1,9 +1,47 @@
|
|
|
1
|
-
import { getAllPathsInDocsNav, getDecoratedNavPageAndSlug } from '@mintlify/common';
|
|
1
|
+
import { getAllPathsInDocsNav, getDecoratedNavPageAndSlug, optionallyRemoveLeadingSlash, optionallyRemoveTrailingSlash, } from '@mintlify/common';
|
|
2
2
|
import { generateDecoratedDocsNavigationFromPages } from '@mintlify/prebuild';
|
|
3
|
+
import { divisions } from '@mintlify/validation';
|
|
3
4
|
import fse from 'fs-extra';
|
|
4
5
|
import pathUtil from 'path';
|
|
5
6
|
import { CMD_EXEC_PATH } from '../../constants.js';
|
|
6
7
|
const pageMetadataBySlug = {};
|
|
8
|
+
const generatedPageMetadataSlugs = new Set();
|
|
9
|
+
const navigationChildKeys = new Set(['root', 'pages', 'groups', ...divisions]);
|
|
10
|
+
const isDecoratedNavigationPage = (value) => {
|
|
11
|
+
return (value != null &&
|
|
12
|
+
typeof value === 'object' &&
|
|
13
|
+
'href' in value &&
|
|
14
|
+
typeof value.href === 'string' &&
|
|
15
|
+
'title' in value &&
|
|
16
|
+
typeof value.title === 'string');
|
|
17
|
+
};
|
|
18
|
+
export const hydratePageMetadataCacheFromGeneratedNav = (generatedDocsNav) => {
|
|
19
|
+
for (const slug of generatedPageMetadataSlugs) {
|
|
20
|
+
delete pageMetadataBySlug[slug];
|
|
21
|
+
}
|
|
22
|
+
generatedPageMetadataSlugs.clear();
|
|
23
|
+
const hydrate = (value) => {
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
value.forEach(hydrate);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (value == null || typeof value !== 'object') {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (isDecoratedNavigationPage(value)) {
|
|
32
|
+
const slug = optionallyRemoveTrailingSlash(optionallyRemoveLeadingSlash(value.href));
|
|
33
|
+
pageMetadataBySlug[slug] = value;
|
|
34
|
+
generatedPageMetadataSlugs.add(slug);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const [key, child] of Object.entries(value)) {
|
|
38
|
+
if (navigationChildKeys.has(key)) {
|
|
39
|
+
hydrate(child);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
hydrate(generatedDocsNav);
|
|
44
|
+
};
|
|
7
45
|
const getPossiblePageFilenames = (pagePath) => {
|
|
8
46
|
const normalizedPath = pagePath.startsWith('/') ? pagePath.slice(1) : pagePath;
|
|
9
47
|
if (normalizedPath.endsWith('.mdx') || normalizedPath.endsWith('.md')) {
|
|
@@ -15,12 +53,15 @@ export const clearPageMetadataCache = () => {
|
|
|
15
53
|
for (const slug of Object.keys(pageMetadataBySlug)) {
|
|
16
54
|
delete pageMetadataBySlug[slug];
|
|
17
55
|
}
|
|
56
|
+
generatedPageMetadataSlugs.clear();
|
|
18
57
|
};
|
|
19
58
|
export const upsertPageMetadataCacheEntry = (slug, pageMetadata) => {
|
|
20
59
|
pageMetadataBySlug[slug] = pageMetadata;
|
|
60
|
+
generatedPageMetadataSlugs.delete(slug);
|
|
21
61
|
};
|
|
22
62
|
export const removePageMetadataCacheEntry = (slug) => {
|
|
23
63
|
delete pageMetadataBySlug[slug];
|
|
64
|
+
generatedPageMetadataSlugs.delete(slug);
|
|
24
65
|
};
|
|
25
66
|
export const removePageMetadataCacheEntryForFile = (filename) => {
|
|
26
67
|
removePageMetadataCacheEntry(filename.replace(/\.mdx?$/, ''));
|
|
@@ -37,18 +78,31 @@ export const upsertPageMetadataCacheEntryForFile = (filename, contentStr) => {
|
|
|
37
78
|
// spec via frontmatter.
|
|
38
79
|
export const ensurePageMetadataCacheForDocsConfig = async (docsConfig) => {
|
|
39
80
|
const navPaths = getAllPathsInDocsNav(docsConfig.navigation);
|
|
40
|
-
const
|
|
81
|
+
for (const navPath of navPaths) {
|
|
82
|
+
const normalizedPath = optionallyRemoveTrailingSlash(optionallyRemoveLeadingSlash(navPath));
|
|
83
|
+
if (generatedPageMetadataSlugs.has(normalizedPath)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const cachedPageMetadata = pageMetadataBySlug[normalizedPath];
|
|
87
|
+
if (cachedPageMetadata) {
|
|
88
|
+
if (cachedPageMetadata.openapi != null || cachedPageMetadata.asyncapi != null) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
41
93
|
for (const filename of getPossiblePageFilenames(navPath)) {
|
|
42
94
|
const filePath = pathUtil.join(CMD_EXEC_PATH, filename);
|
|
43
95
|
if (!(await fse.pathExists(filePath))) {
|
|
44
96
|
continue;
|
|
45
97
|
}
|
|
46
98
|
const contentStr = (await fse.readFile(filePath)).toString();
|
|
47
|
-
|
|
99
|
+
if (upsertPageMetadataCacheEntryForFile(filename, contentStr)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
48
103
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return results.some((hasApiSpecPage) => hasApiSpecPage);
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
52
106
|
};
|
|
53
107
|
export const generateDocsNavFromPageMetadataCache = (docsConfig) => {
|
|
54
108
|
return generateDecoratedDocsNavigationFromPages(pageMetadataBySlug, docsConfig.navigation);
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { OpenApiFile } from '@mintlify/models';
|
|
2
2
|
import type { DecoratedNavigationPage } from '@mintlify/models';
|
|
3
|
-
import { DocsConfig } from '@mintlify/validation';
|
|
3
|
+
import type { DocsConfig } from '@mintlify/validation';
|
|
4
4
|
export declare const updateGeneratedNav: (onError?: (message: string) => void, providedState?: {
|
|
5
5
|
pagesAcc: Record<string, DecoratedNavigationPage>;
|
|
6
6
|
docsConfig: DocsConfig;
|
|
7
7
|
}) => Promise<DocsConfig>;
|
|
8
|
-
export declare const updateGeneratedNavFromPageMetadataCache: (docsConfig: DocsConfig, onError?: (message: string) => void
|
|
8
|
+
export declare const updateGeneratedNavFromPageMetadataCache: (docsConfig: DocsConfig, onError?: (message: string) => void, options?: {
|
|
9
|
+
reuseGeneratedMetadata?: boolean;
|
|
10
|
+
}) => Promise<DocsConfig>;
|
|
9
11
|
export declare const updateOpenApiFiles: (providedOpenApiFiles?: OpenApiFile[], onError?: (message: string) => void) => Promise<void>;
|
|
10
12
|
export declare const upsertOpenApiFile: (openApiFile: OpenApiFile) => Promise<void>;
|
|
11
13
|
export declare const updateCustomLanguages: (docsConfig?: DocsConfig, onError?: (message: string) => void) => Promise<void>;
|
|
@@ -4,7 +4,7 @@ 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
|
+
import { ensurePageMetadataCacheForDocsConfig, generateDocsNavFromPageMetadataCache, hydratePageMetadataCacheFromGeneratedNav, } from './page-metadata-cache.js';
|
|
8
8
|
import { readJsonFile } from './utils.js';
|
|
9
9
|
const writeGeneratedDocsNav = async (generatedDocsNav) => {
|
|
10
10
|
const targetDocsPath = join(NEXT_PROPS_PATH, 'generatedDocsNav.json');
|
|
@@ -18,7 +18,19 @@ export const updateGeneratedNav = async (onError, providedState) => {
|
|
|
18
18
|
await writeGeneratedDocsNav(generatedDocsNav);
|
|
19
19
|
return docsConfig;
|
|
20
20
|
};
|
|
21
|
-
export const updateGeneratedNavFromPageMetadataCache = async (docsConfig, onError) => {
|
|
21
|
+
export const updateGeneratedNavFromPageMetadataCache = async (docsConfig, onError, options = {}) => {
|
|
22
|
+
if (options.reuseGeneratedMetadata) {
|
|
23
|
+
try {
|
|
24
|
+
const existingGeneratedDocsNav = await readJsonFile(join(NEXT_PROPS_PATH, 'generatedDocsNav.json'));
|
|
25
|
+
hydratePageMetadataCacheFromGeneratedNav(existingGeneratedDocsNav);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const isMissingFile = error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
29
|
+
if (!isMissingFile) {
|
|
30
|
+
console.warn(`Failed to reuse generated navigation metadata: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
22
34
|
const hasApiSpecPage = await ensurePageMetadataCacheForDocsConfig(docsConfig);
|
|
23
35
|
if (hasApiSpecPage) {
|
|
24
36
|
// A navigation page references an OpenAPI/AsyncAPI spec via frontmatter, so
|