@mintlify/previewing 4.0.1152 → 4.0.1154

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,60 @@
1
+ import { findAndRemoveImports, replaceVariables } 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
+ normalizePathForComparison: (path) => {
23
+ const normalized = path.startsWith('/') ? path : `/${path}`;
24
+ return normalized.toLowerCase();
25
+ },
26
+ suppressParseError: vi.fn(),
27
+ }));
28
+ import { generateDependentSnippets } from '../local-preview/listener/generateDependentSnippets.js';
29
+ import { initializeImportCache } from '../local-preview/listener/importCache.js';
30
+ describe('generateDependentSnippets', () => {
31
+ let root;
32
+ beforeEach(async () => {
33
+ root = await mkdtemp(join(tmpdir(), 'generate-dependent-snippets-'));
34
+ testPaths.cmd = join(root, 'docs');
35
+ testPaths.public = join(root, 'public');
36
+ await fse.ensureDir(testPaths.cmd);
37
+ await fse.ensureDir(testPaths.public);
38
+ });
39
+ afterEach(async () => {
40
+ await rm(root, { recursive: true, force: true });
41
+ });
42
+ it('regenerates legacy snippet importers of changed v2 snippets', async () => {
43
+ await fse.outputFile(join(testPaths.cmd, 'snippets/base.mdx'), 'export const Base = () => <span>{{name}}</span>;');
44
+ await fse.outputFile(join(testPaths.cmd, '_snippets/legacy.mdx'), "import { Base } from '../snippets/base.mdx';\n\n<Base />");
45
+ await initializeImportCache(testPaths.cmd, new Map([
46
+ ['/snippets/base.mdx', new Set()],
47
+ ['/_snippets/legacy.mdx', new Set(['/snippets/base.mdx'])],
48
+ ]));
49
+ const changedSourcePath = join(testPaths.cmd, 'snippets/base.mdx');
50
+ const changedContent = replaceVariables((await fse.readFile(changedSourcePath)).toString(), {
51
+ name: 'Ada',
52
+ });
53
+ const changedTree = await preparseMdxTree(changedContent, testPaths.cmd, changedSourcePath, vi.fn());
54
+ const changedImportData = await findAndRemoveImports(changedTree);
55
+ const updatedSnippets = await generateDependentSnippets('snippets/base.mdx', changedImportData);
56
+ expect(updatedSnippets).toEqual(['snippets/base.mdx', '_snippets/legacy.mdx']);
57
+ expect(await fse.pathExists(join(testPaths.public, '_snippets/legacy.mdx'))).toBe(true);
58
+ expect((await fse.readFile(join(testPaths.public, '_snippets/legacy.mdx'))).toString()).toContain('Ada');
59
+ });
60
+ });
@@ -1,5 +1,6 @@
1
+ import { getFileListSync } from '@mintlify/prebuild';
1
2
  import fse from 'fs-extra';
2
- import { initializeImportCache, updateImportCacheForFile, removeFromImportCache, getImportedFilesFromCache, } from '../local-preview/listener/importCache.js';
3
+ import { initializeImportCache, updateImportCacheForFile, removeFromImportCache, getImportedFilesFromCache, getFilesImportingPathFromCache, getTransitiveImportersFromCache, } from '../local-preview/listener/importCache.js';
3
4
  vi.mock('fs-extra', () => ({
4
5
  default: { readFile: vi.fn() },
5
6
  }));
@@ -11,9 +12,11 @@ vi.mock('@mintlify/prebuild', async () => {
11
12
  };
12
13
  });
13
14
  const mockReadFile = vi.mocked(fse.readFile);
15
+ const mockGetFileListSync = vi.mocked(getFileListSync);
14
16
  describe('importCache', () => {
15
17
  beforeEach(async () => {
16
18
  vi.clearAllMocks();
19
+ mockGetFileListSync.mockReturnValue([]);
17
20
  await initializeImportCache('/base');
18
21
  });
19
22
  it('starts with empty cache', () => {
@@ -47,6 +50,16 @@ describe('importCache', () => {
47
50
  expect(result.noLongerImported).toEqual([]);
48
51
  expect(getImportedFilesFromCache()).toEqual(new Set(['/shared.mdx']));
49
52
  });
53
+ it('tracks parent-directory relative imports', async () => {
54
+ mockReadFile.mockResolvedValue(Buffer.from(`import Shared from '../shared/snippet.mdx';\n\n<Shared />`));
55
+ await updateImportCacheForFile('/base', 'docs/page.mdx');
56
+ expect(getImportedFilesFromCache()).toEqual(new Set(['/shared/snippet.mdx']));
57
+ });
58
+ it('tracks explicit snippet folder imports', async () => {
59
+ mockReadFile.mockResolvedValue(Buffer.from(`import Callout from '/snippets/callout.mdx';\n\n<Callout />`));
60
+ await updateImportCacheForFile('/base', 'docs/page.mdx');
61
+ expect(getImportedFilesFromCache()).toEqual(new Set(['/snippets/callout.mdx']));
62
+ });
50
63
  it('detects when imports change in a file', async () => {
51
64
  mockReadFile.mockResolvedValue(Buffer.from(`import Old from './old.mdx';\n\n<Old />`));
52
65
  await updateImportCacheForFile('/base', 'page.mdx');
@@ -55,4 +68,35 @@ describe('importCache', () => {
55
68
  expect(result.newlyImported).toEqual(['/new.mdx']);
56
69
  expect(result.noLongerImported).toEqual(['/old.mdx']);
57
70
  });
71
+ it('returns files that directly import a path', async () => {
72
+ mockReadFile.mockResolvedValueOnce(Buffer.from(`import S from './shared.mdx';\n\n<S />`));
73
+ await updateImportCacheForFile('/base', 'a.mdx');
74
+ mockReadFile.mockResolvedValueOnce(Buffer.from(`import S from './shared.mdx';\n\n<S />`));
75
+ await updateImportCacheForFile('/base', 'b.mdx');
76
+ expect(getFilesImportingPathFromCache('/shared.mdx')).toEqual(new Set(['/a.mdx', '/b.mdx']));
77
+ });
78
+ it('preserves importer path casing from file updates', async () => {
79
+ mockReadFile.mockResolvedValue(Buffer.from(`import S from '../Shared.mdx';\n\n<S />`));
80
+ await updateImportCacheForFile('/base', 'Docs/MyPage.mdx');
81
+ expect(getFilesImportingPathFromCache('/Shared.mdx')).toEqual(new Set(['/Docs/MyPage.mdx']));
82
+ });
83
+ it('preserves importer path casing when initialized from a prebuild import map', async () => {
84
+ mockGetFileListSync.mockReturnValue(['Docs/MyPage.mdx']);
85
+ await initializeImportCache('/base', new Map([['/docs/mypage.mdx', new Set(['/shared.mdx'])]]));
86
+ expect(getFilesImportingPathFromCache('/shared.mdx')).toEqual(new Set(['/Docs/MyPage.mdx']));
87
+ });
88
+ it('returns transitive importers for a path', async () => {
89
+ mockReadFile.mockResolvedValueOnce(Buffer.from(`import S from './shared.mdx';\n\n<S />`));
90
+ await updateImportCacheForFile('/base', 'snippet.mdx');
91
+ mockReadFile.mockResolvedValueOnce(Buffer.from(`import Snippet from './snippet.mdx';\n\n<Snippet />`));
92
+ await updateImportCacheForFile('/base', 'page.mdx');
93
+ expect(getTransitiveImportersFromCache('/shared.mdx')).toEqual(new Set(['/snippet.mdx', '/page.mdx']));
94
+ });
95
+ it('preserves transitive importer path casing', async () => {
96
+ mockReadFile.mockResolvedValueOnce(Buffer.from(`import S from '../Shared.mdx';\n\n<S />`));
97
+ await updateImportCacheForFile('/base', 'Snippets/Card.mdx');
98
+ mockReadFile.mockResolvedValueOnce(Buffer.from(`import Card from '../Snippets/Card.mdx';\n\n<Card />`));
99
+ await updateImportCacheForFile('/base', 'Docs/Page.mdx');
100
+ expect(getTransitiveImportersFromCache('/Shared.mdx')).toEqual(new Set(['/Snippets/Card.mdx', '/Docs/Page.mdx']));
101
+ });
58
102
  });
@@ -0,0 +1,80 @@
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
+ import { resolveImportsFromImportClosure } from '../local-preview/listener/resolve-imports-from-import-closure.js';
8
+ const testPaths = vi.hoisted(() => ({
9
+ cmd: '',
10
+ public: '',
11
+ }));
12
+ vi.mock('../constants.js', () => ({
13
+ get CMD_EXEC_PATH() {
14
+ return testPaths.cmd;
15
+ },
16
+ get NEXT_PUBLIC_PATH() {
17
+ return testPaths.public;
18
+ },
19
+ }));
20
+ vi.mock('../local-preview/listener/utils.js', () => ({
21
+ getCurrentVariables: vi.fn(async () => ({ name: 'Ada' })),
22
+ handleParseError: vi.fn(),
23
+ }));
24
+ describe('resolveImportsFromImportClosure', () => {
25
+ let root;
26
+ beforeEach(async () => {
27
+ root = await mkdtemp(join(tmpdir(), 'resolve-imports-from-import-closure-'));
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 resolveImportsFromImportClosure({
43
+ ...importsResponse,
44
+ filename: 'page.mdx',
45
+ });
46
+ const resolvedContent = stringifyTree(resolvedTree);
47
+ expect(resolvedContent).toContain('Ada');
48
+ expect(resolvedContent).not.toContain('{{name}}');
49
+ expect(resolvedContent).not.toContain("import { Nested } from './nested.mdx'");
50
+ });
51
+ it('resolves explicit snippet imports with parent-directory nested imports', async () => {
52
+ await fse.outputFile(join(testPaths.cmd, 'shared/nested.mdx'), 'export const Nested = () => <span>{{name}}</span>;');
53
+ await fse.outputFile(join(testPaths.cmd, 'snippets/foundry/card.mdx'), "import { Nested } from '../../shared/nested.mdx';\n\n<Nested />");
54
+ const pageContent = "import Card from '/snippets/foundry/card.mdx';\n\n<Card />";
55
+ const tree = await preparseMdxTree(pageContent, testPaths.cmd, join(testPaths.cmd, 'docs/page.mdx'), vi.fn());
56
+ const importsResponse = await findAndRemoveImports(tree);
57
+ const resolvedTree = await resolveImportsFromImportClosure({
58
+ ...importsResponse,
59
+ filename: 'docs/page.mdx',
60
+ });
61
+ const resolvedContent = stringifyTree(resolvedTree);
62
+ expect(resolvedContent).toContain('Ada');
63
+ expect(resolvedContent).not.toContain('{{name}}');
64
+ expect(resolvedContent).not.toContain("import { Nested } from '../../shared/nested.mdx'");
65
+ });
66
+ it('resolves named imports from explicit snippets', async () => {
67
+ await fse.outputFile(join(testPaths.cmd, 'snippets/badge.mdx'), 'export const Badge = () => <span>{{name}}</span>;');
68
+ const pageContent = "import { Badge } from '/snippets/badge.mdx';\n\n<Badge />";
69
+ const tree = await preparseMdxTree(pageContent, testPaths.cmd, join(testPaths.cmd, 'docs/page.mdx'), vi.fn());
70
+ const importsResponse = await findAndRemoveImports(tree);
71
+ const resolvedTree = await resolveImportsFromImportClosure({
72
+ ...importsResponse,
73
+ filename: 'docs/page.mdx',
74
+ });
75
+ const resolvedContent = stringifyTree(resolvedTree);
76
+ expect(resolvedContent).toContain('Ada');
77
+ expect(resolvedContent).not.toContain('{{name}}');
78
+ expect(resolvedContent).not.toContain("import { Badge } from '/snippets/badge.mdx'");
79
+ });
80
+ });
@@ -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);
@@ -1,68 +1,56 @@
1
- import { findAndRemoveImports, stringifyTree, topologicalSort, hasImports, optionallyAddLeadingSlash, optionallyRemoveLeadingSlash, resolveImportPath, } from '@mintlify/common';
2
- import { outputFile } from 'fs-extra';
1
+ import { findAndRemoveImports, getFileCategory, stringifyTree, topologicalSort, hasImports, optionallyAddLeadingSlash, optionallyRemoveLeadingSlash, replaceVariables, resolveImportPath, } from '@mintlify/common';
2
+ import { preparseMdxTree } from '@mintlify/prebuild';
3
+ import { promises as _promises } from 'fs';
4
+ import fse from 'fs-extra';
3
5
  import { join } from 'path';
4
- import { NEXT_PUBLIC_PATH } from '../../constants.js';
5
- import { getOriginalSnippets } from './getSnippets.js';
6
- import { resolveAllImports } from './resolveAllImports.js';
7
- const findAllDependents = async (initialFileWithSlash, allSnippets, processedDataCache) => {
8
- const affected = new Set([initialFileWithSlash]);
9
- const queue = [initialFileWithSlash];
10
- while (queue.length > 0) {
11
- const currentSourceFile = queue.shift();
12
- for (const snippet of allSnippets) {
13
- const potentialDependentFile = optionallyAddLeadingSlash(snippet.filename);
14
- if (potentialDependentFile === currentSourceFile)
15
- continue;
16
- let processedData = processedDataCache.get(potentialDependentFile);
17
- if (processedData == null) {
18
- const clonedTree = structuredClone(snippet.tree);
19
- processedData = await findAndRemoveImports(clonedTree);
20
- processedDataCache.set(potentialDependentFile, processedData);
21
- }
22
- const importsCurrentFile = Object.keys(processedData.importMap).some((importPath) => {
23
- const resolvedPath = resolveImportPath(importPath, potentialDependentFile);
24
- return resolvedPath === currentSourceFile;
25
- });
26
- if (importsCurrentFile) {
27
- if (!affected.has(potentialDependentFile)) {
28
- affected.add(potentialDependentFile);
29
- queue.push(potentialDependentFile);
30
- }
31
- }
6
+ import { CMD_EXEC_PATH, NEXT_PUBLIC_PATH } from '../../constants.js';
7
+ import { getImportedFilesFromCache, getTransitiveImportersFromCache } from './importCache.js';
8
+ import { resolveImportsFromImportClosure } from './resolve-imports-from-import-closure.js';
9
+ import { getCurrentVariables, handleParseError } from './utils.js';
10
+ const { readFile } = _promises;
11
+ const getAffectedSnippetFilenames = (changedFilename) => {
12
+ const affectedSnippets = new Set([optionallyAddLeadingSlash(changedFilename)]);
13
+ const importedFiles = getImportedFilesFromCache();
14
+ const transitiveImporters = getTransitiveImportersFromCache(changedFilename);
15
+ for (const importer of transitiveImporters) {
16
+ const category = getFileCategory(importer, { importedFiles });
17
+ if (category === 'snippet' || category === 'snippet-v2') {
18
+ affectedSnippets.add(importer);
32
19
  }
33
20
  }
34
- return affected;
21
+ return affectedSnippets;
22
+ };
23
+ const getSnippetImportData = async (filename, variables) => {
24
+ try {
25
+ const relativePath = optionallyRemoveLeadingSlash(filename);
26
+ const sourcePath = join(CMD_EXEC_PATH, relativePath);
27
+ const content = replaceVariables((await readFile(sourcePath)).toString(), variables);
28
+ const tree = await preparseMdxTree(content, CMD_EXEC_PATH, sourcePath, handleParseError);
29
+ const importData = await findAndRemoveImports(tree);
30
+ return {
31
+ filename: optionallyAddLeadingSlash(filename),
32
+ ...importData,
33
+ };
34
+ }
35
+ catch (err) {
36
+ console.warn(`Failed to parse snippet ${filename}:`, err);
37
+ return null;
38
+ }
35
39
  };
36
40
  export const generateDependentSnippets = async (changedFilename, newImportData) => {
37
- const processedDataCache = new Map();
38
- const allOriginalSnippets = await getOriginalSnippets();
39
41
  const updatedSnippetFileKey = optionallyAddLeadingSlash(changedFilename);
40
- const affectedSnippets = await findAllDependents(updatedSnippetFileKey, allOriginalSnippets, processedDataCache);
42
+ const affectedSnippets = getAffectedSnippetFilenames(changedFilename);
43
+ const variables = await getCurrentVariables();
41
44
  const snippetPromises = Array.from(affectedSnippets).map(async (filename) => {
42
- const cachedData = processedDataCache.get(filename);
43
- if (cachedData)
44
- return { filename, ...cachedData };
45
- const originalSnippet = allOriginalSnippets.find((s) => optionallyAddLeadingSlash(s.filename) === filename);
46
- if (!originalSnippet)
47
- return null;
48
- const processed = await findAndRemoveImports(structuredClone(originalSnippet.tree));
49
- processedDataCache.set(filename, processed);
50
- return { filename, ...processed };
45
+ if (filename === updatedSnippetFileKey) {
46
+ return {
47
+ ...newImportData,
48
+ filename: updatedSnippetFileKey,
49
+ };
50
+ }
51
+ return await getSnippetImportData(filename, variables);
51
52
  });
52
53
  const snippets = (await Promise.all(snippetPromises)).filter((item) => item != null);
53
- const idx = snippets.findIndex((item) => item.filename === updatedSnippetFileKey);
54
- if (idx !== -1) {
55
- snippets[idx] = {
56
- ...newImportData,
57
- filename: updatedSnippetFileKey,
58
- };
59
- }
60
- else {
61
- snippets.push({
62
- ...newImportData,
63
- filename: updatedSnippetFileKey,
64
- });
65
- }
66
54
  const graph = {};
67
55
  snippets.forEach((item) => {
68
56
  graph[item.filename] = Object.keys(item.importMap)
@@ -77,12 +65,12 @@ export const generateDependentSnippets = async (changedFilename, newImportData)
77
65
  const processedSnippets = [];
78
66
  for (const currentSnippet of orderedSnippets) {
79
67
  let processedTree = currentSnippet.tree;
80
- if (currentSnippet.filename !== updatedSnippetFileKey && hasImports(currentSnippet)) {
81
- processedTree = await resolveAllImports(currentSnippet);
68
+ if (hasImports(currentSnippet)) {
69
+ processedTree = await resolveImportsFromImportClosure(currentSnippet);
82
70
  }
83
71
  const targetFilename = optionallyRemoveLeadingSlash(currentSnippet.filename);
84
72
  const targetPath = join(NEXT_PUBLIC_PATH, targetFilename);
85
- await outputFile(targetPath, stringifyTree(processedTree), { flag: 'w' });
73
+ await fse.outputFile(targetPath, stringifyTree(processedTree), { flag: 'w' });
86
74
  processedSnippets.push(targetFilename);
87
75
  }
88
76
  return processedSnippets;
@@ -1,43 +1,42 @@
1
- import { findAndRemoveImports, optionallyRemoveLeadingSlash, resolveAllImports, resolveImportPath, stringifyTree, isSnippetExtension, getFileCategory, } from '@mintlify/common';
2
- import { preparseMdxTree, getFileListSync, getFileExtension } from '@mintlify/prebuild';
1
+ import { findAndRemoveImports, optionallyAddLeadingSlash, optionallyRemoveLeadingSlash, stringifyTree, getFileCategory, } from '@mintlify/common';
2
+ import { preparseMdxTree } from '@mintlify/prebuild';
3
3
  import { promises as _promises } from 'fs';
4
4
  import { outputFile } from 'fs-extra';
5
5
  import { join } from 'path';
6
6
  import { CMD_EXEC_PATH, NEXT_PROPS_PATH } from '../../constants.js';
7
- import { getProcessedSnippets } from './getSnippets.js';
8
- import { getImportedFilesFromCache } from './importCache.js';
7
+ import { getFilesImportingPathFromCache, getImportedFilesFromCache } from './importCache.js';
8
+ import { resolvePageImports } from './resolve-page-imports.js';
9
9
  import { handleParseError } from './utils.js';
10
10
  const { readFile } = _promises;
11
- export const generatePagesWithImports = async (updatedSnippets) => {
12
- const snippets = await getProcessedSnippets();
11
+ const getAffectedPageFilenames = (updatedSnippets) => {
13
12
  const importedFiles = getImportedFilesFromCache();
14
- const pageFilenames = getFileListSync(CMD_EXEC_PATH).filter((file) => {
15
- if (!isSnippetExtension(getFileExtension(file))) {
16
- return false;
13
+ const pageFilenames = new Set();
14
+ for (const snippet of updatedSnippets) {
15
+ for (const importer of getFilesImportingPathFromCache(optionallyAddLeadingSlash(snippet))) {
16
+ const category = getFileCategory(importer, { importedFiles });
17
+ if (category === 'page') {
18
+ pageFilenames.add(optionallyRemoveLeadingSlash(importer));
19
+ }
17
20
  }
18
- const category = getFileCategory(file, { importedFiles });
19
- return category === 'page';
20
- });
21
- await Promise.all(pageFilenames.map(async (pageFilename) => {
21
+ }
22
+ return pageFilenames;
23
+ };
24
+ export const generatePagesWithImports = async (updatedSnippets) => {
25
+ const pageFilenames = getAffectedPageFilenames(updatedSnippets);
26
+ await Promise.all(Array.from(pageFilenames).map(async (pageFilename) => {
22
27
  const sourcePath = join(CMD_EXEC_PATH, pageFilename);
23
28
  const contentStr = (await readFile(sourcePath)).toString();
24
29
  try {
25
30
  const tree = await preparseMdxTree(contentStr, CMD_EXEC_PATH, sourcePath, handleParseError);
26
31
  const importsResponse = await findAndRemoveImports(tree);
27
- if (Object.keys(importsResponse.importMap).some((importPath) => {
28
- const resolvedPath = resolveImportPath(importPath, pageFilename);
29
- return (resolvedPath != null &&
30
- updatedSnippets.has(optionallyRemoveLeadingSlash(resolvedPath)));
31
- })) {
32
- const content = await resolveAllImports({
33
- snippets,
34
- fileWithImports: { ...importsResponse, filename: pageFilename },
35
- });
36
- const targetPath = join(NEXT_PROPS_PATH, pageFilename);
37
- await outputFile(targetPath, stringifyTree(content), {
38
- flag: 'w',
39
- });
40
- }
32
+ const content = await resolvePageImports({
33
+ ...importsResponse,
34
+ filename: pageFilename,
35
+ });
36
+ const targetPath = join(NEXT_PROPS_PATH, pageFilename);
37
+ await outputFile(targetPath, stringifyTree(content), {
38
+ flag: 'w',
39
+ });
41
40
  }
42
41
  catch (err) {
43
42
  console.log('Error generating pages with imports');
@@ -6,4 +6,6 @@ export declare const initializeImportCache: (baseDir: string, prebuildImportMap?
6
6
  export declare const updateImportCacheForFile: (baseDir: string, filename: string) => Promise<ImportCacheChangeResult>;
7
7
  export declare const removeFromImportCache: (filename: string) => ImportCacheChangeResult;
8
8
  export declare const getImportedFilesFromCache: () => Set<string>;
9
+ export declare const getFilesImportingPathFromCache: (path: string) => Set<string>;
10
+ export declare const getTransitiveImportersFromCache: (path: string) => Set<string>;
9
11
  export declare const syncImportedFileLocations: (changes: ImportCacheChangeResult) => Promise<void>;
@@ -8,6 +8,7 @@ import { resolveAllImports } from './resolveAllImports.js';
8
8
  import { handleParseError, normalizePathForComparison, suppressParseError } from './utils.js';
9
9
  const fileImportsMap = new Map();
10
10
  const importerCounts = new Map();
11
+ const originalPathByNormalizedPath = new Map();
11
12
  const addImporter = (path) => {
12
13
  const count = importerCounts.get(path) ?? 0;
13
14
  importerCounts.set(path, count + 1);
@@ -49,9 +50,15 @@ const extractFileImports = async (baseDir, filename) => {
49
50
  export const initializeImportCache = async (baseDir, prebuildImportMap) => {
50
51
  fileImportsMap.clear();
51
52
  importerCounts.clear();
53
+ originalPathByNormalizedPath.clear();
52
54
  if (prebuildImportMap) {
55
+ for (const filePath of getFileListSync(baseDir)) {
56
+ originalPathByNormalizedPath.set(normalizePathForComparison(filePath), optionallyAddLeadingSlash(filePath));
57
+ }
53
58
  for (const [filePath, imports] of prebuildImportMap) {
54
59
  const normalizedPath = normalizePathForComparison(filePath);
60
+ const originalPath = originalPathByNormalizedPath.get(normalizedPath) ?? filePath;
61
+ originalPathByNormalizedPath.set(normalizedPath, optionallyAddLeadingSlash(originalPath));
55
62
  fileImportsMap.set(normalizedPath, imports);
56
63
  for (const imp of imports) {
57
64
  addImporter(imp);
@@ -64,6 +71,7 @@ export const initializeImportCache = async (baseDir, prebuildImportMap) => {
64
71
  const normalizedPath = normalizePathForComparison(filename);
65
72
  const imports = await extractFileImports(baseDir, filename);
66
73
  fileImportsMap.set(normalizedPath, imports);
74
+ originalPathByNormalizedPath.set(normalizedPath, optionallyAddLeadingSlash(filename));
67
75
  for (const imp of imports) {
68
76
  addImporter(imp);
69
77
  }
@@ -76,6 +84,7 @@ export const updateImportCacheForFile = async (baseDir, filename) => {
76
84
  const normalizedPath = normalizePathForComparison(filename);
77
85
  const newImports = await extractFileImports(baseDir, filename);
78
86
  const oldImports = fileImportsMap.get(normalizedPath) ?? new Set();
87
+ originalPathByNormalizedPath.set(normalizedPath, optionallyAddLeadingSlash(filename));
79
88
  const newlyImported = [];
80
89
  const noLongerImported = [];
81
90
  for (const imp of newImports) {
@@ -104,11 +113,40 @@ export const removeFromImportCache = (filename) => {
104
113
  }
105
114
  }
106
115
  fileImportsMap.delete(normalizedPath);
116
+ originalPathByNormalizedPath.delete(normalizedPath);
107
117
  return { newlyImported: [], noLongerImported };
108
118
  };
109
119
  export const getImportedFilesFromCache = () => {
110
120
  return new Set(Array.from(importerCounts.keys(), (key) => key.toLowerCase()));
111
121
  };
122
+ export const getFilesImportingPathFromCache = (path) => {
123
+ const normalizedPath = normalizePathForComparison(path);
124
+ const importers = new Set();
125
+ for (const [filePath, imports] of fileImportsMap) {
126
+ if (imports.has(normalizedPath)) {
127
+ importers.add(originalPathByNormalizedPath.get(filePath) ?? filePath);
128
+ }
129
+ }
130
+ return importers;
131
+ };
132
+ export const getTransitiveImportersFromCache = (path) => {
133
+ const importers = new Set();
134
+ const queue = [normalizePathForComparison(path)];
135
+ while (queue.length > 0) {
136
+ const currentPath = queue.shift();
137
+ if (currentPath == null) {
138
+ continue;
139
+ }
140
+ for (const importer of getFilesImportingPathFromCache(currentPath)) {
141
+ if (importers.has(importer)) {
142
+ continue;
143
+ }
144
+ importers.add(importer);
145
+ queue.push(importer);
146
+ }
147
+ }
148
+ return importers;
149
+ };
112
150
  const isSnippetByFolder = (path) => {
113
151
  const normalized = normalizePathForComparison(path).toLowerCase();
114
152
  return normalized.startsWith('/snippets/') || normalized.startsWith('/_snippets/');
@@ -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;