@mintlify/previewing 4.0.1151 → 4.0.1153

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,48 @@
1
+ import { findAndRemoveImports, stringifyTree } from '@mintlify/common';
2
+ import { preparseMdxTree } from '@mintlify/prebuild';
3
+ import fse from 'fs-extra';
4
+ import { mkdtemp, rm } from 'fs/promises';
5
+ import { tmpdir } from 'os';
6
+ import { join } from 'path';
7
+ const testPaths = vi.hoisted(() => ({
8
+ cmd: '',
9
+ public: '',
10
+ }));
11
+ vi.mock('../constants.js', () => ({
12
+ get CMD_EXEC_PATH() {
13
+ return testPaths.cmd;
14
+ },
15
+ get NEXT_PUBLIC_PATH() {
16
+ return testPaths.public;
17
+ },
18
+ }));
19
+ vi.mock('../local-preview/listener/utils.js', () => ({
20
+ getCurrentVariables: vi.fn(async () => ({ name: 'Ada' })),
21
+ handleParseError: vi.fn(),
22
+ }));
23
+ import { resolvePageImports } from '../local-preview/listener/resolve-page-imports.js';
24
+ describe('resolvePageImports', () => {
25
+ let root;
26
+ beforeEach(async () => {
27
+ root = await mkdtemp(join(tmpdir(), 'resolve-page-imports-'));
28
+ testPaths.cmd = join(root, 'docs');
29
+ testPaths.public = join(root, 'public');
30
+ await fse.ensureDir(testPaths.cmd);
31
+ await fse.ensureDir(testPaths.public);
32
+ });
33
+ afterEach(async () => {
34
+ await rm(root, { recursive: true, force: true });
35
+ });
36
+ it('resolves newly imported source snippets with variables and nested imports', async () => {
37
+ await fse.outputFile(join(testPaths.cmd, 'nested.mdx'), 'export const Nested = () => <span>{{name}}</span>;');
38
+ await fse.outputFile(join(testPaths.cmd, 'snippet.mdx'), "import { Nested } from './nested.mdx';\n\n<Nested />");
39
+ const pageContent = "import Snippet from './snippet.mdx';\n\n<Snippet />";
40
+ const tree = await preparseMdxTree(pageContent, testPaths.cmd, join(testPaths.cmd, 'page.mdx'), vi.fn());
41
+ const importsResponse = await findAndRemoveImports(tree);
42
+ const resolvedTree = await resolvePageImports({ ...importsResponse, filename: 'page.mdx' });
43
+ const resolvedContent = stringifyTree(resolvedTree);
44
+ expect(resolvedContent).toContain('Ada');
45
+ expect(resolvedContent).not.toContain('{{name}}');
46
+ expect(resolvedContent).not.toContain("import { Nested } from './nested.mdx'");
47
+ });
48
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { shouldRegenerateNavForPage } from '../local-preview/listener/utils.js';
2
+ import { seedFrontmatterHashForPage, shouldRegenerateNavForPage, } from '../local-preview/listener/utils.js';
3
3
  describe('shouldRegenerateNavForPage', () => {
4
4
  let frontmatterHashes;
5
5
  beforeEach(() => {
@@ -30,6 +30,13 @@ describe('shouldRegenerateNavForPage', () => {
30
30
  const result = await shouldRegenerateNavForPage('test.mdx', updatedContent, frontmatterHashes);
31
31
  expect(result).toBe(false);
32
32
  });
33
+ it('should return false for a content-only change after seeding frontmatter', async () => {
34
+ const originalContent = `---\ntitle: "Test Page"\ndescription: "A test page"\n---\n# Original Content`;
35
+ const updatedContent = `---\ntitle: "Test Page"\ndescription: "A test page"\n---\n# Updated Content`;
36
+ seedFrontmatterHashForPage('test.mdx', originalContent, frontmatterHashes);
37
+ const result = await shouldRegenerateNavForPage('test.mdx', updatedContent, frontmatterHashes);
38
+ expect(result).toBe(false);
39
+ });
33
40
  it('should handle files with no frontmatter', async () => {
34
41
  const contentNoFrontmatter = '# Just a heading\n\nSome content';
35
42
  await shouldRegenerateNavForPage('test.mdx', contentNoFrontmatter, frontmatterHashes);
@@ -3,5 +3,6 @@ declare const listener: (triggerRefresh: () => void, options?: {
3
3
  localSchema?: boolean;
4
4
  groups?: string[];
5
5
  }) => void;
6
- export { initializeImportCache };
6
+ declare const initializeFrontmatterHashCache: () => Promise<void>;
7
+ export { initializeFrontmatterHashCache, initializeImportCache };
7
8
  export default listener;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { findAndRemoveImports, hasImports, getFileCategory, validate, stringifyTree, isMintIgnored, replaceVariables, } from '@mintlify/common';
3
- import { createPage, MintConfigUpdater, DocsConfigUpdater, preparseMdxTree, prebuild, } from '@mintlify/prebuild';
3
+ import { createPage, getFileListSync, MintConfigUpdater, DocsConfigUpdater, preparseMdxTree, prebuild, } from '@mintlify/prebuild';
4
4
  import Chalk from 'chalk';
5
5
  import chokidar from 'chokidar';
6
6
  import { promises as _promises } from 'fs';
@@ -18,10 +18,28 @@ import { initializeImportCache, updateImportCacheForFile, removeFromImportCache,
18
18
  import { hasTrackedReferencedFile, refreshTrackedReferencedFiles } from './referencedFiles.js';
19
19
  import { regenerateAllSnippets } from './regenerateAllSnippets.js';
20
20
  import { resolveAllImports } from './resolveAllImports.js';
21
+ import { resolvePageImports } from './resolve-page-imports.js';
21
22
  import { updateCustomLanguages, updateGeneratedNav, updateOpenApiFiles, upsertOpenApiFile, } from './update.js';
22
- import { getCurrentVariables, getMintIgnoreGlobs, handleParseError, isFileSizeValid, isJsonValid, shouldRegenerateNavForPage, suppressParseError, } from './utils.js';
23
+ import { getCurrentVariables, getMintIgnoreGlobs, handleParseError, isFileSizeValid, isJsonValid, seedFrontmatterHashForPage, shouldRegenerateNavForPage, suppressParseError, } from './utils.js';
23
24
  const { readFile } = _promises;
24
25
  const frontmatterHashes = new Map();
26
+ const SLOW_UPDATE_LOG_THRESHOLD_MS = 1000;
27
+ const getElapsedMs = (startTime) => {
28
+ return Number((process.hrtime.bigint() - startTime) / 1000000n);
29
+ };
30
+ const formatElapsedSeconds = (durationMs) => {
31
+ return `${(durationMs / 1000).toFixed(2)}s`;
32
+ };
33
+ const shouldLogUpdateTiming = (durationMs) => {
34
+ return process.env.DEV_TIMING === 'true' || durationMs >= SLOW_UPDATE_LOG_THRESHOLD_MS;
35
+ };
36
+ const addUpdateTimingLog = (filename, category, durationMs) => {
37
+ if (!shouldLogUpdateTiming(durationMs)) {
38
+ return;
39
+ }
40
+ const categoryLabel = category == null ? 'uncategorized' : category;
41
+ addChangeLog(_jsx(InfoLog, { message: `processed ${filename} in ${formatElapsedSeconds(durationMs)} (${categoryLabel})` }));
42
+ };
25
43
  const listener = (triggerRefresh, options = {}) => {
26
44
  const mintIgnoreGlobs = getMintIgnoreGlobs();
27
45
  chokidar
@@ -47,7 +65,9 @@ const onAddEvent = async (filename, triggerRefresh, options) => {
47
65
  return;
48
66
  }
49
67
  try {
68
+ const startTime = process.hrtime.bigint();
50
69
  const category = await onUpdateEvent(filename, triggerRefresh, options);
70
+ addUpdateTimingLog(filename, category, getElapsedMs(startTime));
51
71
  if (category !== undefined) {
52
72
  addChangeLog(_jsx(AddedLog, { filename: filename }));
53
73
  }
@@ -61,7 +81,9 @@ const onChangeEvent = async (filename, triggerRefresh, options) => {
61
81
  return;
62
82
  }
63
83
  try {
84
+ const startTime = process.hrtime.bigint();
64
85
  const category = await onUpdateEvent(filename, triggerRefresh, options);
86
+ addUpdateTimingLog(filename, category, getElapsedMs(startTime));
65
87
  if (category !== undefined) {
66
88
  addChangeLog(_jsx(EditedLog, { filename: filename }));
67
89
  }
@@ -77,6 +99,7 @@ const onUnlinkEvent = async (filename, triggerRefresh, options) => {
77
99
  try {
78
100
  const importedFiles = getImportedFilesFromCache();
79
101
  const potentialCategory = getFileCategory(filename, { importedFiles });
102
+ frontmatterHashes.delete(filename);
80
103
  if (hasTrackedReferencedFile(filename)) {
81
104
  try {
82
105
  const { mintConfig, openApiFiles, docsConfig } = await getDocsState(handleParseError);
@@ -136,6 +159,7 @@ const onUnlinkEvent = async (filename, triggerRefresh, options) => {
136
159
  await fse.emptyDir(NEXT_PROPS_PATH);
137
160
  const prebuildResult = await prebuild(CMD_EXEC_PATH, options);
138
161
  await initializeImportCache(CMD_EXEC_PATH, prebuildResult?.fileImportsMap);
162
+ await initializeFrontmatterHashCache();
139
163
  await refreshTrackedReferencedFiles();
140
164
  }
141
165
  catch (err) {
@@ -193,6 +217,21 @@ const validateConfigFiles = async () => {
193
217
  console.error('⚠️ Error validating configuration files:', error);
194
218
  }
195
219
  };
220
+ const initializeFrontmatterHashCache = async () => {
221
+ frontmatterHashes.clear();
222
+ const importedFiles = getImportedFilesFromCache();
223
+ const pageFilenames = getFileListSync(CMD_EXEC_PATH).filter((file) => {
224
+ return getFileCategory(file, { importedFiles }) === 'page';
225
+ });
226
+ await Promise.all(pageFilenames.map(async (filename) => {
227
+ try {
228
+ const filePath = pathUtil.join(CMD_EXEC_PATH, filename);
229
+ const contentStr = (await readFile(filePath)).toString();
230
+ seedFrontmatterHashForPage(filename, contentStr, frontmatterHashes, suppressParseError);
231
+ }
232
+ catch { }
233
+ }));
234
+ };
196
235
  /**
197
236
  * This function is called when a file is added or changed
198
237
  * @param filename
@@ -244,7 +283,7 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
244
283
  const tree = await preparseMdxTree(contentStr, CMD_EXEC_PATH, filePath, suppressParseError);
245
284
  const importsResponse = await findAndRemoveImports(tree);
246
285
  if (hasImports(importsResponse)) {
247
- contentStr = stringifyTree(await resolveAllImports({ ...importsResponse, filename }));
286
+ contentStr = stringifyTree(await resolvePageImports({ ...importsResponse, filename }));
248
287
  }
249
288
  const { pageContent } = await createPage(filename, contentStr, CMD_EXEC_PATH, [], [], handleParseError);
250
289
  await fse.outputFile(targetPath, pageContent, {
@@ -311,6 +350,7 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
311
350
  await fse.emptyDir(NEXT_PROPS_PATH);
312
351
  const prebuildResult = await prebuild(CMD_EXEC_PATH, options);
313
352
  await initializeImportCache(CMD_EXEC_PATH, prebuildResult?.fileImportsMap);
353
+ await initializeFrontmatterHashCache();
314
354
  await refreshTrackedReferencedFiles();
315
355
  }
316
356
  catch (err) {
@@ -377,5 +417,5 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
377
417
  triggerRefresh();
378
418
  return category;
379
419
  };
380
- export { initializeImportCache };
420
+ export { initializeFrontmatterHashCache, initializeImportCache };
381
421
  export default listener;
@@ -0,0 +1,3 @@
1
+ import type { FileWithImports } from '@mintlify/common';
2
+ import type { Root } from 'mdast';
3
+ export declare const resolvePageImports: (fileWithImports: FileWithImports) => Promise<Root>;
@@ -0,0 +1,98 @@
1
+ import { findAndRemoveImports, hasImports, optionallyAddLeadingSlash, optionallyRemoveLeadingSlash, replaceVariables, resolveAllImports as baseResolveAllImports, resolveImportPath, topologicalSort, } from '@mintlify/common';
2
+ import { preparseMdxTree } from '@mintlify/prebuild';
3
+ import fse from 'fs-extra';
4
+ import { join } from 'path';
5
+ import { CMD_EXEC_PATH, NEXT_PUBLIC_PATH } from '../../constants.js';
6
+ import { getCurrentVariables, handleParseError } from './utils.js';
7
+ const getImportSourcePath = async (filename) => {
8
+ const generatedPath = join(NEXT_PUBLIC_PATH, filename);
9
+ if (await fse.pathExists(generatedPath)) {
10
+ return {
11
+ baseDir: NEXT_PUBLIC_PATH,
12
+ path: generatedPath,
13
+ };
14
+ }
15
+ const originalPath = join(CMD_EXEC_PATH, filename);
16
+ if (await fse.pathExists(originalPath)) {
17
+ return {
18
+ baseDir: CMD_EXEC_PATH,
19
+ path: originalPath,
20
+ };
21
+ }
22
+ return null;
23
+ };
24
+ const parseSnippetWithImports = async ({ filename, variables, snippetsByFilename, }) => {
25
+ const normalizedFilename = optionallyAddLeadingSlash(filename);
26
+ if (snippetsByFilename.has(normalizedFilename)) {
27
+ return;
28
+ }
29
+ const importSource = await getImportSourcePath(optionallyRemoveLeadingSlash(normalizedFilename));
30
+ if (importSource == null) {
31
+ return;
32
+ }
33
+ const content = replaceVariables((await fse.readFile(importSource.path)).toString(), variables);
34
+ const tree = await preparseMdxTree(content, importSource.baseDir, importSource.path, handleParseError);
35
+ const importData = await findAndRemoveImports(tree);
36
+ const fileWithImports = {
37
+ filename: normalizedFilename,
38
+ ...importData,
39
+ };
40
+ snippetsByFilename.set(normalizedFilename, fileWithImports);
41
+ await Promise.all(Object.keys(importData.importMap).map(async (source) => {
42
+ const resolvedPath = resolveImportPath(source, normalizedFilename);
43
+ if (resolvedPath == null) {
44
+ return;
45
+ }
46
+ await parseSnippetWithImports({
47
+ filename: resolvedPath,
48
+ variables,
49
+ snippetsByFilename,
50
+ });
51
+ }));
52
+ };
53
+ const resolveSnippetImports = async (snippetsByFilename) => {
54
+ const graph = {};
55
+ for (const snippet of snippetsByFilename.values()) {
56
+ graph[snippet.filename] = Object.keys(snippet.importMap)
57
+ .map((source) => resolveImportPath(source, snippet.filename))
58
+ .filter((resolvedPath) => {
59
+ return resolvedPath != null && snippetsByFilename.has(resolvedPath);
60
+ });
61
+ }
62
+ const orderedFilenames = topologicalSort(graph).reverse();
63
+ const resolvedSnippets = [];
64
+ for (const filename of orderedFilenames) {
65
+ const snippet = snippetsByFilename.get(filename);
66
+ if (snippet == null) {
67
+ continue;
68
+ }
69
+ const tree = hasImports(snippet)
70
+ ? await baseResolveAllImports({
71
+ snippets: resolvedSnippets,
72
+ fileWithImports: snippet,
73
+ })
74
+ : snippet.tree;
75
+ resolvedSnippets.push({ filename: snippet.filename, tree });
76
+ }
77
+ return resolvedSnippets;
78
+ };
79
+ export const resolvePageImports = async (fileWithImports) => {
80
+ const variables = await getCurrentVariables();
81
+ const snippetsByFilename = new Map();
82
+ await Promise.all(Object.keys(fileWithImports.importMap).map(async (source) => {
83
+ const resolvedPath = resolveImportPath(source, fileWithImports.filename);
84
+ if (resolvedPath == null) {
85
+ return;
86
+ }
87
+ await parseSnippetWithImports({
88
+ filename: resolvedPath,
89
+ variables,
90
+ snippetsByFilename,
91
+ });
92
+ }));
93
+ const snippets = await resolveSnippetImports(snippetsByFilename);
94
+ return await baseResolveAllImports({
95
+ snippets,
96
+ fileWithImports,
97
+ });
98
+ };
@@ -8,6 +8,7 @@ export declare const isJsonValid: (filePath: string) => {
8
8
  error?: string;
9
9
  };
10
10
  export declare const shouldRegenerateNavForPage: (filename: string, contentStr: string, frontmatterHashes: Map<string, string>, onError?: (message: string) => void) => Promise<boolean>;
11
+ export declare const seedFrontmatterHashForPage: (filename: string, contentStr: string, frontmatterHashes: Map<string, string>, onError?: (message: string) => void) => boolean;
11
12
  export declare function normalizePathForComparison(filePath: string): string;
12
13
  export declare const handleParseError: (message: string) => void;
13
14
  export declare const suppressParseError: () => void;
@@ -45,16 +45,8 @@ export const isJsonValid = (filePath) => {
45
45
  };
46
46
  export const shouldRegenerateNavForPage = async (filename, contentStr, frontmatterHashes, onError) => {
47
47
  try {
48
- const { attributes: currentFrontmatter } = parseFrontmatter(contentStr);
49
48
  const prevFrontmatterHash = frontmatterHashes.get(filename);
50
- const currentFrontmatterHash = crypto
51
- .createHash('md5')
52
- .update(JSON.stringify(currentFrontmatter))
53
- .digest('hex');
54
- if (!prevFrontmatterHash) {
55
- frontmatterHashes.set(filename, currentFrontmatterHash);
56
- return true;
57
- }
49
+ const currentFrontmatterHash = getFrontmatterHash(contentStr);
58
50
  if (currentFrontmatterHash !== prevFrontmatterHash) {
59
51
  frontmatterHashes.set(filename, currentFrontmatterHash);
60
52
  return true;
@@ -73,6 +65,26 @@ export const shouldRegenerateNavForPage = async (filename, contentStr, frontmatt
73
65
  return true;
74
66
  }
75
67
  };
68
+ export const seedFrontmatterHashForPage = (filename, contentStr, frontmatterHashes, onError) => {
69
+ try {
70
+ frontmatterHashes.set(filename, getFrontmatterHash(contentStr));
71
+ return true;
72
+ }
73
+ catch (error) {
74
+ const message = `Error parsing frontmatter for ${filename}, skipping initial nav cache: ${error instanceof Error ? error.message : String(error)}`;
75
+ if (onError) {
76
+ onError(message);
77
+ }
78
+ else {
79
+ console.warn(message);
80
+ }
81
+ return false;
82
+ }
83
+ };
84
+ const getFrontmatterHash = (contentStr) => {
85
+ const { attributes: currentFrontmatter } = parseFrontmatter(contentStr);
86
+ return crypto.createHash('md5').update(JSON.stringify(currentFrontmatter)).digest('hex');
87
+ };
76
88
  export function normalizePathForComparison(filePath) {
77
89
  return optionallyAddLeadingSlash(filePath).toLowerCase();
78
90
  }
@@ -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, { initializeImportCache } from './listener/index.js';
10
+ import listener, { 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';
@@ -145,6 +145,7 @@ export const run = async (argv) => {
145
145
  process.on('SIGTERM', onExit);
146
146
  });
147
147
  await initializeImportCache(CMD_EXEC_PATH, argv.fileImportsMap);
148
+ await initializeFrontmatterHashCache();
148
149
  await refreshTrackedReferencedFiles();
149
150
  listener(onChange);
150
151
  };