@thallylabs/migrate 0.2.40 → 0.2.41

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/index.js CHANGED
@@ -1370,8 +1370,29 @@ ${trimmed.slice(0, marker).trimEnd()}`;
1370
1370
  function headingIdMarker(id) {
1371
1371
  return `{/* #${id} */}`;
1372
1372
  }
1373
+ var HEADING_ID_WORD = /[\p{L}\p{M}\p{N}_+&/@\u2019\u201c\u201d\u2014\u2013\u2192]/u;
1373
1374
  function mintlifyHeadingSlug(text3) {
1374
- return text3.replace(/\u200b/g, "").normalize("NFC").trim().toLowerCase().replace(/[.\s]+/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "").replace(/[^\p{L}\p{M}\p{N}_\-+&/\u2019\u201c\u201d\u2014\u2013\u2192]/gu, "");
1375
+ const chars = [...text3.replace(/[\u200b*]/g, "").normalize("NFC").trim().toLowerCase()];
1376
+ const isWord = (char) => char !== void 0 && HEADING_ID_WORD.test(char);
1377
+ let id = "";
1378
+ let afterDash = false;
1379
+ let endsWithDash = false;
1380
+ chars.forEach((char, index) => {
1381
+ const before = chars[index - 1];
1382
+ const after = chars[index + 1];
1383
+ endsWithDash = false;
1384
+ if (isWord(char)) {
1385
+ id += char;
1386
+ afterDash = false;
1387
+ } else if (/[\s-]/.test(char) || char === "." && isWord(after) || char === "(" && isWord(before) && (isWord(after) || after === "{") || char === ")" && after !== void 0 && !/\s/.test(after)) {
1388
+ if (id && !afterDash) id += "-";
1389
+ afterDash = true;
1390
+ endsWithDash = true;
1391
+ } else if (char === "{" || char === ":" || char === '"') {
1392
+ afterDash = false;
1393
+ }
1394
+ });
1395
+ return endsWithDash ? id.replace(/-$/, "") : id;
1375
1396
  }
1376
1397
  function thallyHeadingSlug(text3) {
1377
1398
  return text3.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, "-").replace(/(^-|-$)/g, "");
@@ -1383,7 +1404,7 @@ function typesetQuotes(text3) {
1383
1404
  }).join("");
1384
1405
  }
1385
1406
  function plainHeadingText(source) {
1386
- return source.replace(/\{\/\*[\s\S]*?\*\/\}/g, " ").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<\/?[A-Za-z][^>]*>/g, "").replace(/`([^`]*)`/g, "$1").replace(/(\*\*|__|~~|\*)/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/\\([\\`*_{}[\]()#+.!&<>|~-])/g, "$1");
1407
+ return source.replace(/\{\/\*[\s\S]*?\*\/\}/g, " ").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<\/?[A-Za-z][^>]*>/g, "").replace(/`([^`]*)`/g, "$1").replace(/(?<!\\)(\*\*|__|~~|\*)/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/\\([\\`*_{}[\]()#+.!&<>|~-])/g, "$1");
1387
1408
  }
1388
1409
  function markMintlifyHeadings(body) {
1389
1410
  let fence;
@@ -2587,7 +2608,7 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
2587
2608
  }
2588
2609
  if (runMintlify) rewritten = normalizeMintlifyParameterAnchors(rewritten);
2589
2610
  rewritten = replaceOutsideCode(rewritten, (segment) => {
2590
- let result = convertHtmlStyleAttributes(segment).replace(/<!--([\s\S]*?)-->/g, (_match, content) => mdxComment(content)).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Warn(\s[^>]*)?>/g, "<Warning$1>").replace(/<\/Warn>/g, "</Warning>").replace(/<Check(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Check>/g, "</Note>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
2611
+ let result = convertHtmlStyleAttributes(segment).replace(/<!--([\s\S]*?)-->/g, (_match, content) => mdxComment(content)).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Warn(\s[^>]*)?>/g, "<Warning$1>").replace(/<\/Warn>/g, "</Warning>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
2591
2612
  if (runDocusaurus) {
2592
2613
  result = result.replace(/<TabItem\b([^<>]*)>/g, (_match, attributes) => {
2593
2614
  const title = attributes.match(/\blabel=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? attributes.match(/\bvalue=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? "Tab";
@@ -2596,7 +2617,7 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
2596
2617
  result = normalizeDocusaurusLinkTags(result).replace(/<(?:DocCardList|TOCInline)\b[^<>]*\/>/g, "");
2597
2618
  }
2598
2619
  if (runMintlify) {
2599
- result = normalizeMintlifyUpdateLabels(result).replace(/<FileTree(\s[^>]*)?>/g, "<Tree$1>").replace(/<\/FileTree>/g, "</Tree>").replace(/<GitHub\.Repo/g, "<GitHub").replace(/<\/GitHub\.Repo>/g, "</GitHub>").replace(/<Column(\s[^>]*)?>/g, "<div$1>").replace(/<\/Column>/g, "</div>");
2620
+ result = normalizeMintlifyUpdateLabels(result).replace(/<FileTree(\s[^>]*)?>/g, "<Tree$1>").replace(/<\/FileTree>/g, "</Tree>").replace(/<GitHub\.Repo/g, "<GitHub").replace(/<\/GitHub\.Repo>/g, "</GitHub>").replace(/<Callout(\s[^<>]*)?>/g, (tag) => tag.replace(/(\s)type=(?:"[^"]*"|'[^']*'|\{[^{}]*\})/, '$1type="info"')).replace(/<Column(\s[^>]*)?>/g, "<div$1>").replace(/<\/Column>/g, "</div>");
2600
2621
  result = pinStepsTitleSize(result);
2601
2622
  }
2602
2623
  if (runFern) {
@@ -2615,6 +2636,16 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
2615
2636
  }
2616
2637
  return normalizeFlowParagraphContainers(normalized);
2617
2638
  }
2639
+ function externalPageUrl(value) {
2640
+ const url = typeof value === "string" ? value.trim() : "";
2641
+ if (!/^https?:\/\/[^\s]+$/i.test(url)) return void 0;
2642
+ try {
2643
+ new URL(url);
2644
+ return url;
2645
+ } catch {
2646
+ return void 0;
2647
+ }
2648
+ }
2618
2649
  function parseMarkdownPage(input) {
2619
2650
  const parsed = parseFrontmatter(input.raw);
2620
2651
  const fallbackIdentity = {
@@ -2639,6 +2670,7 @@ function parseMarkdownPage(input) {
2639
2670
  const badge = typeof parsed.data.tag === "string" && parsed.data.tag.trim() ? parsed.data.tag.trim() : typeof parsed.data.badge === "string" && parsed.data.badge.trim() ? parsed.data.badge.trim() : void 0;
2640
2671
  const sourceMode = typeof parsed.data.mode === "string" ? parsed.data.mode : void 0;
2641
2672
  const mode = sourceMode === "frame" ? "wide" : ["default", "wide", "custom", "center", "home"].includes(sourceMode ?? "") ? sourceMode : void 0;
2673
+ const externalUrl = input.platform === "mintlify" ? externalPageUrl(parsed.data.url) : void 0;
2642
2674
  const description = typeof parsed.data.description === "string" && parsed.data.description.trim() ? parsed.data.description.trim() : typeof parsed.data.openapi === "string" && parsed.data.openapi.trim() ? "" : firstParagraph(body);
2643
2675
  const page = {
2644
2676
  id: identity.id,
@@ -2654,7 +2686,9 @@ function parseMarkdownPage(input) {
2654
2686
  keywords,
2655
2687
  mode,
2656
2688
  hidden: parsed.data.hidden === true ? true : void 0,
2657
- noindex: parsed.data.noindex === true || parsed.data.noindex === "true" ? true : void 0,
2689
+ // The path of a page that links elsewhere redirects, so it has no business in a sitemap.
2690
+ noindex: parsed.data.noindex === true || parsed.data.noindex === "true" || externalUrl ? true : void 0,
2691
+ ...externalUrl ? { externalUrl } : {},
2658
2692
  ...Object.keys(meta).length > 0 ? { meta } : {},
2659
2693
  openapi: typeof parsed.data.openapi === "string" ? parsed.data.openapi.trim() : void 0,
2660
2694
  ...apiFrontmatter(parsed.data, input.warn),
@@ -4391,12 +4425,17 @@ function addMintlifyHomepageRedirects(config, pages) {
4391
4425
  const homepage = config.tabs.filter((tab) => !tab.hidden).map((tab) => firstPage([...tab.pages ?? [], ...tab.groups ?? []])).find(Boolean);
4392
4426
  if (!homepage) return config;
4393
4427
  const redirects = [...config.redirects ?? []];
4394
- const sources = new Set(redirects.map((redirect) => (redirect.source.replace(/\/$/, "") || "/").toLowerCase()));
4428
+ const routeKey = (route) => (route.split(/[?#]/, 1)[0].replace(/\/$/, "") || "/").toLowerCase();
4429
+ const sources = new Set(redirects.map((redirect) => routeKey(redirect.source)));
4395
4430
  const roots = ["", ...(config.i18n?.locales ?? []).filter((locale) => locale.code !== config.i18n?.defaultLocale).filter((locale) => homepage !== "introduction" || pages.some((page) => page.locale === locale.code && page.navigationId === homepage)).map((locale) => locale.code)];
4431
+ const homeLinksElsewhere = pages.some((page) => page.id === "introduction" && page.externalUrl);
4396
4432
  for (const locale of roots) {
4433
+ if (!locale && homeLinksElsewhere) continue;
4397
4434
  const source = locale ? `/${locale}` : "/";
4398
4435
  if (sources.has(source.toLowerCase())) continue;
4399
- redirects.push({ source, destination: `${locale ? `/${locale}` : ""}/${homepage}`, permanent: true });
4436
+ const destination = `${locale ? `/${locale}` : ""}/${homepage}`;
4437
+ if (redirects.some((redirect) => routeKey(redirect.source) === destination.toLowerCase() && routeKey(redirect.destination) === source.toLowerCase())) continue;
4438
+ redirects.push({ source, destination, permanent: true });
4400
4439
  }
4401
4440
  return { ...config, redirects };
4402
4441
  }
@@ -4593,7 +4632,7 @@ function mergeApiGroups(existing, incoming) {
4593
4632
  return merged.length > 0 ? merged : void 0;
4594
4633
  }
4595
4634
  var hasNavigationChildren = (node) => ["groups", "pages"].some((key) => Array.isArray(node[key]) && node[key].length > 0);
4596
- function mintlifyNavigationApiReferences(config) {
4635
+ function mintlifyNavigationApiReferences(config, tabFor) {
4597
4636
  const navigation = objectValue2(config.navigation) ?? config;
4598
4637
  const references = [];
4599
4638
  const defaultLanguage = pickDefaultLanguage(
@@ -4617,7 +4656,7 @@ function mintlifyNavigationApiReferences(config) {
4617
4656
  references.push({
4618
4657
  value,
4619
4658
  kind,
4620
- tabLabel,
4659
+ tabLabel: tabFor?.(node) ?? tabLabel,
4621
4660
  ...sibling,
4622
4661
  ...typeof directory2 === "string" && directory2.trim() ? { directory: directory2.trim() } : {},
4623
4662
  ...operations.length > 0 ? { operations } : {},
@@ -4972,6 +5011,21 @@ function convertNavigationValues(values, context) {
4972
5011
  return page ? [page] : [];
4973
5012
  });
4974
5013
  }
5014
+ var API_TAB_CONTAINER = /* @__PURE__ */ Symbol("apiTabContainer");
5015
+ function takeApiTabSlots(tabs) {
5016
+ const slots = [];
5017
+ const real = [];
5018
+ tabs.forEach((tab, index) => {
5019
+ const container = tab[API_TAB_CONTAINER];
5020
+ if (!container) {
5021
+ real.push(tab);
5022
+ return;
5023
+ }
5024
+ const template = Object.fromEntries(Object.entries(tab).filter(([key]) => key !== "groups"));
5025
+ slots.push({ container, slot: { tab: template, preceding: tabs.slice(0, index).map((earlier) => earlier.tab).reverse() } });
5026
+ });
5027
+ return { tabs: real, slots };
5028
+ }
4975
5029
  function convertContainerToTabs(containerValue, context, fallbackTab, trace, depth = 0) {
4976
5030
  const rawContainer = objectValue2(containerValue);
4977
5031
  if (!rawContainer) return [];
@@ -5048,6 +5102,17 @@ function convertContainerToTabs(containerValue, context, fallbackTab, trace, dep
5048
5102
  ...convertNavigationValues(values, context),
5049
5103
  ...Array.isArray(container.menu) ? convertMenuItems(container.menu, context) : []
5050
5104
  ];
5105
+ if (children.length === 0 && depth > 0 && context.operationGroups && !context.otherLanguage && !context.operations.gated && !context.operations.hidden && specSource(rawContainer.openapi) && operationGroups(rawContainer).length > 0) {
5106
+ const placeholder2 = {
5107
+ tab: fallbackTab,
5108
+ groups: [],
5109
+ ...containerPresentation(container),
5110
+ ...projectedHref(container.href) ? { href: projectedHref(container.href) } : {},
5111
+ ...container.hidden === true ? { hidden: true } : {},
5112
+ [API_TAB_CONTAINER]: rawContainer
5113
+ };
5114
+ return [placeholder2];
5115
+ }
5051
5116
  return children.length > 0 ? [{
5052
5117
  tab: fallbackTab,
5053
5118
  ...hasRootNodes ? { pages: children } : { groups: children },
@@ -5228,6 +5293,8 @@ function projectedCompatibleConfig(config, warnings) {
5228
5293
  return {
5229
5294
  // Mintlify pages show a group eyebrow above the title and no breadcrumb trail.
5230
5295
  breadcrumbs: false,
5296
+ // Mintlify typesets straight quotes in prose as curly ones.
5297
+ smartQuotes: true,
5231
5298
  // `styling.eyebrows: "breadcrumbs"` shows the whole group path; "section" (the default) shows the nearest group.
5232
5299
  ...objectValue2(config.styling)?.eyebrows === "breadcrumbs" ? { eyebrow: "path" } : {},
5233
5300
  ...projectedTheme(config.theme) ? { theme: projectedTheme(config.theme) } : {},
@@ -5479,6 +5546,9 @@ function projectMintlifyNavigation(config, options = {}) {
5479
5546
  }
5480
5547
  }
5481
5548
  }
5549
+ const taken = takeApiTabSlots(tabs);
5550
+ tabs = taken.tabs;
5551
+ const apiTabSlots = taken.slots;
5482
5552
  if (tabs.length === 0) {
5483
5553
  warnings.push({
5484
5554
  code: "unsupported-config",
@@ -5524,6 +5594,7 @@ function projectMintlifyNavigation(config, options = {}) {
5524
5594
  }) : [];
5525
5595
  if (versions.length > 1) {
5526
5596
  tabs = annotateVersions(tabs);
5597
+ for (const entry of apiTabSlots) [entry.slot.tab] = annotateVersions([entry.slot.tab]);
5527
5598
  for (const [locale, items] of Object.entries(localizedNavigation)) localizedNavigation[locale] = annotateVersions(items);
5528
5599
  }
5529
5600
  const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
@@ -5547,11 +5618,13 @@ function projectMintlifyNavigation(config, options = {}) {
5547
5618
  }];
5548
5619
  }) : [];
5549
5620
  collectReferenceAccess(navigation, OPEN_NAVIGATION_ACCESS, options.pathPrefix, appearances);
5621
+ const keepsCollectionNamedGroup = [tabs, ...Object.values(localizedNavigation)].some((items) => items.some((item) => item.groups?.some((group) => group.group === (item.displayLabel ?? item.tab))));
5550
5622
  return {
5551
5623
  docsConfig: {
5552
5624
  tabs,
5553
- ...projectionTrace.rootContainerKind === "dropdowns" || versions.length > 1 || shortcuts.length > 0 ? { navigation: {
5625
+ ...projectionTrace.rootContainerKind === "dropdowns" || versions.length > 1 || shortcuts.length > 0 || keepsCollectionNamedGroup ? { navigation: {
5554
5626
  ...projectionTrace.rootContainerKind === "dropdowns" ? { display: "dropdown" } : {},
5627
+ ...keepsCollectionNamedGroup ? { groupHeadings: "all" } : {},
5555
5628
  ...versions.length > 1 ? { versions } : {},
5556
5629
  ...shortcuts.length > 0 ? { shortcuts } : {}
5557
5630
  } } : {},
@@ -5563,6 +5636,7 @@ function projectMintlifyNavigation(config, options = {}) {
5563
5636
  pageReferences: references,
5564
5637
  referenceAccess: appearances,
5565
5638
  gatedSourceRefs,
5639
+ apiTabSlots,
5566
5640
  warnings
5567
5641
  };
5568
5642
  }
@@ -9074,6 +9148,9 @@ function destinationRoute(destination) {
9074
9148
  function pageRoutes(pages) {
9075
9149
  return pages.flatMap((page) => [`/${page.id}`, `/${page.id.replace(/\/index$/, "")}`]);
9076
9150
  }
9151
+ function pageEntryRoutes(page) {
9152
+ return [.../* @__PURE__ */ new Set([...pageRoutes([page]), ...page.id === "introduction" ? ["/"] : []])];
9153
+ }
9077
9154
  function servedRoutes(pages) {
9078
9155
  return new Set(pageRoutes(pages).map((route) => route.toLowerCase()));
9079
9156
  }
@@ -9138,11 +9215,13 @@ function dropConflictingRedirects(config, pages, warnings) {
9138
9215
  const loopingByCase = [];
9139
9216
  const folded = redirectChains();
9140
9217
  const asWritten = redirectChains();
9218
+ const rootRedirected = config.redirects.some((redirect) => literalRoute(redirect.source) === "/");
9141
9219
  const redirects = config.redirects.filter((redirect) => {
9142
9220
  const source = literalRoute(redirect.source);
9143
9221
  if (!source) return true;
9144
9222
  const writtenSource = exactRoute(redirect.source);
9145
- if (served.has(source)) {
9223
+ const retiresRootAlias = source === "/introduction" && !rootRedirected && destinationRoute(redirect.destination) === "/";
9224
+ if (served.has(source) && !retiresRootAlias) {
9146
9225
  const hides = servedAsWritten.has(writtenSource) ? hidden : hiddenByCase;
9147
9226
  hides.push(redirect.source);
9148
9227
  return false;
@@ -12337,12 +12416,19 @@ function mintlifyTopLevelApiReferences(config) {
12337
12416
  function specFileCandidates(files) {
12338
12417
  return files.filter((file) => /\.(?:json|ya?ml)$/i.test(file.relativePath)).map((file) => ({ sourcePath: file.relativePath, filename: specRefBaseName(file.relativePath) }));
12339
12418
  }
12340
- function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, nameSpec, pages = []) {
12419
+ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, nameSpec, pages = [], apiTabSlots = []) {
12341
12420
  if (!mintlifyConfig) return [];
12421
+ const slotByContainer = new Map(apiTabSlots.map(({ container, slot }) => [container, slot]));
12422
+ const slotByLabel = new Map(apiTabSlots.map(({ slot }) => [slot.tab.tab, slot]));
12342
12423
  const references = [
12343
12424
  ...mintlifyTopLevelApiReferences(mintlifyConfig),
12344
- ...mintlifyNavigationApiReferences(mintlifyConfig)
12425
+ ...mintlifyNavigationApiReferences(mintlifyConfig, (container) => slotByContainer.get(container)?.tab.tab)
12345
12426
  ];
12427
+ const endpointPagesOf = (reference) => {
12428
+ const directory2 = reference.groups ? endpointDirectory(reference.directory) : void 0;
12429
+ const slot = reference.tabLabel ? slotByLabel.get(reference.tabLabel) : void 0;
12430
+ return directory2 === void 0 ? void 0 : { directory: directory2, ...slot ? { slot } : {} };
12431
+ };
12346
12432
  const known = new Set(references.map((reference) => canonicalSpecRef(reference.value)));
12347
12433
  const specFiles = specFileCandidates(files);
12348
12434
  const namedFiles = /* @__PURE__ */ new Map();
@@ -12383,6 +12469,14 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, n
12383
12469
  if (reference.groups) {
12384
12470
  const merged = mergeApiGroups(shared.target.groups, reference.groups);
12385
12471
  if (merged) shared.target.groups = merged;
12472
+ const own2 = endpointPagesOf(reference);
12473
+ if (shared.directory === void 0) {
12474
+ shared.directory = own2?.directory ?? null;
12475
+ if (own2) shared.target.endpointPages = own2;
12476
+ } else if (shared.directory !== (own2?.directory ?? null)) {
12477
+ shared.directory = null;
12478
+ delete shared.target.endpointPages;
12479
+ }
12386
12480
  } else if (listsNothing(reference)) {
12387
12481
  shared.unlisted = true;
12388
12482
  delete shared.target.groups;
@@ -12401,11 +12495,12 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, n
12401
12495
  continue;
12402
12496
  }
12403
12497
  if (/^https:\/\//i.test(reference.value)) {
12404
- const remote = { url: reference.value, ...reference.tabLabel ? { tabLabel: reference.tabLabel } : {}, ...reference.parentTab ? { parentTab: reference.parentTab } : {}, ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, ...groups ? { groups } : {} };
12498
+ const pagesTarget = endpointPagesOf(reference);
12499
+ const remote = { url: reference.value, ...reference.tabLabel ? { tabLabel: reference.tabLabel } : {}, ...reference.parentTab ? { parentTab: reference.parentTab } : {}, ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, ...groups ? { groups } : {}, ...groups && pagesTarget ? { endpointPages: pagesTarget } : {} };
12405
12500
  mountDirectory(remote, reference);
12406
12501
  remoteSpecs.push(remote);
12407
12502
  remoteByKey.set(dedupeKey, remote);
12408
- groupTargets.set(dedupeKey, { target: remote, unlisted: listsNothing(reference), withheld: reference.operationsWithheld === true });
12503
+ groupTargets.set(dedupeKey, { target: remote, unlisted: listsNothing(reference), withheld: reference.operationsWithheld === true, ...groups ? { directory: pagesTarget?.directory ?? null } : {} });
12409
12504
  warnings.push({
12410
12505
  code: "unsupported-config",
12411
12506
  message: `The remote OpenAPI spec "${reference.value}"${tabSuffix} requires a network download before this import is complete.`,
@@ -12426,12 +12521,12 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, n
12426
12521
  });
12427
12522
  continue;
12428
12523
  }
12429
- if (reference.directory) {
12430
- warnings.push({
12431
- code: "unsupported-config",
12432
- message: `The OpenAPI spec "${reference.value}"${tabSuffix} was limited to pages under "${reference.directory}" in the source, but Thally's API reference always covers a whole tab, so it was migrated as the tab's full API reference. Links in page bodies and Mintlify endpoint URLs under "${reference.directory}/<tag>/<operation>" are redirected to the matching endpoints where one exists (a separate warning counts any endpoint redirects that were skipped); check any other links to "${reference.directory}/..." manually.`
12433
- });
12434
- }
12524
+ const directoryWarning = reference.directory ? {
12525
+ code: "unsupported-config",
12526
+ message: `The OpenAPI spec "${reference.value}"${tabSuffix} was limited to pages under "${reference.directory}" in the source, but Thally's API reference always covers a whole tab, so it was migrated as the tab's full API reference. Links in page bodies and Mintlify endpoint URLs under "${reference.directory}/<tag>/<operation>" are redirected to the matching endpoints where one exists (a separate warning counts any endpoint redirects that were skipped); check any other links to "${reference.directory}/..." manually.`
12527
+ } : void 0;
12528
+ const deferWarning = Boolean(groups && endpointPagesOf(reference));
12529
+ if (directoryWarning && !deferWarning) warnings.push(directoryWarning);
12435
12530
  const content = readSpecFile(match.absolutePath, match.relativePath, warnings);
12436
12531
  if (!content) continue;
12437
12532
  const spec = {
@@ -12444,10 +12539,11 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, n
12444
12539
  ...reference.pageOnly ? { pageOnly: true } : {},
12445
12540
  sourcePath: match.relativePath,
12446
12541
  directory: reference.directory,
12447
- ...groups ? { groups } : {}
12542
+ ...groups ? { groups } : {},
12543
+ ...deferWarning ? { endpointPages: endpointPagesOf(reference), directoryWarning } : {}
12448
12544
  };
12449
12545
  specs.push(spec);
12450
- groupTargets.set(dedupeKey, { target: spec, unlisted: listsNothing(reference), withheld: reference.operationsWithheld === true });
12546
+ groupTargets.set(dedupeKey, { target: spec, unlisted: listsNothing(reference), withheld: reference.operationsWithheld === true, ...groups ? { directory: endpointPagesOf(reference)?.directory ?? null } : {} });
12451
12547
  }
12452
12548
  for (const { target: target2, unlisted, withheld } of groupTargets.values()) {
12453
12549
  if (withheld && !unlisted && !target2.groups) target2.navigation = false;
@@ -12503,20 +12599,52 @@ function apiTabSpecIds(tabs) {
12503
12599
  }
12504
12600
  var flagged = (value) => value === true || value === "true";
12505
12601
  var isWithheld = (node) => flagged(node["x-hidden"]) || flagged(node["x-excluded"]);
12602
+ var isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
12603
+ function resolveLocalSpecRef(document, ref) {
12604
+ if (ref !== "#" && !ref.startsWith("#/")) return void 0;
12605
+ let node = document;
12606
+ for (const segment of ref.slice(2).split("/").filter(Boolean)) {
12607
+ if (!isRecord2(node) && !Array.isArray(node)) return void 0;
12608
+ let decoded = segment;
12609
+ try {
12610
+ decoded = decodeURIComponent(segment);
12611
+ } catch {
12612
+ }
12613
+ node = node[decoded.replace(/~1/g, "/").replace(/~0/g, "~")];
12614
+ }
12615
+ return node;
12616
+ }
12617
+ function viewSpecPathItem(document, entry) {
12618
+ const chain = [entry];
12619
+ let current = entry;
12620
+ while (typeof current.$ref === "string") {
12621
+ const target2 = resolveLocalSpecRef(document, current.$ref);
12622
+ if (!isRecord2(target2) || chain.includes(target2)) break;
12623
+ chain.push(target2);
12624
+ current = target2;
12625
+ }
12626
+ const item = Object.assign({}, ...[...chain].reverse());
12627
+ delete item.$ref;
12628
+ return { item, withheld: chain.some(isWithheld) };
12629
+ }
12506
12630
  function parseOpenApiOperationsForLinking(content) {
12507
- let document;
12631
+ const document = readSpecDocument(content);
12632
+ return visibleOperations(document, isRecord2(document) ? document.paths : void 0);
12633
+ }
12634
+ function readSpecDocument(content) {
12508
12635
  try {
12509
- document = parseSpecDocument(content.toString("utf8")).document;
12636
+ return parseSpecDocument(content.toString("utf8")).document;
12510
12637
  } catch {
12511
- return [];
12638
+ return void 0;
12512
12639
  }
12513
- const paths = document && typeof document === "object" ? document.paths : null;
12514
- if (!paths || typeof paths !== "object") return [];
12640
+ }
12641
+ function visibleOperations(document, entries) {
12642
+ if (!isRecord2(entries)) return [];
12515
12643
  const operations = [];
12516
- for (const [path, pathItem] of Object.entries(paths)) {
12517
- if (!pathItem || typeof pathItem !== "object") continue;
12518
- const item = pathItem;
12519
- if (isWithheld(item)) continue;
12644
+ for (const [path, pathItem] of Object.entries(entries)) {
12645
+ if (!isRecord2(pathItem)) continue;
12646
+ const { item, withheld } = viewSpecPathItem(document, pathItem);
12647
+ if (withheld) continue;
12520
12648
  for (const method of OPENAPI_HTTP_METHODS) {
12521
12649
  const operation = item[method];
12522
12650
  if (!operation || typeof operation !== "object") continue;
@@ -12528,7 +12656,8 @@ function parseOpenApiOperationsForLinking(content) {
12528
12656
  path,
12529
12657
  tag: tags[0],
12530
12658
  summary: typeof op.summary === "string" ? op.summary : void 0,
12531
- operationId: typeof op.operationId === "string" ? op.operationId : void 0
12659
+ operationId: typeof op.operationId === "string" ? op.operationId : void 0,
12660
+ description: typeof op.description === "string" ? op.description : void 0
12532
12661
  });
12533
12662
  }
12534
12663
  }
@@ -12549,13 +12678,26 @@ function servedOperations(source, parsed) {
12549
12678
  });
12550
12679
  }
12551
12680
  var mountPrefix = (directory2) => directory2.replace(/^\/+|\/+$/g, "").toLowerCase();
12681
+ function mintlifyEndpointUrl(directory2, operation) {
12682
+ const tag = mintlifyUrlSlug(operation.tag ?? "default");
12683
+ const leaf = mintlifyUrlSlug(operation.summary?.trim() || operation.operationId?.trim() || `${operation.method} ${operation.path}`);
12684
+ return {
12685
+ tag,
12686
+ leaf,
12687
+ url: `${directory2}/${tag}/${leaf}`.replace(/^\/+|\/+$/g, ""),
12688
+ title: operation.summary?.trim() || operation.operationId?.trim() || `${operation.method.toUpperCase()} ${operation.path}`
12689
+ };
12690
+ }
12552
12691
  function apiOperationLinkMap(sources, docsConfig, otherMounts = []) {
12553
12692
  const map = /* @__PURE__ */ new Map();
12554
12693
  const prefixLandings = /* @__PURE__ */ new Map();
12555
12694
  const mintlifyPages = /* @__PURE__ */ new Map();
12556
12695
  const duplicatePages = [];
12557
12696
  const parsedByFile = /* @__PURE__ */ new Map();
12558
- const bound = apiTabSpecIds(docsConfig.tabs).flatMap(({ tab, specId }) => sources.filter((entry) => tab.api?.source === specAssetPath(entry.filename) && (!entry.tab || entry.tab === tab.tab)).map((source) => ({ source, specId })));
12697
+ const bound = [
12698
+ ...apiTabSpecIds(docsConfig.tabs).flatMap(({ tab, specId }) => sources.filter((entry) => !entry.endpointPages && tab.api?.source === specAssetPath(entry.filename) && (!entry.tab || entry.tab === tab.tab)).map((source) => ({ source, specId }))),
12699
+ ...sources.filter((entry) => entry.endpointPages).map((source) => ({ source }))
12700
+ ];
12559
12701
  const mounts = new Set([...bound.map(({ source }) => source.prefix ? mountPrefix(source.prefix) : ""), ...otherMounts.map(mountPrefix)].filter(Boolean));
12560
12702
  const underDeeperMount = (route, own2) => {
12561
12703
  const lower = route.toLowerCase();
@@ -12565,9 +12707,12 @@ function apiOperationLinkMap(sources, docsConfig, otherMounts = []) {
12565
12707
  if (!source.prefix) return;
12566
12708
  const base = source.prefix.replace(/^\/+|\/+$/g, "");
12567
12709
  const prefix = mountPrefix(source.prefix);
12568
- if (prefix) prefixLandings.set(prefix, specId === "default" ? "/api" : `/api/${specId}`);
12710
+ if (prefix && specId !== void 0) prefixLandings.set(prefix, specId === "default" ? "/api" : `/api/${specId}`);
12569
12711
  for (const operation of servedOperations(source, parsedByFile)) {
12570
- const thallyHref = `/api/${specId}/${thallyOperationSlugSegments(operation.path, operation.method).join("/")}`;
12712
+ const endpoint = mintlifyEndpointUrl(base, operation);
12713
+ if (specId === void 0 && (!endpoint.tag || !endpoint.leaf)) continue;
12714
+ const thallyHref = specId === void 0 ? `/${endpoint.url}` : `/api/${specId}/${thallyOperationSlugSegments(operation.path, operation.method).join("/")}`;
12715
+ if (specId === void 0 && prefix && !prefixLandings.has(prefix)) prefixLandings.set(prefix, thallyHref);
12571
12716
  const leafCandidates = /* @__PURE__ */ new Set();
12572
12717
  if (operation.operationId) {
12573
12718
  const parts = operation.operationId.split(/[_.]/).filter(Boolean);
@@ -12580,17 +12725,17 @@ function apiOperationLinkMap(sources, docsConfig, otherMounts = []) {
12580
12725
  if (operation.operationId) leafCandidates.add(mintlifyOperationSlugSegment(operation.operationId));
12581
12726
  leafCandidates.add(mintlifyOperationSlugSegment(`${operation.method} ${operation.path}`));
12582
12727
  const tagSegment = mintlifyOperationSlugSegment(operation.tag ?? "default");
12583
- const mintlifyTag = mintlifyUrlSlug(operation.tag ?? "default");
12584
- const mintlifyLeaf = mintlifyUrlSlug(operation.summary?.trim() || operation.operationId?.trim() || `${operation.method} ${operation.path}`);
12585
- const canonical2 = `${base}/${mintlifyTag}/${mintlifyLeaf}`.replace(/^\/+|\/+$/g, "");
12728
+ const { tag: mintlifyTag, leaf: mintlifyLeaf, url: canonical2 } = endpoint;
12586
12729
  if (mintlifyTag && mintlifyLeaf && !underDeeperMount(canonical2, prefix)) {
12587
- if (!mintlifyPages.has(canonical2)) mintlifyPages.set(canonical2, thallyHref);
12588
- else if (mintlifyPages.get(canonical2) !== thallyHref && !duplicatePages.includes(canonical2)) duplicatePages.push(canonical2);
12730
+ if (specId !== void 0) {
12731
+ if (!mintlifyPages.has(canonical2)) mintlifyPages.set(canonical2, thallyHref);
12732
+ else if (mintlifyPages.get(canonical2) !== thallyHref && !duplicatePages.includes(canonical2)) duplicatePages.push(canonical2);
12733
+ }
12589
12734
  if (!map.has(canonical2.toLowerCase())) map.set(canonical2.toLowerCase(), thallyHref);
12590
12735
  }
12591
12736
  const tagRoot = `${base}/${mintlifyTag}`.replace(/^\/+|\/+$/g, "");
12592
12737
  if (mintlifyTag && !underDeeperMount(tagRoot, prefix)) {
12593
- if (!mintlifyPages.has(tagRoot)) mintlifyPages.set(tagRoot, thallyHref);
12738
+ if (specId !== void 0 && !mintlifyPages.has(tagRoot)) mintlifyPages.set(tagRoot, thallyHref);
12594
12739
  if (!map.has(tagRoot.toLowerCase())) map.set(tagRoot.toLowerCase(), thallyHref);
12595
12740
  }
12596
12741
  for (const leaf of leafCandidates) {
@@ -12631,6 +12776,150 @@ function linkApiMounts(sources, docsConfig, pages, warnings, reservedMounts = []
12631
12776
  rewriteApiLinksInPages(pages, maps.operationLinks, maps.prefixLandings, warnings, { mounts: reservedMounts, redirects: docsConfig.redirects });
12632
12777
  return mintlifyEndpointRedirects(maps, pages, docsConfig.redirects ?? [], warnings);
12633
12778
  }
12779
+ var DEFAULT_ENDPOINT_DIRECTORY = "api-reference";
12780
+ function endpointDirectory(directory2) {
12781
+ const value = (directory2 ?? DEFAULT_ENDPOINT_DIRECTORY).replace(/^\/+|\/+$/g, "");
12782
+ const plain = (segment) => /^[\p{L}\p{N}_&-][\p{L}\p{N}_&.-]*$/u.test(segment) && Buffer.byteLength(`${segment}.mdx`) <= 255;
12783
+ return value && value.split("/").every(plain) ? value : void 0;
12784
+ }
12785
+ function createEndpointPageLedger(pages) {
12786
+ return { owners: new Map(pages.map((page) => [page.id.toLowerCase(), { id: page.id, operation: null }])), duplicates: [], unnamed: [] };
12787
+ }
12788
+ function reportEndpointPages(ledger, warnings) {
12789
+ const names = (urls) => `${urls.slice(0, 10).join(", ")}${urls.length > 10 ? ", ..." : ""}`;
12790
+ if (ledger.duplicates.length > 0) {
12791
+ warnings.push({
12792
+ code: "unsupported-config",
12793
+ message: `${ledger.duplicates.length} Mintlify endpoint URL${ledger.duplicates.length === 1 ? " is" : "s are"} shared by more than one operation (same tag and summary), so only the first operation has a page at each: ${names(ledger.duplicates.map((url) => `/${url}`))}.`
12794
+ });
12795
+ }
12796
+ if (ledger.unnamed.length > 0) {
12797
+ warnings.push({
12798
+ code: "unsupported-config",
12799
+ message: `${ledger.unnamed.length} listed operation${ledger.unnamed.length === 1 ? " has" : "s have"} no page because ${ledger.unnamed.length === 1 ? "its" : "their"} tag or title leaves no usable page name: ${names(ledger.unnamed)}.`
12800
+ });
12801
+ }
12802
+ }
12803
+ function endpointDescription(description, title) {
12804
+ const text3 = (description ?? "").split(/\r?\n\s*\r?\n/, 1)[0].replace(/<[^>]*>/g, " ").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[`*~]|__/g, "").replace(/^\s*#+\s*/, "").replace(/\s+/g, " ").trim();
12805
+ const sentence = /^(.+?[.!?])(?=\s|$)/.exec(text3)?.[1] ?? text3;
12806
+ const limited = sentence.length > 300 ? `${sentence.slice(0, 297).replace(/\s+\S*$/, "")}...` : sentence;
12807
+ return limited || title;
12808
+ }
12809
+ function insertEndpointTab(tabs, tab, preceding) {
12810
+ if (preceding.length === 0) {
12811
+ tabs.unshift(tab);
12812
+ return;
12813
+ }
12814
+ for (const label of preceding) {
12815
+ const index = tabs.findIndex((candidate) => candidate.tab === label);
12816
+ if (index !== -1) {
12817
+ tabs.splice(index + 1, 0, tab);
12818
+ return;
12819
+ }
12820
+ }
12821
+ tabs.push(tab);
12822
+ }
12823
+ function addEndpointPages(options) {
12824
+ const { tabs, pages, ledger, spec } = options;
12825
+ const operations = parseOpenApiOperationsForLinking(spec.content);
12826
+ if (operations.length === 0) return null;
12827
+ const existing = spec.tabLabel ? tabs.find((tab) => tab.tab === spec.tabLabel) : tabs.find((tab) => !tab.hidden && tab.tab.toLowerCase().includes("api"));
12828
+ if (existing?.api) return null;
12829
+ const byKey = new Map(operations.map((operation) => [`${operation.method} ${operation.path}`, operation]));
12830
+ const listed = [];
12831
+ const pageFor = (reference) => {
12832
+ const [method, ...path] = reference.trim().split(/\s+/);
12833
+ const operation = byKey.get(`${method.toLowerCase()} ${path.join(" ")}`);
12834
+ if (!operation) return void 0;
12835
+ const key = `${operation.method.toUpperCase()} ${operation.path}`;
12836
+ const endpoint = mintlifyEndpointUrl(spec.target.directory, operation);
12837
+ if (!endpoint.tag || !endpoint.leaf || Buffer.byteLength(`${endpoint.tag}.mdx`) > 255 || Buffer.byteLength(`${endpoint.leaf}.mdx`) > 255) {
12838
+ if (!ledger.unnamed.includes(key)) ledger.unnamed.push(key);
12839
+ return void 0;
12840
+ }
12841
+ const identity = `${spec.assetPath} ${key}`;
12842
+ const owner = ledger.owners.get(endpoint.url.toLowerCase());
12843
+ if (owner && owner.operation !== null && owner.operation !== identity) {
12844
+ if (!ledger.duplicates.includes(endpoint.url)) ledger.duplicates.push(endpoint.url);
12845
+ return void 0;
12846
+ }
12847
+ if (!owner) {
12848
+ pages.push({
12849
+ id: endpoint.url,
12850
+ navigationId: endpoint.url,
12851
+ title: endpoint.title,
12852
+ description: endpointDescription(operation.description, endpoint.title),
12853
+ // The summary belongs to metadata; keep full spec prose in the safe
12854
+ // operation Markdown renderer rather than copying it into executable MDX.
12855
+ descriptionPlacement: "operation",
12856
+ keywords: [],
12857
+ body: "",
12858
+ source: "docs.json",
12859
+ openapi: `${spec.assetPath} ${key}`
12860
+ });
12861
+ ledger.owners.set(endpoint.url.toLowerCase(), { id: endpoint.url, operation: identity });
12862
+ }
12863
+ listed.push(key);
12864
+ return owner?.id ?? endpoint.url;
12865
+ };
12866
+ const convert2 = (nodes) => nodes.flatMap((node) => {
12867
+ if (typeof node === "string") {
12868
+ const id = pageFor(node);
12869
+ return id ? [id] : [];
12870
+ }
12871
+ const pagesOf = convert2(node.pages);
12872
+ return pagesOf.length > 0 ? [{ group: node.group, ...node.icon ? { icon: node.icon } : {}, pages: pagesOf }] : [];
12873
+ });
12874
+ const groups = convert2(spec.groups);
12875
+ if (groups.length === 0) return null;
12876
+ if (existing) {
12877
+ if (existing.groups || !existing.pages) existing.groups = [...existing.groups ?? [], ...groups];
12878
+ else existing.pages = [...existing.pages, ...groups];
12879
+ } else {
12880
+ const { slot } = spec.target;
12881
+ const tab = {
12882
+ ...slot?.tab ?? { tab: spec.tabLabel ?? "API Reference", ...spec.icon ? { icon: spec.icon } : {}, ...spec.hidden ? { hidden: true } : {} },
12883
+ groups
12884
+ };
12885
+ if (slot) insertEndpointTab(tabs, tab, slot.preceding);
12886
+ else insertApiTab(tabs, tab, spec.parentTab);
12887
+ }
12888
+ if (!tabs.some((tab) => tab.api?.source === spec.assetPath)) {
12889
+ insertApiTab(tabs, { tab: `OpenAPI: ${spec.source}`, hidden: true, api: { source: spec.assetPath } });
12890
+ }
12891
+ return { operations: listed };
12892
+ }
12893
+ function specLookupOrder(tabs, versions = []) {
12894
+ const collected = (tab) => !tab.hidden || Boolean(tab.version && versions.some((version) => version.label === tab.version));
12895
+ const sources = [...tabs.filter(collected), ...tabs.filter((tab) => !collected(tab))].flatMap((tab) => tab.api ? [tab.api.source] : []);
12896
+ return [...new Set(sources)];
12897
+ }
12898
+ function publishedOperationKeys(content) {
12899
+ const document = readSpecDocument(content);
12900
+ const table = (field) => visibleOperations(document, isRecord2(document) ? document[field] : void 0);
12901
+ return /* @__PURE__ */ new Set([
12902
+ ...table("paths").map((operation) => `${operation.method} ${operation.path}`),
12903
+ ...table("webhooks").map((operation) => `webhook ${operation.path}`)
12904
+ ]);
12905
+ }
12906
+ function qualifyDisplacedBareRefs(options) {
12907
+ const { pages, before, after, content, qualifier } = options;
12908
+ if (before.join("\n") === after.join("\n")) return;
12909
+ const published = /* @__PURE__ */ new Map();
12910
+ const firstPublishing = (order, key) => order.find((source) => {
12911
+ if (!published.has(source)) published.set(source, publishedOperationKeys(content(source) ?? Buffer.alloc(0)));
12912
+ return published.get(source).has(key);
12913
+ });
12914
+ for (const page of pages) {
12915
+ const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;
12916
+ if (!ref || ref.specRef) continue;
12917
+ const [method, ...rest] = ref.operation.trim().split(/\s+/);
12918
+ const key = `${method.toLowerCase()} ${rest.join(" ")}`;
12919
+ const reached = firstPublishing(before, key);
12920
+ if (reached !== void 0 && reached !== firstPublishing(after, key)) page.openapi = withSpecRef(ref, qualifier(reached));
12921
+ }
12922
+ }
12634
12923
  function rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings, answered = {}) {
12635
12924
  if (operationLinks.size === 0 && prefixLandings.size === 0) return;
12636
12925
  const unmatched = /* @__PURE__ */ new Set();
@@ -13878,6 +14167,7 @@ function migrateRepository(options) {
13878
14167
  const referenceOrder = /* @__PURE__ */ new Map();
13879
14168
  let docusaurusSidebars = null;
13880
14169
  let mintlifyConfig = null;
14170
+ let mintlifyApiTabSlots = [];
13881
14171
  const mintlifyNavigationAccess = /* @__PURE__ */ new Map();
13882
14172
  let fernRawConfig = null;
13883
14173
  let fernBasePath = "";
@@ -13922,6 +14212,7 @@ function migrateRepository(options) {
13922
14212
  }
13923
14213
  });
13924
14214
  docsConfig = projected.docsConfig;
14215
+ mintlifyApiTabSlots = projected.apiTabSlots;
13925
14216
  warnings.push(...projected.warnings);
13926
14217
  for (const repo of projected.gatedSourceRefs) gatedSourceRepos.add(repo.toLowerCase());
13927
14218
  for (const appearance of projected.referenceAccess) {
@@ -15029,7 +15320,8 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
15029
15320
  warnings.push({ code: "gated-page", message: sharedSpecMessage(path, excluded), source: path });
15030
15321
  };
15031
15322
  const nameSpec = createSpecAssetNamer();
15032
- const allSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs, nameSpec, pages.filter((page) => !restrictedPages.has(page)));
15323
+ const generatedEndpointPages = /* @__PURE__ */ new Set();
15324
+ const allSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs, nameSpec, pages.filter((page) => !restrictedPages.has(page)), mintlifyApiTabSlots);
15033
15325
  const navigation = navigationSpecContext(mintlifyConfig, (path) => resolvePageIdentity(path)?.id ?? null);
15034
15326
  const defaultSources = /* @__PURE__ */ new Set([
15035
15327
  ...allSpecs.filter((spec) => !spec.pageOnly && spec.tabLabel === void 0).map((spec) => spec.sourcePath),
@@ -15072,12 +15364,61 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
15072
15364
  spec.pageReferenced = namedByPages.has(spec);
15073
15365
  spec.generatedNavigation = Boolean(spec.directory) && !publishedSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, true));
15074
15366
  }
15075
- docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs, warnings);
15367
+ const listingSpecs = resolvedSpecs.filter((spec) => spec.groups && spec.endpointPages);
15368
+ const unbound = docsConfig;
15369
+ docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs.filter((spec) => !listingSpecs.includes(spec)), warnings);
15370
+ const ledger = createEndpointPageLedger(pages);
15371
+ const tabs = docsConfig.tabs.map((tab) => ({ ...tab }));
15372
+ const pagesBefore = pages.length;
15373
+ const endpointSpecs = /* @__PURE__ */ new Set();
15374
+ for (const spec of listingSpecs) {
15375
+ if (!spec.groups || !spec.endpointPages) continue;
15376
+ const added = addEndpointPages({
15377
+ tabs,
15378
+ pages,
15379
+ ledger,
15380
+ spec: {
15381
+ assetPath: specAssetPath(spec.filename),
15382
+ source: spec.sourcePath,
15383
+ content: spec.content,
15384
+ groups: spec.groups,
15385
+ target: spec.endpointPages,
15386
+ tabLabel: spec.tabLabel,
15387
+ parentTab: spec.parentTab,
15388
+ icon: spec.icon,
15389
+ hidden: spec.hidden
15390
+ }
15391
+ });
15392
+ if (!added) continue;
15393
+ spec.endpointOperations = added.operations;
15394
+ endpointSpecs.add(spec);
15395
+ }
15396
+ reportEndpointPages(ledger, warnings);
15397
+ for (const spec of resolvedSpecs) if (spec.directoryWarning && !endpointSpecs.has(spec)) warnings.push(spec.directoryWarning);
15398
+ for (const page of pages.slice(pagesBefore)) generatedEndpointPages.add(page);
15399
+ docsConfig = injectOpenApiSpecs({ ...docsConfig, tabs }, listingSpecs.filter((spec) => !endpointSpecs.has(spec)), warnings);
15400
+ if (remoteApiSpecs.length === 0 && endpointSpecs.size > 0) {
15401
+ const bySource = new Map(resolvedSpecs.map((spec) => [specAssetPath(spec.filename), spec]));
15402
+ qualifyDisplacedBareRefs({
15403
+ pages,
15404
+ before: specLookupOrder(injectOpenApiSpecs(unbound, resolvedSpecs).tabs, docsConfig.navigation?.versions),
15405
+ after: specLookupOrder(docsConfig.tabs, docsConfig.navigation?.versions),
15406
+ content: (source) => bySource.get(source)?.content,
15407
+ qualifier: (source) => bySource.get(source)?.sourcePath ?? source
15408
+ });
15409
+ }
15410
+ const mountOf = (spec) => endpointSpecs.has(spec) ? { prefix: spec.endpointPages?.directory, operations: spec.endpointOperations } : { prefix: spec.directory };
15076
15411
  if (remoteApiSpecs.length > 0) {
15077
- localApiMounts.push(...resolvedSpecs.flatMap((spec) => spec.directory ? [{ filename: spec.filename, directory: spec.directory, ...spec.tabLabel ? { tab: spec.tabLabel } : {} }] : []));
15412
+ localApiMounts.push(...resolvedSpecs.flatMap((spec) => {
15413
+ const { prefix, operations } = mountOf(spec);
15414
+ return prefix ? [{ filename: spec.filename, directory: prefix, ...spec.tabLabel ? { tab: spec.tabLabel } : {}, ...operations ? { endpointOperations: operations } : {} }] : [];
15415
+ }));
15078
15416
  } else {
15079
15417
  const endpointRedirects = linkApiMounts(
15080
- resolvedSpecs.map((spec) => ({ filename: spec.filename, content: spec.content, prefix: spec.directory, ...spec.tabLabel ? { tab: spec.tabLabel } : {} })),
15418
+ resolvedSpecs.map((spec) => {
15419
+ const { prefix, operations } = mountOf(spec);
15420
+ return { filename: spec.filename, content: spec.content, prefix, ...operations ? { endpointPages: true, operations } : {}, ...spec.tabLabel ? { tab: spec.tabLabel } : {} };
15421
+ }),
15081
15422
  docsConfig,
15082
15423
  pages,
15083
15424
  warnings
@@ -15134,7 +15475,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
15134
15475
  }
15135
15476
  return sameName ? "the file could not be read as an OpenAPI document" : `no file exists at "${key}"`;
15136
15477
  };
15137
- rewriteMintlifyPageSpecRefs(pages, pageSpecs, new Set(remoteApiSpecs.map((spec) => spec.url)), warnings, explainMissingSpec);
15478
+ rewriteMintlifyPageSpecRefs(pages.filter((page) => !generatedEndpointPages.has(page)), pageSpecs, new Set(remoteApiSpecs.map((spec) => spec.url)), warnings, explainMissingSpec);
15138
15479
  if (docsConfig.tabs.some((tab) => tab.api)) docsConfig = { ...docsConfig, api: { ...docsConfig.api, specLink: false } };
15139
15480
  }
15140
15481
  if (platform === "mintlify") {
@@ -15356,6 +15697,24 @@ ${body}`;
15356
15697
  if (docsConfig.redirects) {
15357
15698
  docsConfig = dropConflictingRedirects(dropUnroutableRedirects(docsConfig, warnings), [...pages, ...sourceRefPages], warnings);
15358
15699
  }
15700
+ const redirectSources = new Map((docsConfig.redirects ?? []).map((redirect) => [(redirect.source.replace(/\/+$/, "") || "/").toLowerCase(), redirect.destination]));
15701
+ const linkRedirects = [];
15702
+ for (const page of [...pages, ...sourceRefPages]) {
15703
+ if (!page.externalUrl) continue;
15704
+ for (const source of pageEntryRoutes(page)) {
15705
+ const existing = redirectSources.get(source.toLowerCase());
15706
+ if (existing !== void 0) {
15707
+ warnings.push({
15708
+ code: "unsupported-config",
15709
+ message: `Page "${page.id}" links to ${stripControlCharacters(page.externalUrl)}, but "${source}" already redirects to ${stripControlCharacters(existing)}; that redirect was kept, so "${source}" does not open the link.`
15710
+ });
15711
+ continue;
15712
+ }
15713
+ redirectSources.set(source.toLowerCase(), page.externalUrl);
15714
+ linkRedirects.push({ source, destination: page.externalUrl, permanent: false });
15715
+ }
15716
+ }
15717
+ if (linkRedirects.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...linkRedirects] };
15359
15718
  if (pages.length === 0) {
15360
15719
  const reasons = warnings.map((warning) => warning.message);
15361
15720
  throw new Error(reasons.length ? `No importable Markdown or MDX pages were found in the repository. Warnings encountered during migration:
@@ -15439,7 +15798,7 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
15439
15798
  });
15440
15799
  }
15441
15800
  if (!docsConfig.navigation?.display && docsConfig.tabs.filter((tab) => !tab.hidden).length > 6) {
15442
- docsConfig = { ...docsConfig, navigation: { display: "dropdown" } };
15801
+ docsConfig = { ...docsConfig, navigation: { display: "dropdown", ...docsConfig.navigation?.groupHeadings ? { groupHeadings: docsConfig.navigation.groupHeadings } : {} } };
15443
15802
  }
15444
15803
  const themeColors = mintlifyConfig ? mintlifyThemeColors(mintlifyConfig.colors) : fernRawConfig ? fernThemeColors(fernRawConfig.colors) ?? (fernRawConfig["global-theme"] === "nvidia" ? { primary: "#76B900" } : void 0) : isTopLevelDocusaurus ? presentation?.colors ?? readDocusaurusThemeColor(docusaurusProjectRoot) : void 0;
15445
15804
  const assetPaths = new Set(assets.map((asset) => asset.path));
@@ -15635,7 +15994,7 @@ function renderPage(bundle, page) {
15635
15994
  page.icon ? `icon: ${yamlString(page.icon)}` : null,
15636
15995
  page.iconType ? `iconType: ${yamlString(page.iconType)}` : null,
15637
15996
  `description: ${yamlString(page.description)}`,
15638
- page.descriptionPlacement === "body" ? "descriptionPlacement: body" : null,
15997
+ page.descriptionPlacement === "body" || page.descriptionPlacement === "operation" ? `descriptionPlacement: ${page.descriptionPlacement}` : null,
15639
15998
  page.badge ? `badge: ${yamlString(page.badge)}` : null,
15640
15999
  page.keywords.length > 0 ? `keywords: [${page.keywords.map(yamlString).join(", ")}]` : null,
15641
16000
  page.mode ? `mode: ${yamlString(page.mode)}` : null,
@@ -18164,6 +18523,12 @@ function slug(value) {
18164
18523
  function operationTarget(specId, operation) {
18165
18524
  return `/api/${specId}/${thallyOperationSlugSegments(operation.path, operation.method).join("/")}`;
18166
18525
  }
18526
+ function bindApiTab(tabs, reference, api) {
18527
+ const tab = reference.tabLabel ? tabs.find((candidate) => candidate.tab === reference.tabLabel) : tabs.find((candidate) => candidate.tab.toLowerCase().includes("api"));
18528
+ if (tab) tab.api = api;
18529
+ else insertApiTab(tabs, { tab: reference.tabLabel ?? "API Reference", ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, api }, reference.parentTab);
18530
+ return tab?.tab ?? reference.tabLabel ?? "API Reference";
18531
+ }
18167
18532
  async function hydrateRemoteApiSpecs(bundle, fetcher) {
18168
18533
  if (!bundle.remoteApiSpecs?.length) return bundle;
18169
18534
  const assets = [...bundle.assets];
@@ -18172,7 +18537,8 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18172
18537
  const warnings = [...bundle.warnings];
18173
18538
  const pages = bundle.pages.map((page) => ({ ...page }));
18174
18539
  const authoredLinks = /* @__PURE__ */ new Set();
18175
- const served = servedRoutes(pages);
18540
+ const ledger = createEndpointPageLedger(pages);
18541
+ const pagesBefore = pages.length;
18176
18542
  const endpointUrls = /* @__PURE__ */ new Map();
18177
18543
  for (const page of bundle.pages) {
18178
18544
  for (const match of page.body.matchAll(/\/[a-z0-9_/-]+/gi)) {
@@ -18183,6 +18549,10 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18183
18549
  }
18184
18550
  }
18185
18551
  const bound = [];
18552
+ const apiTabs = bundle.docsConfig.tabs.map((tab) => ({ ...tab }));
18553
+ for (const mount of bundle.localApiMounts ?? []) {
18554
+ if (mount.endpointOperations) bindApiTab(apiTabs, { tabLabel: mount.tab }, { source: `openapi/${mount.filename}` });
18555
+ }
18186
18556
  for (const reference of bundle.remoteApiSpecs) {
18187
18557
  for (let index = warnings.length - 1; index >= 0; index--) {
18188
18558
  if (warnings[index].source === reference.url && warnings[index].message.includes("requires a network download")) warnings.splice(index, 1);
@@ -18202,11 +18572,29 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18202
18572
  const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;
18203
18573
  if (ref?.specRef === reference.url) page.openapi = withSpecRef(ref, `openapi/${filename}`);
18204
18574
  }
18205
- const tab = reference.tabLabel ? tabs.find((candidate) => candidate.tab === reference.tabLabel) : tabs.find((candidate) => candidate.tab.toLowerCase().includes("api"));
18575
+ bindApiTab(apiTabs, reference, { source: `openapi/${filename}` });
18576
+ const endpoint = reference.groups && reference.endpointPages ? addEndpointPages({
18577
+ tabs,
18578
+ pages,
18579
+ ledger,
18580
+ spec: {
18581
+ assetPath: `openapi/${filename}`,
18582
+ source: reference.url,
18583
+ content: body,
18584
+ groups: reference.groups,
18585
+ target: reference.endpointPages,
18586
+ tabLabel: reference.tabLabel,
18587
+ parentTab: reference.parentTab,
18588
+ icon: reference.icon,
18589
+ hidden: reference.hidden
18590
+ }
18591
+ }) : null;
18592
+ if (endpoint) {
18593
+ bound.push({ reference, filename, content: body, operations, tabName: reference.tabLabel ?? "API Reference", endpointOperations: endpoint.operations });
18594
+ continue;
18595
+ }
18206
18596
  const api = { source: `openapi/${filename}`, ...reference.navigation === false ? { navigation: false } : {}, ...reference.groups ? { groups: reference.groups } : {} };
18207
- if (tab) tab.api = api;
18208
- else insertApiTab(tabs, { tab: reference.tabLabel ?? "API Reference", ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, api }, reference.parentTab);
18209
- bound.push({ reference, filename, content: body, operations, tabName: tab?.tab ?? reference.tabLabel ?? "API Reference" });
18597
+ bound.push({ reference, filename, content: body, operations, tabName: bindApiTab(tabs, reference, api) });
18210
18598
  } catch (error) {
18211
18599
  warnings.push({
18212
18600
  code: "fetch-failed",
@@ -18215,19 +18603,22 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18215
18603
  });
18216
18604
  }
18217
18605
  }
18606
+ const pageMounts = bound.flatMap(({ reference, endpointOperations }) => endpointOperations && reference.endpointPages ? [reference.endpointPages.directory] : []);
18218
18607
  const directories = [
18219
18608
  ...(bundle.localApiMounts ?? []).map(({ directory: directory2 }) => directory2),
18220
- ...bundle.remoteApiSpecs.flatMap((reference) => (reference.directories ?? []).map(({ directory: directory2 }) => directory2))
18609
+ ...bundle.remoteApiSpecs.flatMap((reference) => (reference.directories ?? []).map(({ directory: directory2 }) => directory2)),
18610
+ ...pageMounts
18221
18611
  ];
18222
18612
  const endpointRedirects = linkApiMounts(
18223
18613
  [
18224
- ...(bundle.localApiMounts ?? []).map(({ filename, directory: directory2, tab }) => ({
18614
+ ...(bundle.localApiMounts ?? []).map(({ filename, directory: directory2, tab, endpointOperations }) => ({
18225
18615
  filename,
18226
18616
  content: Buffer.from(assets.find((asset) => asset.path === `openapi/${filename}`)?.content ?? []),
18227
18617
  prefix: directory2,
18618
+ ...endpointOperations ? { endpointPages: true, operations: endpointOperations } : {},
18228
18619
  ...tab ? { tab } : {}
18229
18620
  })),
18230
- ...bound.flatMap(({ reference, filename, content, tabName }) => (reference.directories ?? []).map(({ directory: directory2, operations: listed }) => ({ filename, content, prefix: directory2, tab: tabName, ...listed ? { operations: listed } : {} })))
18621
+ ...bound.flatMap(({ reference, filename, content, tabName, endpointOperations }) => endpointOperations && reference.endpointPages ? [{ filename, content, prefix: reference.endpointPages.directory, tab: tabName, endpointPages: true, operations: endpointOperations }] : (reference.directories ?? []).map(({ directory: directory2, operations: listed }) => ({ filename, content, prefix: directory2, tab: tabName, ...listed ? { operations: listed } : {} })))
18231
18622
  ],
18232
18623
  { ...bundle.docsConfig, tabs, redirects },
18233
18624
  pages,
@@ -18238,6 +18629,18 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18238
18629
  redirects.push(redirect);
18239
18630
  endpointUrls.set(literalRoute(redirect.source) ?? redirect.source, redirect.destination);
18240
18631
  }
18632
+ reportEndpointPages(ledger, warnings);
18633
+ qualifyDisplacedBareRefs({
18634
+ pages,
18635
+ before: specLookupOrder(apiTabs, bundle.docsConfig.navigation?.versions),
18636
+ after: specLookupOrder(tabs, bundle.docsConfig.navigation?.versions),
18637
+ content: (source) => {
18638
+ const asset = assets.find((entry) => entry.path === source);
18639
+ return asset ? Buffer.from(asset.content) : void 0;
18640
+ },
18641
+ qualifier: (source) => source
18642
+ });
18643
+ const served = servedRoutes(pages);
18241
18644
  const existingSources = new Set(redirects.map((redirect) => literalRoute(redirect.source) ?? redirect.source.toLowerCase()));
18242
18645
  const mounts = directories.map(mountPrefix).filter(Boolean);
18243
18646
  const underMount = (source) => mounts.some((prefix) => source.toLowerCase() === `/${prefix}` || source.toLowerCase().startsWith(`/${prefix}/`));
@@ -18262,12 +18665,19 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18262
18665
  const endpoint = endpointUrls.get(literalRoute(redirect.destination) ?? "");
18263
18666
  return endpoint ? { ...redirect, destination: endpoint } : redirect;
18264
18667
  });
18668
+ const boundConfig = { ...bundle.docsConfig, tabs, ...direct.length > 0 ? { redirects: direct } : {} };
18669
+ const newPages = pages.slice(pagesBefore);
18670
+ const withDirectories = bundle.platform === "mintlify" && newPages.length > 0 ? addMintlifyDirectoryRedirects(boundConfig, newPages) : boundConfig;
18671
+ const linkRedirects = new Set(pages.flatMap((page) => page.externalUrl ? pageEntryRoutes(page).map((source) => `${source} ${page.externalUrl}`) : []));
18672
+ const isLinkRedirect = (redirect) => linkRedirects.has(`${redirect.source} ${redirect.destination}`);
18673
+ const links = (withDirectories.redirects ?? []).filter(isLinkRedirect);
18674
+ const unchecked = links.length > 0 ? { ...withDirectories, redirects: withDirectories.redirects.filter((redirect) => !isLinkRedirect(redirect)) } : withDirectories;
18675
+ const checked = dropConflictingRedirects(dropUnroutableRedirects(unchecked, warnings), pages, warnings);
18265
18676
  return {
18266
18677
  ...bundle,
18267
18678
  pages,
18268
18679
  assets,
18269
- // Redirects added above meet the same checks as any other: none may fail the build, hide a page that exists, or loop.
18270
- docsConfig: dropConflictingRedirects(dropUnroutableRedirects({ ...bundle.docsConfig, tabs, ...direct.length > 0 ? { redirects: direct } : {} }, warnings), pages, warnings),
18680
+ docsConfig: links.length > 0 ? { ...checked, redirects: [...checked.redirects ?? [], ...links] } : checked,
18271
18681
  warnings
18272
18682
  };
18273
18683
  }