@mintlify/prebuild 1.0.1129 → 1.0.1131

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.
@@ -1,7 +1,11 @@
1
1
  import { AsyncAPIFile } from '@mintlify/common';
2
2
  import { OpenApiFile } from '@mintlify/models';
3
3
  import type { InvalidSpecFile } from './invalidSpecFiles.js';
4
- export declare const categorizeFilePaths: (contentDirectoryPath: string, mintIgnore?: string[], disableOpenApi?: boolean) => Promise<{
4
+ export type PageScan = {
5
+ frontmatter: string;
6
+ rssCandidate: boolean;
7
+ };
8
+ export declare const categorizeFilePaths: (contentDirectoryPath: string, mintIgnore?: string[], disableOpenApi?: boolean, lazyPages?: boolean) => Promise<{
5
9
  contentFilenames: string[];
6
10
  staticFilenames: string[];
7
11
  openApiFiles: OpenApiFile[];
@@ -10,4 +14,5 @@ export declare const categorizeFilePaths: (contentDirectoryPath: string, mintIgn
10
14
  snippets: string[];
11
15
  snippetsV2: string[];
12
16
  fileImportsMap: Map<string, Set<string>>;
17
+ pageScans: Map<string, PageScan>;
13
18
  }>;
@@ -1,46 +1,135 @@
1
- import { validate, validateAsyncApi, buildImportMap, getFileCategory, isSnippetExtension, getAST, } from '@mintlify/common';
1
+ import { validate, validateAsyncApi, extractImportSources, getFileCategory, isSnippetExtension, getAST, resolveImportPath, } from '@mintlify/common';
2
2
  import { readFile } from 'fs/promises';
3
3
  import yaml from 'js-yaml';
4
4
  import * as path from 'path';
5
5
  import { formatError } from '../errorMessages/formatError.js';
6
6
  import { getFileList } from '../fs/index.js';
7
7
  import { getFileExtension } from '../utils.js';
8
- export const categorizeFilePaths = async (contentDirectoryPath, mintIgnore = [], disableOpenApi) => {
9
- const allFiles = getFileList(contentDirectoryPath, contentDirectoryPath, mintIgnore);
10
- const mdxFiles = [];
8
+ const MDX_IMPORT_PATTERN = /^import\s/m;
9
+ const IMPORT_SOURCE_PATTERN = /import\s+(?:[\w$*{},\s]+?\s+from\s+)?['"]([^'"]+)['"]/g;
10
+ const FRONTMATTER_PATTERN = /^\uFEFF?---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/;
11
+ const MDX_SCAN_CONCURRENCY = 16;
12
+ const FENCE_PATTERN = /^(`{3,}|~{3,})/;
13
+ const extractImportSourcesFromContent = (content) => {
14
+ const sources = [];
15
+ let needsAst = false;
16
+ let fence;
17
+ for (const line of content.split('\n')) {
18
+ const trimmed = line.trimStart();
19
+ const fenceRun = FENCE_PATTERN.exec(trimmed)?.[1];
20
+ if (fence !== undefined) {
21
+ if (fenceRun !== undefined &&
22
+ fenceRun.charAt(0) === fence.char &&
23
+ fenceRun.length >= fence.length) {
24
+ fence = undefined;
25
+ }
26
+ continue;
27
+ }
28
+ if (fenceRun !== undefined) {
29
+ fence = { char: fenceRun.charAt(0), length: fenceRun.length };
30
+ continue;
31
+ }
32
+ if (!line.startsWith('import ') && !line.startsWith('import\t')) {
33
+ continue;
34
+ }
35
+ const matches = Array.from(line.matchAll(IMPORT_SOURCE_PATTERN));
36
+ if (matches.length === 0) {
37
+ needsAst = true;
38
+ continue;
39
+ }
40
+ for (const match of matches) {
41
+ const source = match[1];
42
+ if (source) {
43
+ sources.push(source);
44
+ }
45
+ }
46
+ }
47
+ return { sources, needsAst };
48
+ };
49
+ const getImportSources = (content, filePath, lazyPages) => {
50
+ if (lazyPages) {
51
+ if (!MDX_IMPORT_PATTERN.test(content)) {
52
+ return [];
53
+ }
54
+ const { sources, needsAst } = extractImportSourcesFromContent(content);
55
+ if (!needsAst) {
56
+ return sources;
57
+ }
58
+ }
59
+ return extractImportSources(getAST(content, filePath));
60
+ };
61
+ export const categorizeFilePaths = async (contentDirectoryPath, mintIgnore = [], disableOpenApi, lazyPages) => {
62
+ const allFilenames = [];
63
+ for await (const filename of getFileList(contentDirectoryPath, contentDirectoryPath, mintIgnore)) {
64
+ allFilenames.push(filename);
65
+ }
66
+ const mdxCandidates = [];
11
67
  const nonMdxFiles = [];
12
- for await (const filename of allFiles) {
68
+ for (const filename of allFilenames) {
13
69
  const extension = getFileExtension(filename);
14
70
  if (isSnippetExtension(extension)) {
71
+ mdxCandidates.push(filename);
72
+ }
73
+ else {
74
+ nonMdxFiles.push({ filename, extension });
75
+ }
76
+ }
77
+ const importedFiles = new Set();
78
+ const fileImportsMap = new Map();
79
+ const pageScans = new Map();
80
+ const mdxResults = new Array(mdxCandidates.length);
81
+ const queue = mdxCandidates.map((filename, index) => ({ filename, index }));
82
+ await Promise.all(Array.from({ length: Math.min(MDX_SCAN_CONCURRENCY, queue.length) }, async () => {
83
+ let entry = queue.shift();
84
+ while (entry) {
85
+ const { filename, index } = entry;
15
86
  const filePath = path.join(contentDirectoryPath, filename);
16
- const content = await readFile(filePath, 'utf8');
17
87
  try {
18
- const tree = getAST(content, filePath);
19
- mdxFiles.push({ path: filename, tree });
88
+ const content = await readFile(filePath, 'utf8');
89
+ const resolvedImports = new Set();
90
+ for (const source of getImportSources(content, filePath, lazyPages)) {
91
+ const resolved = resolveImportPath(source, filename);
92
+ if (resolved) {
93
+ const normalized = resolved.toLowerCase();
94
+ importedFiles.add(normalized);
95
+ resolvedImports.add(normalized);
96
+ }
97
+ }
98
+ fileImportsMap.set(filename.toLowerCase(), resolvedImports);
99
+ if (lazyPages) {
100
+ pageScans.set(filename, {
101
+ frontmatter: FRONTMATTER_PATTERN.exec(content)?.[0] ?? '',
102
+ rssCandidate: content.includes('<Update'),
103
+ });
104
+ }
105
+ mdxResults[index] = filename;
20
106
  }
21
107
  catch (error) {
22
108
  console.error(formatError(error, filePath, contentDirectoryPath));
23
109
  }
110
+ entry = queue.shift();
24
111
  }
25
- else {
26
- nonMdxFiles.push({ filename, extension });
112
+ }));
113
+ const mdxFilenames = [];
114
+ for (const filename of mdxResults) {
115
+ if (filename !== undefined) {
116
+ mdxFilenames.push(filename);
27
117
  }
28
118
  }
29
- const { importedFiles, fileImportsMap } = buildImportMap(mdxFiles);
30
119
  const contentFilenames = [];
31
120
  const snippets = [];
32
121
  const snippetsV2 = [];
33
- for (const file of mdxFiles) {
34
- const category = getFileCategory(file.path, { importedFiles });
122
+ for (const filename of mdxFilenames) {
123
+ const category = getFileCategory(filename, { importedFiles });
35
124
  switch (category) {
36
125
  case 'snippet':
37
- snippets.push(file.path);
126
+ snippets.push(filename);
38
127
  break;
39
128
  case 'snippet-v2':
40
- snippetsV2.push(file.path);
129
+ snippetsV2.push(filename);
41
130
  break;
42
131
  case 'page':
43
- contentFilenames.push(file.path);
132
+ contentFilenames.push(filename);
44
133
  break;
45
134
  }
46
135
  }
@@ -124,5 +213,6 @@ export const categorizeFilePaths = async (contentDirectoryPath, mintIgnore = [],
124
213
  snippets,
125
214
  snippetsV2,
126
215
  fileImportsMap,
216
+ pageScans,
127
217
  };
128
218
  };
@@ -1,12 +1,13 @@
1
1
  export interface PrebuildResult {
2
2
  fileImportsMap: Map<string, Set<string>>;
3
3
  }
4
- export declare const prebuild: (contentDirectoryPath: string, { localSchema, groups, disableOpenApi, strict, allowSourceRefs, }?: {
4
+ export declare const prebuild: (contentDirectoryPath: string, { localSchema, groups, disableOpenApi, strict, allowSourceRefs, lazyPages, }?: {
5
5
  localSchema?: boolean;
6
6
  groups?: string[];
7
7
  disableOpenApi?: boolean;
8
8
  strict?: boolean;
9
9
  allowSourceRefs?: boolean;
10
+ lazyPages?: boolean;
10
11
  }) => Promise<PrebuildResult | undefined>;
11
12
  export * from './categorizeFilePaths.js';
12
13
  export * from './getOpenApiFiles.js';
@@ -3,7 +3,7 @@ import { categorizeFilePaths } from './categorizeFilePaths.js';
3
3
  import { warnInvalidSpecFiles } from './invalidSpecFiles.js';
4
4
  import { update } from './update/index.js';
5
5
  import { clearWarnings, checkStrictMode } from './warnings.js';
6
- export const prebuild = async (contentDirectoryPath, { localSchema, groups, disableOpenApi, strict, allowSourceRefs, } = {}) => {
6
+ export const prebuild = async (contentDirectoryPath, { localSchema, groups, disableOpenApi, strict, allowSourceRefs, lazyPages, } = {}) => {
7
7
  if (process.env.IS_MULTI_TENANT === 'true') {
8
8
  console.log('Skipping prebuild in multi-tenant mode.');
9
9
  return;
@@ -16,7 +16,7 @@ export const prebuild = async (contentDirectoryPath, { localSchema, groups, disa
16
16
  throw Error('must be run in a directory where a docs.json file exists.');
17
17
  }
18
18
  const mintIgnore = await getMintIgnore(contentDirectoryPath);
19
- const { contentFilenames, staticFilenames, openApiFiles, asyncApiFiles, invalidSpecFiles, snippets, snippetsV2, fileImportsMap, } = await categorizeFilePaths(contentDirectoryPath, mintIgnore, disableOpenApi);
19
+ const { contentFilenames, staticFilenames, openApiFiles, asyncApiFiles, invalidSpecFiles, snippets, snippetsV2, fileImportsMap, pageScans, } = await categorizeFilePaths(contentDirectoryPath, mintIgnore, disableOpenApi, lazyPages);
20
20
  await update({
21
21
  contentDirectoryPath,
22
22
  staticFilenames,
@@ -33,6 +33,9 @@ export const prebuild = async (contentDirectoryPath, { localSchema, groups, disa
33
33
  strict,
34
34
  invalidSpecFiles,
35
35
  allowSourceRefs,
36
+ lazyPages,
37
+ pageScans,
38
+ fileImportsMap,
36
39
  });
37
40
  // Deferred until after update so files that fail the build don't also warn
38
41
  warnInvalidSpecFiles(invalidSpecFiles);
@@ -1,6 +1,7 @@
1
1
  import { AsyncAPIFile } from '@mintlify/common';
2
2
  import type { OpenApiFile } from '@mintlify/models';
3
3
  import type { Root } from 'mdast';
4
+ import type { PageScan } from '../categorizeFilePaths.js';
4
5
  import type { InvalidSpecFile } from '../invalidSpecFiles.js';
5
6
  type UpdateArgs = {
6
7
  contentDirectoryPath: string;
@@ -18,8 +19,11 @@ type UpdateArgs = {
18
19
  strict?: boolean;
19
20
  invalidSpecFiles?: InvalidSpecFile[];
20
21
  allowSourceRefs?: boolean;
22
+ lazyPages?: boolean;
23
+ pageScans?: Map<string, PageScan>;
24
+ fileImportsMap?: Map<string, Set<string>>;
21
25
  };
22
- export declare const update: ({ contentDirectoryPath, staticFilenames, openApiFiles, asyncApiFiles, contentFilenames, snippets, snippetV2Filenames, docsConfigPath, localSchema, groups, mintIgnore, disableOpenApi, strict, invalidSpecFiles, allowSourceRefs, }: UpdateArgs) => Promise<{
26
+ export declare const update: ({ contentDirectoryPath, staticFilenames, openApiFiles, asyncApiFiles, contentFilenames, snippets, snippetV2Filenames, docsConfigPath, localSchema, groups, mintIgnore, disableOpenApi, strict, invalidSpecFiles, allowSourceRefs, lazyPages, pageScans, fileImportsMap, }: UpdateArgs) => Promise<{
23
27
  name: string;
24
28
  $schema: string;
25
29
  theme: "mint";
@@ -4,6 +4,7 @@ import { outputFile } from 'fs-extra';
4
4
  import { join } from 'path';
5
5
  import { updateDocsConfigFile } from './docsConfig/index.js';
6
6
  import { updateMintConfigFile } from './mintConfig/index.js';
7
+ import { scanPageMetadata } from './read/read-page-metadata.js';
7
8
  import { readPageContents, readSnippetsV2Contents } from './read/readContent.js';
8
9
  import { resolveImportsAndWriteFiles } from './resolveImportsAndWriteFiles.js';
9
10
  import { updateFavicons } from './updateFavicons.js';
@@ -12,7 +13,52 @@ import { writeAsyncApiFiles } from './write/writeAsyncApiFiles.js';
12
13
  import { writeFiles, writeFile } from './write/writeFiles.js';
13
14
  import { writeOpenApiData } from './write/writeOpenApiData.js';
14
15
  import { writeRssFiles } from './write/writeRssFiles.js';
15
- export const update = async ({ contentDirectoryPath, staticFilenames, openApiFiles, asyncApiFiles, contentFilenames, snippets, snippetV2Filenames, docsConfigPath, localSchema, groups, mintIgnore, disableOpenApi, strict, invalidSpecFiles, allowSourceRefs, }) => {
16
+ const expandRssCandidates = (rssCandidateFilenames, contentFilenames, fileImportsMap, pageScans) => {
17
+ const carriers = new Set();
18
+ for (const [filename, scan] of pageScans) {
19
+ if (scan.rssCandidate) {
20
+ carriers.add(filename.toLowerCase());
21
+ }
22
+ }
23
+ let changed = carriers.size > 0;
24
+ while (changed) {
25
+ changed = false;
26
+ for (const [filename, imports] of fileImportsMap) {
27
+ if (carriers.has(filename))
28
+ continue;
29
+ for (const imported of imports) {
30
+ if (carriers.has(imported)) {
31
+ carriers.add(filename);
32
+ changed = true;
33
+ break;
34
+ }
35
+ }
36
+ }
37
+ }
38
+ const candidates = new Set(rssCandidateFilenames);
39
+ for (const filename of contentFilenames) {
40
+ if (carriers.has(filename.toLowerCase())) {
41
+ candidates.add(filename);
42
+ }
43
+ }
44
+ return [...candidates];
45
+ };
46
+ const filterToImportClosure = (snippetV2Filenames, rootFilenames, fileImportsMap) => {
47
+ const needed = new Set();
48
+ const stack = rootFilenames.map((filename) => filename.toLowerCase());
49
+ let current = stack.pop();
50
+ while (current !== undefined) {
51
+ for (const imported of fileImportsMap.get(current) ?? []) {
52
+ if (!needed.has(imported)) {
53
+ needed.add(imported);
54
+ stack.push(imported);
55
+ }
56
+ }
57
+ current = stack.pop();
58
+ }
59
+ return snippetV2Filenames.filter((filename) => needed.has(filename.toLowerCase()));
60
+ };
61
+ export const update = async ({ contentDirectoryPath, staticFilenames, openApiFiles, asyncApiFiles, contentFilenames, snippets, snippetV2Filenames, docsConfigPath, localSchema, groups, mintIgnore, disableOpenApi, strict, invalidSpecFiles, allowSourceRefs, lazyPages, pageScans, fileImportsMap, }) => {
16
62
  const mintConfigResult = await updateMintConfigFile(contentDirectoryPath, openApiFiles, localSchema, strict, invalidSpecFiles);
17
63
  // we used the original mint config without openapi pages injected
18
64
  // because we will do it in `updateDocsConfigFile`, this will avoid duplicated openapi pages
@@ -28,14 +74,32 @@ export const update = async ({ contentDirectoryPath, staticFilenames, openApiFil
28
74
  invalidSpecFiles,
29
75
  allowSourceRefs,
30
76
  });
77
+ let contentFilenamesToParse = contentFilenames;
78
+ if (lazyPages) {
79
+ const { rssCandidateFilenames } = await scanPageMetadata({
80
+ contentDirectoryPath,
81
+ openApiFiles: newOpenApiFiles,
82
+ asyncApiFiles: newAsyncApiFiles,
83
+ contentFilenames,
84
+ pagesAcc,
85
+ pageScans,
86
+ });
87
+ contentFilenamesToParse =
88
+ fileImportsMap && pageScans
89
+ ? expandRssCandidates(rssCandidateFilenames, contentFilenames, fileImportsMap, pageScans)
90
+ : rssCandidateFilenames;
91
+ }
31
92
  const pagePromises = readPageContents({
32
93
  contentDirectoryPath,
33
94
  openApiFiles: newOpenApiFiles,
34
95
  asyncApiFiles: newAsyncApiFiles,
35
- contentFilenames,
96
+ contentFilenames: contentFilenamesToParse,
36
97
  pagesAcc,
37
98
  });
38
- const snippetV2Promises = readSnippetsV2Contents(contentDirectoryPath, snippetV2Filenames, newDocsConfig.variables ?? undefined);
99
+ const snippetV2FilenamesToParse = lazyPages && fileImportsMap
100
+ ? filterToImportClosure(snippetV2Filenames, contentFilenamesToParse, fileImportsMap)
101
+ : snippetV2Filenames;
102
+ const snippetV2Promises = readSnippetsV2Contents(contentDirectoryPath, snippetV2FilenamesToParse, newDocsConfig.variables ?? undefined);
39
103
  const [snippetV2Contents, { mdxFilesWithNoImports, filesWithImports }] = await Promise.all([
40
104
  snippetV2Promises,
41
105
  pagePromises,
@@ -0,0 +1,15 @@
1
+ import { AsyncAPIFile } from '@mintlify/common';
2
+ import type { DecoratedNavigationPage, OpenApiFile } from '@mintlify/models';
3
+ import type { PageScan } from '../../categorizeFilePaths.js';
4
+ type ScanPageMetadataArgs = {
5
+ contentDirectoryPath: string;
6
+ openApiFiles: OpenApiFile[];
7
+ asyncApiFiles: AsyncAPIFile[];
8
+ contentFilenames: string[];
9
+ pagesAcc: Record<string, DecoratedNavigationPage>;
10
+ pageScans?: Map<string, PageScan>;
11
+ };
12
+ export declare const scanPageMetadata: ({ contentDirectoryPath, openApiFiles, asyncApiFiles, contentFilenames, pagesAcc, pageScans, }: ScanPageMetadataArgs) => Promise<{
13
+ rssCandidateFilenames: string[];
14
+ }>;
15
+ export {};
@@ -0,0 +1,46 @@
1
+ import { getDecoratedNavPageAndSlug, parseFrontmatter } from '@mintlify/common';
2
+ import { promises as _promises } from 'fs';
3
+ import { join } from 'path';
4
+ import { preserveAutoGeneratedMetadata } from '../preserveAutoGeneratedMetadata.js';
5
+ const { readFile } = _promises;
6
+ const SCAN_CONCURRENCY = 16;
7
+ const hasRssFrontmatter = (contentStr) => {
8
+ try {
9
+ return parseFrontmatter(contentStr).attributes.rss === true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ };
15
+ export const scanPageMetadata = async ({ contentDirectoryPath, openApiFiles, asyncApiFiles, contentFilenames, pagesAcc, pageScans, }) => {
16
+ const rssCandidateFilenames = [];
17
+ const queue = [...contentFilenames];
18
+ const scanFile = async (filename) => {
19
+ const scan = pageScans?.get(filename);
20
+ const contentStr = scan?.frontmatter ?? (await readFile(join(contentDirectoryPath, filename), 'utf8'));
21
+ const { slug, pageMetadata } = getDecoratedNavPageAndSlug(filename, contentStr, openApiFiles, asyncApiFiles);
22
+ preserveAutoGeneratedMetadata(contentStr, slug, pageMetadata, pagesAcc);
23
+ const rssCandidate = scan
24
+ ? scan.rssCandidate || hasRssFrontmatter(contentStr)
25
+ : contentStr.includes('<Update') || hasRssFrontmatter(contentStr);
26
+ if (rssCandidate) {
27
+ rssCandidateFilenames.push(filename);
28
+ }
29
+ };
30
+ await Promise.all(Array.from({ length: Math.min(SCAN_CONCURRENCY, queue.length) }, async () => {
31
+ let filename = queue.shift();
32
+ while (filename !== undefined) {
33
+ try {
34
+ await scanFile(filename);
35
+ }
36
+ catch (error) {
37
+ const errorMessage = error instanceof Error
38
+ ? error.message
39
+ : 'Unknown error occurred reading page metadata.';
40
+ console.error(`${join(contentDirectoryPath, filename)}: ${errorMessage}`);
41
+ }
42
+ filename = queue.shift();
43
+ }
44
+ }));
45
+ return { rssCandidateFilenames };
46
+ };
@@ -1,13 +1,31 @@
1
1
  import fse from 'fs-extra';
2
2
  import path from 'path';
3
+ const isUnchangedFile = async (sourcePath, targetPath) => {
4
+ try {
5
+ const [sourceStat, targetStat] = await Promise.all([
6
+ fse.stat(sourcePath),
7
+ fse.stat(targetPath),
8
+ ]);
9
+ return (sourceStat.isFile() &&
10
+ targetStat.isFile() &&
11
+ sourceStat.size === targetStat.size &&
12
+ Math.abs(sourceStat.mtimeMs - targetStat.mtimeMs) < 2);
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ };
3
18
  export const writeFiles = (contentDirectoryPath, targetDirectoryPath, filenames) => {
4
19
  const filePromises = [];
5
20
  filenames.forEach((filename) => {
6
21
  filePromises.push((async () => {
7
22
  const sourcePath = path.join(contentDirectoryPath, filename);
8
23
  const targetPath = path.join(targetDirectoryPath, filename);
24
+ if (await isUnchangedFile(sourcePath, targetPath)) {
25
+ return;
26
+ }
9
27
  await fse.remove(targetPath);
10
- await fse.copy(sourcePath, targetPath);
28
+ await fse.copy(sourcePath, targetPath, { preserveTimestamps: true });
11
29
  })());
12
30
  });
13
31
  return filePromises;