@mintlify/prebuild 1.0.1335 → 1.0.1337

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.
@@ -2,6 +2,7 @@ import { isSdkFrontmatterValue } from '@mintlify/common';
2
2
  import { type DecoratedNavigationPage, type ResolvedSdkTarget, type SdkTarget } from '@mintlify/models';
3
3
  import { type SdkFormat, type SdkReference } from '@mintlify/scraping';
4
4
  import type { DocsConfig } from '@mintlify/validation';
5
+ import type { PageScan } from '../../categorizeFilePaths.js';
5
6
  import { type LoadRemoteFileOptions } from './loadRemoteGraphqlSdl.js';
6
7
  type SdkConfig = {
7
8
  format: SdkFormat;
@@ -48,7 +49,7 @@ export declare function composeSdkFrontmatterPage(args: {
48
49
  inherited?: SdkConfig;
49
50
  }): Promise<GeneratedSdkPage | undefined>;
50
51
  export { isSdkFrontmatterValue };
51
- export declare function readContainedMarkdown(contentRoot: string, slug: string): Promise<{
52
+ export declare function readContainedMarkdown(contentRoot: string, slug: string, pageScans?: ReadonlyMap<string, PageScan>): Promise<{
52
53
  content: string;
53
54
  extension: '.mdx' | '.md';
54
55
  } | undefined>;
@@ -1,4 +1,4 @@
1
- import { isSdkFrontmatterValue, parseFrontmatter, parseSdkTargetFromMetadata, } from '@mintlify/common';
1
+ import { isSdkFrontmatterValue, parseFrontmatter, parseSdkTargetFromMetadata, toPosixPath, } from '@mintlify/common';
2
2
  import { generateSdkReference, renderSdkTarget, } from '@mintlify/scraping';
3
3
  import { Unzip, UnzipInflate } from 'fflate';
4
4
  import fse from 'fs-extra';
@@ -296,7 +296,7 @@ function stripFrontmatter(content) {
296
296
  return content.slice(end + 4).replace(/^\n+/, '');
297
297
  }
298
298
  export { isSdkFrontmatterValue };
299
- export async function readContainedMarkdown(contentRoot, slug) {
299
+ export async function readContainedMarkdown(contentRoot, slug, pageScans) {
300
300
  for (const extension of ['.mdx', '.md']) {
301
301
  const filePath = path.resolve(contentRoot, `${slug}${extension}`);
302
302
  const relativePath = path.relative(contentRoot, filePath);
@@ -314,7 +314,9 @@ export async function readContainedMarkdown(contentRoot, slug) {
314
314
  path.isAbsolute(realRelative)) {
315
315
  continue;
316
316
  }
317
- return { content: await fse.readFile(realFilePath, 'utf8'), extension };
317
+ // Reuse cached headers only after validating the file's real path containment.
318
+ const frontmatter = pageScans?.get(`/${toPosixPath(relativePath)}`)?.frontmatter;
319
+ return { content: frontmatter ?? (await fse.readFile(realFilePath, 'utf8')), extension };
318
320
  }
319
321
  return undefined;
320
322
  }
@@ -2,8 +2,9 @@ import { AsyncAPIFile, type OverlayRegistry } from '@mintlify/common';
2
2
  import { OpenApiFile, DecoratedNavigationPage } from '@mintlify/models';
3
3
  import type { SdkFormat } from '@mintlify/scraping';
4
4
  import { DocsConfig } from '@mintlify/validation';
5
+ import type { PageScan } from '../../categorizeFilePaths.js';
5
6
  import type { InvalidSpecFile } from '../../invalidSpecFiles.js';
6
- export declare function updateDocsConfigFile({ contentDirectoryPath, openApiFiles, asyncApiFiles, docsConfig, localSchema, disableOpenApi, strict, invalidSpecFiles, allowSourceRefs, overlayRegistry, }: {
7
+ export declare function updateDocsConfigFile({ contentDirectoryPath, openApiFiles, asyncApiFiles, docsConfig, localSchema, disableOpenApi, strict, invalidSpecFiles, allowSourceRefs, overlayRegistry, pageScans, }: {
7
8
  contentDirectoryPath: string;
8
9
  openApiFiles: OpenApiFile[];
9
10
  asyncApiFiles: AsyncAPIFile[];
@@ -14,6 +15,7 @@ export declare function updateDocsConfigFile({ contentDirectoryPath, openApiFile
14
15
  invalidSpecFiles?: InvalidSpecFile[];
15
16
  allowSourceRefs?: boolean;
16
17
  overlayRegistry?: OverlayRegistry;
18
+ pageScans?: ReadonlyMap<string, PageScan>;
17
19
  }): Promise<{
18
20
  docsConfig: DocsConfig;
19
21
  pagesAcc: Record<string, DecoratedNavigationPage>;
@@ -1,60 +1,14 @@
1
- import { parseFrontmatter } from '@mintlify/common';
2
- import fse from 'fs-extra';
3
1
  import { getConfigPath } from '../../../utils.js';
4
2
  import { DocsConfigUpdater } from '../ConfigUpdater.js';
3
+ import { scanSdkFrontmatterMetadata } from '../read/read-sdk-page-metadata.js';
5
4
  import { generateAsyncApiDivisions } from './generateAsyncApiDivisions.js';
6
5
  import { assertNoGeneratedRouteCollisions } from './generatedRouteCollisions.js';
7
6
  import { generateGraphqlDivisionsFromDocsConfig, loadGraphqlSdlSource, writeGraphqlArtifacts, } from './generateGraphqlDivisions.js';
8
7
  import { generateOpenApiDivisions } from './generateOpenApiDivisions.js';
9
- import { generateSdkDivisionsFromDocsConfig, loadSdkReferenceSource, readContainedMarkdown, writeSdkArtifacts, } from './generateSdkDivisions.js';
8
+ import { generateSdkDivisionsFromDocsConfig, loadSdkReferenceSource, writeSdkArtifacts, } from './generateSdkDivisions.js';
10
9
  import { getCustomLanguages } from './getCustomLanguages.js';
11
10
  const NOT_CORRECT_PATH_ERROR = 'must be run in a directory where a docs.json file exists.';
12
- function isPlainObject(value) {
13
- return typeof value === 'object' && value !== null && !Array.isArray(value);
14
- }
15
- function collectNavigationPageSlugs(value, slugs = new Set()) {
16
- if (typeof value === 'string') {
17
- slugs.add(value.replace(/^\//, '').replace(/\.mdx?$/, ''));
18
- return slugs;
19
- }
20
- if (Array.isArray(value)) {
21
- value.forEach((entry) => collectNavigationPageSlugs(entry, slugs));
22
- return slugs;
23
- }
24
- if (!isPlainObject(value))
25
- return slugs;
26
- if ('root' in value)
27
- collectNavigationPageSlugs(value.root, slugs);
28
- if ('pages' in value)
29
- collectNavigationPageSlugs(value.pages, slugs);
30
- if ('groups' in value)
31
- collectNavigationPageSlugs(value.groups, slugs);
32
- for (const key of ['tabs', 'anchors', 'dropdowns', 'versions', 'languages', 'products', 'menu']) {
33
- if (key in value)
34
- collectNavigationPageSlugs(value[key], slugs);
35
- }
36
- return slugs;
37
- }
38
- async function scanSdkFrontmatterMetadata(contentDirectoryPath, navigation) {
39
- const contentRoot = await fse.realpath(contentDirectoryPath);
40
- const metadataBySlug = {};
41
- await Promise.all([...collectNavigationPageSlugs(navigation)].map(async (slug) => {
42
- try {
43
- const file = await readContainedMarkdown(contentRoot, slug);
44
- if (file === undefined)
45
- return;
46
- const { attributes } = parseFrontmatter(file.content);
47
- if (attributes.sdk !== undefined) {
48
- metadataBySlug[slug] = { sdk: attributes.sdk };
49
- }
50
- }
51
- catch {
52
- // ignore unreadable/invalid frontmatter during scan
53
- }
54
- }));
55
- return metadataBySlug;
56
- }
57
- export async function updateDocsConfigFile({ contentDirectoryPath, openApiFiles, asyncApiFiles, docsConfig, localSchema, disableOpenApi, strict, invalidSpecFiles, allowSourceRefs, overlayRegistry, }) {
11
+ export async function updateDocsConfigFile({ contentDirectoryPath, openApiFiles, asyncApiFiles, docsConfig, localSchema, disableOpenApi, strict, invalidSpecFiles, allowSourceRefs, overlayRegistry, pageScans, }) {
58
12
  const configPath = await getConfigPath(contentDirectoryPath, 'docs');
59
13
  if (configPath == null && docsConfig == null) {
60
14
  throw Error(NOT_CORRECT_PATH_ERROR);
@@ -79,7 +33,7 @@ export async function updateDocsConfigFile({ contentDirectoryPath, openApiFiles,
79
33
  flushWrites: async () => { },
80
34
  };
81
35
  const { newDocsConfig: docsConfigWithAsyncApiPages, pagesAcc: pagesAccWithAsyncApiPages, asyncApiFiles: newAsyncApiFiles, flushWrites: flushAsyncApiWrites, } = await generateAsyncApiDivisions(docsConfigWithOpenApiPages, asyncApiFiles, undefined, localSchema, pagesAccWithGraphqlPages, true, true);
82
- const pageMetadataBySlug = await scanSdkFrontmatterMetadata(contentDirectoryPath, docsConfigWithAsyncApiPages.navigation);
36
+ const pageMetadataBySlug = await scanSdkFrontmatterMetadata(contentDirectoryPath, docsConfigWithAsyncApiPages.navigation, pageScans);
83
37
  const sdkResult = await generateSdkDivisionsFromDocsConfig(docsConfigWithAsyncApiPages, (config) => loadSdkReferenceSource(config, contentDirectoryPath), { pageMetadataBySlug });
84
38
  const { newDocsConfig: docsConfigWithSdkPages, pagesAcc: pagesAccWithSdkPages } = sdkResult;
85
39
  const sdkRoutes = { label: 'SDK', pages: pagesAccWithSdkPages };
@@ -74,6 +74,7 @@ export const update = async ({ contentDirectoryPath, staticFilenames, openApiFil
74
74
  invalidSpecFiles,
75
75
  allowSourceRefs,
76
76
  overlayRegistry,
77
+ pageScans,
77
78
  });
78
79
  let contentFilenamesToParse = contentFilenames;
79
80
  if (lazyPages) {
@@ -1,7 +1,8 @@
1
1
  import type { DecoratedNavigationPage } from '@mintlify/models';
2
+ export declare const createGeneratedGraphqlRouteIndex: (pagesAcc: Record<string, DecoratedNavigationPage>) => Map<string, string>;
2
3
  /**
3
4
  * When user MDX doesn't explicitly set title/description, preserve the
4
5
  * auto-generated values (e.g. from OpenAPI schemas) that already exist
5
6
  * in pagesAcc. Mutates pageMetadata and pagesAcc in place.
6
7
  */
7
- export declare const preserveAutoGeneratedMetadata: (contentStr: string, slug: string, pageMetadata: DecoratedNavigationPage, pagesAcc: Record<string, DecoratedNavigationPage>) => void;
8
+ export declare const preserveAutoGeneratedMetadata: (contentStr: string, slug: string, pageMetadata: DecoratedNavigationPage, pagesAcc: Record<string, DecoratedNavigationPage>, graphqlRoutes?: Map<string, string>) => void;
@@ -1,14 +1,26 @@
1
1
  import { parseFrontmatter } from '@mintlify/common';
2
2
  import { getGeneratedRouteKey } from './docsConfig/generatedRouteCollisions.js';
3
+ export const createGeneratedGraphqlRouteIndex = (pagesAcc) => {
4
+ const routes = new Map();
5
+ for (const [slug, metadata] of Object.entries(pagesAcc)) {
6
+ if (metadata.graphql != null) {
7
+ const routeKey = getGeneratedRouteKey(slug);
8
+ if (!routes.has(routeKey))
9
+ routes.set(routeKey, slug);
10
+ }
11
+ }
12
+ return routes;
13
+ };
3
14
  /**
4
15
  * When user MDX doesn't explicitly set title/description, preserve the
5
16
  * auto-generated values (e.g. from OpenAPI schemas) that already exist
6
17
  * in pagesAcc. Mutates pageMetadata and pagesAcc in place.
7
18
  */
8
- export const preserveAutoGeneratedMetadata = (contentStr, slug, pageMetadata, pagesAcc) => {
9
- const graphqlCollision = Object.entries(pagesAcc).find(([existingSlug, metadata]) => metadata.graphql != null && getGeneratedRouteKey(existingSlug) === getGeneratedRouteKey(slug));
10
- if (graphqlCollision) {
11
- throw new Error(`User page route "${slug}" collides with generated GraphQL route "${graphqlCollision[0]}"`);
19
+ export const preserveAutoGeneratedMetadata = (contentStr, slug, pageMetadata, pagesAcc, graphqlRoutes = createGeneratedGraphqlRouteIndex(pagesAcc)) => {
20
+ const routeKey = getGeneratedRouteKey(slug);
21
+ const graphqlCollision = graphqlRoutes.get(routeKey);
22
+ if (graphqlCollision !== undefined) {
23
+ throw new Error(`User page route "${slug}" collides with generated GraphQL route "${graphqlCollision}"`);
12
24
  }
13
25
  const existing = pagesAcc[slug];
14
26
  if (existing) {
@@ -24,4 +36,6 @@ export const preserveAutoGeneratedMetadata = (contentStr, slug, pageMetadata, pa
24
36
  catch { }
25
37
  }
26
38
  pagesAcc[slug] = pageMetadata;
39
+ if (pageMetadata.graphql != null)
40
+ graphqlRoutes.set(routeKey, slug);
27
41
  };
@@ -1,7 +1,7 @@
1
1
  import { getDecoratedNavPageAndSlug, parseFrontmatter } from '@mintlify/common';
2
2
  import { promises as _promises } from 'fs';
3
3
  import { join } from 'path';
4
- import { preserveAutoGeneratedMetadata } from '../preserveAutoGeneratedMetadata.js';
4
+ import { createGeneratedGraphqlRouteIndex, preserveAutoGeneratedMetadata, } from '../preserveAutoGeneratedMetadata.js';
5
5
  const { readFile } = _promises;
6
6
  const SCAN_CONCURRENCY = 16;
7
7
  const hasRssFrontmatter = (contentStr) => {
@@ -15,11 +15,12 @@ const hasRssFrontmatter = (contentStr) => {
15
15
  export const scanPageMetadata = async ({ contentDirectoryPath, openApiFiles, asyncApiFiles, contentFilenames, pagesAcc, pageScans, }) => {
16
16
  const rssCandidateFilenames = [];
17
17
  const queue = [...contentFilenames];
18
+ const graphqlRoutes = createGeneratedGraphqlRouteIndex(pagesAcc);
18
19
  const scanFile = async (filename) => {
19
20
  const scan = pageScans?.get(filename);
20
21
  const contentStr = scan?.frontmatter ?? (await readFile(join(contentDirectoryPath, filename), 'utf8'));
21
22
  const { slug, pageMetadata } = getDecoratedNavPageAndSlug(filename, contentStr, openApiFiles, asyncApiFiles);
22
- preserveAutoGeneratedMetadata(contentStr, slug, pageMetadata, pagesAcc);
23
+ preserveAutoGeneratedMetadata(contentStr, slug, pageMetadata, pagesAcc, graphqlRoutes);
23
24
  const rssCandidate = scan
24
25
  ? scan.rssCandidate || hasRssFrontmatter(contentStr)
25
26
  : contentStr.includes('<Update') || hasRssFrontmatter(contentStr);
@@ -0,0 +1,5 @@
1
+ import type { DocsConfig } from '@mintlify/validation';
2
+ import type { PageScan } from '../../categorizeFilePaths.js';
3
+ export declare function scanSdkFrontmatterMetadata(contentDirectoryPath: string, navigation: DocsConfig['navigation'], pageScans?: ReadonlyMap<string, PageScan>): Promise<Record<string, {
4
+ sdk?: unknown;
5
+ }>>;
@@ -0,0 +1,54 @@
1
+ import { parseFrontmatter } from '@mintlify/common';
2
+ import fse from 'fs-extra';
3
+ import { readContainedMarkdown } from '../docsConfig/generateSdkDivisions.js';
4
+ const SDK_SCAN_CONCURRENCY = 16;
5
+ function isPlainObject(value) {
6
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
7
+ }
8
+ function collectNavigationPageSlugs(value, slugs = new Set()) {
9
+ if (typeof value === 'string') {
10
+ slugs.add(value.replace(/^\//, '').replace(/\.mdx?$/, ''));
11
+ return slugs;
12
+ }
13
+ if (Array.isArray(value)) {
14
+ value.forEach((entry) => collectNavigationPageSlugs(entry, slugs));
15
+ return slugs;
16
+ }
17
+ if (!isPlainObject(value))
18
+ return slugs;
19
+ if ('root' in value)
20
+ collectNavigationPageSlugs(value.root, slugs);
21
+ if ('pages' in value)
22
+ collectNavigationPageSlugs(value.pages, slugs);
23
+ if ('groups' in value)
24
+ collectNavigationPageSlugs(value.groups, slugs);
25
+ for (const key of ['tabs', 'anchors', 'dropdowns', 'versions', 'languages', 'products', 'menu']) {
26
+ if (key in value)
27
+ collectNavigationPageSlugs(value[key], slugs);
28
+ }
29
+ return slugs;
30
+ }
31
+ export async function scanSdkFrontmatterMetadata(contentDirectoryPath, navigation, pageScans) {
32
+ const contentRoot = await fse.realpath(contentDirectoryPath);
33
+ const metadataBySlug = {};
34
+ const slugs = collectNavigationPageSlugs(navigation);
35
+ // Workers share an iterator so large navigation trees don't enqueue every file at once.
36
+ const queue = slugs.values();
37
+ await Promise.all(Array.from({ length: Math.min(SDK_SCAN_CONCURRENCY, slugs.size) }, async () => {
38
+ for (const slug of queue) {
39
+ try {
40
+ const file = await readContainedMarkdown(contentRoot, slug, pageScans);
41
+ if (file === undefined)
42
+ continue;
43
+ const { attributes } = parseFrontmatter(file.content);
44
+ if (attributes.sdk !== undefined) {
45
+ metadataBySlug[slug] = { sdk: attributes.sdk };
46
+ }
47
+ }
48
+ catch {
49
+ // Ignore unreadable/invalid frontmatter during the scan.
50
+ }
51
+ }
52
+ }));
53
+ return metadataBySlug;
54
+ }
@@ -4,13 +4,14 @@ import { outputFile } from 'fs-extra';
4
4
  import { join } from 'path';
5
5
  import { preparseMdxTree } from '../../../createPage/preparseMdx/index.js';
6
6
  import { warnOnParseError } from '../../warnings.js';
7
- import { preserveAutoGeneratedMetadata } from '../preserveAutoGeneratedMetadata.js';
7
+ import { createGeneratedGraphqlRouteIndex, preserveAutoGeneratedMetadata, } from '../preserveAutoGeneratedMetadata.js';
8
8
  const { readFile } = _promises;
9
9
  const PAGE_PARSE_CONCURRENCY = 8;
10
10
  export const readPageContents = async ({ contentDirectoryPath, openApiFiles, asyncApiFiles, contentFilenames, pagesAcc, }) => {
11
11
  const filesWithImports = [];
12
12
  const mdxFilesWithNoImports = [];
13
13
  const remainingFilenames = [...contentFilenames];
14
+ const graphqlRoutes = createGeneratedGraphqlRouteIndex(pagesAcc);
14
15
  const processFile = async (filename) => {
15
16
  const sourcePath = join(contentDirectoryPath, filename);
16
17
  const targetPath = join('src', '_props', filename);
@@ -34,7 +35,7 @@ export const readPageContents = async ({ contentDirectoryPath, openApiFiles, asy
34
35
  return;
35
36
  }
36
37
  const { slug, pageMetadata } = getDecoratedNavPageAndSlug(filename, contentStr, openApiFiles, asyncApiFiles);
37
- preserveAutoGeneratedMetadata(contentStr, slug, pageMetadata, pagesAcc);
38
+ preserveAutoGeneratedMetadata(contentStr, slug, pageMetadata, pagesAcc, graphqlRoutes);
38
39
  // Only RSS candidates are needed by later phases (writeRssFiles). All
39
40
  // other import-free pages are written and dropped immediately so peak
40
41
  // memory does not grow with the number of pages.
@@ -2,7 +2,7 @@ import { resolveAllImports, hasImports, findAndRemoveImports, getDecoratedNavPag
2
2
  import { outputFile } from 'fs-extra';
3
3
  import { join } from 'path';
4
4
  import { addWarning } from '../warnings.js';
5
- import { preserveAutoGeneratedMetadata } from './preserveAutoGeneratedMetadata.js';
5
+ import { createGeneratedGraphqlRouteIndex, preserveAutoGeneratedMetadata, } from './preserveAutoGeneratedMetadata.js';
6
6
  export const resolveImportsAndWriteFiles = async ({ openApiFiles, asyncApiFiles, pagesAcc, snippetsV2, filesWithImports, }) => {
7
7
  const snippetsWithResolvedImports = await resolveImportsInSnippets(snippetsV2);
8
8
  const writeSnippetsWithImportsPromises = await writeSnippets(snippetsWithResolvedImports);
@@ -50,6 +50,7 @@ const writeSnippets = async (snippetsWithImports) => snippetsWithImports.map(asy
50
50
  });
51
51
  });
52
52
  const resolveImportsInPages = async ({ openApiFiles, asyncApiFiles, snippetsWithResolvedImports, filesWithImports, pagesAcc, }) => {
53
+ const graphqlRoutes = createGeneratedGraphqlRouteIndex(pagesAcc);
53
54
  const filesWithResolvedImportsPromises = filesWithImports.map(async (fileWithImports) => {
54
55
  const tree = await resolveAllImports({
55
56
  snippets: snippetsWithResolvedImports,
@@ -58,7 +59,7 @@ const resolveImportsInPages = async ({ openApiFiles, asyncApiFiles, snippetsWith
58
59
  });
59
60
  const contentStr = stringifyTree(tree);
60
61
  const { slug, pageMetadata } = getDecoratedNavPageAndSlug(fileWithImports.filename, contentStr, openApiFiles, asyncApiFiles);
61
- preserveAutoGeneratedMetadata(contentStr, slug, pageMetadata, pagesAcc);
62
+ preserveAutoGeneratedMetadata(contentStr, slug, pageMetadata, pagesAcc, graphqlRoutes);
62
63
  const targetPath = join('src', '_props', fileWithImports.filename);
63
64
  return {
64
65
  targetPath,