@mintlify/prebuild 1.0.1237 → 1.0.1239

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,6 +1,9 @@
1
- import { generateSdkReference } from '@mintlify/scraping';
1
+ import { parseFrontmatter, parseSdkTargetFromMetadata, potentiallyParseSdkString, } from '@mintlify/common';
2
+ import { SDK_FORMATS, SDK_SYMBOL_KINDS, } from '@mintlify/models';
3
+ import { generateSdkReference, renderSdkTarget, } from '@mintlify/scraping';
2
4
  import { Unzip, UnzipInflate } from 'fflate';
3
5
  import fse from 'fs-extra';
6
+ import yaml from 'js-yaml';
4
7
  import os from 'node:os';
5
8
  import path from 'node:path';
6
9
  import { getGeneratedRouteKey } from './generatedRouteCollisions.js';
@@ -10,9 +13,108 @@ const REMOTE_ARTIFACT_LABEL = 'SDK artifact';
10
13
  const MAX_REMOTE_SDK_ARTIFACT_BYTES = 50 * 1024 * 1024;
11
14
  const MAX_EXTRACTED_SDK_ARTIFACT_BYTES = 200 * 1024 * 1024;
12
15
  const REMOTE_SDK_ARTIFACT_TIMEOUT_MS = 30_000;
13
- export async function generateSdkDivisionsFromDocsConfig(docsConfig, loadReference) {
16
+ function isSdkConfig(value) {
17
+ if (typeof value !== 'object' || value === null)
18
+ return false;
19
+ const record = value;
20
+ return typeof record.format === 'string' && typeof record.source === 'string';
21
+ }
22
+ function collectPageSlugs(value, slugs = [], atRoot = false) {
23
+ if (typeof value === 'string') {
24
+ slugs.push(value.replace(/^\//, '').replace(/\.mdx?$/, ''));
25
+ return slugs;
26
+ }
27
+ if (Array.isArray(value)) {
28
+ for (const entry of value)
29
+ collectPageSlugs(entry, slugs);
30
+ return slugs;
31
+ }
32
+ if (typeof value !== 'object' || value === null)
33
+ return slugs;
34
+ const node = value;
35
+ // Pages under a nested sdk owner belong to that owner's skip-bulk decision.
36
+ if (!atRoot && 'sdk' in node && isSdkConfig(node.sdk))
37
+ return slugs;
38
+ if ('root' in node)
39
+ collectPageSlugs(node.root, slugs);
40
+ if ('pages' in node)
41
+ collectPageSlugs(node.pages, slugs);
42
+ if ('groups' in node)
43
+ collectPageSlugs(node.groups, slugs);
44
+ for (const key of ['tabs', 'anchors', 'dropdowns', 'versions', 'languages', 'products', 'menu']) {
45
+ if (key in node)
46
+ collectPageSlugs(node[key], slugs);
47
+ }
48
+ return slugs;
49
+ }
50
+ function hasExplicitSdkPages(node, pageMetadataBySlug, inherited) {
51
+ if (!pageMetadataBySlug)
52
+ return false;
53
+ return collectPageSlugs(node, [], true).some((slug) => {
54
+ const metadata = pageMetadataBySlug[slug] ?? pageMetadataBySlug[`/${slug}`];
55
+ return parseSdkTargetFromMetadata({ sdk: metadata?.sdk }, inherited) !== undefined;
56
+ });
57
+ }
58
+ export function navigationHasSdkConfig(value) {
59
+ if (Array.isArray(value))
60
+ return value.some(navigationHasSdkConfig);
61
+ if (typeof value !== 'object' || value === null)
62
+ return false;
63
+ if (('tab' in value || 'group' in value) &&
64
+ 'sdk' in value &&
65
+ typeof value.sdk === 'object' &&
66
+ value.sdk !== null) {
67
+ return true;
68
+ }
69
+ return Object.values(value).some(navigationHasSdkConfig);
70
+ }
71
+ function toGeneratedPage(page, config, directory, referenceSlugs, autogeneratedBySdk) {
72
+ assertSafeSlug(page.slug, config);
73
+ const slug = path.posix.join(directory, page.slug);
74
+ const sdk = page.target
75
+ ? {
76
+ format: config.format,
77
+ source: config.source,
78
+ kind: page.target.kind,
79
+ name: page.target.name,
80
+ ...(page.target.parent ? { parent: page.target.parent } : {}),
81
+ }
82
+ : undefined;
83
+ return {
84
+ slug,
85
+ title: page.title,
86
+ ...(page.description ? { description: page.description } : {}),
87
+ ...(page.tag ? { tag: page.tag } : {}),
88
+ content: prefixInternalLinks(stripLeadingDescription(page.content, page.description), directory, referenceSlugs),
89
+ format: config.format,
90
+ source: config.source,
91
+ ...(sdk ? { sdk } : {}),
92
+ ...(autogeneratedBySdk ? { autogeneratedBySdk: true } : {}),
93
+ };
94
+ }
95
+ function expandSdkGroups(reference, directory) {
96
+ const pageBySlug = new Map(reference.pages.map((page) => [page.slug, page]));
97
+ const groups = reference.groups.map((group) => ({
98
+ group: group.group,
99
+ pages: group.pages.map((slug) => path.posix.join(directory, slug)),
100
+ }));
101
+ const orphanedSlugs = reference.pages
102
+ .filter((page) => !page.target?.parent)
103
+ .map((page) => page.slug)
104
+ .filter((slug) => !reference.groups.some((group) => group.pages.includes(slug)));
105
+ if (orphanedSlugs.length > 0 && pageBySlug.size > 0) {
106
+ groups.push({
107
+ group: 'Reference',
108
+ pages: orphanedSlugs.map((slug) => path.posix.join(directory, slug)),
109
+ });
110
+ }
111
+ return groups;
112
+ }
113
+ export async function generateSdkDivisionsFromDocsConfig(docsConfig, loadReference, options = {}) {
14
114
  const references = new Map();
15
115
  const pagesByRouteKey = new Map();
116
+ const inheritedSdkBySlug = {};
117
+ const { pageMetadataBySlug } = options;
16
118
  function load(config) {
17
119
  const key = `${config.format}:${config.source}`;
18
120
  let pending = references.get(key);
@@ -22,57 +124,104 @@ export async function generateSdkDivisionsFromDocsConfig(docsConfig, loadReferen
22
124
  }
23
125
  return pending;
24
126
  }
25
- async function processNode(value) {
26
- if (Array.isArray(value))
27
- return Promise.all(value.map(processNode));
127
+ function rememberGenerated(generatedPage) {
128
+ const routeKey = getGeneratedRouteKey(generatedPage.slug);
129
+ const existingPage = pagesByRouteKey.get(routeKey);
130
+ if (existingPage &&
131
+ (existingPage.slug !== generatedPage.slug ||
132
+ existingPage.format !== generatedPage.format ||
133
+ existingPage.source !== generatedPage.source)) {
134
+ throw new Error(`Multiple SDK references generate colliding routes "${existingPage.slug}" and "${generatedPage.slug}"`);
135
+ }
136
+ pagesByRouteKey.set(routeKey, generatedPage);
137
+ }
138
+ async function materializeBulk(config) {
139
+ const directory = config.directory ?? DEFAULT_OUTPUT_DIR;
140
+ const reference = await load(config);
141
+ const topLevelPages = reference.pages.filter((page) => !page.target?.parent);
142
+ const referenceSlugs = new Set(topLevelPages.map((page) => page.slug));
143
+ for (const page of topLevelPages) {
144
+ rememberGenerated(toGeneratedPage(page, config, directory, referenceSlugs, true));
145
+ }
146
+ return { groups: expandSdkGroups({ ...reference, pages: topLevelPages }, directory) };
147
+ }
148
+ async function processPages(pages, inherited) {
149
+ if (!Array.isArray(pages))
150
+ return processNode(pages, inherited);
151
+ return Promise.all(pages.map((entry) => {
152
+ if (typeof entry === 'string') {
153
+ const slug = entry.replace(/^\//, '').replace(/\.mdx?$/, '');
154
+ if (inherited)
155
+ inheritedSdkBySlug[slug] = inherited;
156
+ return entry;
157
+ }
158
+ return processNode(entry, inherited);
159
+ }));
160
+ }
161
+ async function processNode(value, inherited) {
162
+ if (Array.isArray(value)) {
163
+ return Promise.all(value.map((entry) => processNode(entry, inherited)));
164
+ }
165
+ if (typeof value === 'string') {
166
+ return value;
167
+ }
28
168
  if (typeof value !== 'object' || value === null)
29
169
  return value;
30
170
  const node = value;
31
- if ('tab' in node && 'sdk' in node) {
32
- const { sdk, ...tab } = node;
33
- const config = sdk;
34
- const directory = config.directory ?? DEFAULT_OUTPUT_DIR;
35
- const reference = await load(config);
36
- const pageBySlug = new Map(reference.pages.map((page) => [page.slug, page]));
37
- const referenceSlugs = new Set(reference.pages.map((page) => page.slug));
38
- const groups = reference.groups.map((group) => ({
39
- group: group.group,
40
- pages: group.pages.map((slug) => path.posix.join(directory, slug)),
41
- }));
42
- for (const page of reference.pages) {
43
- assertSafeSlug(page.slug, config);
44
- const slug = path.posix.join(directory, page.slug);
45
- const generatedPage = {
46
- slug,
47
- title: page.title,
48
- ...(page.description ? { description: page.description } : {}),
49
- ...(page.tag ? { tag: page.tag } : {}),
50
- content: prefixInternalLinks(stripLeadingDescription(page.content, page.description), directory, referenceSlugs),
51
- format: config.format,
52
- source: config.source,
53
- };
54
- const routeKey = getGeneratedRouteKey(slug);
55
- const existingPage = pagesByRouteKey.get(routeKey);
56
- if (existingPage &&
57
- (existingPage.slug !== slug ||
58
- existingPage.format !== generatedPage.format ||
59
- existingPage.source !== generatedPage.source)) {
60
- throw new Error(`Multiple SDK references generate colliding routes "${existingPage.slug}" and "${slug}"`);
171
+ const ownSdk = 'sdk' in node && isSdkConfig(node.sdk) ? node.sdk : undefined;
172
+ const nextInherited = ownSdk ?? inherited;
173
+ const isTab = 'tab' in node && typeof node.tab === 'string';
174
+ const isGroup = 'group' in node && typeof node.group === 'string';
175
+ if (isGroup && typeof node.root === 'string' && nextInherited) {
176
+ const slug = node.root.replace(/^\//, '').replace(/\.mdx?$/, '');
177
+ inheritedSdkBySlug[slug] = nextInherited;
178
+ }
179
+ if (ownSdk && (isTab || isGroup)) {
180
+ const skipBulk = hasExplicitSdkPages(node, pageMetadataBySlug, ownSdk);
181
+ if (!skipBulk) {
182
+ const { groups } = await materializeBulk(ownSdk);
183
+ if (isTab) {
184
+ const { sdk: _sdk, ...tab } = node;
185
+ const existingGroups = Array.isArray(tab.groups)
186
+ ? (await processNode(tab.groups, ownSdk))
187
+ : [];
188
+ const existingPages = Array.isArray(tab.pages)
189
+ ? (await processPages(tab.pages, ownSdk))
190
+ : [];
191
+ const restEntries = await Promise.all(Object.entries(tab)
192
+ .filter(([key]) => key !== 'groups' && key !== 'pages')
193
+ .map(async ([key, entry]) => [key, await processNode(entry, ownSdk)]));
194
+ return {
195
+ ...Object.fromEntries(restEntries),
196
+ groups: [
197
+ ...existingGroups,
198
+ ...(existingPages.length > 0 ? [{ group: 'Overview', pages: existingPages }] : []),
199
+ ...groups,
200
+ ],
201
+ };
61
202
  }
62
- pagesByRouteKey.set(routeKey, generatedPage);
203
+ const { sdk: _sdk, ...group } = node;
204
+ const processedPages = Array.isArray(group.pages)
205
+ ? (await processPages(group.pages, ownSdk))
206
+ : [];
207
+ return {
208
+ ...group,
209
+ pages: [...processedPages, ...groups],
210
+ };
63
211
  }
64
- const orphanedSlugs = reference.pages
65
- .map((page) => page.slug)
66
- .filter((slug) => !reference.groups.some((group) => group.pages.includes(slug)));
67
- if (orphanedSlugs.length > 0 && pageBySlug.size > 0) {
68
- groups.push({
69
- group: 'Reference',
70
- pages: orphanedSlugs.map((slug) => path.posix.join(directory, slug)),
71
- });
212
+ if (isTab) {
213
+ const { sdk: _sdk, ...tab } = node;
214
+ return processNode(tab, ownSdk);
72
215
  }
73
- return { ...tab, groups: [...(tab.groups ?? []), ...groups] };
216
+ const { sdk: _sdk, ...group } = node;
217
+ return processNode(group, ownSdk);
74
218
  }
75
- return Object.fromEntries(await Promise.all(Object.entries(node).map(async ([key, entry]) => [key, await processNode(entry)])));
219
+ return Object.fromEntries(await Promise.all(Object.entries(node).map(async ([key, entry]) => [
220
+ key,
221
+ key === 'pages'
222
+ ? await processPages(entry, nextInherited)
223
+ : await processNode(entry, nextInherited),
224
+ ])));
76
225
  }
77
226
  const navigation = (await processNode(docsConfig.navigation));
78
227
  const generatedPages = [...pagesByRouteKey.values()].sort((left, right) => left.slug.localeCompare(right.slug));
@@ -83,6 +232,7 @@ export async function generateSdkDivisionsFromDocsConfig(docsConfig, loadReferen
83
232
  title: page.title,
84
233
  ...(page.description ? { description: page.description } : {}),
85
234
  ...(page.tag ? { tag: page.tag } : {}),
235
+ ...(page.sdk ? { sdk: page.sdk } : {}),
86
236
  },
87
237
  ]));
88
238
  return {
@@ -90,8 +240,121 @@ export async function generateSdkDivisionsFromDocsConfig(docsConfig, loadReferen
90
240
  pagesAcc,
91
241
  generatedPages,
92
242
  generatedRoutes: generatedPages.map(({ slug }) => slug),
243
+ inheritedSdkBySlug,
244
+ };
245
+ }
246
+ export async function composeSdkFrontmatterPage(args) {
247
+ const target = parseSdkTargetFromMetadata(args.metadata, args.inherited);
248
+ if (!target)
249
+ return undefined;
250
+ const reference = await args.loadReference({
251
+ format: target.format,
252
+ source: target.source,
253
+ });
254
+ let page;
255
+ try {
256
+ page = renderSdkTarget(reference, target);
257
+ }
258
+ catch (error) {
259
+ console.error(`unable to resolve sdk target for "${args.slug}":`, error);
260
+ return undefined;
261
+ }
262
+ const slug = args.slug.replace(/^\//, '').replace(/\.mdx?$/, '');
263
+ const slugDirectory = path.posix.dirname(slug);
264
+ const directory = args.inherited === undefined
265
+ ? slugDirectory === '.'
266
+ ? DEFAULT_OUTPUT_DIR
267
+ : slugDirectory
268
+ : (args.inherited.directory ?? DEFAULT_OUTPUT_DIR);
269
+ const referenceSlugs = new Set(reference.pages.map((entry) => entry.slug));
270
+ const generatedBody = prefixInternalLinks(stripLeadingDescription(page.content, page.description), directory, referenceSlugs);
271
+ const authorBody = stripFrontmatter(args.authorContent).trim();
272
+ const content = [authorBody, generatedBody].filter(Boolean).join('\n\n');
273
+ return {
274
+ slug,
275
+ title: typeof args.metadata.title === 'string' && args.metadata.title.length > 0
276
+ ? args.metadata.title
277
+ : page.title,
278
+ description: typeof args.metadata.description === 'string' && args.metadata.description.length > 0
279
+ ? args.metadata.description
280
+ : page.description,
281
+ tag: typeof args.metadata.tag === 'string' && args.metadata.tag.length > 0
282
+ ? args.metadata.tag
283
+ : page.tag,
284
+ content,
285
+ generatedBody,
286
+ format: target.format,
287
+ source: target.source,
288
+ sdk: target,
93
289
  };
94
290
  }
291
+ function stripFrontmatter(content) {
292
+ if (!content.startsWith('---'))
293
+ return content;
294
+ const end = content.indexOf('\n---', 3);
295
+ if (end === -1)
296
+ return content;
297
+ return content.slice(end + 4).replace(/^\n+/, '');
298
+ }
299
+ function isSdkSymbolKind(value) {
300
+ for (const kind of SDK_SYMBOL_KINDS) {
301
+ if (kind === value)
302
+ return true;
303
+ }
304
+ return false;
305
+ }
306
+ function isSdkFormatName(value) {
307
+ for (const format of SDK_FORMATS) {
308
+ if (format === value)
309
+ return true;
310
+ }
311
+ return false;
312
+ }
313
+ export function isSdkFrontmatterValue(value) {
314
+ if (typeof value === 'string')
315
+ return potentiallyParseSdkString(value) !== undefined;
316
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
317
+ return false;
318
+ if (!('kind' in value) || !('name' in value))
319
+ return false;
320
+ if (typeof value.kind !== 'string' || !isSdkSymbolKind(value.kind))
321
+ return false;
322
+ if (typeof value.name !== 'string' || value.name.length === 0)
323
+ return false;
324
+ if ('format' in value &&
325
+ value.format !== undefined &&
326
+ (typeof value.format !== 'string' || !isSdkFormatName(value.format))) {
327
+ return false;
328
+ }
329
+ if ('source' in value &&
330
+ value.source !== undefined &&
331
+ (typeof value.source !== 'string' || value.source.length === 0)) {
332
+ return false;
333
+ }
334
+ return true;
335
+ }
336
+ export async function readContainedMarkdown(contentRoot, slug) {
337
+ for (const extension of ['.mdx', '.md']) {
338
+ const filePath = path.resolve(contentRoot, `${slug}${extension}`);
339
+ const relativePath = path.relative(contentRoot, filePath);
340
+ if (relativePath.startsWith(`..${path.sep}`) ||
341
+ relativePath === '..' ||
342
+ path.isAbsolute(relativePath)) {
343
+ continue;
344
+ }
345
+ if (!(await fse.pathExists(filePath)))
346
+ continue;
347
+ const realFilePath = await fse.realpath(filePath);
348
+ const realRelative = path.relative(contentRoot, realFilePath);
349
+ if (realRelative.startsWith(`..${path.sep}`) ||
350
+ realRelative === '..' ||
351
+ path.isAbsolute(realRelative)) {
352
+ continue;
353
+ }
354
+ return { content: await fse.readFile(realFilePath, 'utf8'), extension };
355
+ }
356
+ return undefined;
357
+ }
95
358
  export const isRemoteSdkSource = (source) => {
96
359
  try {
97
360
  return new URL(source).protocol === 'https:';
@@ -252,12 +515,27 @@ function frontmatter(page) {
252
515
  `title: ${JSON.stringify(page.title)}`,
253
516
  ...(page.description ? [`description: ${JSON.stringify(page.description)}`] : []),
254
517
  ...(page.tag ? [`tag: ${JSON.stringify(page.tag)}`] : []),
518
+ ...(page.sdk ? [`sdk: ${JSON.stringify(page.sdk)}`] : []),
255
519
  ];
256
520
  return `---\n${fields.join('\n')}\n---\n\n`;
257
521
  }
258
- function getContainedSdkPagePath(targetDir, page) {
522
+ /** Everything after this marker in a composed `_props` page is regenerated on each run. */
523
+ const SDK_GENERATED_MARKER = '{/* mintlify:sdk-generated */}';
524
+ function stripSdkGeneratedSection(body) {
525
+ const markerIndex = body.indexOf(SDK_GENERATED_MARKER);
526
+ return (markerIndex === -1 ? body : body.slice(0, markerIndex)).trim();
527
+ }
528
+ function stringifyMarkdownFrontmatter(attributes) {
529
+ const cleaned = {};
530
+ for (const [key, value] of Object.entries(attributes)) {
531
+ if (value !== undefined)
532
+ cleaned[key] = value;
533
+ }
534
+ return `---\n${yaml.dump(cleaned, { lineWidth: -1 }).trimEnd()}\n---\n\n`;
535
+ }
536
+ function getContainedSdkPagePath(targetDir, page, extension = '.mdx') {
259
537
  const propsDirectory = path.resolve(targetDir ?? '', 'src', '_props');
260
- const pagePath = path.resolve(propsDirectory, `${page}.mdx`);
538
+ const pagePath = path.resolve(propsDirectory, `${page}${extension}`);
261
539
  const relativePath = path.relative(propsDirectory, pagePath);
262
540
  if (relativePath.startsWith(`..${path.sep}`) ||
263
541
  relativePath === '..' ||
@@ -266,6 +544,112 @@ function getContainedSdkPagePath(targetDir, page) {
266
544
  }
267
545
  return pagePath;
268
546
  }
547
+ export async function writeSdkFrontmatterPages({ contentDirectoryPath, inheritedSdkBySlug, pageMetadataBySlug, loadReference, targetDir, }) {
548
+ const references = new Map();
549
+ const load = (config) => {
550
+ const key = `${config.format}:${config.source}`;
551
+ let pending = references.get(key);
552
+ if (!pending) {
553
+ pending = loadReference(config);
554
+ references.set(key, pending);
555
+ }
556
+ return pending;
557
+ };
558
+ const slugs = new Set([...Object.keys(pageMetadataBySlug), ...Object.keys(inheritedSdkBySlug)]);
559
+ const contentRoot = await fse.realpath(contentDirectoryPath);
560
+ await Promise.all([...slugs].map(async (rawSlug) => {
561
+ const slug = rawSlug.replace(/^\//, '').replace(/\.mdx?$/, '');
562
+ const metadata = pageMetadataBySlug[slug] ?? pageMetadataBySlug[`/${slug}`];
563
+ const inherited = inheritedSdkBySlug[slug] ?? inheritedSdkBySlug[`/${slug}`];
564
+ if (!metadata || parseSdkTargetFromMetadata({ sdk: metadata.sdk }, inherited) === undefined) {
565
+ return;
566
+ }
567
+ const authorFile = await readContainedMarkdown(contentRoot, slug);
568
+ if (!authorFile)
569
+ return;
570
+ const { content: authorContent, extension } = authorFile;
571
+ let title;
572
+ let description;
573
+ let tag;
574
+ try {
575
+ const attributes = parseFrontmatter(authorContent).attributes;
576
+ title = attributes.title;
577
+ description = attributes.description;
578
+ tag = attributes.tag;
579
+ }
580
+ catch {
581
+ // keep generated title/description/tag when author frontmatter cannot be parsed
582
+ }
583
+ const composed = await composeSdkFrontmatterPage({
584
+ slug,
585
+ authorContent,
586
+ metadata: {
587
+ sdk: metadata.sdk,
588
+ title,
589
+ description,
590
+ tag,
591
+ },
592
+ loadReference: load,
593
+ inherited,
594
+ });
595
+ if (!composed)
596
+ return;
597
+ const pagePath = getContainedSdkPagePath(targetDir, composed.slug, extension);
598
+ if (!pagePath)
599
+ return;
600
+ let attributes = {};
601
+ let processedBody = stripSdkGeneratedSection(stripFrontmatter(authorContent));
602
+ let usedProcessedOutput = false;
603
+ if (await fse.pathExists(pagePath)) {
604
+ try {
605
+ const existing = await fse.readFile(pagePath, 'utf8');
606
+ attributes = parseFrontmatter(existing).attributes;
607
+ processedBody = stripSdkGeneratedSection(stripFrontmatter(existing));
608
+ usedProcessedOutput = true;
609
+ }
610
+ catch {
611
+ try {
612
+ attributes = parseFrontmatter(authorContent).attributes;
613
+ }
614
+ catch {
615
+ // keep generated title/description/tag when author frontmatter cannot be parsed
616
+ }
617
+ }
618
+ }
619
+ else {
620
+ try {
621
+ attributes = parseFrontmatter(authorContent).attributes;
622
+ }
623
+ catch {
624
+ // keep generated title/description/tag when author frontmatter cannot be parsed
625
+ }
626
+ }
627
+ // Raw author content with unresolved imports must not shadow the lazy
628
+ // rendering path; those pages are composed once their processed output exists.
629
+ if (!usedProcessedOutput && /^import\s/m.test(processedBody)) {
630
+ return;
631
+ }
632
+ if (composed.sdk)
633
+ attributes.sdk = composed.sdk;
634
+ if (typeof attributes.title !== 'string' || attributes.title.length === 0) {
635
+ attributes.title = composed.title;
636
+ }
637
+ if ((typeof attributes.description !== 'string' || attributes.description.length === 0) &&
638
+ composed.description) {
639
+ attributes.description = composed.description;
640
+ }
641
+ if ((typeof attributes.tag !== 'string' || attributes.tag.length === 0) && composed.tag) {
642
+ attributes.tag = composed.tag;
643
+ }
644
+ const generatedBody = composed.generatedBody ?? composed.content;
645
+ const content = [processedBody, `${SDK_GENERATED_MARKER}\n\n${generatedBody}`]
646
+ .filter(Boolean)
647
+ .join('\n\n');
648
+ await fse.outputFile(pagePath, `${stringifyMarkdownFrontmatter(attributes)}${content}\n`, {
649
+ flag: 'w',
650
+ });
651
+ }));
652
+ }
269
653
  export async function writeSdkArtifacts(result, targetDir) {
270
654
  const propsDirectory = path.resolve(targetDir ?? '', 'src', '_props');
271
655
  const manifestPath = path.join(propsDirectory, 'sdk-data.json');
@@ -1,5 +1,6 @@
1
1
  import { AsyncAPIFile } from '@mintlify/common';
2
2
  import { OpenApiFile, DecoratedNavigationPage } from '@mintlify/models';
3
+ import type { SdkFormat } from '@mintlify/scraping';
3
4
  import { DocsConfig } from '@mintlify/validation';
4
5
  import type { InvalidSpecFile } from '../../invalidSpecFiles.js';
5
6
  export declare function updateDocsConfigFile({ contentDirectoryPath, openApiFiles, asyncApiFiles, docsConfig, localSchema, disableOpenApi, strict, invalidSpecFiles, allowSourceRefs, }: {
@@ -21,11 +22,19 @@ export declare function updateDocsConfigFile({ contentDirectoryPath, openApiFile
21
22
  content: string;
22
23
  filePath: string;
23
24
  }[];
25
+ inheritedSdkBySlug: Record<string, {
26
+ format: SdkFormat;
27
+ source: string;
28
+ directory?: string;
29
+ }>;
30
+ pageMetadataBySlug: Record<string, {
31
+ sdk?: unknown;
32
+ }>;
24
33
  }>;
25
34
  export { generateOpenApiDivisions } from './generateOpenApiDivisions.js';
26
35
  export { generateOpenApiFromDocsConfig } from './generateOpenApiFromDocsConfig.js';
27
- export { generateSdkDivisionsFromDocsConfig, loadSdkReferenceSource, writeSdkArtifacts, } from './generateSdkDivisions.js';
28
- export type { GeneratedSdkPage, GenerateSdkDivisionsResult, SdkReferenceLoader, } from './generateSdkDivisions.js';
36
+ export { generateSdkDivisionsFromDocsConfig, loadSdkReferenceSource, writeSdkArtifacts, writeSdkFrontmatterPages, composeSdkFrontmatterPage, isSdkFrontmatterValue, navigationHasSdkConfig, } from './generateSdkDivisions.js';
37
+ export type { GeneratedSdkPage, GenerateSdkDivisionsResult, GenerateSdkDivisionsOptions, SdkReferenceLoader, } from './generateSdkDivisions.js';
29
38
  export { getOpenApiFilesFromConfig } from '../read/getOpenApiFilesFromConfig.js';
30
39
  export { generateAsyncApiDivisions } from './generateAsyncApiDivisions.js';
31
40
  export { generateAsyncApiFromDocsConfig } from './generateAsyncApiFromDocsConfig.js';
@@ -1,12 +1,59 @@
1
+ import { parseFrontmatter } from '@mintlify/common';
2
+ import fse from 'fs-extra';
1
3
  import { getConfigPath } from '../../../utils.js';
2
4
  import { DocsConfigUpdater } from '../ConfigUpdater.js';
3
5
  import { generateAsyncApiDivisions } from './generateAsyncApiDivisions.js';
4
6
  import { assertNoGeneratedRouteCollisions } from './generatedRouteCollisions.js';
5
7
  import { generateGraphqlDivisionsFromDocsConfig, loadGraphqlSdlSource, writeGraphqlArtifacts, } from './generateGraphqlDivisions.js';
6
8
  import { generateOpenApiDivisions } from './generateOpenApiDivisions.js';
7
- import { generateSdkDivisionsFromDocsConfig, loadSdkReferenceSource, writeSdkArtifacts, } from './generateSdkDivisions.js';
9
+ import { generateSdkDivisionsFromDocsConfig, loadSdkReferenceSource, readContainedMarkdown, writeSdkArtifacts, } from './generateSdkDivisions.js';
8
10
  import { getCustomLanguages } from './getCustomLanguages.js';
9
11
  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
+ }
10
57
  export async function updateDocsConfigFile({ contentDirectoryPath, openApiFiles, asyncApiFiles, docsConfig, localSchema, disableOpenApi, strict, invalidSpecFiles, allowSourceRefs, }) {
11
58
  const configPath = await getConfigPath(contentDirectoryPath, 'docs');
12
59
  if (configPath == null && docsConfig == null) {
@@ -32,7 +79,8 @@ export async function updateDocsConfigFile({ contentDirectoryPath, openApiFiles,
32
79
  flushWrites: async () => { },
33
80
  };
34
81
  const { newDocsConfig: docsConfigWithAsyncApiPages, pagesAcc: pagesAccWithAsyncApiPages, asyncApiFiles: newAsyncApiFiles, flushWrites: flushAsyncApiWrites, } = await generateAsyncApiDivisions(docsConfigWithOpenApiPages, asyncApiFiles, undefined, localSchema, pagesAccWithGraphqlPages, true, true);
35
- const sdkResult = await generateSdkDivisionsFromDocsConfig(docsConfigWithAsyncApiPages, (config) => loadSdkReferenceSource(config, contentDirectoryPath));
82
+ const pageMetadataBySlug = await scanSdkFrontmatterMetadata(contentDirectoryPath, docsConfigWithAsyncApiPages.navigation);
83
+ const sdkResult = await generateSdkDivisionsFromDocsConfig(docsConfigWithAsyncApiPages, (config) => loadSdkReferenceSource(config, contentDirectoryPath), { pageMetadataBySlug });
36
84
  const { newDocsConfig: docsConfigWithSdkPages, pagesAcc: pagesAccWithSdkPages } = sdkResult;
37
85
  const sdkRoutes = { label: 'SDK', pages: pagesAccWithSdkPages };
38
86
  assertNoGeneratedRouteCollisions({ label: 'GraphQL', pages: pagesAccWithGraphqlPages }, sdkRoutes);
@@ -54,11 +102,13 @@ export async function updateDocsConfigFile({ contentDirectoryPath, openApiFiles,
54
102
  newOpenApiFiles,
55
103
  newAsyncApiFiles,
56
104
  customLanguages,
105
+ inheritedSdkBySlug: sdkResult.inheritedSdkBySlug,
106
+ pageMetadataBySlug,
57
107
  };
58
108
  }
59
109
  export { generateOpenApiDivisions } from './generateOpenApiDivisions.js';
60
110
  export { generateOpenApiFromDocsConfig } from './generateOpenApiFromDocsConfig.js';
61
- export { generateSdkDivisionsFromDocsConfig, loadSdkReferenceSource, writeSdkArtifacts, } from './generateSdkDivisions.js';
111
+ export { generateSdkDivisionsFromDocsConfig, loadSdkReferenceSource, writeSdkArtifacts, writeSdkFrontmatterPages, composeSdkFrontmatterPage, isSdkFrontmatterValue, navigationHasSdkConfig, } from './generateSdkDivisions.js';
62
112
  export { getOpenApiFilesFromConfig } from '../read/getOpenApiFilesFromConfig.js';
63
113
  export { generateAsyncApiDivisions } from './generateAsyncApiDivisions.js';
64
114
  export { generateAsyncApiFromDocsConfig } from './generateAsyncApiFromDocsConfig.js';