@mintlify/previewing 4.0.1153 → 4.0.1155
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__/generate-dependent-snippets.test.d.ts +1 -0
- package/dist/__tests__/generate-dependent-snippets.test.js +60 -0
- package/dist/__tests__/importCache.test.js +45 -1
- package/dist/__tests__/resolve-imports-from-import-closure.test.d.ts +1 -0
- package/dist/__tests__/resolve-imports-from-import-closure.test.js +80 -0
- package/dist/local-preview/listener/generateDependentSnippets.js +47 -59
- package/dist/local-preview/listener/generatePagesWithImports.js +26 -27
- package/dist/local-preview/listener/importCache.d.ts +2 -0
- package/dist/local-preview/listener/importCache.js +38 -0
- package/dist/local-preview/listener/index.js +2 -2
- package/dist/local-preview/listener/resolve-imports-from-import-closure.d.ts +3 -0
- package/dist/local-preview/listener/resolve-imports-from-import-closure.js +98 -0
- package/dist/local-preview/listener/resolve-page-imports.js +2 -96
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
});
|
|
@@ -1,68 +1,56 @@
|
|
|
1
|
-
import { findAndRemoveImports, stringifyTree, topologicalSort, hasImports, optionallyAddLeadingSlash, optionallyRemoveLeadingSlash, resolveImportPath, } from '@mintlify/common';
|
|
2
|
-
import {
|
|
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 {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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 =
|
|
42
|
+
const affectedSnippets = getAffectedSnippetFilenames(changedFilename);
|
|
43
|
+
const variables = await getCurrentVariables();
|
|
41
44
|
const snippetPromises = Array.from(affectedSnippets).map(async (filename) => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 (
|
|
81
|
-
processedTree = await
|
|
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,
|
|
2
|
-
import { preparseMdxTree
|
|
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 {
|
|
8
|
-
import {
|
|
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
|
-
|
|
12
|
-
const snippets = await getProcessedSnippets();
|
|
11
|
+
const getAffectedPageFilenames = (updatedSnippets) => {
|
|
13
12
|
const importedFiles = getImportedFilesFromCache();
|
|
14
|
-
const pageFilenames =
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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/');
|
|
@@ -17,7 +17,7 @@ import { getDocsState } from './getDocsState.js';
|
|
|
17
17
|
import { initializeImportCache, updateImportCacheForFile, removeFromImportCache, getImportedFilesFromCache, syncImportedFileLocations, } from './importCache.js';
|
|
18
18
|
import { hasTrackedReferencedFile, refreshTrackedReferencedFiles } from './referencedFiles.js';
|
|
19
19
|
import { regenerateAllSnippets } from './regenerateAllSnippets.js';
|
|
20
|
-
import {
|
|
20
|
+
import { resolveImportsFromImportClosure } from './resolve-imports-from-import-closure.js';
|
|
21
21
|
import { resolvePageImports } from './resolve-page-imports.js';
|
|
22
22
|
import { updateCustomLanguages, updateGeneratedNav, updateOpenApiFiles, upsertOpenApiFile, } from './update.js';
|
|
23
23
|
import { getCurrentVariables, getMintIgnoreGlobs, handleParseError, isFileSizeValid, isJsonValid, seedFrontmatterHashForPage, shouldRegenerateNavForPage, suppressParseError, } from './utils.js';
|
|
@@ -301,7 +301,7 @@ const onUpdateEvent = async (filename, triggerRefresh, options = {}) => {
|
|
|
301
301
|
const tree = await preparseMdxTree(contentStr, CMD_EXEC_PATH, filePath, handleParseError);
|
|
302
302
|
const importsResponse = await findAndRemoveImports(tree);
|
|
303
303
|
if (hasImports(importsResponse)) {
|
|
304
|
-
contentStr = stringifyTree(await
|
|
304
|
+
contentStr = stringifyTree(await resolveImportsFromImportClosure({ ...importsResponse, filename }));
|
|
305
305
|
}
|
|
306
306
|
await fse.outputFile(targetPath, contentStr, {
|
|
307
307
|
flag: 'w',
|
|
@@ -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 resolveImportsFromImportClosure = 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
|
+
};
|