@thallylabs/migrate 0.2.9 → 0.2.11

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
@@ -31,6 +31,20 @@ import { unified as unified2 } from "unified";
31
31
 
32
32
  // src/frontmatter.ts
33
33
  import { parse as parseYaml } from "yaml";
34
+ function salvageFrontmatterLines(matter) {
35
+ const data = {};
36
+ for (const line of matter.split(/\r?\n/)) {
37
+ if (!/^[A-Za-z_][\w-]*:\s?/.test(line)) continue;
38
+ try {
39
+ const parsedLine = parseYaml(line);
40
+ if (parsedLine && typeof parsedLine === "object" && !Array.isArray(parsedLine)) {
41
+ Object.assign(data, parsedLine);
42
+ }
43
+ } catch {
44
+ }
45
+ }
46
+ return data;
47
+ }
34
48
  function parseFrontmatter(raw) {
35
49
  const source = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
36
50
  const opening = /^---([^\r\n]*)\r?\n/.exec(source);
@@ -45,14 +59,23 @@ function parseFrontmatter(raw) {
45
59
  if (matter.trim() === "" || !["", "yaml", "yml"].includes(language)) {
46
60
  return { content, data: {} };
47
61
  }
48
- const parsed = parseYaml(matter);
49
- return {
50
- content,
51
- data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
52
- };
62
+ try {
63
+ const parsed = parseYaml(matter);
64
+ return {
65
+ content,
66
+ data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
67
+ };
68
+ } catch (err) {
69
+ return {
70
+ content,
71
+ data: salvageFrontmatterLines(matter),
72
+ error: err instanceof Error ? err.message : String(err)
73
+ };
74
+ }
53
75
  }
54
76
 
55
77
  // src/mdx.ts
78
+ import { posix } from "path";
56
79
  import { compileSync } from "@mdx-js/mdx";
57
80
  import { nameToEmoji } from "gemoji";
58
81
  import remarkGfm from "remark-gfm";
@@ -232,8 +255,9 @@ function unwrapDocusaurusMdxCodeBlocks(body) {
232
255
  }
233
256
  return isMdxCodeBlock ? body : output.join("\n");
234
257
  }
235
- function normalizeExplicitHeadingIds(body) {
236
- return replaceOutsideCode(body, (segment) => segment.split("\n").map((line) => {
258
+ function normalizeExplicitHeadingIds(raw) {
259
+ const { front, body } = splitFrontmatterBlock(raw);
260
+ return front + replaceOutsideCode(body, (segment) => segment.split("\n").map((line) => {
237
261
  if (!/^[ \t]{0,3}#{1,6}[ \t]+/.test(line)) return line;
238
262
  const trimmed = line.trimEnd();
239
263
  if (trimmed.endsWith("}")) {
@@ -275,6 +299,7 @@ function removeGlobalDocusaurusImports(body) {
275
299
  }
276
300
  var JS_LITERAL_KEYWORDS = /* @__PURE__ */ new Set(["true", "false", "null", "undefined"]);
277
301
  var BARE_IDENTIFIER_PATH = /^[\p{ID_Start}$_][\p{ID_Continue}$‌‍]*(?:\.[\p{ID_Start}$_][\p{ID_Continue}$‌‍]*)*$/u;
302
+ var BARE_OBJECT_SHORTHAND = /^\{\s*([\p{ID_Start}$_][\p{ID_Continue}$‌‍]*)\s*\}$/u;
278
303
  var SAFE_BUILTIN_ROOTS = /* @__PURE__ */ new Set([
279
304
  "Math",
280
305
  "JSON",
@@ -499,12 +524,13 @@ function escapeFernLiteralBraces(raw) {
499
524
  const end = node.position?.end.offset;
500
525
  const root2 = value.split(".")[0];
501
526
  const safeBuiltinPath = value.includes(".") && SAFE_BUILTIN_ROOTS.has(root2);
502
- const isTemplatePlaceholder = /^\{\s*[A-Za-z_$][\w$.-]*\s*\}$/.test(value);
527
+ const isBareIdentifier = BARE_IDENTIFIER_PATH.test(value) && !JS_LITERAL_KEYWORDS.has(value) && !declared.has(root2) && !safeBuiltinPath;
528
+ const shorthandMatch = value.match(BARE_OBJECT_SHORTHAND);
529
+ const isDoubleBraceTemplate = shorthandMatch !== null && !declared.has(shorthandMatch[1]);
503
530
  const isIssueList = /^[A-Za-z][A-Za-z0-9]*-\d+(?:\s*,\s*[A-Za-z][A-Za-z0-9]*-\d+)*$/.test(value);
504
- if ((isTemplatePlaceholder || isIssueList) && start !== void 0 && end !== void 0) {
505
- edits.push({ start, end, value: body.slice(start, end).replace(/\\/g, "\\\\").replace(/[{}]/g, "\\$&") });
506
- } else if (BARE_IDENTIFIER_PATH.test(value) && !JS_LITERAL_KEYWORDS.has(value) && !declared.has(root2) && !safeBuiltinPath && start !== void 0 && end !== void 0) {
507
- edits.push({ start, end, value: `\\{${value}\\}` });
531
+ if ((isBareIdentifier || isDoubleBraceTemplate || isIssueList) && start !== void 0 && end !== void 0) {
532
+ const rawSpan = body.slice(start, end);
533
+ edits.push({ start, end, value: rawSpan.replace(/\\/g, "\\\\").replace(/[{}]/g, "\\$&") });
508
534
  }
509
535
  return;
510
536
  }
@@ -644,6 +670,66 @@ function replaceOutsideCode(body, transform) {
644
670
  const { masked, unmask } = maskCode(body);
645
671
  return unmask(transform(masked));
646
672
  }
673
+ function rewriteFernRelativePageLinks(body, currentSourcePath, routesBySourcePath) {
674
+ const rewrite = (target) => {
675
+ if (!target || target.startsWith("/") || target.startsWith("#") || target.startsWith("?") || target.includes("\\") || /[\u0000-\u001f\u007f]/.test(target) || /^[A-Za-z][A-Za-z\d+.-]*:/.test(target)) return target;
676
+ const suffixAt = target.search(/[?#]/);
677
+ const pathname = suffixAt < 0 ? target : target.slice(0, suffixAt);
678
+ const suffix = suffixAt < 0 ? "" : target.slice(suffixAt);
679
+ if (!pathname || pathname.startsWith("//") || pathname.split("/").some((part) => /%2f|%5c/i.test(part))) return target;
680
+ const source = posix.normalize(posix.join(posix.dirname(currentSourcePath.replace(/\\/g, "/")), pathname));
681
+ const candidates = /\.mdx?$/i.test(source) ? [source] : [source, `${source}.mdx`, `${source}.md`, `${source}/index.mdx`, `${source}/index.md`];
682
+ const route = candidates.map((candidate) => routesBySourcePath.get(candidate)).find(Boolean);
683
+ return route ? `/${route.replace(/^\/+/, "")}${suffix}` : target;
684
+ };
685
+ return replaceOutsideCode(body, (text) => text.replace(
686
+ /(\]\()(<[^>\n]+>|[^\s)]+)(?=\s|\))/g,
687
+ (_match, opening, target) => `${opening}${target.startsWith("<") && target.endsWith(">") ? `<${rewrite(target.slice(1, -1))}>` : rewrite(target)}`
688
+ ).replace(
689
+ /(\b(?:href|to)\s*=\s*)(["'])([^"'\n]+)\2/g,
690
+ (_match, opening, quote, target) => `${opening}${quote}${rewrite(target)}${quote}`
691
+ ).replace(
692
+ /(\b(?:href|to)\s*=\s*\{\s*)(["'])([^"'\n]+)\2(?=\s*\})/g,
693
+ (_match, opening, quote, target) => `${opening}${quote}${rewrite(target)}${quote}`
694
+ ));
695
+ }
696
+ function normalizeFernFileTrees(body) {
697
+ return replaceOutsideCode(body, (whole) => whole.replace(
698
+ /<FileTree\s*>\s*\n([\s\S]*?)\n\s*<\/FileTree>/g,
699
+ (original, contents) => {
700
+ const lines = contents.split(/\r?\n/).filter((line) => line.trim());
701
+ if (!lines.length) return original;
702
+ const nodes = [];
703
+ for (const line of lines) {
704
+ const match = /^( *)(?:-|\*|\+)\s+(.+?)\s*$/.exec(line);
705
+ if (!match || match[1].length % 2 !== 0 || match[2].includes("\0")) return original;
706
+ nodes.push({ indent: match[1].length / 2, name: match[2], isFolder: match[2].endsWith("/") });
707
+ }
708
+ const output = ["<Tree>"];
709
+ const folders = [];
710
+ for (const node of nodes) {
711
+ while (folders.length > node.indent) {
712
+ output.push("</Folder>");
713
+ folders.pop();
714
+ }
715
+ if (node.indent > folders.length || node.indent > 0 && folders.length === 0) return original;
716
+ const name = `{${JSON.stringify(node.name)}}`;
717
+ if (node.isFolder) {
718
+ output.push(`<Folder name=${name} defaultOpen>`);
719
+ folders.push(node.indent);
720
+ } else {
721
+ output.push(`<File name=${name} />`);
722
+ }
723
+ }
724
+ while (folders.length) {
725
+ output.push("</Folder>");
726
+ folders.pop();
727
+ }
728
+ output.push("</Tree>");
729
+ return output.join("\n");
730
+ }
731
+ ));
732
+ }
647
733
  function normalizeHtmlComments(body) {
648
734
  return replaceOutsideCode(body, (whole) => whole.replace(/<!--([\s\S]*?)-->/g, (_match, comment) => `{/*${comment}*/}`));
649
735
  }
@@ -1013,7 +1099,7 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1013
1099
  const runMintlify = platform === "mintlify";
1014
1100
  const runDocusaurus = platform === "docusaurus";
1015
1101
  const sourceBody = runDocusaurus && unwrapMdxCodeBlocks ? unwrapDocusaurusMdxCodeBlocks(body) : body;
1016
- let rewritten = normalizeDocusaurusAdmonitions(normalizeExplicitHeadingIds(runFern ? normalizeFernCallouts(sourceBody) : sourceBody));
1102
+ let rewritten = normalizeDocusaurusAdmonitions(normalizeExplicitHeadingIds(runFern ? normalizeFernFileTrees(normalizeFernCallouts(sourceBody)) : sourceBody));
1017
1103
  if (runDocusaurus) {
1018
1104
  rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1019
1105
  /:([+\w-]+):/g,
@@ -1092,7 +1178,8 @@ function parseMarkdownPage(input) {
1092
1178
  noindex: parsed.data.noindex === true || parsed.data.noindex === "true" ? true : void 0,
1093
1179
  openapi: typeof parsed.data.openapi === "string" ? parsed.data.openapi.trim() : void 0,
1094
1180
  body,
1095
- source: input.source
1181
+ source: input.source,
1182
+ ...parsed.error ? { frontmatterError: parsed.error } : {}
1096
1183
  };
1097
1184
  }
1098
1185
 
@@ -1145,6 +1232,11 @@ function mintlifyLocalizedReference(value, locale, defaultPageIds = /* @__PURE__
1145
1232
  const candidateId = pageIdFromReference(candidate, true);
1146
1233
  if (candidateId && defaultPageIds.has(candidateId)) return candidate;
1147
1234
  }
1235
+ const segments = reference.split("/");
1236
+ const localeIndex = segments.findIndex((segment) => segment.toLowerCase() === locale.toLowerCase());
1237
+ if (localeIndex > 0) {
1238
+ return [...segments.slice(0, localeIndex), ...segments.slice(localeIndex + 1)].join("/");
1239
+ }
1148
1240
  return value;
1149
1241
  }
1150
1242
  function resolveWithin(root, candidate) {
@@ -1493,9 +1585,14 @@ function implicitReactImports(source) {
1493
1585
  visit(source);
1494
1586
  const hooks = [...REACT_GLOBALS].filter((name) => references.has(name) && !bindings.has(name)).sort();
1495
1587
  const usesMintlifyComponents = references.has("MintlifyComponents") && !bindings.has("MintlifyComponents");
1588
+ const implicitComponents = MINTLIFY_COMPONENT_MODULES.map(({ module, names }) => {
1589
+ const needed = names.filter((name) => references.has(name) && !bindings.has(name));
1590
+ return needed.length ? `import { ${needed.join(", ")} } from '${module}';` : "";
1591
+ }).filter(Boolean);
1496
1592
  return [
1497
1593
  references.has("React") && !bindings.has("React") ? "import * as React from 'react';" : "",
1498
1594
  hooks.length ? `import { ${hooks.join(", ")} } from 'react';` : "",
1595
+ ...implicitComponents,
1499
1596
  usesMintlifyComponents ? mintlifyComponentsShim() : ""
1500
1597
  ].filter(Boolean).join("\n");
1501
1598
  }
@@ -1598,6 +1695,19 @@ function declarationsReferenceBrowserGlobal(body) {
1598
1695
  }
1599
1696
  return found;
1600
1697
  }
1698
+ function neutralizeUnresolvableImportsWithoutAst(content, currentFile, warn) {
1699
+ const importPattern = /^import\s+(?:\{\s*(?:default\s+as\s+)?([A-Z][A-Za-z0-9_]*)\s*\}|([A-Z][A-Za-z0-9_]*))\s+from\s+(['"])((?:@site\/|\.\.?\/|\/)[^'"]+)\3\s*;?[ \t]*$/gm;
1700
+ const dropped = [];
1701
+ let result = content.replace(importPattern, (_match, namedComponent, defaultComponent, _quote, specifier) => {
1702
+ dropped.push({ name: namedComponent ?? defaultComponent, specifier });
1703
+ return "";
1704
+ });
1705
+ for (const { name, specifier } of dropped) {
1706
+ warn(`Component import ${JSON.stringify(specifier)} could not be resolved because this page has invalid MDX (see the previous warning), so the import and its usage were removed to keep the site building.`, currentFile);
1707
+ result = result.replace(new RegExp(`<${name}(?:\\s[^>]*)?/>`, "g"), `{/* Removed <${name}>: unresolved during fallback migration */}`).replace(new RegExp(`<${name}(?:\\s[^>]*)?>[\\s\\S]*?<\\/${name}>`, "g"), `{/* Removed <${name}>: unresolved during fallback migration */}`);
1708
+ }
1709
+ return result;
1710
+ }
1601
1711
  function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIdentity) {
1602
1712
  const root = resolve2(siteRoot);
1603
1713
  const confined = resolve2(confinementRoot);
@@ -1792,16 +1902,17 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1792
1902
  function transform(raw, currentFile) {
1793
1903
  const parsedFrontmatter = parseFrontmatter(raw).content;
1794
1904
  const frontmatter = raw.slice(0, raw.length - parsedFrontmatter.length);
1795
- const content = normalizeIndentedFences(normalizeHtmlComments(parsedFrontmatter));
1905
+ const content = normalizeExplicitHeadingIds(normalizeIndentedFences(normalizeHtmlComments(parsedFrontmatter)));
1796
1906
  let tree;
1797
1907
  try {
1798
1908
  tree = parser.parse(content);
1799
1909
  } catch {
1800
1910
  warn("Custom component analysis could not parse this MDX; its source was preserved for manual migration.", currentFile);
1801
- return raw;
1911
+ return frontmatter + neutralizeUnresolvableImportsWithoutAst(content, currentFile, warn);
1802
1912
  }
1803
1913
  const aliases = /* @__PURE__ */ new Map();
1804
1914
  const unsupportedImports = /* @__PURE__ */ new Map();
1915
+ const droppedMdxDataBindings = /* @__PURE__ */ new Set();
1805
1916
  const edits = [];
1806
1917
  const declarations = [];
1807
1918
  const moduleImports = [];
@@ -1901,14 +2012,31 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1901
2012
  }
1902
2013
  continue;
1903
2014
  }
1904
- if (/\.mdx?$/.test(specifier)) continue;
2015
+ if (/\.mdx?$/.test(specifier)) {
2016
+ if (bindings.length > 0 && bindings.every((binding) => !/^[A-Z]/.test(binding.local))) {
2017
+ warn(`MDX named import of ${JSON.stringify(rawSpecifier)} isn't a component and has no equivalent in the migrated site (for example Docusaurus's generated 'toc' export), so it was removed, along with any '...${bindings.map((binding) => binding.local).join(", '...")}' spread of it.`, currentFile);
2018
+ edits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
2019
+ for (const binding of bindings) droppedMdxDataBindings.add(binding.local);
2020
+ }
2021
+ continue;
2022
+ }
1905
2023
  if (bindings.length && RESCUABLE_ASSET_EXTENSIONS.has(extname(specifier).toLowerCase()) && !bindings.some((binding) => usedAsJsxTagName(binding.local))) {
1906
2024
  try {
1907
2025
  const href = copyPublicAsset(resolveDependency(specifier, currentFile));
1908
2026
  edits.push({
1909
2027
  start: node.position.start.offset + statement.getStart(ast),
1910
2028
  end: node.position.start.offset + statement.end,
1911
- value: bindings.map((binding) => `const ${binding.local} = ${JSON.stringify(href)};`).join("\n")
2029
+ // `export`, not a bare `const`: this replaces an `import`
2030
+ // statement in place, and the final MDX compile (a fresh
2031
+ // parse of this text, not a reuse of this pass's own AST)
2032
+ // only recognizes a top-level JS block as ESM — evaluated and
2033
+ // its bindings put in scope — when it starts with `import` or
2034
+ // `export`. A bare `const` there is just prose to that parser
2035
+ // and renders as literal paragraph text instead, leaving
2036
+ // every reference to this binding a `ReferenceError` at
2037
+ // render (reproduced against hasura/graphql-engine's
2038
+ // databases/overview.mdx, which binds asset URLs this way).
2039
+ value: bindings.map((binding) => `export const ${binding.local} = ${JSON.stringify(href)};`).join("\n")
1912
2040
  });
1913
2041
  warn(`Asset import ${JSON.stringify(rawSpecifier)} was copied to ${href} and bound to that URL instead of importing it as a component.`, currentFile);
1914
2042
  continue;
@@ -1976,7 +2104,9 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1976
2104
  edits.push({
1977
2105
  start: importStart,
1978
2106
  end: importEnd,
1979
- value: bindings.map((binding) => `const ${binding.local} = ${JSON.stringify(rescuedAssetHref)};`).join("\n")
2107
+ // See the identical `export const` note on the other asset
2108
+ // rescue above — same reason.
2109
+ value: bindings.map((binding) => `export const ${binding.local} = ${JSON.stringify(rescuedAssetHref)};`).join("\n")
1980
2110
  });
1981
2111
  warn(`Asset import ${JSON.stringify(rawSpecifier)} could not be copied as a component; it was copied to ${rescuedAssetHref} and bound to that URL instead.`, currentFile);
1982
2112
  } else {
@@ -1986,6 +2116,27 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1986
2116
  }
1987
2117
  }
1988
2118
  }
2119
+ if (droppedMdxDataBindings.size > 0) {
2120
+ for (const node of esmNodes) {
2121
+ let stripDroppedSpreads2 = function(inner) {
2122
+ if (ts.isSpreadElement(inner) && ts.isIdentifier(inner.expression) && droppedMdxDataBindings.has(inner.expression.text)) {
2123
+ const start = nodeStart + inner.getStart(ast);
2124
+ let end = nodeStart + inner.end;
2125
+ const following = nodeText.slice(inner.end).match(/^\s*,/);
2126
+ if (following) end += following[0].length;
2127
+ edits.push({ start, end, value: "" });
2128
+ return;
2129
+ }
2130
+ ts.forEachChild(inner, stripDroppedSpreads2);
2131
+ };
2132
+ var stripDroppedSpreads = stripDroppedSpreads2;
2133
+ if (node.value === void 0 || node.position?.start.offset === void 0) continue;
2134
+ const nodeStart = node.position.start.offset;
2135
+ const nodeText = node.value;
2136
+ const ast = sourceFile(nodeText, "inline.tsx");
2137
+ for (const statement of ast.statements) stripDroppedSpreads2(statement);
2138
+ }
2139
+ }
1989
2140
  walk(tree, (node) => {
1990
2141
  for (const attribute of node.attributes ?? []) {
1991
2142
  if (attribute.type !== "mdxJsxAttribute" || !attribute.value || typeof attribute.value !== "object") continue;
@@ -2242,14 +2393,14 @@ import { customComponents as ${name} } from ${JSON.stringify(registryPath)}
2242
2393
 
2243
2394
  // src/docusaurus.ts
2244
2395
  import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3 } from "fs";
2245
- import { extname as extname3, posix } from "path";
2396
+ import { extname as extname3, posix as posix2 } from "path";
2246
2397
  import JSON5 from "json5";
2247
2398
  import { parse as parseYaml2 } from "yaml";
2248
2399
 
2249
2400
  // src/navigation.ts
2250
2401
  import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2, realpathSync } from "fs";
2251
2402
  import { dirname as dirname2, extname as extname2, relative as relative3 } from "path";
2252
- var MAX_MINTLIFY_CONFIG_BYTES = 2e6;
2403
+ var MAX_MINTLIFY_CONFIG_BYTES = 2e7;
2253
2404
  function addMintlifyDirectoryRedirects(config, pages) {
2254
2405
  const pageIds = new Set(pages.map((page) => page.id));
2255
2406
  const redirects = [...config.redirects ?? []];
@@ -2297,15 +2448,10 @@ function addMintlifyDirectoryRedirects(config, pages) {
2297
2448
  }
2298
2449
  }
2299
2450
  }
2300
- for (const redirect of config.redirects ?? []) {
2301
- const destination = redirect.destination.split(/[?#]/, 1)[0].replace(/\/:[A-Za-z_][A-Za-z0-9_]*\*\/?$/, "");
2302
- if (!destination.startsWith("/") || destination.startsWith("//") || /[:*()[\]{}]/.test(destination)) continue;
2303
- const directory = trimEdgeSlashes(destination);
2304
- if (!directory || pageIds.has(directory)) continue;
2451
+ for (const [directory, landing] of firstDescendantByDirectory) {
2452
+ if (pageIds.has(directory)) continue;
2305
2453
  const source = `/${directory}`;
2306
2454
  if (hasAuthoredRedirect(source)) continue;
2307
- const landing = firstDescendantByDirectory.get(directory);
2308
- if (!landing) continue;
2309
2455
  redirects.push({ source, destination: `/${landing.id}`, permanent: false });
2310
2456
  redirectSources.add(source);
2311
2457
  }
@@ -2356,6 +2502,18 @@ var LANGUAGE_LABELS = {
2356
2502
  function objectValue(value) {
2357
2503
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2358
2504
  }
2505
+ function withFlattenedProductGroups(container) {
2506
+ if (!Array.isArray(container.productGroups)) return container;
2507
+ const flattened = container.productGroups.flatMap((entry) => {
2508
+ const group = objectValue(entry);
2509
+ return group && Array.isArray(group.products) ? group.products : [];
2510
+ });
2511
+ if (flattened.length === 0) return container;
2512
+ return {
2513
+ ...container,
2514
+ products: [...Array.isArray(container.products) ? container.products : [], ...flattened]
2515
+ };
2516
+ }
2359
2517
  function projectedHref(value) {
2360
2518
  if (typeof value !== "string") return void 0;
2361
2519
  const href = value.trim();
@@ -2443,12 +2601,23 @@ function mintlifyNavigationApiReferences(config) {
2443
2601
  const references = [];
2444
2602
  const containerKeys = ["tabs", "anchors", "products", "dropdowns", "versions", "menus", "languages"];
2445
2603
  const nestedKeys = ["groups", "pages"];
2446
- function visit(node, tabLabel) {
2447
- if (typeof node.openapi === "string" && node.openapi.trim()) {
2448
- references.push({ value: node.openapi.trim(), kind: "openapi", tabLabel });
2449
- }
2450
- if (typeof node.asyncapi === "string" && node.asyncapi.trim()) {
2451
- references.push({ value: node.asyncapi.trim(), kind: "asyncapi", tabLabel });
2604
+ function visit(rawNode, tabLabel) {
2605
+ const node = withFlattenedProductGroups(rawNode);
2606
+ for (const kind of ["openapi", "asyncapi"]) {
2607
+ const value = node[kind];
2608
+ if (typeof value === "string" && value.trim()) {
2609
+ references.push({ value: value.trim(), kind, tabLabel });
2610
+ continue;
2611
+ }
2612
+ const object = objectValue(value);
2613
+ if (object && typeof object.source === "string" && object.source.trim()) {
2614
+ references.push({
2615
+ value: object.source.trim(),
2616
+ kind,
2617
+ tabLabel,
2618
+ ...typeof object.directory === "string" && object.directory.trim() ? { directory: object.directory.trim() } : {}
2619
+ });
2620
+ }
2452
2621
  }
2453
2622
  for (const key of containerKeys) {
2454
2623
  const entries = node[key];
@@ -2509,8 +2678,19 @@ function registerReference(value, context) {
2509
2678
  }
2510
2679
  return navigationId;
2511
2680
  }
2681
+ var OPENAPI_OPERATION_REF = /^(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+\S/;
2512
2682
  function convertPage(value, context) {
2513
- if (typeof value === "string") return registerReference(value, context);
2683
+ if (typeof value === "string") {
2684
+ if (OPENAPI_OPERATION_REF.test(value)) {
2685
+ warnOnce(
2686
+ context,
2687
+ "openapi-operation-list",
2688
+ 'One or more navigation groups hand-pick or reorder individual OpenAPI operations (e.g. "GET /users"); Thally always renders every operation of a bound spec and has no manual-selection equivalent, so that list was dropped. The operations themselves are still available through the bound API reference.'
2689
+ );
2690
+ return null;
2691
+ }
2692
+ return registerReference(value, context);
2693
+ }
2514
2694
  const object = objectValue(value);
2515
2695
  if (!object) return null;
2516
2696
  if (typeof object.page === "string") return registerReference(object.page, context);
@@ -2553,8 +2733,9 @@ function convertNavigationValues(values, context) {
2553
2733
  });
2554
2734
  }
2555
2735
  function convertContainerToTabs(containerValue, context, fallbackTab, trace, depth = 0) {
2556
- const container = objectValue(containerValue);
2557
- if (!container) return [];
2736
+ const rawContainer = objectValue(containerValue);
2737
+ if (!rawContainer) return [];
2738
+ const container = withFlattenedProductGroups(rawContainer);
2558
2739
  const containerKeys = ["tabs", "anchors", "products", "dropdowns", "versions", "menus"];
2559
2740
  for (const key of containerKeys) {
2560
2741
  if (!Array.isArray(container[key])) continue;
@@ -2776,6 +2957,47 @@ function isRedirectPathSafe(rawSource, rawDestination) {
2776
2957
  }
2777
2958
  return true;
2778
2959
  }
2960
+ function mintlifyDefaultVersionPrefixes(config) {
2961
+ const navigation = objectValue(config.navigation) ?? config;
2962
+ const prefixes = /* @__PURE__ */ new Set();
2963
+ function collect(container) {
2964
+ if (Array.isArray(container.versions)) {
2965
+ const entries = container.versions.map((value) => objectValue(value)).filter((value) => value !== null);
2966
+ const defaultEntry = entries.find((entry) => entry.default === true) ?? entries[0];
2967
+ if (defaultEntry && typeof defaultEntry.version === "string" && defaultEntry.version.trim()) {
2968
+ prefixes.add(defaultEntry.version.trim());
2969
+ }
2970
+ }
2971
+ if (Array.isArray(container.languages)) {
2972
+ for (const value of container.languages) {
2973
+ const language = objectValue(value);
2974
+ if (language) collect(language);
2975
+ }
2976
+ }
2977
+ }
2978
+ collect(navigation);
2979
+ return prefixes;
2980
+ }
2981
+ function mintlifyAllVersionPrefixes(config) {
2982
+ const navigation = objectValue(config.navigation) ?? config;
2983
+ const prefixes = /* @__PURE__ */ new Set();
2984
+ function collect(container) {
2985
+ if (Array.isArray(container.versions)) {
2986
+ for (const value of container.versions) {
2987
+ const entry = objectValue(value);
2988
+ if (entry && typeof entry.version === "string" && entry.version.trim()) prefixes.add(entry.version.trim());
2989
+ }
2990
+ }
2991
+ if (Array.isArray(container.languages)) {
2992
+ for (const value of container.languages) {
2993
+ const language = objectValue(value);
2994
+ if (language) collect(language);
2995
+ }
2996
+ }
2997
+ }
2998
+ collect(navigation);
2999
+ return prefixes;
3000
+ }
2779
3001
  function projectMintlifyNavigation(config, options = {}) {
2780
3002
  const warnings = [];
2781
3003
  const references = [];
@@ -3021,7 +3243,7 @@ function pruneMissingNavigationPages(config, availableIds) {
3021
3243
  }
3022
3244
 
3023
3245
  // src/docusaurus.ts
3024
- var MAX_CONFIG_BYTES = 1e6;
3246
+ var MAX_CONFIG_BYTES = 2e7;
3025
3247
  var CATEGORY_FILENAMES = ["_category_.json", "_category_.yml", "_category_.yaml"];
3026
3248
  var SIDEBAR_FILENAMES = [
3027
3249
  "sidebars.json",
@@ -3063,19 +3285,19 @@ function storageIdForRoute(routeId) {
3063
3285
  function titleCase2(value) {
3064
3286
  return stripNumberPrefix(value).replace(/sidebar$/i, "").split(/[-_]/).filter(Boolean).map((word) => ["api", "cli", "sdk", "ui"].includes(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
3065
3287
  }
3066
- function routeFromSlug(slug, fallback, sourceDirectory) {
3067
- if (slug === "/") return "introduction";
3068
- if (slug.startsWith("/")) return docusaurusRouteId(slug);
3069
- const fallbackDirectory = sourceDirectory ?? (fallback === "introduction" ? "" : posix.dirname(fallback));
3070
- const resolved = posix.normalize(posix.join(fallbackDirectory, slug));
3288
+ function routeFromSlug(slug2, fallback, sourceDirectory) {
3289
+ if (slug2 === "/") return "introduction";
3290
+ if (slug2.startsWith("/")) return docusaurusRouteId(slug2);
3291
+ const fallbackDirectory = sourceDirectory ?? (fallback === "introduction" ? "" : posix2.dirname(fallback));
3292
+ const resolved = posix2.normalize(posix2.join(fallbackDirectory, slug2));
3071
3293
  if (resolved === ".." || resolved.startsWith("../")) return null;
3072
3294
  return docusaurusRouteId(resolved);
3073
3295
  }
3074
3296
  function resolveDocusaurusPageIdentity(sourcePath, frontmatter, fallback) {
3075
3297
  const sourceDocId = normalizeDocId(sourcePath);
3076
- const sourceDirectory = posix.dirname(sourceDocId);
3298
+ const sourceDirectory = posix2.dirname(sourceDocId);
3077
3299
  const configuredId = typeof frontmatter.id === "string" && frontmatter.id.trim() ? stripNumberPrefix(frontmatter.id.trim()) : null;
3078
- const docId = configuredId ? normalizeDocId(sourceDirectory === "." ? configuredId : posix.join(sourceDirectory, configuredId)) : sourceDocId;
3300
+ const docId = configuredId ? normalizeDocId(sourceDirectory === "." ? configuredId : posix2.join(sourceDirectory, configuredId)) : sourceDocId;
3079
3301
  const defaultNavigationId = defaultDocusaurusRouteId(docId) ?? fallback.navigationId;
3080
3302
  const configuredSlug = typeof frontmatter.slug === "string" ? frontmatter.slug.trim() : "";
3081
3303
  const navigationId = configuredSlug ? routeFromSlug(
@@ -3097,6 +3319,105 @@ function resolveDocusaurusPageIdentity(sourcePath, frontmatter, fallback) {
3097
3319
  function sourceReferenceKey(value) {
3098
3320
  return value.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\.(?:mdx?)$/i, "").replace(/\/$/, "");
3099
3321
  }
3322
+ function sourceHeadings(body) {
3323
+ const headings = [];
3324
+ const lines = body.split("\n");
3325
+ let fence = null;
3326
+ for (let index = 0; index < lines.length; index++) {
3327
+ const line = lines[index];
3328
+ const marker = line.match(/^ {0,3}(`{3,}|~{3,})/);
3329
+ if (marker) {
3330
+ if (!fence) fence = marker[1];
3331
+ else if (marker[1][0] === fence[0] && marker[1].length >= fence.length && /^\s*$/.test(line.slice(marker[0].length))) fence = null;
3332
+ continue;
3333
+ }
3334
+ if (fence) continue;
3335
+ const heading = line.match(/^ {0,3}(#{2,6})\s+(.+?)\s*#*\s*$/);
3336
+ if (!heading) continue;
3337
+ const preceding = lines[index - 1]?.match(/^<a id="([\p{L}\p{N}_.:$-]{1,120})"><\/a>$/u);
3338
+ const plainText = !/[<>{}\[\]`*_]/.test(heading[2]);
3339
+ const generatedId = plainText ? heading[2].normalize("NFC").toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, "-").replace(/(^-|-$)/g, "") : null;
3340
+ headings.push({ index, level: heading[1].length, id: preceding?.[1] ?? generatedId });
3341
+ }
3342
+ return headings;
3343
+ }
3344
+ function addDocusaurusTranslatedHeadingAliases(pages) {
3345
+ const byRoute = new Map(pages.map((page) => [page.navigationId, page]));
3346
+ for (const page of pages) {
3347
+ const locale = page.source.match(/(?:^|[/#])i18n\/([A-Za-z]{2,8}(?:-[A-Za-z0-9]{2,8})*)\/docusaurus-plugin-content-docs\//)?.[1];
3348
+ if (!locale) continue;
3349
+ const originalRoute = page.navigationId === locale ? "introduction" : page.navigationId.startsWith(`${locale}/`) ? page.navigationId.slice(locale.length + 1) : null;
3350
+ if (!originalRoute) continue;
3351
+ const original = byRoute.get(originalRoute);
3352
+ if (!original) continue;
3353
+ const originalHeadings = sourceHeadings(original.body);
3354
+ const translatedHeadings = sourceHeadings(page.body);
3355
+ if (!originalHeadings.length || originalHeadings.length !== translatedHeadings.length || originalHeadings.some((heading, index) => heading.level !== translatedHeadings[index].level)) continue;
3356
+ const occupied = new Set(translatedHeadings.map((heading) => heading.id).filter((id) => !!id));
3357
+ for (const match of page.body.matchAll(/<a\s+id="([^"]+)"/g)) occupied.add(match[1]);
3358
+ const originalIdCounts = /* @__PURE__ */ new Map();
3359
+ for (const heading of originalHeadings) {
3360
+ if (heading.id) originalIdCounts.set(heading.id, (originalIdCounts.get(heading.id) ?? 0) + 1);
3361
+ }
3362
+ const aliases = /* @__PURE__ */ new Map();
3363
+ for (let index = 0; index < originalHeadings.length; index++) {
3364
+ const id = originalHeadings[index].id;
3365
+ if (!id || occupied.has(id) || originalIdCounts.get(id) !== 1) continue;
3366
+ aliases.set(translatedHeadings[index].index, id);
3367
+ occupied.add(id);
3368
+ }
3369
+ if (aliases.size) page.body = page.body.split("\n").flatMap((line, index) => {
3370
+ const alias = aliases.get(index);
3371
+ return alias ? [`<a id="${alias}"></a>`, line] : [line];
3372
+ }).join("\n");
3373
+ }
3374
+ }
3375
+ function expandDocusaurusDocCardLists(pages, config, sourceRoutes) {
3376
+ const pageByRoute = new Map(pages.map((page) => [page.navigationId, page]));
3377
+ const containsRoute = (items, route) => items.some((item) => typeof item === "string" ? item === route : containsRoute(item.pages, route));
3378
+ const findGroup = (items, route) => {
3379
+ for (const item of items) {
3380
+ if (typeof item === "string" || !containsRoute(item.pages, route)) continue;
3381
+ return findGroup(item.pages, route) ?? item;
3382
+ }
3383
+ return void 0;
3384
+ };
3385
+ const firstRoute = (items) => {
3386
+ for (const item of items) {
3387
+ if (typeof item === "string") return item;
3388
+ const candidate = firstRoute(item.pages);
3389
+ if (candidate) return candidate;
3390
+ }
3391
+ return void 0;
3392
+ };
3393
+ for (const page of pages) {
3394
+ if (!sourceRoutes.has(page.navigationId)) continue;
3395
+ const group = config.tabs.map((tab) => findGroup([
3396
+ ...tab.pages ?? [],
3397
+ ...tab.groups ?? []
3398
+ ], page.navigationId)).find(Boolean);
3399
+ if (!group) {
3400
+ page.body = page.body.replace(/<div data-thally-doc-card-list=""\s*\/>/g, "");
3401
+ continue;
3402
+ }
3403
+ const cards = group.pages.flatMap((item) => {
3404
+ if (typeof item === "string") {
3405
+ if (item === page.navigationId) return [];
3406
+ const target = pageByRoute.get(item);
3407
+ return [{ title: target?.title ?? item.split("/").at(-1) ?? item, route: item }];
3408
+ }
3409
+ const slug2 = slugifySegment(item.group);
3410
+ const routes = [...item.pages].flatMap((child) => typeof child === "string" ? [child] : []);
3411
+ const route = routes.find((candidate) => candidate === slug2 || candidate.endsWith(`/${slug2}`)) ?? firstRoute(item.pages);
3412
+ return route ? [{ title: item.group, route }] : [];
3413
+ });
3414
+ const markup = cards.length ? `<CardGroup>
3415
+ ${cards.map(({ title, route }) => ` <Card title={${JSON.stringify(title)}} href={${JSON.stringify(`/${route === "introduction" ? "" : route}`)}} />`).join("\n")}
3416
+ </CardGroup>` : "";
3417
+ page.body = page.body.replace(/<div data-thally-doc-card-list=""\s*\/>/g, markup);
3418
+ if (markup && !page.body.includes(markup)) page.body = [page.body.trim(), markup].filter(Boolean).join("\n\n");
3419
+ }
3420
+ }
3100
3421
  function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
3101
3422
  const routes = /* @__PURE__ */ new Map();
3102
3423
  for (const descriptor of descriptors) {
@@ -3107,18 +3428,31 @@ function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
3107
3428
  const sourceRoute = pageIdFromReference(sourceKey);
3108
3429
  if (sourceRoute) routes.set(sourceRoute, descriptor.navigationId);
3109
3430
  }
3110
- const currentDirectory = posix.dirname(sourceReferenceKey(current.sourcePath));
3431
+ const currentDirectory = posix2.dirname(sourceReferenceKey(current.sourcePath));
3432
+ function copiedAssetTarget(target) {
3433
+ if (!target.startsWith("/") || target.startsWith("//") || target.includes("\\")) return null;
3434
+ const suffixIndex = target.search(/[?#]/);
3435
+ const path = suffixIndex >= 0 ? target.slice(0, suffixIndex) : target;
3436
+ const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
3437
+ if (path.startsWith("/docs/") && options.assetPaths?.has(path.slice("/docs/".length))) {
3438
+ return `/${path.slice("/docs/".length)}${suffix}`;
3439
+ }
3440
+ return options.assetPaths?.has(path.slice(1)) ? target : null;
3441
+ }
3111
3442
  function rewriteTarget(target) {
3112
- if (!target || target.startsWith("#") || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(target)) return target;
3443
+ if (!target || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(target)) return target;
3444
+ if (target.startsWith("#")) return target;
3113
3445
  const suffixIndex = target.search(/[?#]/);
3114
3446
  const path = suffixIndex >= 0 ? target.slice(0, suffixIndex) : target;
3115
3447
  const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
3448
+ const copiedAsset = copiedAssetTarget(target);
3449
+ if (copiedAsset) return copiedAsset;
3116
3450
  const candidates = path.startsWith("/") ? [
3117
3451
  sourceReferenceKey(path),
3118
3452
  sourceReferenceKey(path).replace(/^docs\//, ""),
3119
3453
  sourceReferenceKey(path).replace(/^docs\/(?:next|current|latest)\//, "")
3120
3454
  ] : [
3121
- sourceReferenceKey(posix.normalize(posix.join(currentDirectory, path))),
3455
+ sourceReferenceKey(posix2.normalize(posix2.join(currentDirectory, path))),
3122
3456
  sourceReferenceKey(path)
3123
3457
  ];
3124
3458
  const route = candidates.map((candidate) => routes.get(candidate)).find(Boolean);
@@ -3140,7 +3474,7 @@ function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
3140
3474
  return line;
3141
3475
  }
3142
3476
  if (codeFence) return line;
3143
- return line.replace(/(\]\()([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(target)}`).replace(/(\bhref=")([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(target)}${suffix}`).replace(/^(\s{0,3}\[[^\]]+\]:\s*)(<[^>]+>|\S+)/, (_match, prefix, destination) => {
3477
+ return line.replace(/(\]\()([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(target)}`).replace(/(\bhref=")([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(target)}${suffix}`).replace(/(\bsrc=")([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${copiedAssetTarget(target) ?? target}${suffix}`).replace(/^(\s{0,3}\[[^\]]+\]:\s*)(<[^>]+>|\S+)/, (_match, prefix, destination) => {
3144
3478
  const bracketed = destination.startsWith("<") && destination.endsWith(">");
3145
3479
  const target = bracketed ? destination.slice(1, -1) : destination;
3146
3480
  const rewritten = rewriteTarget(target);
@@ -3161,7 +3495,7 @@ function readDocusaurusSiteOrigin(repositoryRoot) {
3161
3495
  }
3162
3496
  function readBoundedText(path) {
3163
3497
  if (lstatSync3(path).size > MAX_CONFIG_BYTES) {
3164
- throw new Error("Docusaurus sidebar config exceeded the 1 MB static-parser limit.");
3498
+ throw new Error("The Docusaurus sidebar config is larger than 20 MB and could not be imported.");
3165
3499
  }
3166
3500
  return readFileSync3(path, "utf8");
3167
3501
  }
@@ -3418,7 +3752,7 @@ function readDocusaurusRedirects(repositoryRoot, warnings) {
3418
3752
  }
3419
3753
  function readCategoryMetadata(contentRoot, directory) {
3420
3754
  for (const filename of CATEGORY_FILENAMES) {
3421
- const path = resolveWithin(contentRoot, posix.join(directory, filename));
3755
+ const path = resolveWithin(contentRoot, posix2.join(directory, filename));
3422
3756
  if (!existsSync3(path) || !lstatSync3(path).isFile()) continue;
3423
3757
  try {
3424
3758
  const raw = readBoundedText(path);
@@ -3463,7 +3797,7 @@ function generatedIndexPage(label, linkValue, context) {
3463
3797
  if (link.type !== "generated-index") return null;
3464
3798
  const fallback = `category/${slugifySegment(label)}`;
3465
3799
  const unprefixedNavigationId = typeof link.slug === "string" ? routeFromSlug(link.slug, fallback) ?? fallback : fallback;
3466
- const navigationId = context.routePrefix && unprefixedNavigationId !== context.routePrefix && !unprefixedNavigationId.startsWith(`${context.routePrefix}/`) ? posix.join(context.routePrefix, unprefixedNavigationId) : unprefixedNavigationId;
3800
+ const navigationId = context.routePrefix && unprefixedNavigationId !== context.routePrefix && !unprefixedNavigationId.startsWith(`${context.routePrefix}/`) ? posix2.join(context.routePrefix, unprefixedNavigationId) : unprefixedNavigationId;
3467
3801
  if (!context.generatedIds.has(navigationId) && !context.descriptors.some((page) => page.navigationId === navigationId)) {
3468
3802
  const description = typeof link.description === "string" ? link.description : `Browse the ${label} documentation.`;
3469
3803
  context.generatedPages.push({
@@ -3601,7 +3935,7 @@ function staticStringField(source, name, bindings) {
3601
3935
  }
3602
3936
  }
3603
3937
  function readDocusaurusSiteSettings(repositoryRoot) {
3604
- const empty = { docsRouteBasePath: "docs", navbarLinks: [], footerLinks: [] };
3938
+ const empty = { docsRouteBasePath: "docs", defaultColorMode: "light", navbarLinks: [], footerLinks: [] };
3605
3939
  const configPath = findDocusaurusConfigPath(repositoryRoot);
3606
3940
  if (!configPath) return empty;
3607
3941
  const source = readBoundedText(configPath);
@@ -3611,6 +3945,10 @@ function readDocusaurusSiteSettings(repositoryRoot) {
3611
3945
  const docsRouteBasePath = classicDocs && staticStringField(classicDocs, "routeBasePath") || "docs";
3612
3946
  const themeStart = propertyStart(source, "themeConfig");
3613
3947
  const theme = themeStart < 0 ? null : matchingObjectLiteral(source, themeStart);
3948
+ const colorModeStart = theme ? propertyStart(theme, "colorMode") : -1;
3949
+ const colorMode = colorModeStart < 0 ? null : matchingObjectLiteral(theme, colorModeStart);
3950
+ const configuredColorMode = colorMode ? staticStringField(colorMode, "defaultMode") : void 0;
3951
+ const defaultColorMode = configuredColorMode === "dark" ? "dark" : "light";
3614
3952
  const navbarStart = theme ? propertyStart(theme, "navbar") : -1;
3615
3953
  const navbar = navbarStart < 0 ? null : matchingObjectLiteral(theme, navbarStart);
3616
3954
  const logoStart = navbar ? propertyStart(navbar, "logo") : -1;
@@ -3646,6 +3984,8 @@ function readDocusaurusSiteSettings(repositoryRoot) {
3646
3984
  return {
3647
3985
  name: staticStringField(source, "title"),
3648
3986
  description: staticStringField(source, "tagline"),
3987
+ ...navbar && staticStringField(navbar, "title") ? { navbarTitle: staticStringField(navbar, "title") } : {},
3988
+ defaultColorMode,
3649
3989
  docsRouteBasePath,
3650
3990
  ...logo && staticStringField(logo, "src") ? {
3651
3991
  logo: {
@@ -3670,32 +4010,47 @@ function autogeneratedItems(dirName, context) {
3670
4010
  const childDirectories = /* @__PURE__ */ new Set();
3671
4011
  for (const descriptor of inDirectory) {
3672
4012
  const source = descriptor.sourcePath.replace(/\.(?:mdx?)$/i, "");
3673
- const relativePath = normalizedDir ? posix.relative(normalizedDir, source) : source;
4013
+ const relativePath = normalizedDir ? posix2.relative(normalizedDir, source) : source;
3674
4014
  const segments = relativePath.split("/").filter(Boolean);
3675
4015
  if (segments.length <= 1) directPages.push(descriptor);
3676
4016
  else childDirectories.add(segments[0]);
3677
4017
  }
3678
- const items = directPages.sort(descriptorSort).map((descriptor) => {
3679
- context.referencedNavigationIds.add(descriptor.navigationId);
3680
- return descriptor.navigationId;
3681
- });
4018
+ const pages = directPages.map((descriptor) => ({
4019
+ // An index document is the category landing page in Docusaurus. Thally
4020
+ // renders it as a child link, so put it before the category's other docs.
4021
+ position: descriptor.sidebarPosition ?? (/(?:^|\/)(?:index|readme)\.mdx?$/i.test(descriptor.sourcePath) ? -1 : Number.MAX_SAFE_INTEGER),
4022
+ sortKey: descriptor.sourcePath,
4023
+ item: descriptor.navigationId
4024
+ }));
3682
4025
  const groups = [...childDirectories].map((segment) => {
3683
- const directory = normalizedDir ? posix.join(normalizedDir, segment) : segment;
4026
+ const directory = normalizedDir ? posix2.join(normalizedDir, segment) : segment;
3684
4027
  const metadata = readCategoryMetadata(context.contentRoot, directory);
3685
- const pages = autogeneratedItems(directory, context);
4028
+ const pages2 = autogeneratedItems(directory, context);
4029
+ if (pages2.length === 1 && typeof pages2[0] === "string" && !metadata.label && metadata.position === void 0 && !metadata.link) {
4030
+ const onlyPage = context.descriptors.find((descriptor) => descriptor.navigationId === pages2[0]);
4031
+ if (onlyPage && /(?:^|\/)(?:index|readme)\.mdx?$/i.test(onlyPage.sourcePath)) {
4032
+ return {
4033
+ position: onlyPage.sidebarPosition ?? Number.MAX_SAFE_INTEGER,
4034
+ sortKey: segment,
4035
+ item: pages2[0]
4036
+ };
4037
+ }
4038
+ }
3686
4039
  const landing = generatedIndexPage(metadata.label ?? titleCase2(segment), metadata.link, context);
3687
- if (landing && !pages.includes(landing)) pages.unshift(landing);
4040
+ if (landing && !pages2.includes(landing)) pages2.unshift(landing);
3688
4041
  return {
3689
4042
  position: metadata.position ?? Number.MAX_SAFE_INTEGER,
3690
- segment,
3691
- group: {
4043
+ sortKey: segment,
4044
+ item: {
3692
4045
  group: metadata.label ?? titleCase2(segment),
3693
- pages
4046
+ pages: pages2
3694
4047
  }
3695
4048
  };
3696
- }).sort((left, right) => left.position - right.position || left.segment.localeCompare(right.segment, void 0, { numeric: true }));
3697
- items.push(...groups.map((entry) => entry.group));
3698
- return items;
4049
+ });
4050
+ return [...pages, ...groups].sort((left, right) => left.position - right.position || left.sortKey.localeCompare(right.sortKey, void 0, { numeric: true })).map(({ item }) => {
4051
+ if (typeof item === "string") context.referencedNavigationIds.add(item);
4052
+ return item;
4053
+ });
3699
4054
  }
3700
4055
  function convertItems(value, context) {
3701
4056
  if (typeof value === "string") {
@@ -3772,7 +4127,7 @@ import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as rea
3772
4127
  import { basename as basename2, dirname as dirname3, extname as extname4, relative as relative4 } from "path";
3773
4128
  import { load } from "cheerio";
3774
4129
  import { parse as parseYaml3 } from "yaml";
3775
- var MAX_CONFIG_BYTES2 = 2e6;
4130
+ var MAX_CONFIG_BYTES2 = 2e7;
3776
4131
  function announcementMarkdown(message) {
3777
4132
  const document = load(message);
3778
4133
  document("script, style, iframe, object, embed, svg, math").remove();
@@ -3807,7 +4162,7 @@ function titleCase3(value) {
3807
4162
  }
3808
4163
  function readBoundedYaml(path) {
3809
4164
  if (lstatSync4(path).size > MAX_CONFIG_BYTES2) {
3810
- throw new Error("Fern config exceeded the 2 MB static-parser limit.");
4165
+ throw new Error("The Fern config is larger than 20 MB and could not be imported.");
3811
4166
  }
3812
4167
  return parseYaml3(readFileSync4(path, "utf8"));
3813
4168
  }
@@ -3869,7 +4224,7 @@ function registerPageAt(rawPath, base, context, navTitle, hidden = false) {
3869
4224
  let sourcePath;
3870
4225
  try {
3871
4226
  const absolute = resolveWithinRoot(
3872
- context.pathPrefix ? resolveWithin(context.fernRoot, context.pathPrefix) : context.fernRoot,
4227
+ context.pathPrefix ? resolveWithinRoot(context.fernRoot, context.pathPrefix, context.repositoryRoot) : context.fernRoot,
3873
4228
  rawPath.trim(),
3874
4229
  context.repositoryRoot
3875
4230
  );
@@ -3898,7 +4253,7 @@ function registerFolder(object, parentSegments, context) {
3898
4253
  let folder;
3899
4254
  try {
3900
4255
  folder = resolveWithinRoot(
3901
- context.pathPrefix ? resolveWithin(context.fernRoot, context.pathPrefix) : context.fernRoot,
4256
+ context.pathPrefix ? resolveWithinRoot(context.fernRoot, context.pathPrefix, context.repositoryRoot) : context.fernRoot,
3902
4257
  rawPath,
3903
4258
  context.repositoryRoot
3904
4259
  );
@@ -3984,18 +4339,31 @@ function convertNode(node, parentSegments, context) {
3984
4339
  };
3985
4340
  }
3986
4341
  if (typeof object.link === "string") {
3987
- warnOnce2(
3988
- context,
3989
- "fern-link",
3990
- 'Fern navigation "link" entries are external and cannot be nested inside a Thally group; add them to the navbar manually.'
3991
- );
4342
+ const label = object.link.trim();
4343
+ const rawHref = typeof object.href === "string" ? object.href.trim() : "";
4344
+ let href;
4345
+ if (/^https?:\/\//i.test(rawHref) && !/[\\\u0000-\u0020\u007f]/.test(rawHref)) {
4346
+ try {
4347
+ const url = new URL(rawHref);
4348
+ if (["http:", "https:"].includes(url.protocol) && !url.username && !url.password) href = url.toString();
4349
+ } catch {
4350
+ }
4351
+ }
4352
+ if (label && href && object.hidden !== true && !context.hiddenInherited) {
4353
+ if (!context.externalLinks.some((link) => link.label === label && link.href === href)) {
4354
+ context.externalLinks.push({ label, href });
4355
+ }
4356
+ } else if (!href && object.hidden !== true && !context.hiddenInherited) {
4357
+ warnOnce2(context, `fern-link-${rawHref}`, `Fern navigation link "${label}" has an unsafe or invalid external URL and was skipped.`);
4358
+ }
3992
4359
  return null;
3993
4360
  }
3994
4361
  if (typeof object.api === "string") {
4362
+ const ownSegment = segmentFor(object, object.api, context);
3995
4363
  context.apiSections.push({
3996
4364
  name: typeof object["api-name"] === "string" ? object["api-name"] : object.api,
3997
4365
  nameExplicit: typeof object["api-name"] === "string",
3998
- routeSegments: parentSegments
4366
+ routeSegments: ownSegment ? [...parentSegments, ownSegment] : parentSegments
3999
4367
  });
4000
4368
  return null;
4001
4369
  }
@@ -4037,7 +4405,7 @@ function resolveVersionedNavigation(config, configDir, fernRoot, context) {
4037
4405
  }
4038
4406
  if (!chosen || typeof chosen.path !== "string") return config;
4039
4407
  try {
4040
- const versionPath = resolveWithinRoot(configDir, chosen.path, fernRoot);
4408
+ const versionPath = resolveWithinRoot(configDir, chosen.path, context.repositoryRoot);
4041
4409
  if (!existsSync4(versionPath) || !lstatSync4(versionPath).isFile()) {
4042
4410
  warnOnce2(context, "fern-version-not-file", `Fern version file "${chosen.path}" is not a regular file and was skipped.`);
4043
4411
  return config;
@@ -4189,6 +4557,7 @@ function projectFernNavigation(input) {
4189
4557
  changelogIndexes: [],
4190
4558
  warnings: [],
4191
4559
  warningKeys: /* @__PURE__ */ new Set(),
4560
+ externalLinks: [],
4192
4561
  bareRouteRedirects: [],
4193
4562
  segmentAliases: /* @__PURE__ */ new Map(),
4194
4563
  pathPrefix: "",
@@ -4209,6 +4578,11 @@ function projectFernNavigation(input) {
4209
4578
  const label = typeof link?.text === "string" ? link.text : typeof link?.label === "string" ? link.label : link?.type === "github" ? "GitHub" : void 0;
4210
4579
  return href && label ? [{ label, href, ...link?.type === "github" ? { type: "github" } : {} }] : [];
4211
4580
  }) : [];
4581
+ for (const link of context.externalLinks) {
4582
+ if (!navbarLinks.some((existing) => existing.label === link.label && existing.href === link.href)) {
4583
+ navbarLinks.push(link);
4584
+ }
4585
+ }
4212
4586
  const announcement = objectValue3(config.announcement);
4213
4587
  const announcementMessage = typeof announcement?.message === "string" ? announcement.message.trim() : "";
4214
4588
  const bannerContent = announcementMarkdown(announcementMessage);
@@ -4269,7 +4643,16 @@ function projectFernNavigation(input) {
4269
4643
  return section;
4270
4644
  }
4271
4645
  const distinguishingSegment = section.routeSegments.at(-1);
4272
- const tabLabel = distinguishingSegment ? `${section.tabLabel}: ${titleCase3(distinguishingSegment)}` : section.tabLabel;
4646
+ let tabLabel = distinguishingSegment ? `${section.tabLabel}: ${titleCase3(distinguishingSegment)}` : section.tabLabel;
4647
+ if (!tabLabel || tabLabel === `${section.tabLabel}: ${section.tabLabel}` || claimedTabLabels.has(tabLabel)) {
4648
+ let ordinal = 2;
4649
+ let candidate = `${section.tabLabel} (${ordinal})`;
4650
+ while (claimedTabLabels.has(candidate)) {
4651
+ ordinal += 1;
4652
+ candidate = `${section.tabLabel} (${ordinal})`;
4653
+ }
4654
+ tabLabel = candidate;
4655
+ }
4273
4656
  claimedTabLabels.add(tabLabel);
4274
4657
  return { ...section, tabLabel };
4275
4658
  });
@@ -4319,11 +4702,14 @@ var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
4319
4702
  ".ico",
4320
4703
  ".jpeg",
4321
4704
  ".jpg",
4705
+ ".m4a",
4322
4706
  ".mp3",
4323
4707
  ".mp4",
4708
+ ".ogg",
4324
4709
  ".pdf",
4325
4710
  ".png",
4326
4711
  ".svg",
4712
+ ".wav",
4327
4713
  ".webm",
4328
4714
  ".webp"
4329
4715
  ]);
@@ -4511,9 +4897,92 @@ function readDocusaurusConfigSource(projectRoot) {
4511
4897
  if (!path || lstatSync5(path).size > 1e6) return "";
4512
4898
  return readFileSync5(path, "utf8");
4513
4899
  }
4900
+ function matchingBraceIndex(source, openBraceIndex) {
4901
+ let depth = 0;
4902
+ for (let i = openBraceIndex; i < source.length; i++) {
4903
+ const ch = source[i];
4904
+ if (ch === '"' || ch === "'" || ch === "`") {
4905
+ const quote = ch;
4906
+ i++;
4907
+ while (i < source.length && source[i] !== quote) {
4908
+ if (source[i] === "\\") i++;
4909
+ i++;
4910
+ }
4911
+ continue;
4912
+ }
4913
+ if (ch === "/" && source[i + 1] === "/") {
4914
+ const newline = source.indexOf("\n", i);
4915
+ i = newline === -1 ? source.length : newline;
4916
+ continue;
4917
+ }
4918
+ if (ch === "/" && source[i + 1] === "*") {
4919
+ const end = source.indexOf("*/", i + 2);
4920
+ i = end === -1 ? source.length : end + 1;
4921
+ continue;
4922
+ }
4923
+ if (ch === "{") depth++;
4924
+ else if (ch === "}") {
4925
+ depth--;
4926
+ if (depth === 0) return i;
4927
+ }
4928
+ }
4929
+ return -1;
4930
+ }
4931
+ function topLevelStringProperty(objectBody, key) {
4932
+ let depth = 0;
4933
+ const keyPattern = new RegExp(`^${key}\\s*:\\s*(['"])`);
4934
+ for (let i = 0; i < objectBody.length; i++) {
4935
+ const ch = objectBody[i];
4936
+ if (ch === '"' || ch === "'" || ch === "`") {
4937
+ const quote = ch;
4938
+ i++;
4939
+ while (i < objectBody.length && objectBody[i] !== quote) {
4940
+ if (objectBody[i] === "\\") i++;
4941
+ i++;
4942
+ }
4943
+ continue;
4944
+ }
4945
+ if (ch === "/" && objectBody[i + 1] === "/") {
4946
+ const newline = objectBody.indexOf("\n", i);
4947
+ i = newline === -1 ? objectBody.length : newline;
4948
+ continue;
4949
+ }
4950
+ if (ch === "/" && objectBody[i + 1] === "*") {
4951
+ const end = objectBody.indexOf("*/", i + 2);
4952
+ i = end === -1 ? objectBody.length : end + 1;
4953
+ continue;
4954
+ }
4955
+ if (ch === "{" || ch === "[" || ch === "(") {
4956
+ depth++;
4957
+ continue;
4958
+ }
4959
+ if (ch === "}" || ch === "]" || ch === ")") {
4960
+ depth--;
4961
+ continue;
4962
+ }
4963
+ if (depth === 0) {
4964
+ const match = keyPattern.exec(objectBody.slice(i));
4965
+ if (match) {
4966
+ const quote = match[1];
4967
+ const valueStart = i + match[0].length;
4968
+ const valueEnd = objectBody.indexOf(quote, valueStart);
4969
+ if (valueEnd !== -1) return objectBody.slice(valueStart, valueEnd);
4970
+ }
4971
+ }
4972
+ }
4973
+ return void 0;
4974
+ }
4514
4975
  function primaryDocusaurusDocsDirectory(projectRoot) {
4515
4976
  const source = readDocusaurusConfigSource(projectRoot);
4516
- const configured = source.match(/\bdocs\s*:\s*\{[\s\S]{0,4000}?\bpath\s*:\s*(['"])([^'"]+)\1/)?.[2];
4977
+ const docsKeyMatch = source.match(/\bdocs\s*:\s*\{/);
4978
+ let configured;
4979
+ if (docsKeyMatch?.index !== void 0) {
4980
+ const openBrace = docsKeyMatch.index + docsKeyMatch[0].length - 1;
4981
+ const closeBrace = matchingBraceIndex(source, openBrace);
4982
+ if (closeBrace !== -1) {
4983
+ configured = topLevelStringProperty(source.slice(openBrace + 1, closeBrace), "path");
4984
+ }
4985
+ }
4517
4986
  return trimTrailingSlashes(configured?.replace(/^\.\//, "") ?? "") || "docs";
4518
4987
  }
4519
4988
  function docusaurusArchiveRoots(projectRoot, repositoryDir, warnings) {
@@ -4565,7 +5034,7 @@ function docusaurusArchiveRoots(projectRoot, repositoryDir, warnings) {
4565
5034
  function additionalDocusaurusPluginRoots(repositoryDir, projectRoot, warnings) {
4566
5035
  const source = readDocusaurusConfigSource(projectRoot);
4567
5036
  const plugins = [];
4568
- const matcher = /['"]@docusaurus\/plugin-content-docs['"][\s\S]{0,3000}?\bpath\s*:\s*(['"])([^'"]+)\1[\s\S]{0,1000}?\brouteBasePath\s*:\s*(['"])([^'"]+)\3/g;
5037
+ const matcher = /['"](?:@docusaurus\/plugin-)?content-docs['"][\s\S]{0,3000}?\bpath\s*:\s*(['"])([^'"]+)\1[\s\S]{0,1000}?\brouteBasePath\s*:\s*(['"])([^'"]+)\3/g;
4569
5038
  for (const match of source.matchAll(matcher)) {
4570
5039
  const localPath = trimTrailingSlashes(match[2].replace(/^\.\//, ""));
4571
5040
  const routePrefix = trimEdgeSlashes(match[4]);
@@ -4803,8 +5272,10 @@ function detectRepositoryDocsDir(repositoryDir) {
4803
5272
  }
4804
5273
  return "";
4805
5274
  }
4806
- function scanFiles(root, confinementRoot = root, warnings) {
5275
+ var MAX_RANKED_WALK_FILES = MAX_SOURCE_FILES * 20;
5276
+ function scanFiles(root, confinementRoot = root, warnings, rank) {
4807
5277
  const files = [];
5278
+ const walkCap = rank ? MAX_RANKED_WALK_FILES : MAX_SOURCE_FILES;
4808
5279
  let confinementReal;
4809
5280
  try {
4810
5281
  confinementReal = realpathSync2(confinementRoot);
@@ -4813,7 +5284,7 @@ function scanFiles(root, confinementRoot = root, warnings) {
4813
5284
  }
4814
5285
  const visitedRealPaths = /* @__PURE__ */ new Set();
4815
5286
  function visit(directory, logicalDirectory = directory) {
4816
- if (files.length >= MAX_SOURCE_FILES) return;
5287
+ if (files.length >= walkCap) return;
4817
5288
  let directoryReal;
4818
5289
  try {
4819
5290
  directoryReal = realpathSync2(directory);
@@ -4829,7 +5300,7 @@ function scanFiles(root, confinementRoot = root, warnings) {
4829
5300
  return;
4830
5301
  }
4831
5302
  for (const entry of entries) {
4832
- if (files.length >= MAX_SOURCE_FILES) return;
5303
+ if (files.length >= walkCap) return;
4833
5304
  if (isIgnoredContentDirectory(entry.name)) {
4834
5305
  if (warnings && entry.isDirectory() && !entry.isSymbolicLink() && containsMarkdown(resolveWithin(directory, entry.name))) {
4835
5306
  warnings.push({
@@ -4866,8 +5337,44 @@ function scanFiles(root, confinementRoot = root, warnings) {
4866
5337
  }
4867
5338
  }
4868
5339
  visit(root);
5340
+ if (rank && files.length >= walkCap) {
5341
+ warnings?.push({
5342
+ code: "limit-reached",
5343
+ message: `Stopped scanning after ${walkCap} files, so the rest of the repository was not looked at. Run the migration on a smaller part of the repository with --docs-dir.`
5344
+ });
5345
+ }
4869
5346
  return files;
4870
5347
  }
5348
+ function selectGroupWithinBudget(scanned, rank, warnings, allVersionPrefixes, label) {
5349
+ if (scanned.length <= MAX_SOURCE_FILES) return scanned;
5350
+ const ranked = scanned.map((file) => ({ file, priority: rank(file.relativePath) })).sort((left, right) => left.priority - right.priority || (left.file.relativePath < right.file.relativePath ? -1 : left.file.relativePath > right.file.relativePath ? 1 : 0));
5351
+ const dropped = ranked.slice(MAX_SOURCE_FILES).map(({ file }) => file);
5352
+ if (warnings) {
5353
+ const droppedPages = label === "file" ? dropped.filter((file) => isDocumentationExtension(file.relativePath)) : dropped;
5354
+ const noun = label === "file" ? "page" : "image or media file";
5355
+ const droppedVersions = /* @__PURE__ */ new Set();
5356
+ for (const file of dropped) {
5357
+ const firstSegment = file.relativePath.split("/", 1)[0];
5358
+ if (allVersionPrefixes.has(firstSegment)) droppedVersions.add(firstSegment);
5359
+ }
5360
+ const examples = droppedPages.slice(0, 3).map((file) => file.relativePath);
5361
+ const rest = droppedPages.length - examples.length;
5362
+ warnings.push({
5363
+ code: "limit-reached",
5364
+ message: `This repository has more than ${MAX_SOURCE_FILES} ${label === "file" ? "files" : "assets"}, so only the first ${MAX_SOURCE_FILES} (in navigation order, default version first) were migrated. ${droppedPages.length} ${noun}(s) were left out` + (examples.length > 0 ? `: ${examples.join(", ")}${rest > 0 ? `, and ${rest} more` : ""}` : "") + (droppedVersions.size > 0 ? ` (versions: ${[...droppedVersions].slice(0, 5).join(", ")}${droppedVersions.size > 5 ? `, and ${droppedVersions.size - 5} more` : ""})` : "") + (label === "file" ? ". To include them, run the migration on a smaller part of the repository with --docs-dir." : ". Copy them into public/ manually if your pages use them.")
5365
+ });
5366
+ }
5367
+ return ranked.slice(0, MAX_SOURCE_FILES).map(({ file }) => file);
5368
+ }
5369
+ function selectFilesWithinBudget(scanned, rank, warnings, allVersionPrefixes) {
5370
+ const isAsset = (file) => ASSET_EXTENSIONS.has(extname5(file.relativePath).toLowerCase());
5371
+ const assetFiles = scanned.filter(isAsset);
5372
+ const otherFiles = scanned.filter((file) => !isAsset(file));
5373
+ return [
5374
+ ...selectGroupWithinBudget(otherFiles, rank, warnings, allVersionPrefixes, "file"),
5375
+ ...selectGroupWithinBudget(assetFiles, rank, warnings, allVersionPrefixes, "asset")
5376
+ ];
5377
+ }
4871
5378
  function readMintignoreMatcher(mintlifyRoot) {
4872
5379
  const path = resolveWithin(mintlifyRoot, MINTIGNORE_FILENAME);
4873
5380
  if (!existsSync5(path) || !lstatSync5(path).isFile()) return null;
@@ -4942,7 +5449,7 @@ function mintlifyTopLevelApiReferences(config) {
4942
5449
  }
4943
5450
  return references;
4944
5451
  }
4945
- function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings) {
5452
+ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
4946
5453
  if (!mintlifyConfig) return [];
4947
5454
  const references = [
4948
5455
  ...mintlifyTopLevelApiReferences(mintlifyConfig),
@@ -4962,13 +5469,19 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings) {
4962
5469
  });
4963
5470
  continue;
4964
5471
  }
4965
- if (/^https?:\/\//i.test(reference.value)) {
5472
+ if (/^https:\/\//i.test(reference.value)) {
5473
+ remoteSpecs.push({ url: reference.value, ...reference.tabLabel ? { tabLabel: reference.tabLabel } : {} });
4966
5474
  warnings.push({
4967
5475
  code: "unsupported-config",
4968
- message: `The remote OpenAPI spec "${reference.value}"${tabSuffix} was not downloaded. Download it manually, add it to public/, and set its API source in docs.json to the local path.`
5476
+ message: `The remote OpenAPI spec "${reference.value}"${tabSuffix} requires a network download before this import is complete.`,
5477
+ source: reference.value
4969
5478
  });
4970
5479
  continue;
4971
5480
  }
5481
+ if (/^http:\/\//i.test(reference.value)) {
5482
+ warnings.push({ code: "unsupported-config", message: `The OpenAPI spec "${reference.value}"${tabSuffix} uses insecure HTTP and was not imported.`, source: reference.value });
5483
+ continue;
5484
+ }
4972
5485
  const key = reference.value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/");
4973
5486
  const match = files.find((file) => file.relativePath === key);
4974
5487
  if (!match) {
@@ -4978,10 +5491,159 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings) {
4978
5491
  });
4979
5492
  continue;
4980
5493
  }
4981
- specs.push({ filename: basename3(match.relativePath), content: readFileSync5(match.absolutePath), tabLabel: reference.tabLabel });
5494
+ if (reference.directory) {
5495
+ warnings.push({
5496
+ code: "unsupported-config",
5497
+ 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. Update any links to "${reference.directory}/..." pages manually.`
5498
+ });
5499
+ }
5500
+ specs.push({
5501
+ filename: basename3(match.relativePath),
5502
+ content: readFileSync5(match.absolutePath),
5503
+ tabLabel: reference.tabLabel,
5504
+ sourcePath: match.relativePath,
5505
+ directory: reference.directory
5506
+ });
5507
+ }
5508
+ const filenameOwners = /* @__PURE__ */ new Map();
5509
+ for (const spec of specs) {
5510
+ const groupKey = spec.tabLabel ?? "\0default";
5511
+ const owner = filenameOwners.get(spec.filename);
5512
+ if (owner !== void 0 && owner !== groupKey) {
5513
+ let disambiguated = spec.sourcePath.replace(/\//g, "-");
5514
+ while (filenameOwners.has(disambiguated) && filenameOwners.get(disambiguated) !== groupKey) {
5515
+ disambiguated = `${groupKey.replace(/\W+/g, "-")}-${disambiguated}`;
5516
+ }
5517
+ spec.filename = disambiguated;
5518
+ }
5519
+ filenameOwners.set(spec.filename, groupKey);
4982
5520
  }
4983
5521
  return specs;
4984
5522
  }
5523
+ var OPENAPI_HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
5524
+ var FERN_CRUD_METHOD_NAMES = {
5525
+ findall: "list",
5526
+ findone: "get",
5527
+ remove: "delete"
5528
+ };
5529
+ function mintlifyOperationSlugSegment(value) {
5530
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
5531
+ }
5532
+ function thallyOperationSlugSegments(path, method) {
5533
+ const cleaned = path.split("/").filter(Boolean).map((segment) => segment.replace(/[{}]/g, "").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase());
5534
+ if (!cleaned.length) cleaned.push("root");
5535
+ cleaned.push(method.toLowerCase());
5536
+ return cleaned;
5537
+ }
5538
+ function slugifyApiSpecId(value) {
5539
+ return value.toLowerCase().replace(/[^a-z0-9/]+/g, "-").replace(/(^-|-$)+/g, "").replace(/\//g, "-") || value.toLowerCase();
5540
+ }
5541
+ function parseOpenApiOperationsForLinking(content) {
5542
+ let document;
5543
+ try {
5544
+ document = parseYaml4(content.toString("utf8"));
5545
+ } catch {
5546
+ return [];
5547
+ }
5548
+ const paths = document && typeof document === "object" ? document.paths : null;
5549
+ if (!paths || typeof paths !== "object") return [];
5550
+ const operations = [];
5551
+ for (const [path, pathItem] of Object.entries(paths)) {
5552
+ if (!pathItem || typeof pathItem !== "object") continue;
5553
+ for (const method of OPENAPI_HTTP_METHODS) {
5554
+ const operation = pathItem[method];
5555
+ if (!operation || typeof operation !== "object") continue;
5556
+ const op = operation;
5557
+ const tags = Array.isArray(op.tags) ? op.tags.filter((tag) => typeof tag === "string") : [];
5558
+ operations.push({
5559
+ method,
5560
+ path,
5561
+ tag: tags[0],
5562
+ summary: typeof op.summary === "string" ? op.summary : void 0,
5563
+ operationId: typeof op.operationId === "string" ? op.operationId : void 0
5564
+ });
5565
+ }
5566
+ }
5567
+ return operations;
5568
+ }
5569
+ function apiOperationLinkMap(sources, docsConfig) {
5570
+ const map = /* @__PURE__ */ new Map();
5571
+ const prefixLandings = /* @__PURE__ */ new Map();
5572
+ const apiTabs = docsConfig.tabs.filter((tab) => !tab.hidden && tab.api);
5573
+ apiTabs.forEach((tab, index) => {
5574
+ const source = sources.find((entry) => tab.api?.source === `/${entry.filename}`);
5575
+ if (!source || !source.prefix) return;
5576
+ const specId = index === 0 ? "default" : slugifyApiSpecId(tab.tab);
5577
+ const prefix = source.prefix.replace(/^\/+|\/+$/g, "").toLowerCase();
5578
+ if (prefix) prefixLandings.set(prefix, specId === "default" ? "/api" : `/api/${specId}`);
5579
+ for (const operation of parseOpenApiOperationsForLinking(source.content)) {
5580
+ const thallyHref = `/api/${specId}/${thallyOperationSlugSegments(operation.path, operation.method).join("/")}`;
5581
+ const leafCandidates = /* @__PURE__ */ new Set();
5582
+ if (operation.operationId) {
5583
+ const parts = operation.operationId.split(/[_.]/).filter(Boolean);
5584
+ const lastPart = parts.at(-1) ?? operation.operationId;
5585
+ const conventional = FERN_CRUD_METHOD_NAMES[lastPart.toLowerCase()];
5586
+ if (conventional) leafCandidates.add(conventional);
5587
+ leafCandidates.add(mintlifyOperationSlugSegment(lastPart));
5588
+ }
5589
+ if (operation.summary) leafCandidates.add(mintlifyOperationSlugSegment(operation.summary));
5590
+ if (operation.operationId) leafCandidates.add(mintlifyOperationSlugSegment(operation.operationId));
5591
+ leafCandidates.add(mintlifyOperationSlugSegment(`${operation.method} ${operation.path}`));
5592
+ const tagSegment = mintlifyOperationSlugSegment(operation.tag ?? "default");
5593
+ for (const leaf of leafCandidates) {
5594
+ if (!leaf) continue;
5595
+ const tagged = `${source.prefix}/${tagSegment}/${leaf}`.replace(/^\/+|\/+$/g, "").toLowerCase();
5596
+ const flat = `${source.prefix}/${leaf}`.replace(/^\/+|\/+$/g, "").toLowerCase();
5597
+ if (!map.has(tagged)) map.set(tagged, thallyHref);
5598
+ if (!map.has(flat)) map.set(flat, thallyHref);
5599
+ }
5600
+ }
5601
+ });
5602
+ return { operationLinks: map, prefixLandings };
5603
+ }
5604
+ function rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings) {
5605
+ if (operationLinks.size === 0 && prefixLandings.size === 0) return;
5606
+ const unmatched = /* @__PURE__ */ new Set();
5607
+ for (const page of pages) {
5608
+ page.body = rewriteApiOperationLinks(page.body, operationLinks, prefixLandings, unmatched);
5609
+ }
5610
+ if (unmatched.size === 0) return;
5611
+ const sorted = [...unmatched].sort();
5612
+ warnings.push({
5613
+ code: "unsupported-config",
5614
+ message: `${sorted.length} link(s) to API endpoints matched no endpoint in the OpenAPI spec, so they now point to the API section's overview page instead: ${sorted.slice(0, 10).join(", ")}${sorted.length > 10 ? `, and ${sorted.length - 10} more` : ""}. Update those links if you want them to reach a specific endpoint.`
5615
+ });
5616
+ }
5617
+ function rewriteApiOperationLinks(body, linkMap, prefixLandings, unmatched) {
5618
+ if (linkMap.size === 0 && prefixLandings.size === 0) return body;
5619
+ const orderedPrefixes = [...prefixLandings.entries()].sort((a, b) => b[0].length - a[0].length);
5620
+ let codeFence = null;
5621
+ return body.split("\n").map((line) => {
5622
+ const fence = line.match(/^\s*(`{3,}|~{3,})/);
5623
+ if (fence) {
5624
+ if (!codeFence) codeFence = fence[1][0];
5625
+ else if (fence[1][0] === codeFence) codeFence = null;
5626
+ return line;
5627
+ }
5628
+ if (codeFence) return line;
5629
+ const rewriteTarget = (target) => {
5630
+ const suffixIndex = target.search(/[?#]/);
5631
+ const path = (suffixIndex >= 0 ? target.slice(0, suffixIndex) : target).replace(/^\/+/, "").toLowerCase();
5632
+ const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
5633
+ const mapped = linkMap.get(path);
5634
+ if (mapped) return `${mapped}${suffix}`;
5635
+ for (const [prefix, landing] of orderedPrefixes) {
5636
+ if (path === prefix) return `${landing}${suffix}`;
5637
+ if (path.startsWith(`${prefix}/`)) {
5638
+ unmatched.add(target);
5639
+ return `${landing}${suffix}`;
5640
+ }
5641
+ }
5642
+ return target;
5643
+ };
5644
+ return line.replace(/(\]\()\/([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(`/${target}`)}`).replace(/(\bhref=")\/([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(`/${target}`)}${suffix}`);
5645
+ }).join("\n");
5646
+ }
4985
5647
  var MAX_FERN_GENERATORS_BYTES = 2e6;
4986
5648
  function fernGeneratorsOpenApiPaths(config) {
4987
5649
  const api = config.api;
@@ -5364,9 +6026,9 @@ function addDatedHeadingAliases(body, date) {
5364
6026
  if (open) return [line];
5365
6027
  const heading = line.match(/^#{2,6}\s+(.+?)\s*#*\s*$/);
5366
6028
  if (!heading) return [line];
5367
- const slug = heading[1].replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_]/g, "").normalize("NFKD").toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/\s+/g, "-");
5368
- const id = `${date}-${slug}`;
5369
- if (!slug || existingIds.has(id)) return [line];
6029
+ const slug2 = heading[1].replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_]/g, "").normalize("NFKD").toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/\s+/g, "-");
6030
+ const id = `${date}-${slug2}`;
6031
+ if (!slug2 || existingIds.has(id)) return [line];
5370
6032
  existingIds.add(id);
5371
6033
  return [`<a id="${id}" />`, "", line];
5372
6034
  }).join("\n");
@@ -5598,17 +6260,27 @@ ${result.slice(prefix.length)}`;
5598
6260
  }
5599
6261
  return depth === 0 ? hoistMdxImports(result) : result;
5600
6262
  }
5601
- function injectOpenApiSpecs(config, specs) {
6263
+ function injectOpenApiSpecs(config, specs, warnings) {
5602
6264
  let tabs = config.tabs.map((tab) => ({ ...tab }));
5603
6265
  for (const spec of specs) {
5604
6266
  const apiTab = spec.tabLabel ? tabs.find((tab) => tab.tab === spec.tabLabel) : tabs.find((tab) => tab.tab.toLowerCase().includes("api"));
5605
- if (apiTab) apiTab.api = {
5606
- source: `/${spec.filename}`,
5607
- // An API-only tab needs generated endpoint navigation; an authored
5608
- // page tab keeps its own groups alongside the bound spec.
5609
- ...apiTab.groups?.length || apiTab.pages?.length ? { navigation: false } : {}
5610
- };
5611
- else tabs = [...tabs, { tab: spec.tabLabel ?? "API Reference", api: { source: `/${spec.filename}` } }];
6267
+ if (apiTab) {
6268
+ if (apiTab.api && warnings) {
6269
+ warnings.push({
6270
+ code: "unsupported-config",
6271
+ message: `Tab "${apiTab.tab}" already uses another OpenAPI spec, and Thally supports one API spec per tab, so "/${spec.filename}" was not added. Put it in its own tab to include it.`
6272
+ });
6273
+ continue;
6274
+ }
6275
+ apiTab.api = {
6276
+ source: `/${spec.filename}`,
6277
+ // An API-only tab needs generated endpoint navigation; an authored
6278
+ // page tab keeps its own groups alongside the bound spec.
6279
+ ...apiTab.groups?.length || apiTab.pages?.length ? { navigation: false } : {}
6280
+ };
6281
+ } else {
6282
+ tabs = [...tabs, { tab: spec.tabLabel ?? "API Reference", api: { source: `/${spec.filename}` } }];
6283
+ }
5612
6284
  }
5613
6285
  return { ...config, tabs };
5614
6286
  }
@@ -5648,6 +6320,8 @@ function migrateRepository(options) {
5648
6320
  const componentRoot = mintlifyProjectRoot ?? docusaurusProjectRoot ?? fernProjectRoot ?? repositoryDir;
5649
6321
  const componentMigrator = platform === "mintlify" || platform === "docusaurus" || platform === "fern" ? createComponentMigrator(componentRoot, repositoryDir, warnings, componentSourceIdentity(options.sourceUrl, repositoryDir, componentRoot)) : void 0;
5650
6322
  let docsConfig = { tabs: [] };
6323
+ let defaultVersionPrefixes = /* @__PURE__ */ new Set();
6324
+ let allVersionPrefixes = /* @__PURE__ */ new Set();
5651
6325
  const referenceMap = /* @__PURE__ */ new Map();
5652
6326
  const exactReferenceMap = /* @__PURE__ */ new Map();
5653
6327
  const referenceOrder = /* @__PURE__ */ new Map();
@@ -5657,6 +6331,7 @@ function migrateRepository(options) {
5657
6331
  let fernBasePath = "";
5658
6332
  let fernVersionPath = "";
5659
6333
  let fernApiSections = [];
6334
+ const fernExternalSourcePaths = /* @__PURE__ */ new Set();
5660
6335
  let fernChangelogIndexes = [];
5661
6336
  const fernReferencedPaths = /* @__PURE__ */ new Set();
5662
6337
  const fernSourceLinkAliases = /* @__PURE__ */ new Map();
@@ -5667,6 +6342,8 @@ function migrateRepository(options) {
5667
6342
  const config = readMintlifyConfig(mintlifyProjectRoot ?? repositoryDir);
5668
6343
  if (config) {
5669
6344
  mintlifyConfig = config;
6345
+ defaultVersionPrefixes = mintlifyDefaultVersionPrefixes(config);
6346
+ allVersionPrefixes = mintlifyAllVersionPrefixes(config);
5670
6347
  const projected = projectMintlifyNavigation(config);
5671
6348
  docsConfig = projected.docsConfig;
5672
6349
  warnings.push(...projected.warnings);
@@ -5717,6 +6394,7 @@ function migrateRepository(options) {
5717
6394
  const exactKey = exactReferenceKey(descriptor.sourcePath);
5718
6395
  if (!exactReferenceMap.has(exactKey)) exactReferenceMap.set(exactKey, { navigationId: descriptor.navigationId });
5719
6396
  if (!referenceOrder.has(key)) referenceOrder.set(key, index);
6397
+ if (descriptor.sourcePath.startsWith("../")) fernExternalSourcePaths.add(descriptor.sourcePath);
5720
6398
  }
5721
6399
  }
5722
6400
  } catch (error) {
@@ -5741,15 +6419,35 @@ function migrateRepository(options) {
5741
6419
  throw new Error(`Documentation directory does not exist: ${configuredDocsDir || "."}`);
5742
6420
  }
5743
6421
  const mintignoreMatcher = platform === "mintlify" && mintlifyProjectRoot ? readMintignoreMatcher(mintlifyProjectRoot) : null;
5744
- const files = mintignoreMatcher ? scanFiles(contentRoot, repositoryDir, warnings).filter((file) => !mintignoreMatcher.ignores(file.relativePath)) : scanFiles(contentRoot, repositoryDir, warnings);
6422
+ const lastReferencedIndexByVersionLocale = /* @__PURE__ */ new Map();
6423
+ for (const [key, index] of referenceOrder) {
6424
+ const blockKey = key.split("/").slice(0, 2).join("/");
6425
+ const current = lastReferencedIndexByVersionLocale.get(blockKey);
6426
+ if (current === void 0 || index > current) lastReferencedIndexByVersionLocale.set(blockKey, index);
6427
+ }
6428
+ const discoveryRank = referenceOrder.size > 0 ? (relativePath) => {
6429
+ const referencedIndex = referenceOrder.get(normalizedReferenceKey(relativePath));
6430
+ if (referencedIndex !== void 0) return referencedIndex;
6431
+ const blockKey = relativePath.split("/").slice(0, 2).join("/");
6432
+ const versionCeiling = lastReferencedIndexByVersionLocale.get(blockKey);
6433
+ return versionCeiling !== void 0 ? versionCeiling + 1 : Number.MAX_SAFE_INTEGER;
6434
+ } : void 0;
6435
+ const scannedFiles = scanFiles(contentRoot, repositoryDir, warnings, discoveryRank);
6436
+ const mintignoreFilteredFiles = mintignoreMatcher ? scannedFiles.filter((file) => !mintignoreMatcher.ignores(file.relativePath)) : scannedFiles;
6437
+ const discoveryBudgetApplied = discoveryRank !== void 0 && mintignoreFilteredFiles.length > MAX_SOURCE_FILES;
6438
+ const files = discoveryRank ? selectFilesWithinBudget(mintignoreFilteredFiles, discoveryRank, warnings, allVersionPrefixes) : mintignoreFilteredFiles;
5745
6439
  if (platform === "fern" && fernProjectRoot) {
5746
- const scanned = new Set(files.map((file) => file.absolutePath));
5747
- for (const sourcePath of fernReferencedPaths) {
6440
+ const discovered2 = new Set(files.map((file) => file.relativePath));
6441
+ for (const sourcePath of fernExternalSourcePaths) {
6442
+ if (discovered2.has(sourcePath)) continue;
5748
6443
  try {
5749
6444
  const absolutePath = resolveWithinRoot(fernProjectRoot, sourcePath, repositoryDir);
5750
- if (scanned.has(absolutePath) || !existsSync5(absolutePath) || !lstatSync5(absolutePath).isFile()) continue;
5751
- files.push({ absolutePath, relativePath: sourcePath });
5752
- scanned.add(absolutePath);
6445
+ if (existsSync5(absolutePath) && lstatSync5(absolutePath).isFile()) {
6446
+ files.push({ absolutePath, relativePath: sourcePath });
6447
+ discovered2.add(sourcePath);
6448
+ } else {
6449
+ warnings.push({ code: "missing-page", message: "A Fern navigation path could not be read within this repository.", source: sourcePath });
6450
+ }
5753
6451
  } catch {
5754
6452
  warnings.push({ code: "missing-page", message: "A Fern navigation path could not be read within this repository.", source: sourcePath });
5755
6453
  }
@@ -5757,6 +6455,7 @@ function migrateRepository(options) {
5757
6455
  }
5758
6456
  const pages = [];
5759
6457
  const assets = [];
6458
+ const remoteApiSpecs = [];
5760
6459
  const referencedAssetPaths = /* @__PURE__ */ new Map();
5761
6460
  const addAssetReference = (assetPath, referencingPage) => {
5762
6461
  const referrers = referencedAssetPaths.get(assetPath);
@@ -5764,6 +6463,7 @@ function migrateRepository(options) {
5764
6463
  else referencedAssetPaths.set(assetPath, /* @__PURE__ */ new Set([referencingPage]));
5765
6464
  };
5766
6465
  const docusaurusDescriptors = [];
6466
+ const docusaurusDocCardRoutes = /* @__PURE__ */ new Set();
5767
6467
  const seenPageIds = /* @__PURE__ */ new Set();
5768
6468
  const fernIdRenames = /* @__PURE__ */ new Map();
5769
6469
  let skipped = 0;
@@ -5833,6 +6533,9 @@ function migrateRepository(options) {
5833
6533
  mintlifyProjectRoot ?? docusaurusProjectRoot ?? fernProjectRoot ?? repositoryDir,
5834
6534
  snippetAliases
5835
6535
  );
6536
+ if (platform === "fern" || platform === "mintlify" || platform === "docusaurus") {
6537
+ raw = normalizeExplicitHeadingIds(raw);
6538
+ }
5836
6539
  if (componentMigrator) {
5837
6540
  const warningsBeforeTransform = warnings.length;
5838
6541
  raw = componentMigrator.transform(raw, file.absolutePath);
@@ -5858,6 +6561,10 @@ function migrateRepository(options) {
5858
6561
  }
5859
6562
  raw = escapeFernLiteralBraces(protectedMath.body);
5860
6563
  }
6564
+ const hasDocusaurusDocCardList = platform === "docusaurus" && /<DocCardList\b[^>]*\/>/.test(raw);
6565
+ if (hasDocusaurusDocCardList) {
6566
+ raw = raw.replace(/<DocCardList\b[^>]*\/>/g, '<div data-thally-doc-card-list="" />');
6567
+ }
5861
6568
  let docusaurusDescriptor;
5862
6569
  const page = parseMarkdownPage({
5863
6570
  id,
@@ -5891,9 +6598,9 @@ function migrateRepository(options) {
5891
6598
  // hierarchy (never just its own segment) and wins over a docs.yml slug.
5892
6599
  ...platform === "fern" ? {
5893
6600
  resolveIdentity: (frontmatter, fallback) => {
5894
- const slug = typeof frontmatter.slug === "string" ? frontmatter.slug.trim() : "";
5895
- if (!slug) return fallback;
5896
- const overridden = pageIdFromReference(slug, true);
6601
+ const slug2 = typeof frontmatter.slug === "string" ? frontmatter.slug.trim() : "";
6602
+ if (!slug2) return fallback;
6603
+ const overridden = pageIdFromReference(slug2, true);
5897
6604
  if (!overridden) return fallback;
5898
6605
  fernIdRenames.set(fallback.navigationId, overridden);
5899
6606
  return { ...fallback, id: overridden, navigationId: overridden };
@@ -5904,6 +6611,13 @@ function migrateRepository(options) {
5904
6611
  skipped++;
5905
6612
  continue;
5906
6613
  }
6614
+ if (page.frontmatterError) {
6615
+ warnings.push({
6616
+ code: "unsupported-config",
6617
+ message: `The page's frontmatter is not valid YAML (${page.frontmatterError}); the page was kept and as much of its metadata as could be read was used.`,
6618
+ source: relative5(repositoryDir, file.absolutePath).replace(/\\/g, "/")
6619
+ });
6620
+ }
5907
6621
  if (platform === "fern" && !page.navTitle) {
5908
6622
  const navTitle = fernNavTitles.get(navigationId);
5909
6623
  if (navTitle && navTitle !== page.title) page.navTitle = navTitle;
@@ -5918,6 +6632,9 @@ function migrateRepository(options) {
5918
6632
  page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, fernProjectRoot, (assetPath) => {
5919
6633
  addAssetReference(assetPath, file.relativePath);
5920
6634
  });
6635
+ page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, repositoryDir, (assetPath) => {
6636
+ addAssetReference(assetPath, file.relativePath);
6637
+ });
5921
6638
  page.body = stripFernBasePathFromLinks(page.body, fernBasePath);
5922
6639
  page.body = stripFernBasePathFromLinks(page.body, fernVersionPath);
5923
6640
  }
@@ -5972,11 +6689,21 @@ function migrateRepository(options) {
5972
6689
  }
5973
6690
  seenPageIds.add(page.id);
5974
6691
  pages.push(page);
6692
+ if (hasDocusaurusDocCardList) {
6693
+ docusaurusDocCardRoutes.add(page.navigationId);
6694
+ }
5975
6695
  if (platform === "mintlify") {
5976
6696
  const sourcePath = exactReferenceKey(file.relativePath);
5977
6697
  if (sourcePath !== page.id && /^[A-Za-z0-9_./-]+$/.test(sourcePath)) {
5978
6698
  routeAliases.push({ source: `/${sourcePath}`, destination: `/${page.id}`, permanent: false });
5979
6699
  }
6700
+ const versionPrefix = [...defaultVersionPrefixes].find((prefix) => sourcePath === prefix || sourcePath.startsWith(`${prefix}/`));
6701
+ if (versionPrefix) {
6702
+ const unversioned = sourcePath.slice(versionPrefix.length).replace(/^\/+/, "");
6703
+ if (unversioned && unversioned !== sourcePath && unversioned !== page.id && /^[A-Za-z0-9_./-]+$/.test(unversioned) && !routeAliases.some((redirect) => redirect.source === `/${unversioned}`)) {
6704
+ routeAliases.push({ source: `/${unversioned}`, destination: `/${page.id}`, permanent: false });
6705
+ }
6706
+ }
5980
6707
  }
5981
6708
  if (docusaurusDescriptor) {
5982
6709
  docusaurusDescriptors.push({ ...docusaurusDescriptor, title: page.title });
@@ -5989,6 +6716,43 @@ function migrateRepository(options) {
5989
6716
  }
5990
6717
  }
5991
6718
  const docusaurusAssetRoot = docusaurusProjectRoot ?? repositoryDir;
6719
+ const fernBrandAssetPaths = /* @__PURE__ */ new Map();
6720
+ const fernReferencedAssets = [];
6721
+ if (platform === "fern" && fernProjectRoot) {
6722
+ const seen = /* @__PURE__ */ new Set();
6723
+ const addFernAsset = (candidate, source) => {
6724
+ try {
6725
+ const absolutePath = source ? resolveWithinRoot(fernProjectRoot, source.replace(/^\/+/, ""), repositoryDir) : resolveWithin(repositoryDir, candidate);
6726
+ if (!existsSync5(absolutePath) || !lstatSync5(absolutePath).isFile()) return;
6727
+ const realRoot = realpathSync2(repositoryDir);
6728
+ const realPath = realpathSync2(absolutePath);
6729
+ const confined = relative5(realRoot, realPath);
6730
+ if (confined === ".." || confined.startsWith(`..${sep2}`) || isAbsolute2(confined)) return;
6731
+ const withinFern = relative5(fernProjectRoot, absolutePath);
6732
+ const assetRelative = withinFern !== ".." && !withinFern.startsWith(`..${sep2}`) && !isAbsolute2(withinFern) ? withinFern : relative5(repositoryDir, absolutePath);
6733
+ const assetPath = normalizeAssetPath(assetRelative.replace(/\\/g, "/"));
6734
+ if (!assetPath || !ASSET_EXTENSIONS.has(extname5(assetPath).toLowerCase())) return;
6735
+ if (source) {
6736
+ fernBrandAssetPaths.set(source, assetPath);
6737
+ addAssetReference(assetPath, "fern/docs.yml");
6738
+ }
6739
+ if (seen.has(absolutePath)) return;
6740
+ seen.add(absolutePath);
6741
+ fernReferencedAssets.push({ absolutePath, relativePath: assetPath });
6742
+ } catch {
6743
+ }
6744
+ };
6745
+ for (const path of referencedAssetPaths.keys()) addFernAsset(path);
6746
+ for (const value of [fernRawConfig?.logo, fernRawConfig?.favicon]) {
6747
+ if (typeof value === "string") addFernAsset("", value);
6748
+ else if (value && typeof value === "object" && !Array.isArray(value)) {
6749
+ const variants = value;
6750
+ for (const candidate of [variants.light, variants.dark, variants.src, variants.srcDark]) {
6751
+ if (typeof candidate === "string") addFernAsset("", candidate);
6752
+ }
6753
+ }
6754
+ }
6755
+ }
5992
6756
  const repositoryAssets = platform === "docusaurus" && configuredDocsDir && !options.docusaurusSkipAssets ? ["static", "public"].flatMap((directory) => {
5993
6757
  const root = resolveWithin(docusaurusAssetRoot, directory);
5994
6758
  if (!existsSync5(root) || !lstatSync5(root).isDirectory()) return [];
@@ -5998,10 +6762,10 @@ function migrateRepository(options) {
5998
6762
  }));
5999
6763
  }) : [];
6000
6764
  const assetCandidates = [];
6001
- for (const file of [...files, ...repositoryAssets]) {
6765
+ for (const file of [...files, ...repositoryAssets, ...fernReferencedAssets]) {
6002
6766
  const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
6003
6767
  if (!ASSET_EXTENSIONS.has(extname5(file.relativePath).toLowerCase())) continue;
6004
- if (platform !== "mintlify" && !ASSET_DIRECTORIES.has(firstSegment)) continue;
6768
+ if (platform !== "mintlify" && !ASSET_DIRECTORIES.has(firstSegment) && !(platform === "fern" && referencedAssetPaths.has(file.relativePath))) continue;
6005
6769
  const isDocusaurusStatic = platform === "docusaurus" && firstSegment === "static";
6006
6770
  const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : isDocusaurusStatic ? file.relativePath.slice("static/".length) : file.relativePath);
6007
6771
  if (!assetPath) continue;
@@ -6018,7 +6782,7 @@ function migrateRepository(options) {
6018
6782
  const referencingPages = referencedAssetPaths.get(assetPath);
6019
6783
  warnings.push({
6020
6784
  code: "limit-reached",
6021
- message: referencingPages ? `Asset was skipped because the migration asset budget was exhausted, but it is referenced by: ${[...referencingPages].join(", ")}.` : "An asset was skipped because the migration asset budget was exhausted.",
6785
+ message: "This file was not copied because it is larger than 25 MB or the copied files would exceed 500 MB in total. Add it to public/ manually" + (referencingPages ? `; it is used by ${[...referencingPages].slice(0, 3).join(", ")}${referencingPages.size > 3 ? `, and ${referencingPages.size - 3} more` : ""}.` : "."),
6022
6786
  source: file.relativePath
6023
6787
  });
6024
6788
  continue;
@@ -6035,8 +6799,8 @@ function migrateRepository(options) {
6035
6799
  assets.push({ path: assetPath, content });
6036
6800
  totalAssetBytes += size;
6037
6801
  }
6038
- if (files.length >= MAX_SOURCE_FILES) {
6039
- warnings.push({ code: "limit-reached", message: `Repository discovery stopped at ${MAX_SOURCE_FILES} files.` });
6802
+ if (!discoveryBudgetApplied && files.length >= MAX_SOURCE_FILES) {
6803
+ warnings.push({ code: "limit-reached", message: `Stopped scanning after ${MAX_SOURCE_FILES} files, so the rest of the repository was not looked at. Run the migration on a smaller part of the repository with --docs-dir.` });
6040
6804
  }
6041
6805
  if (platform === "docusaurus") {
6042
6806
  const projected = projectDocusaurusNavigation({
@@ -6047,6 +6811,7 @@ function migrateRepository(options) {
6047
6811
  routePrefix: options.docusaurusRoutePrefix
6048
6812
  });
6049
6813
  docsConfig = projected.docsConfig;
6814
+ expandDocusaurusDocCardLists(pages, docsConfig, docusaurusDocCardRoutes);
6050
6815
  warnings.push(...projected.warnings);
6051
6816
  for (const page of projected.generatedPages) {
6052
6817
  if (seenPageIds.has(page.id)) continue;
@@ -6062,7 +6827,8 @@ function migrateRepository(options) {
6062
6827
  const descriptor = descriptorByNavigationId.get(page.navigationId);
6063
6828
  if (descriptor) page.body = rewriteDocusaurusLinks(page.body, descriptor, docusaurusDescriptors, {
6064
6829
  sourceOrigin,
6065
- onExternalLink: (target) => externalizedLinks.add(target)
6830
+ onExternalLink: (target) => externalizedLinks.add(target),
6831
+ assetPaths: new Set(assets.map((asset) => asset.path))
6066
6832
  });
6067
6833
  }
6068
6834
  if (externalizedLinks.size > 0) warnings.push({
@@ -6120,10 +6886,11 @@ function migrateRepository(options) {
6120
6886
  filename = `${prefix}${filename}`;
6121
6887
  }
6122
6888
  specFilenameSources.set(filename, spec.absolutePath);
6889
+ const specContent = readFileSync5(spec.absolutePath);
6123
6890
  if (!assets.some((asset) => asset.path === filename)) {
6124
- assets.push({ path: filename, content: readFileSync5(spec.absolutePath) });
6891
+ assets.push({ path: filename, content: specContent });
6125
6892
  }
6126
- resolvedSpecs.push({ filename, tabLabel: section.tabLabel });
6893
+ resolvedSpecs.push({ filename, tabLabel: section.tabLabel, content: specContent, routeSegments: section.routeSegments ?? [] });
6127
6894
  continue;
6128
6895
  }
6129
6896
  if (resolution.unsupported.length > 0) {
@@ -6151,16 +6918,28 @@ function migrateRepository(options) {
6151
6918
  message: checked.length ? `No OpenAPI/AsyncAPI spec could be found for the "${section.name}" API. Checked: ${checked.join(", ")}. Point generators.yml at a spec file that exists.` : `No OpenAPI/AsyncAPI spec could be found for the "${section.name}" API, because its api-name is not a valid folder name. Add the spec manually.`
6152
6919
  });
6153
6920
  }
6154
- if (resolvedSpecs.length > 0) docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs);
6921
+ if (resolvedSpecs.length > 0) {
6922
+ docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs, warnings);
6923
+ const { operationLinks, prefixLandings } = apiOperationLinkMap(
6924
+ resolvedSpecs.map((spec) => ({ filename: spec.filename, content: spec.content, prefix: spec.routeSegments.join("/") })),
6925
+ docsConfig
6926
+ );
6927
+ rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings);
6928
+ }
6155
6929
  } else if (platform === "mintlify") {
6156
- const resolvedSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings);
6930
+ const resolvedSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs);
6157
6931
  for (const spec of resolvedSpecs) {
6158
6932
  if (!assets.some((asset) => asset.path === spec.filename)) {
6159
6933
  assets.push({ path: spec.filename, content: spec.content });
6160
6934
  }
6161
6935
  }
6162
6936
  if (resolvedSpecs.length > 0) {
6163
- docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs);
6937
+ docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs, warnings);
6938
+ const { operationLinks, prefixLandings } = apiOperationLinkMap(
6939
+ resolvedSpecs.map((spec) => ({ filename: spec.filename, content: spec.content, prefix: spec.directory })),
6940
+ docsConfig
6941
+ );
6942
+ rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings);
6164
6943
  } else {
6165
6944
  const fallback = findOpenApi(files);
6166
6945
  if (fallback) {
@@ -6193,7 +6972,7 @@ function migrateRepository(options) {
6193
6972
  ];
6194
6973
  for (const plugin of additionalRoots) {
6195
6974
  if (discovered >= MAX_SOURCE_FILES) {
6196
- warnings.push({ code: "limit-reached", message: `Docusaurus docs import stopped at the ${MAX_SOURCE_FILES}-file discovery budget.` });
6975
+ warnings.push({ code: "limit-reached", message: `Stopped importing Docusaurus docs plugins after ${MAX_SOURCE_FILES} files; the remaining plugin docs were not migrated.` });
6197
6976
  break;
6198
6977
  }
6199
6978
  if (plugin.docsDir === configuredDocsDir) continue;
@@ -6234,6 +7013,15 @@ function migrateRepository(options) {
6234
7013
  }
6235
7014
  }
6236
7015
  if (platform === "docusaurus") {
7016
+ const copiedAssets = new Set(assets.map((asset) => asset.path));
7017
+ for (const page of pages) {
7018
+ page.body = rewriteDocusaurusLinks(page.body, {
7019
+ sourcePath: page.source ?? page.navigationId,
7020
+ docId: page.navigationId,
7021
+ navigationId: page.navigationId,
7022
+ title: page.title
7023
+ }, [], { assetPaths: copiedAssets });
7024
+ }
6237
7025
  const importedRoutes = new Set(pages.map((page) => page.navigationId));
6238
7026
  docsConfig.tabs = docsConfig.tabs.map((tab) => {
6239
7027
  if (tab.href) return tab;
@@ -6293,6 +7081,27 @@ ${body}`;
6293
7081
  }
6294
7082
  }
6295
7083
  docsConfig = pruneMissingNavigationPages(docsConfig, new Set(pages.map((page) => page.navigationId)));
7084
+ if (platform === "docusaurus") addDocusaurusTranslatedHeadingAliases(pages);
7085
+ if (platform === "fern" && fernProjectRoot) {
7086
+ const sourcePath = (page) => {
7087
+ const fragment = page.source.split("#", 2)[1];
7088
+ if (!fragment) return null;
7089
+ try {
7090
+ return relative5(fernProjectRoot, resolveWithin(repositoryDir, decodeURIComponent(fragment))).replace(/\\/g, "/");
7091
+ } catch {
7092
+ return null;
7093
+ }
7094
+ };
7095
+ const routes = /* @__PURE__ */ new Map();
7096
+ for (const page of pages) {
7097
+ const source = sourcePath(page);
7098
+ if (source) routes.set(source, page.id);
7099
+ }
7100
+ for (const page of pages) {
7101
+ const source = sourcePath(page);
7102
+ if (source) page.body = rewriteFernRelativePageLinks(page.body, source, routes);
7103
+ }
7104
+ }
6296
7105
  preserveLinkedAnchors(pages);
6297
7106
  if (platform === "fern" && fernRawConfig) {
6298
7107
  const sourceSiteUrl = fernSourceSiteUrl(fernRawConfig);
@@ -6410,7 +7219,7 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
6410
7219
  const assetPaths = new Set(assets.map((asset) => asset.path));
6411
7220
  const publicBrandPath = (value) => {
6412
7221
  if (typeof value !== "string" || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(value)) return void 0;
6413
- const normalized = normalizeAssetPath(value);
7222
+ const normalized = fernBrandAssetPaths.get(value) ?? normalizeAssetPath(value);
6414
7223
  return normalized && assetPaths.has(normalized) ? `/${normalized}` : void 0;
6415
7224
  };
6416
7225
  const brandVariants = (value) => {
@@ -6445,6 +7254,34 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
6445
7254
  }
6446
7255
  }
6447
7256
  };
7257
+ if (platform === "docusaurus" && docusaurusSettings?.navbarTitle && !logoLight) {
7258
+ docsConfig = { ...docsConfig, navbar: { ...docsConfig.navbar, logo: null } };
7259
+ }
7260
+ if (mintlifyConfig) {
7261
+ const appearance = mintlifyConfig.appearance && typeof mintlifyConfig.appearance === "object" ? mintlifyConfig.appearance : null;
7262
+ const defaultMode = appearance?.default;
7263
+ if (defaultMode === "light" || defaultMode === "dark" || defaultMode === "system") {
7264
+ docsConfig = { ...docsConfig, appearance: { ...docsConfig.appearance, default: defaultMode } };
7265
+ }
7266
+ const background = mintlifyConfig.background && typeof mintlifyConfig.background === "object" ? mintlifyConfig.background : null;
7267
+ if (background) {
7268
+ const decoration = background.decoration;
7269
+ const image = publicBrandPath(background.image);
7270
+ const imageDark = publicBrandPath(background.imageDark);
7271
+ docsConfig = {
7272
+ ...docsConfig,
7273
+ background: {
7274
+ ...docsConfig.background,
7275
+ ...decoration === "none" || decoration === "grid" || decoration === "gradient" ? { decoration } : {},
7276
+ ...image ? { image } : {},
7277
+ ...imageDark ? { imageDark } : {}
7278
+ }
7279
+ };
7280
+ }
7281
+ }
7282
+ if (platform === "docusaurus") {
7283
+ docsConfig = { ...docsConfig, appearance: { ...docsConfig.appearance, default: docusaurusSettings?.defaultColorMode ?? "light" } };
7284
+ }
6448
7285
  if (faviconLight) docsConfig = {
6449
7286
  ...docsConfig,
6450
7287
  favicon: { light: faviconLight, ...faviconDark ? { dark: faviconDark } : {} }
@@ -6467,6 +7304,7 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
6467
7304
  platform,
6468
7305
  pages,
6469
7306
  assets,
7307
+ ...remoteApiSpecs.length > 0 ? { remoteApiSpecs } : {},
6470
7308
  ...componentMigrator ? { componentFiles: componentMigrator.files() } : {},
6471
7309
  docsConfig,
6472
7310
  ...mintlifyConfig ? {
@@ -6482,7 +7320,7 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
6482
7320
  }
6483
7321
  } : docusaurusSettings && (docusaurusSettings.name || docusaurusSettings.description || themeColors) ? {
6484
7322
  site: {
6485
- ...docusaurusSettings.name ? { name: docusaurusSettings.name } : {},
7323
+ ...docusaurusSettings.navbarTitle || docusaurusSettings.name ? { name: docusaurusSettings.navbarTitle ?? docusaurusSettings.name } : {},
6486
7324
  ...docusaurusSettings.description ? { description: docusaurusSettings.description } : {},
6487
7325
  ...themeColors ? { colors: themeColors } : {}
6488
7326
  }
@@ -6754,10 +7592,10 @@ async function readResponseBody(response) {
6754
7592
  }
6755
7593
  return new TextDecoder().decode(body);
6756
7594
  }
6757
- var defaultMigrationFetcher = async (url, request) => {
7595
+ var defaultMigrationFetcher = async (url, request2) => {
6758
7596
  const response = await fetch(url, {
6759
7597
  headers: {
6760
- Accept: request.accept,
7598
+ Accept: request2.accept,
6761
7599
  "User-Agent": "Thally-Migrate/1.0 (+https://thally.io)"
6762
7600
  },
6763
7601
  redirect: "follow",
@@ -8172,6 +9010,202 @@ async function migrateUrl(options) {
8172
9010
  }
8173
9011
  };
8174
9012
  }
9013
+
9014
+ // src/remote-api.ts
9015
+ import { createHash as createHash2 } from "crypto";
9016
+ import { lookup } from "dns/promises";
9017
+ import { request } from "https";
9018
+ import { isIP } from "net";
9019
+ import { parse as parseYaml6 } from "yaml";
9020
+ var MAX_SPEC_BYTES = 25e6;
9021
+ var MAX_REDIRECTS = 3;
9022
+ function isPublicAddress(address, family) {
9023
+ if (family === 4) {
9024
+ const parts = address.split(".").map(Number);
9025
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
9026
+ const [a, b] = parts;
9027
+ return !(a === 0 || a === 10 || a === 127 || a >= 224 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && (b === 0 || b === 168) || a === 198 && (b === 18 || b === 19) || a === 203 && b === 0 || a === 198 && b === 51);
9028
+ }
9029
+ return family === 6 && /^[23][0-9a-f]{0,3}:/i.test(address) && !/^2001:db8:/i.test(address);
9030
+ }
9031
+ function validatedSpecUrl(value) {
9032
+ const url = new URL(value);
9033
+ if (url.protocol !== "https:" || url.username || url.password || isIP(url.hostname)) {
9034
+ throw new Error("Remote OpenAPI references must use an HTTPS domain without credentials or an IP literal.");
9035
+ }
9036
+ url.hash = "";
9037
+ return url;
9038
+ }
9039
+ async function getPinnedAddress(url) {
9040
+ const addresses = await lookup(url.hostname, { all: true });
9041
+ if (addresses.length === 0 || addresses.some((candidate) => !isPublicAddress(candidate.address, candidate.family))) {
9042
+ throw new Error("The OpenAPI host did not resolve exclusively to public addresses.");
9043
+ }
9044
+ return addresses[0];
9045
+ }
9046
+ async function requestPinnedSpec(url) {
9047
+ const pinned = await getPinnedAddress(url);
9048
+ return new Promise((resolve3, reject) => {
9049
+ const req = request(url, {
9050
+ headers: { Accept: "application/json, application/yaml, text/yaml, */*", "User-Agent": "Thally-Migrate/1.0 (+https://thally.io)" },
9051
+ signal: AbortSignal.timeout(3e4),
9052
+ // Node's dual-stack connector asks lookup for `all: true`; that
9053
+ // callback must receive address records, not a single address string.
9054
+ lookup: (_hostname, options, callback) => {
9055
+ if (typeof options === "object" && options.all) callback(null, [pinned]);
9056
+ else callback(null, pinned.address, pinned.family);
9057
+ }
9058
+ }, (response) => {
9059
+ const status = response.statusCode ?? 0;
9060
+ if (status >= 300 && status < 400) {
9061
+ response.resume();
9062
+ resolve3({ status, location: response.headers.location, body: Buffer.alloc(0) });
9063
+ return;
9064
+ }
9065
+ if (status < 200 || status >= 300) {
9066
+ response.resume();
9067
+ reject(new Error(`OpenAPI server returned ${status}.`));
9068
+ return;
9069
+ }
9070
+ const statedBytes = Number(response.headers["content-length"] ?? 0);
9071
+ if (statedBytes > MAX_SPEC_BYTES) {
9072
+ response.destroy();
9073
+ reject(new Error("Remote OpenAPI spec exceeds the 25 MB import limit."));
9074
+ return;
9075
+ }
9076
+ const chunks = [];
9077
+ let bytes = 0;
9078
+ response.on("data", (chunk) => {
9079
+ bytes += chunk.length;
9080
+ if (bytes > MAX_SPEC_BYTES) {
9081
+ response.destroy(new Error("Remote OpenAPI spec exceeds the 25 MB import limit."));
9082
+ return;
9083
+ }
9084
+ chunks.push(chunk);
9085
+ });
9086
+ response.on("end", () => resolve3({ status, body: Buffer.concat(chunks) }));
9087
+ response.on("error", reject);
9088
+ });
9089
+ req.on("error", reject);
9090
+ req.end();
9091
+ });
9092
+ }
9093
+ async function downloadSpec(value, fetcher) {
9094
+ let url = validatedSpecUrl(value);
9095
+ if (fetcher) {
9096
+ const response = await fetcher(url, { accept: "application/json, application/yaml, text/yaml" });
9097
+ validatedSpecUrl(response.finalUrl.toString());
9098
+ const body = Buffer.from(response.body);
9099
+ if (body.length > MAX_SPEC_BYTES) throw new Error("Remote OpenAPI spec exceeds the 25 MB import limit.");
9100
+ return body;
9101
+ }
9102
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
9103
+ const response = await requestPinnedSpec(url);
9104
+ if (response.status >= 300 && response.status < 400) {
9105
+ if (!response.location || hop === MAX_REDIRECTS) throw new Error("Remote OpenAPI spec redirected too many times.");
9106
+ url = validatedSpecUrl(new URL(response.location, url).toString());
9107
+ continue;
9108
+ }
9109
+ return response.body;
9110
+ }
9111
+ throw new Error("Remote OpenAPI spec redirected too many times.");
9112
+ }
9113
+ function parseOpenApi(content, extension) {
9114
+ const text = content.toString("utf8");
9115
+ const parsed = extension === "yaml" || !text.trimStart().startsWith("{") ? parseYaml6(text) : JSON.parse(text);
9116
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("OpenAPI spec is not an object.");
9117
+ const document = parsed;
9118
+ if (typeof document.openapi !== "string" || !/^3\./.test(document.openapi)) {
9119
+ throw new Error("Remote specification is not OpenAPI 3.x.");
9120
+ }
9121
+ if (!document.paths || typeof document.paths !== "object" || Array.isArray(document.paths)) {
9122
+ throw new Error("Remote OpenAPI spec has no paths object.");
9123
+ }
9124
+ const paths = Object.entries(document.paths);
9125
+ if (paths.length > 5e3) throw new Error("Remote OpenAPI spec has too many paths to import.");
9126
+ const operations = [];
9127
+ for (const [path, value] of paths) {
9128
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
9129
+ for (const [method, raw] of Object.entries(value)) {
9130
+ if (!/^(get|post|put|patch|delete|head|options|trace)$/i.test(method) || !raw || typeof raw !== "object" || Array.isArray(raw)) continue;
9131
+ const operation = raw;
9132
+ const tags = Array.isArray(operation.tags) ? operation.tags : [];
9133
+ operations.push({
9134
+ method: method.toLowerCase(),
9135
+ path,
9136
+ tag: typeof tags[0] === "string" ? tags[0] : "API Reference",
9137
+ summary: typeof operation.summary === "string" ? operation.summary : String(operation.operationId ?? "")
9138
+ });
9139
+ }
9140
+ }
9141
+ return operations;
9142
+ }
9143
+ function slug(value) {
9144
+ return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
9145
+ }
9146
+ function operationTarget(operation) {
9147
+ const path = operation.path.split("/").filter(Boolean).map((part) => part.replace(/[{}]/g, "").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase());
9148
+ return `/api/default/${[...path, operation.method].join("/")}`;
9149
+ }
9150
+ async function hydrateRemoteApiSpecs(bundle, fetcher) {
9151
+ if (!bundle.remoteApiSpecs?.length) return bundle;
9152
+ const assets = [...bundle.assets];
9153
+ const tabs = bundle.docsConfig.tabs.map((tab) => ({ ...tab }));
9154
+ const redirects = [...bundle.docsConfig.redirects ?? []];
9155
+ const warnings = [...bundle.warnings];
9156
+ const authoredLinks = /* @__PURE__ */ new Set();
9157
+ for (const page of bundle.pages) {
9158
+ for (const match of page.body.matchAll(/\/[a-z0-9_/-]+/gi)) {
9159
+ const segments = match[0].split("/").filter(Boolean);
9160
+ if (segments.length < 3) continue;
9161
+ const section = segments[0].toLowerCase();
9162
+ if (section.includes("api") || section.includes("reference")) authoredLinks.add(match[0]);
9163
+ }
9164
+ }
9165
+ for (const reference of bundle.remoteApiSpecs) {
9166
+ for (let index = warnings.length - 1; index >= 0; index--) {
9167
+ if (warnings[index].source === reference.url && warnings[index].message.includes("requires a network download")) warnings.splice(index, 1);
9168
+ }
9169
+ try {
9170
+ const url = validatedSpecUrl(reference.url);
9171
+ const extension = /\.ya?ml$/i.test(url.pathname) ? "yaml" : "json";
9172
+ const body = await downloadSpec(reference.url, fetcher);
9173
+ const operations = parseOpenApi(body, extension);
9174
+ const filename = `openapi-${createHash2("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
9175
+ assets.push({ path: filename, content: body });
9176
+ const tab = reference.tabLabel ? tabs.find((candidate) => candidate.tab === reference.tabLabel) : tabs.find((candidate) => candidate.tab.toLowerCase().includes("api"));
9177
+ if (tab) tab.api = { source: `/${filename}` };
9178
+ else tabs.push({ tab: reference.tabLabel ?? "API Reference", api: { source: `/${filename}` } });
9179
+ const routes = /* @__PURE__ */ new Map();
9180
+ for (const operation of operations) {
9181
+ const key = `${slug(operation.tag)}/${slug(operation.summary)}`;
9182
+ if (routes.has(key)) routes.set(key, null);
9183
+ else routes.set(key, operationTarget(operation));
9184
+ }
9185
+ const existingSources = new Set(redirects.map((redirect) => redirect.source));
9186
+ for (const source of authoredLinks) {
9187
+ const parts = source.split("/").filter(Boolean);
9188
+ const key = parts.slice(-2).join("/");
9189
+ const destination = routes.get(key);
9190
+ if (!destination || existingSources.has(source)) continue;
9191
+ redirects.push({ source, destination, permanent: false });
9192
+ existingSources.add(source);
9193
+ }
9194
+ } catch (error) {
9195
+ warnings.push({
9196
+ code: "fetch-failed",
9197
+ source: reference.url,
9198
+ message: `Remote OpenAPI spec could not be imported: ${error instanceof Error ? error.message : String(error)}`
9199
+ });
9200
+ }
9201
+ }
9202
+ return {
9203
+ ...bundle,
9204
+ assets,
9205
+ docsConfig: { ...bundle.docsConfig, tabs, ...redirects.length > 0 ? { redirects } : {} },
9206
+ warnings
9207
+ };
9208
+ }
8175
9209
  export {
8176
9210
  addMintlifyDirectoryRedirects,
8177
9211
  buildNavigationFromPages,
@@ -8180,6 +9214,7 @@ export {
8180
9214
  detectRepositoryDocsDir,
8181
9215
  detectRepositoryPlatform,
8182
9216
  gitmodulePaths,
9217
+ hydrateRemoteApiSpecs,
8183
9218
  mergeMigrationConfig,
8184
9219
  migrateRepository,
8185
9220
  migrateUrl,