@mintlify/previewing 4.0.1365 → 4.0.1367

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.
@@ -18,6 +18,7 @@ vi.mock('../constants.js', () => ({
18
18
  }));
19
19
  vi.mock('../local-preview/listener/utils.js', () => ({
20
20
  getCurrentVariables: vi.fn(async () => ({ name: 'Ada' })),
21
+ getMintIgnoreGlobs: vi.fn(() => []),
21
22
  handleParseError: vi.fn(),
22
23
  normalizePathForComparison: (path) => {
23
24
  const normalized = path.startsWith('/') ? path : `/${path}`;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,82 @@
1
+ import chokidar from 'chokidar';
2
+ import { appendFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { createPreviewPathIgnored } from '../local-preview/listener/previewPathIgnored.js';
6
+ const watchRoot = (root, globs) => chokidar.watch(root, {
7
+ ignoreInitial: true,
8
+ ignored: createPreviewPathIgnored(root, globs),
9
+ cwd: root,
10
+ });
11
+ const waitForReady = (watcher) => new Promise((resolve) => {
12
+ watcher.once('ready', () => resolve());
13
+ });
14
+ describe('createPreviewPathIgnored', () => {
15
+ let root;
16
+ beforeEach(async () => {
17
+ root = await mkdtemp(path.join(os.tmpdir(), 'mint-preview-ignore-unit-'));
18
+ });
19
+ afterEach(async () => {
20
+ await rm(root, { recursive: true, force: true });
21
+ });
22
+ it('skips a directory named junk for junk/ even when stats are omitted', async () => {
23
+ await mkdir(path.join(root, 'junk'));
24
+ const ignored = createPreviewPathIgnored(root, ['junk/']);
25
+ expect(ignored('junk')).toBe(true);
26
+ expect(ignored('junk', { isDirectory: () => true })).toBe(true);
27
+ expect(ignored('page.mdx')).toBe(false);
28
+ expect(ignored('.mintlify/skills', { isDirectory: () => true })).toBe(false);
29
+ });
30
+ it('does not treat a regular file named junk as the ignored junk/ directory', async () => {
31
+ await writeFile(path.join(root, 'junk'), 'notes\n');
32
+ const ignored = createPreviewPathIgnored(root, ['junk/']);
33
+ expect(ignored('junk')).toBe(false);
34
+ expect(ignored('junk', { isDirectory: () => false })).toBe(false);
35
+ expect(ignored('junk', { isDirectory: () => true })).toBe(true);
36
+ });
37
+ });
38
+ describe('preview watcher ignore restart', () => {
39
+ let root;
40
+ let watcher;
41
+ beforeEach(async () => {
42
+ root = await mkdtemp(path.join(os.tmpdir(), 'mint-preview-ignore-'));
43
+ await mkdir(path.join(root, 'junk'), { recursive: true });
44
+ await writeFile(path.join(root, 'page.mdx'), '# Page\n');
45
+ await writeFile(path.join(root, 'junk', 'hidden.mdx'), '# Hidden\n');
46
+ });
47
+ afterEach(async () => {
48
+ if (watcher) {
49
+ await watcher.close();
50
+ watcher = undefined;
51
+ }
52
+ await rm(root, { recursive: true, force: true });
53
+ });
54
+ it('starts watching a previously ignored directory after the matcher is rebuilt', async () => {
55
+ watcher = watchRoot(root, ['junk/']);
56
+ await waitForReady(watcher);
57
+ const ignoredChanges = [];
58
+ watcher.on('change', (filename) => ignoredChanges.push(filename));
59
+ await appendFile(path.join(root, 'junk', 'hidden.mdx'), 'ignored\n');
60
+ await new Promise((resolve) => setTimeout(resolve, 400));
61
+ expect(ignoredChanges).toEqual([]);
62
+ await watcher.close();
63
+ watcher = watchRoot(root, []);
64
+ await waitForReady(watcher);
65
+ const includedChanges = new Promise((resolve) => {
66
+ watcher?.once('change', (filename) => resolve(filename));
67
+ });
68
+ await appendFile(path.join(root, 'junk', 'hidden.mdx'), 'watched\n');
69
+ await expect(includedChanges).resolves.toMatch(/hidden\.mdx$/);
70
+ }, 10_000);
71
+ it('watches a regular file named junk while junk/ is ignored', async () => {
72
+ await rm(path.join(root, 'junk'), { recursive: true, force: true });
73
+ await writeFile(path.join(root, 'junk'), 'notes\n');
74
+ watcher = watchRoot(root, ['junk/']);
75
+ await waitForReady(watcher);
76
+ const includedChanges = new Promise((resolve) => {
77
+ watcher?.once('change', (filename) => resolve(filename));
78
+ });
79
+ await appendFile(path.join(root, 'junk'), 'watched\n');
80
+ await expect(includedChanges).resolves.toMatch(/junk$/);
81
+ }, 10_000);
82
+ });
@@ -4,12 +4,12 @@ import { promises as _promises } from 'fs';
4
4
  import { join } from 'path';
5
5
  import { CMD_EXEC_PATH, NEXT_PUBLIC_PATH } from '../../constants.js';
6
6
  import { getImportedFilesFromCache } from './importCache.js';
7
- import { handleParseError, getCurrentVariables } from './utils.js';
7
+ import { getMintIgnoreGlobs, handleParseError, getCurrentVariables } from './utils.js';
8
8
  const { readFile } = _promises;
9
9
  const getSnippetBase = async (baseDir) => {
10
10
  const importedFiles = getImportedFilesFromCache();
11
11
  const variables = await getCurrentVariables();
12
- const allSnippetFilenames = getFileListSync(baseDir).filter((file) => {
12
+ const allSnippetFilenames = getFileListSync(baseDir, baseDir, getMintIgnoreGlobs()).filter((file) => {
13
13
  if (!isSnippetExtension(getFileExtension(file))) {
14
14
  return false;
15
15
  }
@@ -5,7 +5,7 @@ import fse from 'fs-extra';
5
5
  import { join } from 'path';
6
6
  import { CMD_EXEC_PATH, NEXT_PROPS_PATH, NEXT_PUBLIC_PATH } from '../../constants.js';
7
7
  import { resolveAllImports } from './resolveAllImports.js';
8
- import { handleParseError, normalizePathForComparison, suppressParseError } from './utils.js';
8
+ import { getMintIgnoreGlobs, handleParseError, normalizePathForComparison, suppressParseError, } from './utils.js';
9
9
  const fileImportsMap = new Map();
10
10
  const importerCounts = new Map();
11
11
  const originalPathByNormalizedPath = new Map();
@@ -52,7 +52,7 @@ export const initializeImportCache = async (baseDir, prebuildImportMap) => {
52
52
  importerCounts.clear();
53
53
  originalPathByNormalizedPath.clear();
54
54
  if (prebuildImportMap) {
55
- for (const filePath of getFileListSync(baseDir)) {
55
+ for (const filePath of getFileListSync(baseDir, baseDir, getMintIgnoreGlobs())) {
56
56
  originalPathByNormalizedPath.set(normalizePathForComparison(filePath), optionallyAddLeadingSlash(filePath));
57
57
  }
58
58
  for (const [filePath, imports] of prebuildImportMap) {
@@ -66,7 +66,7 @@ export const initializeImportCache = async (baseDir, prebuildImportMap) => {
66
66
  }
67
67
  return;
68
68
  }
69
- const allFiles = getFileListSync(baseDir).filter((file) => isSnippetExtension(getFileExtension(file)));
69
+ const allFiles = getFileListSync(baseDir, baseDir, getMintIgnoreGlobs()).filter((file) => isSnippetExtension(getFileExtension(file)));
70
70
  await Promise.all(allFiles.map(async (filename) => {
71
71
  const normalizedPath = normalizePathForComparison(filename);
72
72
  const imports = await extractFileImports(baseDir, filename);
@@ -19,6 +19,7 @@ import { generatePagesWithImports } from './generatePagesWithImports.js';
19
19
  import { getDocsState } from './getDocsState.js';
20
20
  import { initializeImportCache, updateImportCacheForFile, removeFromImportCache, getImportedFilesFromCache, syncImportedFileLocations, } from './importCache.js';
21
21
  import { clearPageMetadataCache, removePageMetadataCacheEntryForFile, upsertPageMetadataCacheEntry, upsertPageMetadataCacheEntryForFile, } from './page-metadata-cache.js';
22
+ import { createPreviewPathIgnored } from './previewPathIgnored.js';
22
23
  import { hasTrackedReferencedFile, refreshTrackedReferencedFiles, shouldRegenerateGraphqlSource, } from './referencedFiles.js';
23
24
  import { regenerateAllSnippets } from './regenerateAllSnippets.js';
24
25
  import { resolveImportsFromImportClosure } from './resolve-imports-from-import-closure.js';
@@ -107,27 +108,34 @@ const addUpdateTimingLog = (filename, category, durationMs) => {
107
108
  addChangeLog(_jsx(InfoLog, { message: `processed ${filename} in ${formatElapsedSeconds(durationMs)} (${categoryLabel})` }));
108
109
  };
109
110
  let watcherGate;
111
+ let previewWatcher;
112
+ let previewWatcherRestart = Promise.resolve();
113
+ const startPreviewWatcher = (triggerRefresh, options) => {
114
+ previewWatcherRestart = previewWatcherRestart
115
+ .catch(() => undefined)
116
+ .then(async () => {
117
+ const previous = previewWatcher;
118
+ previewWatcher = undefined;
119
+ if (previous) {
120
+ await previous.close();
121
+ }
122
+ const isPreviewPathIgnored = createPreviewPathIgnored(CMD_EXEC_PATH, getMintIgnoreGlobs());
123
+ previewWatcher = chokidar
124
+ .watch(CMD_EXEC_PATH, {
125
+ ignoreInitial: true,
126
+ ignored: isPreviewPathIgnored,
127
+ cwd: CMD_EXEC_PATH,
128
+ })
129
+ .on('add', (filename) => onAddEvent(filename, triggerRefresh, options))
130
+ .on('change', (filename) => onChangeEvent(filename, triggerRefresh, options))
131
+ .on('unlink', (filename) => onUnlinkEvent(filename, triggerRefresh, options));
132
+ });
133
+ return previewWatcherRestart;
134
+ };
110
135
  const listener = (triggerRefresh, options = {}, gate) => {
111
136
  watcherGate = gate;
112
137
  const previewOptions = { ...options, allowSourceRefs: true };
113
- const mintIgnoreGlobs = getMintIgnoreGlobs();
114
- chokidar
115
- .watch(CMD_EXEC_PATH, {
116
- ignoreInitial: true,
117
- ignored: (filePath) => {
118
- const relativePath = pathUtil.isAbsolute(filePath)
119
- ? pathUtil.relative(CMD_EXEC_PATH, filePath)
120
- : filePath;
121
- if (!relativePath) {
122
- return false;
123
- }
124
- return isMintIgnored(relativePath, mintIgnoreGlobs);
125
- },
126
- cwd: CMD_EXEC_PATH,
127
- })
128
- .on('add', (filename) => onAddEvent(filename, triggerRefresh, previewOptions))
129
- .on('change', (filename) => onChangeEvent(filename, triggerRefresh, previewOptions))
130
- .on('unlink', (filename) => onUnlinkEvent(filename, triggerRefresh, previewOptions));
138
+ void startPreviewWatcher(triggerRefresh, previewOptions);
131
139
  };
132
140
  const onAddEvent = async (filename, triggerRefresh, options) => {
133
141
  if (watcherGate) {
@@ -270,6 +278,7 @@ const onUnlinkEvent = async (filename, triggerRefresh, options) => {
270
278
  await initializeImportCache(CMD_EXEC_PATH, prebuildResult?.fileImportsMap);
271
279
  await initializeFrontmatterHashCache();
272
280
  await refreshTrackedReferencedFiles();
281
+ await startPreviewWatcher(triggerRefresh, options);
273
282
  }
274
283
  catch (err) {
275
284
  console.error('Error rebuilding after .mintignore deletion:', err);
@@ -330,7 +339,7 @@ const initializeFrontmatterHashCache = async () => {
330
339
  frontmatterHashes.clear();
331
340
  clearPageMetadataCache();
332
341
  const importedFiles = getImportedFilesFromCache();
333
- const pageFilenames = getFileListSync(CMD_EXEC_PATH).filter((file) => {
342
+ const pageFilenames = getFileListSync(CMD_EXEC_PATH, CMD_EXEC_PATH, getMintIgnoreGlobs()).filter((file) => {
334
343
  return getFileCategory(file, { importedFiles }) === 'page';
335
344
  });
336
345
  await Promise.all(pageFilenames.map(async (filename) => {
@@ -581,6 +590,7 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
581
590
  await initializeImportCache(CMD_EXEC_PATH, prebuildResult?.fileImportsMap);
582
591
  await initializeFrontmatterHashCache();
583
592
  await refreshTrackedReferencedFiles();
593
+ await startPreviewWatcher(triggerRefresh, options);
584
594
  }
585
595
  catch (err) {
586
596
  console.error(err.message);
@@ -0,0 +1,3 @@
1
+ export declare const createPreviewPathIgnored: (root: string, globs: string[]) => ((filePath: string, stats?: {
2
+ isDirectory: () => boolean;
3
+ }) => boolean);
@@ -0,0 +1,29 @@
1
+ import { createMintIgnoreMatcher, isMintIgnoredBy } from '@mintlify/common';
2
+ import { statSync } from 'fs';
3
+ import pathUtil from 'path';
4
+ const isDirectoryPath = (absolutePath, stats) => {
5
+ if (stats) {
6
+ return stats.isDirectory();
7
+ }
8
+ try {
9
+ return statSync(absolutePath).isDirectory();
10
+ }
11
+ catch {
12
+ // Path may already be gone (unlink). Treat as a file so a directory-only
13
+ // pattern like junk/ does not hide a same-named file's events.
14
+ return false;
15
+ }
16
+ };
17
+ export const createPreviewPathIgnored = (root, globs) => {
18
+ const matcher = createMintIgnoreMatcher(globs);
19
+ return (filePath, stats) => {
20
+ const relativePath = pathUtil.isAbsolute(filePath)
21
+ ? pathUtil.relative(root, filePath)
22
+ : filePath;
23
+ if (!relativePath) {
24
+ return false;
25
+ }
26
+ const absolutePath = pathUtil.isAbsolute(filePath) ? filePath : pathUtil.join(root, filePath);
27
+ return isMintIgnoredBy(relativePath, matcher, isDirectoryPath(absolutePath, stats));
28
+ };
29
+ };
@@ -5,10 +5,10 @@ import { readFile } from 'fs/promises';
5
5
  import { join } from 'path';
6
6
  import { CMD_EXEC_PATH, NEXT_PUBLIC_PATH } from '../../constants.js';
7
7
  import { getImportedFilesFromCache } from './importCache.js';
8
- import { getCurrentVariables, handleParseError } from './utils.js';
8
+ import { getCurrentVariables, getMintIgnoreGlobs, handleParseError } from './utils.js';
9
9
  const getV2SnippetFilenames = () => {
10
10
  const importedFiles = getImportedFilesFromCache();
11
- return getFileListSync(CMD_EXEC_PATH).filter((file) => {
11
+ return getFileListSync(CMD_EXEC_PATH, CMD_EXEC_PATH, getMintIgnoreGlobs()).filter((file) => {
12
12
  if (!isSnippetExtension(getFileExtension(file)))
13
13
  return false;
14
14
  if (file.startsWith('snippets/'))