@thallylabs/migrate 0.2.8 → 0.2.10

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
@@ -14,7 +14,7 @@ import {
14
14
  statSync
15
15
  } from "fs";
16
16
  import { createRequire } from "module";
17
- import { basename as basename3, dirname as dirname4, extname as extname5, relative as relative5, resolve as resolvePath, sep as sep2 } from "path";
17
+ import { basename as basename3, dirname as dirname4, extname as extname5, isAbsolute as isAbsolute2, relative as relative5, resolve as resolvePath, sep as sep2 } from "path";
18
18
  import { parse as parseYaml4 } from "yaml";
19
19
  import * as ts2 from "typescript";
20
20
 
@@ -53,7 +53,10 @@ function parseFrontmatter(raw) {
53
53
  }
54
54
 
55
55
  // src/mdx.ts
56
+ import { posix } from "path";
56
57
  import { compileSync } from "@mdx-js/mdx";
58
+ import { nameToEmoji } from "gemoji";
59
+ import remarkGfm from "remark-gfm";
57
60
  import remarkMdx from "remark-mdx";
58
61
  import remarkParse from "remark-parse";
59
62
  import { unified } from "unified";
@@ -129,7 +132,7 @@ function titleFromId(id) {
129
132
  if (value === "introduction") return "Introduction";
130
133
  return value.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
131
134
  }
132
- var descriptionParser = unified().use(remarkParse).use(remarkMdx);
135
+ var descriptionParser = unified().use(remarkParse).use(remarkGfm).use(remarkMdx);
133
136
  function firstParagraph(content) {
134
137
  function plainText(node) {
135
138
  if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
@@ -286,6 +289,20 @@ var SAFE_BUILTIN_ROOTS = /* @__PURE__ */ new Set([
286
289
  "BigInt",
287
290
  "Reflect"
288
291
  ]);
292
+ function leadingPageHeading(body) {
293
+ try {
294
+ const root = descriptionParser.parse(body);
295
+ const first = root.children?.find((node) => node.type !== "mdxjsEsm" && node.type !== "html");
296
+ if (first?.type !== "heading" || first.depth !== 1) return void 0;
297
+ const text = (node) => node.type === "mdxTextExpression" || node.type === "mdxFlowExpression" ? "" : node.value ?? (node.children ?? []).map(text).join("");
298
+ const title = text(first).replace(/\s+/g, " ").trim();
299
+ const start = first.position?.start.offset;
300
+ const end = first.position?.end.offset;
301
+ return title && start !== void 0 && end !== void 0 ? { title, start, end } : void 0;
302
+ } catch {
303
+ return void 0;
304
+ }
305
+ }
289
306
  function collectPatternNames(pattern, names) {
290
307
  switch (pattern.type) {
291
308
  case "Identifier":
@@ -628,6 +645,66 @@ function replaceOutsideCode(body, transform) {
628
645
  const { masked, unmask } = maskCode(body);
629
646
  return unmask(transform(masked));
630
647
  }
648
+ function rewriteFernRelativePageLinks(body, currentSourcePath, routesBySourcePath) {
649
+ const rewrite = (target) => {
650
+ 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;
651
+ const suffixAt = target.search(/[?#]/);
652
+ const pathname = suffixAt < 0 ? target : target.slice(0, suffixAt);
653
+ const suffix = suffixAt < 0 ? "" : target.slice(suffixAt);
654
+ if (!pathname || pathname.startsWith("//") || pathname.split("/").some((part) => /%2f|%5c/i.test(part))) return target;
655
+ const source = posix.normalize(posix.join(posix.dirname(currentSourcePath.replace(/\\/g, "/")), pathname));
656
+ const candidates = /\.mdx?$/i.test(source) ? [source] : [source, `${source}.mdx`, `${source}.md`, `${source}/index.mdx`, `${source}/index.md`];
657
+ const route = candidates.map((candidate) => routesBySourcePath.get(candidate)).find(Boolean);
658
+ return route ? `/${route.replace(/^\/+/, "")}${suffix}` : target;
659
+ };
660
+ return replaceOutsideCode(body, (text) => text.replace(
661
+ /(\]\()(<[^>\n]+>|[^\s)]+)(?=\s|\))/g,
662
+ (_match, opening, target) => `${opening}${target.startsWith("<") && target.endsWith(">") ? `<${rewrite(target.slice(1, -1))}>` : rewrite(target)}`
663
+ ).replace(
664
+ /(\b(?:href|to)\s*=\s*)(["'])([^"'\n]+)\2/g,
665
+ (_match, opening, quote, target) => `${opening}${quote}${rewrite(target)}${quote}`
666
+ ).replace(
667
+ /(\b(?:href|to)\s*=\s*\{\s*)(["'])([^"'\n]+)\2(?=\s*\})/g,
668
+ (_match, opening, quote, target) => `${opening}${quote}${rewrite(target)}${quote}`
669
+ ));
670
+ }
671
+ function normalizeFernFileTrees(body) {
672
+ return replaceOutsideCode(body, (whole) => whole.replace(
673
+ /<FileTree\s*>\s*\n([\s\S]*?)\n\s*<\/FileTree>/g,
674
+ (original, contents) => {
675
+ const lines = contents.split(/\r?\n/).filter((line) => line.trim());
676
+ if (!lines.length) return original;
677
+ const nodes = [];
678
+ for (const line of lines) {
679
+ const match = /^( *)(?:-|\*|\+)\s+(.+?)\s*$/.exec(line);
680
+ if (!match || match[1].length % 2 !== 0 || match[2].includes("\0")) return original;
681
+ nodes.push({ indent: match[1].length / 2, name: match[2], isFolder: match[2].endsWith("/") });
682
+ }
683
+ const output = ["<Tree>"];
684
+ const folders = [];
685
+ for (const node of nodes) {
686
+ while (folders.length > node.indent) {
687
+ output.push("</Folder>");
688
+ folders.pop();
689
+ }
690
+ if (node.indent > folders.length || node.indent > 0 && folders.length === 0) return original;
691
+ const name = `{${JSON.stringify(node.name)}}`;
692
+ if (node.isFolder) {
693
+ output.push(`<Folder name=${name} defaultOpen>`);
694
+ folders.push(node.indent);
695
+ } else {
696
+ output.push(`<File name=${name} />`);
697
+ }
698
+ }
699
+ while (folders.length) {
700
+ output.push("</Folder>");
701
+ folders.pop();
702
+ }
703
+ output.push("</Tree>");
704
+ return output.join("\n");
705
+ }
706
+ ));
707
+ }
631
708
  function normalizeHtmlComments(body) {
632
709
  return replaceOutsideCode(body, (whole) => whole.replace(/<!--([\s\S]*?)-->/g, (_match, comment) => `{/*${comment}*/}`));
633
710
  }
@@ -997,8 +1074,12 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
997
1074
  const runMintlify = platform === "mintlify";
998
1075
  const runDocusaurus = platform === "docusaurus";
999
1076
  const sourceBody = runDocusaurus && unwrapMdxCodeBlocks ? unwrapDocusaurusMdxCodeBlocks(body) : body;
1000
- let rewritten = normalizeDocusaurusAdmonitions(normalizeExplicitHeadingIds(runFern ? normalizeFernCallouts(sourceBody) : sourceBody));
1077
+ let rewritten = normalizeDocusaurusAdmonitions(normalizeExplicitHeadingIds(runFern ? normalizeFernFileTrees(normalizeFernCallouts(sourceBody)) : sourceBody));
1001
1078
  if (runDocusaurus) {
1079
+ rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1080
+ /:([+\w-]+):/g,
1081
+ (shortcode, name) => nameToEmoji[name] ?? shortcode
1082
+ ));
1002
1083
  rewritten = normalizeDocusaurusTabBlocks(normalizeDocusaurusTabs(removeGlobalDocusaurusImports(rewritten)));
1003
1084
  rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1004
1085
  /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s+(['"])(@site\/[^'"]+)\2\s*;?/g,
@@ -1045,9 +1126,13 @@ function parseMarkdownPage(input) {
1045
1126
  ...input.locale ? { locale: input.locale } : {}
1046
1127
  };
1047
1128
  const identity = input.resolveIdentity?.(parsed.data, fallbackIdentity) ?? fallbackIdentity;
1048
- const body = normalizeMdx(parsed.content, input.platform).trim();
1129
+ let body = normalizeMdx(parsed.content, input.platform).trim();
1049
1130
  const keywords = Array.isArray(parsed.data.keywords) ? parsed.data.keywords.filter((value) => typeof value === "string") : [];
1050
- const title = typeof parsed.data.title === "string" && parsed.data.title.trim() ? parsed.data.title.trim() : titleFromId(identity.navigationId);
1131
+ const heading = leadingPageHeading(body);
1132
+ const title = typeof parsed.data.title === "string" && parsed.data.title.trim() ? parsed.data.title.trim() : heading?.title ?? titleFromId(identity.navigationId);
1133
+ if (heading && heading.title === title) {
1134
+ body = `${body.slice(0, heading.start)}${body.slice(heading.end)}`.trim();
1135
+ }
1051
1136
  const navTitle = typeof parsed.data.sidebarTitle === "string" && parsed.data.sidebarTitle.trim() ? parsed.data.sidebarTitle.trim() : typeof parsed.data.navTitle === "string" && parsed.data.navTitle.trim() ? parsed.data.navTitle.trim() : void 0;
1052
1137
  const badge = typeof parsed.data.tag === "string" && parsed.data.tag.trim() ? parsed.data.tag.trim() : typeof parsed.data.badge === "string" && parsed.data.badge.trim() ? parsed.data.badge.trim() : void 0;
1053
1138
  const sourceMode = typeof parsed.data.mode === "string" ? parsed.data.mode : void 0;
@@ -1060,6 +1145,7 @@ function parseMarkdownPage(input) {
1060
1145
  title,
1061
1146
  navTitle,
1062
1147
  description,
1148
+ ...input.platform === "docusaurus" || input.platform === "fern" ? { descriptionPlacement: "body" } : {},
1063
1149
  badge,
1064
1150
  keywords,
1065
1151
  mode,
@@ -1468,9 +1554,14 @@ function implicitReactImports(source) {
1468
1554
  visit(source);
1469
1555
  const hooks = [...REACT_GLOBALS].filter((name) => references.has(name) && !bindings.has(name)).sort();
1470
1556
  const usesMintlifyComponents = references.has("MintlifyComponents") && !bindings.has("MintlifyComponents");
1557
+ const implicitComponents = MINTLIFY_COMPONENT_MODULES.map(({ module, names }) => {
1558
+ const needed = names.filter((name) => references.has(name) && !bindings.has(name));
1559
+ return needed.length ? `import { ${needed.join(", ")} } from '${module}';` : "";
1560
+ }).filter(Boolean);
1471
1561
  return [
1472
1562
  references.has("React") && !bindings.has("React") ? "import * as React from 'react';" : "",
1473
1563
  hooks.length ? `import { ${hooks.join(", ")} } from 'react';` : "",
1564
+ ...implicitComponents,
1474
1565
  usesMintlifyComponents ? mintlifyComponentsShim() : ""
1475
1566
  ].filter(Boolean).join("\n");
1476
1567
  }
@@ -2217,7 +2308,7 @@ import { customComponents as ${name} } from ${JSON.stringify(registryPath)}
2217
2308
 
2218
2309
  // src/docusaurus.ts
2219
2310
  import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3 } from "fs";
2220
- import { extname as extname3, posix } from "path";
2311
+ import { extname as extname3, posix as posix2 } from "path";
2221
2312
  import JSON5 from "json5";
2222
2313
  import { parse as parseYaml2 } from "yaml";
2223
2314
 
@@ -2770,6 +2861,7 @@ function projectMintlifyNavigation(config, options = {}) {
2770
2861
  }) : [];
2771
2862
  let tabs = [];
2772
2863
  let i18n;
2864
+ const localizedNavigation = {};
2773
2865
  const projectionTrace = {};
2774
2866
  if (languages.length > 0) {
2775
2867
  const defaultLanguage = languages.find((entry) => entry.default === true) ?? languages[0];
@@ -2805,8 +2897,11 @@ function projectMintlifyNavigation(config, options = {}) {
2805
2897
  if (language === defaultLanguage) {
2806
2898
  tabs = languageTabs;
2807
2899
  for (const reference of references) defaultPageIds.add(reference.navigationId);
2900
+ } else if (languageTabs.length > 0) {
2901
+ localizedNavigation[locale] = languageTabs;
2808
2902
  }
2809
2903
  }
2904
+ if (Object.keys(localizedNavigation).length > 0) i18n.navigation = localizedNavigation;
2810
2905
  } else {
2811
2906
  const context = { references, seenReferences, warnings, warningKeys, pathPrefix: options.pathPrefix };
2812
2907
  tabs = convertContainerToTabs(navigation, context, "Documentation", projectionTrace);
@@ -2964,11 +3059,18 @@ function isDocumentationExtension(filename) {
2964
3059
  return [".md", ".mdx", ".rst", ".txt"].includes(extname2(filename).toLowerCase());
2965
3060
  }
2966
3061
  function pruneMissingNavigationPages(config, availableIds) {
2967
- const pruneNodes = (nodes) => nodes.flatMap((node) => {
2968
- if (typeof node === "string") return availableIds.has(node) ? [node] : [];
2969
- const pages = pruneNodes(node.pages);
2970
- return pages.length > 0 ? [{ ...node, pages }] : [];
2971
- });
3062
+ const pruneNodes = (nodes) => {
3063
+ const seenSiblings = /* @__PURE__ */ new Set();
3064
+ return nodes.flatMap((node) => {
3065
+ if (typeof node === "string") {
3066
+ if (!availableIds.has(node) || seenSiblings.has(node)) return [];
3067
+ seenSiblings.add(node);
3068
+ return [node];
3069
+ }
3070
+ const pages = pruneNodes(node.pages);
3071
+ return pages.length > 0 ? [{ ...node, pages }] : [];
3072
+ });
3073
+ };
2972
3074
  const tabs = config.tabs.flatMap((tab) => {
2973
3075
  const hadPages = (tab.pages?.length ?? 0) > 0 || (tab.groups?.length ?? 0) > 0;
2974
3076
  const pages = tab.pages ? pruneNodes(tab.pages) : void 0;
@@ -3027,19 +3129,19 @@ function storageIdForRoute(routeId) {
3027
3129
  function titleCase2(value) {
3028
3130
  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(" ");
3029
3131
  }
3030
- function routeFromSlug(slug, fallback, sourceDirectory) {
3031
- if (slug === "/") return "introduction";
3032
- if (slug.startsWith("/")) return docusaurusRouteId(slug);
3033
- const fallbackDirectory = sourceDirectory ?? (fallback === "introduction" ? "" : posix.dirname(fallback));
3034
- const resolved = posix.normalize(posix.join(fallbackDirectory, slug));
3132
+ function routeFromSlug(slug2, fallback, sourceDirectory) {
3133
+ if (slug2 === "/") return "introduction";
3134
+ if (slug2.startsWith("/")) return docusaurusRouteId(slug2);
3135
+ const fallbackDirectory = sourceDirectory ?? (fallback === "introduction" ? "" : posix2.dirname(fallback));
3136
+ const resolved = posix2.normalize(posix2.join(fallbackDirectory, slug2));
3035
3137
  if (resolved === ".." || resolved.startsWith("../")) return null;
3036
3138
  return docusaurusRouteId(resolved);
3037
3139
  }
3038
3140
  function resolveDocusaurusPageIdentity(sourcePath, frontmatter, fallback) {
3039
3141
  const sourceDocId = normalizeDocId(sourcePath);
3040
- const sourceDirectory = posix.dirname(sourceDocId);
3142
+ const sourceDirectory = posix2.dirname(sourceDocId);
3041
3143
  const configuredId = typeof frontmatter.id === "string" && frontmatter.id.trim() ? stripNumberPrefix(frontmatter.id.trim()) : null;
3042
- const docId = configuredId ? normalizeDocId(sourceDirectory === "." ? configuredId : posix.join(sourceDirectory, configuredId)) : sourceDocId;
3144
+ const docId = configuredId ? normalizeDocId(sourceDirectory === "." ? configuredId : posix2.join(sourceDirectory, configuredId)) : sourceDocId;
3043
3145
  const defaultNavigationId = defaultDocusaurusRouteId(docId) ?? fallback.navigationId;
3044
3146
  const configuredSlug = typeof frontmatter.slug === "string" ? frontmatter.slug.trim() : "";
3045
3147
  const navigationId = configuredSlug ? routeFromSlug(
@@ -3061,6 +3163,105 @@ function resolveDocusaurusPageIdentity(sourcePath, frontmatter, fallback) {
3061
3163
  function sourceReferenceKey(value) {
3062
3164
  return value.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\.(?:mdx?)$/i, "").replace(/\/$/, "");
3063
3165
  }
3166
+ function sourceHeadings(body) {
3167
+ const headings = [];
3168
+ const lines = body.split("\n");
3169
+ let fence = null;
3170
+ for (let index = 0; index < lines.length; index++) {
3171
+ const line = lines[index];
3172
+ const marker = line.match(/^ {0,3}(`{3,}|~{3,})/);
3173
+ if (marker) {
3174
+ if (!fence) fence = marker[1];
3175
+ else if (marker[1][0] === fence[0] && marker[1].length >= fence.length && /^\s*$/.test(line.slice(marker[0].length))) fence = null;
3176
+ continue;
3177
+ }
3178
+ if (fence) continue;
3179
+ const heading = line.match(/^ {0,3}(#{2,6})\s+(.+?)\s*#*\s*$/);
3180
+ if (!heading) continue;
3181
+ const preceding = lines[index - 1]?.match(/^<a id="([\p{L}\p{N}_.:$-]{1,120})"><\/a>$/u);
3182
+ const plainText = !/[<>{}\[\]`*_]/.test(heading[2]);
3183
+ const generatedId = plainText ? heading[2].normalize("NFC").toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, "-").replace(/(^-|-$)/g, "") : null;
3184
+ headings.push({ index, level: heading[1].length, id: preceding?.[1] ?? generatedId });
3185
+ }
3186
+ return headings;
3187
+ }
3188
+ function addDocusaurusTranslatedHeadingAliases(pages) {
3189
+ const byRoute = new Map(pages.map((page) => [page.navigationId, page]));
3190
+ for (const page of pages) {
3191
+ const locale = page.source.match(/(?:^|[/#])i18n\/([A-Za-z]{2,8}(?:-[A-Za-z0-9]{2,8})*)\/docusaurus-plugin-content-docs\//)?.[1];
3192
+ if (!locale) continue;
3193
+ const originalRoute = page.navigationId === locale ? "introduction" : page.navigationId.startsWith(`${locale}/`) ? page.navigationId.slice(locale.length + 1) : null;
3194
+ if (!originalRoute) continue;
3195
+ const original = byRoute.get(originalRoute);
3196
+ if (!original) continue;
3197
+ const originalHeadings = sourceHeadings(original.body);
3198
+ const translatedHeadings = sourceHeadings(page.body);
3199
+ if (!originalHeadings.length || originalHeadings.length !== translatedHeadings.length || originalHeadings.some((heading, index) => heading.level !== translatedHeadings[index].level)) continue;
3200
+ const occupied = new Set(translatedHeadings.map((heading) => heading.id).filter((id) => !!id));
3201
+ for (const match of page.body.matchAll(/<a\s+id="([^"]+)"/g)) occupied.add(match[1]);
3202
+ const originalIdCounts = /* @__PURE__ */ new Map();
3203
+ for (const heading of originalHeadings) {
3204
+ if (heading.id) originalIdCounts.set(heading.id, (originalIdCounts.get(heading.id) ?? 0) + 1);
3205
+ }
3206
+ const aliases = /* @__PURE__ */ new Map();
3207
+ for (let index = 0; index < originalHeadings.length; index++) {
3208
+ const id = originalHeadings[index].id;
3209
+ if (!id || occupied.has(id) || originalIdCounts.get(id) !== 1) continue;
3210
+ aliases.set(translatedHeadings[index].index, id);
3211
+ occupied.add(id);
3212
+ }
3213
+ if (aliases.size) page.body = page.body.split("\n").flatMap((line, index) => {
3214
+ const alias = aliases.get(index);
3215
+ return alias ? [`<a id="${alias}"></a>`, line] : [line];
3216
+ }).join("\n");
3217
+ }
3218
+ }
3219
+ function expandDocusaurusDocCardLists(pages, config, sourceRoutes) {
3220
+ const pageByRoute = new Map(pages.map((page) => [page.navigationId, page]));
3221
+ const containsRoute = (items, route) => items.some((item) => typeof item === "string" ? item === route : containsRoute(item.pages, route));
3222
+ const findGroup = (items, route) => {
3223
+ for (const item of items) {
3224
+ if (typeof item === "string" || !containsRoute(item.pages, route)) continue;
3225
+ return findGroup(item.pages, route) ?? item;
3226
+ }
3227
+ return void 0;
3228
+ };
3229
+ const firstRoute = (items) => {
3230
+ for (const item of items) {
3231
+ if (typeof item === "string") return item;
3232
+ const candidate = firstRoute(item.pages);
3233
+ if (candidate) return candidate;
3234
+ }
3235
+ return void 0;
3236
+ };
3237
+ for (const page of pages) {
3238
+ if (!sourceRoutes.has(page.navigationId)) continue;
3239
+ const group = config.tabs.map((tab) => findGroup([
3240
+ ...tab.pages ?? [],
3241
+ ...tab.groups ?? []
3242
+ ], page.navigationId)).find(Boolean);
3243
+ if (!group) {
3244
+ page.body = page.body.replace(/<div data-thally-doc-card-list=""\s*\/>/g, "");
3245
+ continue;
3246
+ }
3247
+ const cards = group.pages.flatMap((item) => {
3248
+ if (typeof item === "string") {
3249
+ if (item === page.navigationId) return [];
3250
+ const target = pageByRoute.get(item);
3251
+ return [{ title: target?.title ?? item.split("/").at(-1) ?? item, route: item }];
3252
+ }
3253
+ const slug2 = slugifySegment(item.group);
3254
+ const routes = [...item.pages].flatMap((child) => typeof child === "string" ? [child] : []);
3255
+ const route = routes.find((candidate) => candidate === slug2 || candidate.endsWith(`/${slug2}`)) ?? firstRoute(item.pages);
3256
+ return route ? [{ title: item.group, route }] : [];
3257
+ });
3258
+ const markup = cards.length ? `<CardGroup>
3259
+ ${cards.map(({ title, route }) => ` <Card title={${JSON.stringify(title)}} href={${JSON.stringify(`/${route === "introduction" ? "" : route}`)}} />`).join("\n")}
3260
+ </CardGroup>` : "";
3261
+ page.body = page.body.replace(/<div data-thally-doc-card-list=""\s*\/>/g, markup);
3262
+ if (markup && !page.body.includes(markup)) page.body = [page.body.trim(), markup].filter(Boolean).join("\n\n");
3263
+ }
3264
+ }
3064
3265
  function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
3065
3266
  const routes = /* @__PURE__ */ new Map();
3066
3267
  for (const descriptor of descriptors) {
@@ -3071,18 +3272,30 @@ function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
3071
3272
  const sourceRoute = pageIdFromReference(sourceKey);
3072
3273
  if (sourceRoute) routes.set(sourceRoute, descriptor.navigationId);
3073
3274
  }
3074
- const currentDirectory = posix.dirname(sourceReferenceKey(current.sourcePath));
3275
+ const currentDirectory = posix2.dirname(sourceReferenceKey(current.sourcePath));
3276
+ function copiedAssetTarget(target) {
3277
+ if (!target.startsWith("/") || target.startsWith("//") || target.includes("\\")) return null;
3278
+ const suffixIndex = target.search(/[?#]/);
3279
+ const path = suffixIndex >= 0 ? target.slice(0, suffixIndex) : target;
3280
+ const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
3281
+ if (path.startsWith("/docs/") && options.assetPaths?.has(path.slice("/docs/".length))) {
3282
+ return `/${path.slice("/docs/".length)}${suffix}`;
3283
+ }
3284
+ return options.assetPaths?.has(path.slice(1)) ? target : null;
3285
+ }
3075
3286
  function rewriteTarget(target) {
3076
3287
  if (!target || target.startsWith("#") || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(target)) return target;
3077
3288
  const suffixIndex = target.search(/[?#]/);
3078
3289
  const path = suffixIndex >= 0 ? target.slice(0, suffixIndex) : target;
3079
3290
  const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
3291
+ const copiedAsset = copiedAssetTarget(target);
3292
+ if (copiedAsset) return copiedAsset;
3080
3293
  const candidates = path.startsWith("/") ? [
3081
3294
  sourceReferenceKey(path),
3082
3295
  sourceReferenceKey(path).replace(/^docs\//, ""),
3083
3296
  sourceReferenceKey(path).replace(/^docs\/(?:next|current|latest)\//, "")
3084
3297
  ] : [
3085
- sourceReferenceKey(posix.normalize(posix.join(currentDirectory, path))),
3298
+ sourceReferenceKey(posix2.normalize(posix2.join(currentDirectory, path))),
3086
3299
  sourceReferenceKey(path)
3087
3300
  ];
3088
3301
  const route = candidates.map((candidate) => routes.get(candidate)).find(Boolean);
@@ -3104,7 +3317,12 @@ function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
3104
3317
  return line;
3105
3318
  }
3106
3319
  if (codeFence) return line;
3107
- return line.replace(/(\]\()([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(target)}`).replace(/(\bhref=")([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(target)}${suffix}`);
3320
+ 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) => {
3321
+ const bracketed = destination.startsWith("<") && destination.endsWith(">");
3322
+ const target = bracketed ? destination.slice(1, -1) : destination;
3323
+ const rewritten = rewriteTarget(target);
3324
+ return `${prefix}${bracketed ? `<${rewritten}>` : rewritten}`;
3325
+ });
3108
3326
  }).join("\n");
3109
3327
  }
3110
3328
  function readDocusaurusSiteOrigin(repositoryRoot) {
@@ -3279,6 +3497,14 @@ function configuredSidebarPath(repositoryRoot) {
3279
3497
  const path = resolveWithin(repositoryRoot, filename);
3280
3498
  if (!existsSync3(path)) continue;
3281
3499
  const source = readBoundedText(path);
3500
+ for (const docs of source.matchAll(/\bdocs\s*:\s*/g)) {
3501
+ const object = matchingObjectLiteral(source, (docs.index ?? 0) + docs[0].length);
3502
+ const candidate2 = object && staticStringField(object, "sidebarPath")?.replace(/^\.\//, "");
3503
+ if (candidate2) {
3504
+ const resolved2 = resolveWithin(repositoryRoot, candidate2);
3505
+ if (existsSync3(resolved2) && lstatSync3(resolved2).isFile()) return candidate2;
3506
+ }
3507
+ }
3282
3508
  const match = source.match(/\bsidebarPath\s*:\s*(?:require\.resolve\(\s*)?(['"])([^'"]+)\1/);
3283
3509
  if (!match) continue;
3284
3510
  const candidate = match[2].replace(/^\.\//, "");
@@ -3369,7 +3595,7 @@ function readDocusaurusRedirects(repositoryRoot, warnings) {
3369
3595
  }
3370
3596
  function readCategoryMetadata(contentRoot, directory) {
3371
3597
  for (const filename of CATEGORY_FILENAMES) {
3372
- const path = resolveWithin(contentRoot, posix.join(directory, filename));
3598
+ const path = resolveWithin(contentRoot, posix2.join(directory, filename));
3373
3599
  if (!existsSync3(path) || !lstatSync3(path).isFile()) continue;
3374
3600
  try {
3375
3601
  const raw = readBoundedText(path);
@@ -3414,7 +3640,7 @@ function generatedIndexPage(label, linkValue, context) {
3414
3640
  if (link.type !== "generated-index") return null;
3415
3641
  const fallback = `category/${slugifySegment(label)}`;
3416
3642
  const unprefixedNavigationId = typeof link.slug === "string" ? routeFromSlug(link.slug, fallback) ?? fallback : fallback;
3417
- const navigationId = context.routePrefix && unprefixedNavigationId !== context.routePrefix && !unprefixedNavigationId.startsWith(`${context.routePrefix}/`) ? posix.join(context.routePrefix, unprefixedNavigationId) : unprefixedNavigationId;
3643
+ const navigationId = context.routePrefix && unprefixedNavigationId !== context.routePrefix && !unprefixedNavigationId.startsWith(`${context.routePrefix}/`) ? posix2.join(context.routePrefix, unprefixedNavigationId) : unprefixedNavigationId;
3418
3644
  if (!context.generatedIds.has(navigationId) && !context.descriptors.some((page) => page.navigationId === navigationId)) {
3419
3645
  const description = typeof link.description === "string" ? link.description : `Browse the ${label} documentation.`;
3420
3646
  context.generatedPages.push({
@@ -3460,15 +3686,162 @@ function readDocusaurusThemeColor(repositoryRoot) {
3460
3686
  if (lightMode) colors.dark = lightMode;
3461
3687
  return Object.keys(colors).length > 0 ? colors : void 0;
3462
3688
  }
3463
- function readDocusaurusBrandAssetPaths(repositoryRoot) {
3689
+ function propertyStart(source, name) {
3690
+ const pattern = new RegExp(`(?:^|[,{\\n])\\s*["']?${name}["']?\\s*:\\s*`, "m");
3691
+ const match = pattern.exec(source);
3692
+ return match ? match.index + match[0].length : -1;
3693
+ }
3694
+ function staticStringBindings(source) {
3695
+ const bindings = /* @__PURE__ */ new Map();
3696
+ for (const match of source.matchAll(/\bconst\s+([A-Z][A-Z0-9_]*)\s*=\s*((?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'))/g)) {
3697
+ try {
3698
+ const value = JSON5.parse(match[2]);
3699
+ if (typeof value === "string") bindings.set(match[1], value);
3700
+ } catch {
3701
+ }
3702
+ }
3703
+ return bindings;
3704
+ }
3705
+ function staticArrayField(source, name) {
3706
+ const start = propertyStart(source, name);
3707
+ return start < 0 ? null : matchingArrayLiteral(source, start);
3708
+ }
3709
+ function staticObjectEntries(array) {
3710
+ if (!array) return [];
3711
+ const entries = [];
3712
+ let afterDelimiter = 1;
3713
+ let quote = "";
3714
+ let lineComment = false;
3715
+ let blockComment = false;
3716
+ for (let index = 1; index < array.length - 1; index++) {
3717
+ const char = array[index];
3718
+ const next = array[index + 1];
3719
+ if (lineComment) {
3720
+ if (char === "\n") lineComment = false;
3721
+ continue;
3722
+ }
3723
+ if (blockComment) {
3724
+ if (char === "*" && next === "/") {
3725
+ blockComment = false;
3726
+ index++;
3727
+ }
3728
+ continue;
3729
+ }
3730
+ if (quote) {
3731
+ if (char === "\\") {
3732
+ index++;
3733
+ continue;
3734
+ }
3735
+ if (char === quote) quote = "";
3736
+ continue;
3737
+ }
3738
+ if (char === "/" && next === "/") {
3739
+ lineComment = true;
3740
+ index++;
3741
+ continue;
3742
+ }
3743
+ if (char === "/" && next === "*") {
3744
+ blockComment = true;
3745
+ index++;
3746
+ continue;
3747
+ }
3748
+ if (char === '"' || char === "'" || char === "`") {
3749
+ quote = char;
3750
+ continue;
3751
+ }
3752
+ if (char === ",") {
3753
+ afterDelimiter = index + 1;
3754
+ continue;
3755
+ }
3756
+ if (char !== "{") continue;
3757
+ const literal = matchingObjectLiteral(array, index);
3758
+ if (!literal) continue;
3759
+ const lead = array.slice(afterDelimiter, index).replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, "").trim();
3760
+ if (!lead) entries.push(literal);
3761
+ index += literal.length - 1;
3762
+ }
3763
+ return entries;
3764
+ }
3765
+ function staticStringField(source, name, bindings) {
3766
+ const start = propertyStart(source, name);
3767
+ if (start < 0) return void 0;
3768
+ const quoted = source.slice(start).match(/^("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/);
3769
+ if (!quoted) {
3770
+ const binding = source.slice(start).match(/^([A-Z][A-Z0-9_]*)\b/)?.[1];
3771
+ return binding ? bindings?.get(binding) : void 0;
3772
+ }
3773
+ try {
3774
+ const parsed = JSON5.parse(quoted[1]);
3775
+ return typeof parsed === "string" ? parsed : void 0;
3776
+ } catch {
3777
+ return void 0;
3778
+ }
3779
+ }
3780
+ function readDocusaurusSiteSettings(repositoryRoot) {
3781
+ const empty = { docsRouteBasePath: "docs", defaultColorMode: "light", navbarLinks: [], footerLinks: [] };
3464
3782
  const configPath = findDocusaurusConfigPath(repositoryRoot);
3465
- if (!configPath) return [];
3466
- const configSource = readBoundedText(configPath);
3467
- const favicon = configSource.match(/\bfavicon\s*:\s*(['"])([^'"]+)\1/)?.[2];
3468
- const logoBlock = configSource.match(/\blogo\s*:\s*\{([^}]*)\}/)?.[1];
3469
- const logo = logoBlock?.match(/\bsrc\s*:\s*(['"])([^'"]+)\1/)?.[2];
3470
- const logoDark = logoBlock?.match(/\bsrcDark\s*:\s*(['"])([^'"]+)\1/)?.[2];
3471
- return [favicon, logo, logoDark].filter((path) => typeof path === "string");
3783
+ if (!configPath) return empty;
3784
+ const source = readBoundedText(configPath);
3785
+ const bindings = staticStringBindings(source);
3786
+ const presets = staticArrayField(source, "presets");
3787
+ const classicDocs = presets && [...presets.matchAll(/["']?docs["']?\s*:\s*/g)].map((match) => matchingObjectLiteral(presets, (match.index ?? 0) + match[0].length)).find((object) => Boolean(object));
3788
+ const docsRouteBasePath = classicDocs && staticStringField(classicDocs, "routeBasePath") || "docs";
3789
+ const themeStart = propertyStart(source, "themeConfig");
3790
+ const theme = themeStart < 0 ? null : matchingObjectLiteral(source, themeStart);
3791
+ const colorModeStart = theme ? propertyStart(theme, "colorMode") : -1;
3792
+ const colorMode = colorModeStart < 0 ? null : matchingObjectLiteral(theme, colorModeStart);
3793
+ const configuredColorMode = colorMode ? staticStringField(colorMode, "defaultMode") : void 0;
3794
+ const defaultColorMode = configuredColorMode === "dark" ? "dark" : "light";
3795
+ const navbarStart = theme ? propertyStart(theme, "navbar") : -1;
3796
+ const navbar = navbarStart < 0 ? null : matchingObjectLiteral(theme, navbarStart);
3797
+ const logoStart = navbar ? propertyStart(navbar, "logo") : -1;
3798
+ const logo = logoStart < 0 ? null : matchingObjectLiteral(navbar, logoStart);
3799
+ const footerStart = theme ? propertyStart(theme, "footer") : -1;
3800
+ const footer = footerStart < 0 ? null : matchingObjectLiteral(theme, footerStart);
3801
+ const navbarLinks = navbar ? staticObjectEntries(staticArrayField(navbar, "items")).flatMap((item) => {
3802
+ if (propertyStart(item, "items") >= 0 || propertyStart(item, "dropdownItemsAfter") >= 0) return [];
3803
+ const docsPluginId = staticStringField(item, "docsPluginId", bindings);
3804
+ const href = staticStringField(item, "to", bindings) ?? staticStringField(item, "href", bindings);
3805
+ const docId = staticStringField(item, "docId", bindings);
3806
+ const sidebarId = staticStringField(item, "sidebarId", bindings);
3807
+ const label = staticStringField(item, "label", bindings) ?? staticStringField(item, "aria-label", bindings);
3808
+ return label && (href || docId || sidebarId || docsPluginId) ? [{
3809
+ label,
3810
+ ...href ? { href } : {},
3811
+ ...docId ? { docId } : {},
3812
+ ...sidebarId ? { sidebarId } : {},
3813
+ ...docsPluginId ? { docsPluginId } : {}
3814
+ }] : [];
3815
+ }) : [];
3816
+ const footerLinks = footer ? staticObjectEntries(staticArrayField(footer, "links")).flatMap((column) => {
3817
+ const heading = staticStringField(column, "title", bindings);
3818
+ if (!heading) return [];
3819
+ const items = staticObjectEntries(staticArrayField(column, "items")).flatMap((item) => {
3820
+ const href = staticStringField(item, "to", bindings) ?? staticStringField(item, "href", bindings);
3821
+ const label = staticStringField(item, "label", bindings);
3822
+ return label && href ? [{ label, href }] : [];
3823
+ });
3824
+ return items.length > 0 ? [{ heading, items }] : [];
3825
+ }) : [];
3826
+ const copyright = footer && propertyStart(footer, "copyright") >= 0 ? staticStringField(footer, "copyright") ?? footer.slice(propertyStart(footer, "copyright")).match(/^`([^`]+)`/)?.[1]?.replace(/\$\{new Date\(\)\.getFullYear\(\)\}/g, "{year}") : void 0;
3827
+ return {
3828
+ name: staticStringField(source, "title"),
3829
+ description: staticStringField(source, "tagline"),
3830
+ ...navbar && staticStringField(navbar, "title") ? { navbarTitle: staticStringField(navbar, "title") } : {},
3831
+ defaultColorMode,
3832
+ docsRouteBasePath,
3833
+ ...logo && staticStringField(logo, "src") ? {
3834
+ logo: {
3835
+ light: staticStringField(logo, "src"),
3836
+ ...staticStringField(logo, "srcDark") ? { dark: staticStringField(logo, "srcDark") } : {},
3837
+ showTitle: Boolean(navbar && staticStringField(navbar, "title"))
3838
+ }
3839
+ } : {},
3840
+ ...staticStringField(source, "favicon") ? { favicon: staticStringField(source, "favicon") } : {},
3841
+ navbarLinks,
3842
+ footerLinks,
3843
+ ...copyright ? { copyright } : {}
3844
+ };
3472
3845
  }
3473
3846
  function autogeneratedItems(dirName, context) {
3474
3847
  const normalizedDir = dirName === "." ? "" : normalizeDocId(dirName);
@@ -3480,32 +3853,47 @@ function autogeneratedItems(dirName, context) {
3480
3853
  const childDirectories = /* @__PURE__ */ new Set();
3481
3854
  for (const descriptor of inDirectory) {
3482
3855
  const source = descriptor.sourcePath.replace(/\.(?:mdx?)$/i, "");
3483
- const relativePath = normalizedDir ? posix.relative(normalizedDir, source) : source;
3856
+ const relativePath = normalizedDir ? posix2.relative(normalizedDir, source) : source;
3484
3857
  const segments = relativePath.split("/").filter(Boolean);
3485
3858
  if (segments.length <= 1) directPages.push(descriptor);
3486
3859
  else childDirectories.add(segments[0]);
3487
3860
  }
3488
- const items = directPages.sort(descriptorSort).map((descriptor) => {
3489
- context.referencedNavigationIds.add(descriptor.navigationId);
3490
- return descriptor.navigationId;
3491
- });
3861
+ const pages = directPages.map((descriptor) => ({
3862
+ // An index document is the category landing page in Docusaurus. Thally
3863
+ // renders it as a child link, so put it before the category's other docs.
3864
+ position: descriptor.sidebarPosition ?? (/(?:^|\/)(?:index|readme)\.mdx?$/i.test(descriptor.sourcePath) ? -1 : Number.MAX_SAFE_INTEGER),
3865
+ sortKey: descriptor.sourcePath,
3866
+ item: descriptor.navigationId
3867
+ }));
3492
3868
  const groups = [...childDirectories].map((segment) => {
3493
- const directory = normalizedDir ? posix.join(normalizedDir, segment) : segment;
3869
+ const directory = normalizedDir ? posix2.join(normalizedDir, segment) : segment;
3494
3870
  const metadata = readCategoryMetadata(context.contentRoot, directory);
3495
- const pages = autogeneratedItems(directory, context);
3871
+ const pages2 = autogeneratedItems(directory, context);
3872
+ if (pages2.length === 1 && typeof pages2[0] === "string" && !metadata.label && metadata.position === void 0 && !metadata.link) {
3873
+ const onlyPage = context.descriptors.find((descriptor) => descriptor.navigationId === pages2[0]);
3874
+ if (onlyPage && /(?:^|\/)(?:index|readme)\.mdx?$/i.test(onlyPage.sourcePath)) {
3875
+ return {
3876
+ position: onlyPage.sidebarPosition ?? Number.MAX_SAFE_INTEGER,
3877
+ sortKey: segment,
3878
+ item: pages2[0]
3879
+ };
3880
+ }
3881
+ }
3496
3882
  const landing = generatedIndexPage(metadata.label ?? titleCase2(segment), metadata.link, context);
3497
- if (landing && !pages.includes(landing)) pages.unshift(landing);
3883
+ if (landing && !pages2.includes(landing)) pages2.unshift(landing);
3498
3884
  return {
3499
3885
  position: metadata.position ?? Number.MAX_SAFE_INTEGER,
3500
- segment,
3501
- group: {
3886
+ sortKey: segment,
3887
+ item: {
3502
3888
  group: metadata.label ?? titleCase2(segment),
3503
- pages
3889
+ pages: pages2
3504
3890
  }
3505
3891
  };
3506
- }).sort((left, right) => left.position - right.position || left.segment.localeCompare(right.segment, void 0, { numeric: true }));
3507
- items.push(...groups.map((entry) => entry.group));
3508
- return items;
3892
+ });
3893
+ return [...pages, ...groups].sort((left, right) => left.position - right.position || left.sortKey.localeCompare(right.sortKey, void 0, { numeric: true })).map(({ item }) => {
3894
+ if (typeof item === "string") context.referencedNavigationIds.add(item);
3895
+ return item;
3896
+ });
3509
3897
  }
3510
3898
  function convertItems(value, context) {
3511
3899
  if (typeof value === "string") {
@@ -3555,19 +3943,19 @@ function projectDocusaurusNavigation(input) {
3555
3943
  ]]];
3556
3944
  const tabs = sidebarEntries.flatMap(([sidebarId, value]) => {
3557
3945
  const converted = convertItems(value, context);
3558
- const loosePages = converted.filter((entry) => typeof entry === "string");
3559
- const groups = converted.filter((entry) => typeof entry !== "string");
3560
- if (loosePages.length > 0) groups.unshift({ group: "Overview", pages: loosePages });
3561
- if (groups.length === 0) return [];
3946
+ if (converted.length === 0) return [];
3562
3947
  return [{
3563
3948
  tab: sidebarEntries.length === 1 ? "Documentation" : titleCase2(sidebarId),
3564
- groups
3949
+ // Docusaurus categories are disclosures, including at the sidebar
3950
+ // root. Keeping them as Thally root nodes preserves their collapsed
3951
+ // state and the authored interleaving of documents and categories.
3952
+ pages: converted
3565
3953
  }];
3566
3954
  });
3567
3955
  const unreferenced = input.descriptors.filter((page) => !context.referencedNavigationIds.has(page.navigationId)).sort(descriptorSort).map((page) => page.navigationId);
3568
3956
  if (unreferenced.length > 0) {
3569
- if (tabs.length === 0) tabs.push({ tab: "Documentation", groups: [] });
3570
- tabs[0].groups?.push({ group: "Additional", pages: unreferenced });
3957
+ if (tabs.length === 0) tabs.push({ tab: "Documentation", pages: [] });
3958
+ tabs[0].pages?.push({ group: "Additional", pages: unreferenced });
3571
3959
  }
3572
3960
  return {
3573
3961
  docsConfig: { tabs },
@@ -3580,8 +3968,35 @@ function projectDocusaurusNavigation(input) {
3580
3968
  // src/fern.ts
3581
3969
  import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as readFileSync4, readdirSync } from "fs";
3582
3970
  import { basename as basename2, dirname as dirname3, extname as extname4, relative as relative4 } from "path";
3971
+ import { load } from "cheerio";
3583
3972
  import { parse as parseYaml3 } from "yaml";
3584
3973
  var MAX_CONFIG_BYTES2 = 2e6;
3974
+ function announcementMarkdown(message) {
3975
+ const document = load(message);
3976
+ document("script, style, iframe, object, embed, svg, math").remove();
3977
+ const escapeText = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/([\\`*_\[\]{}()!])/g, "\\$1");
3978
+ const links = /* @__PURE__ */ new Map();
3979
+ document("a").each((index, element) => {
3980
+ const marker = `THALLYANNOUNCEMENTLINK${index}TOKEN`;
3981
+ const label = document(element).text().trim();
3982
+ const rawHref = document(element).attr("href")?.trim() ?? "";
3983
+ let href;
3984
+ if (/^https?:\/\//i.test(rawHref)) {
3985
+ try {
3986
+ href = new URL(rawHref).toString();
3987
+ } catch {
3988
+ }
3989
+ } else if (rawHref.startsWith("/") && !rawHref.startsWith("//") && !/[\\<>\s]/.test(rawHref)) {
3990
+ href = rawHref;
3991
+ }
3992
+ const destination = href?.replace(/\(/g, "%28").replace(/\)/g, "%29");
3993
+ links.set(marker, destination && label ? `[${escapeText(label)}](${destination})` : escapeText(label));
3994
+ document(element).replaceWith(marker);
3995
+ });
3996
+ let text = escapeText(document("body").text());
3997
+ for (const [marker, link] of links) text = text.replaceAll(marker, link);
3998
+ return text.trim();
3999
+ }
3585
4000
  function objectValue3(value) {
3586
4001
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3587
4002
  }
@@ -3648,7 +4063,7 @@ function uniqueNavigationId(base, context) {
3648
4063
  while (context.seenNavigationIds.has(`${base}-${index}`)) index++;
3649
4064
  return `${base}-${index}`;
3650
4065
  }
3651
- function registerPageAt(rawPath, base, context) {
4066
+ function registerPageAt(rawPath, base, context, navTitle, hidden = false) {
3652
4067
  let sourcePath;
3653
4068
  try {
3654
4069
  const absolute = resolveWithinRoot(
@@ -3663,9 +4078,16 @@ function registerPageAt(rawPath, base, context) {
3663
4078
  }
3664
4079
  const navigationId = uniqueNavigationId(base || "introduction", context);
3665
4080
  context.seenNavigationIds.add(navigationId);
3666
- context.descriptors.push({ sourcePath, navigationId });
4081
+ const isHidden = hidden || context.hiddenInherited === true;
4082
+ context.descriptors.push({ sourcePath, navigationId, ...navTitle ? { navTitle } : {}, ...isHidden ? { hidden: true } : {} });
3667
4083
  const kept = context.sourcePathToNavigationId.get(sourcePath);
3668
- if (kept) return kept;
4084
+ if (kept) {
4085
+ if (!isHidden) {
4086
+ const owner = context.descriptors.find((descriptor) => descriptor.navigationId === kept);
4087
+ if (owner) owner.hidden = false;
4088
+ }
4089
+ return kept;
4090
+ }
3669
4091
  context.sourcePathToNavigationId.set(sourcePath, navigationId);
3670
4092
  return navigationId;
3671
4093
  }
@@ -3702,8 +4124,11 @@ function registerFolder(object, parentSegments, context) {
3702
4124
  const stem = basename2(entry.name, extname4(entry.name));
3703
4125
  const pageSegment = stem.toLowerCase() === "index" ? null : fernBasicSlug(stem);
3704
4126
  const route = [...routeSegments, ...pageSegment ? [pageSegment] : []].join("/");
3705
- const registered = registerPageAt(relative4(context.fernRoot, path), route, { ...context, pathPrefix: "" });
3706
- if (registered) pages2.push(registered);
4127
+ const source = lstatSync4(path).size <= MAX_CONFIG_BYTES2 ? readFileSync4(path, "utf8") : "";
4128
+ const frontmatter = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] ?? "";
4129
+ const hidden = /^hidden:\s*true\s*$/m.test(frontmatter);
4130
+ const registered = registerPageAt(relative4(context.fernRoot, path), route, { ...context, pathPrefix: "" }, void 0, hidden);
4131
+ if (registered && !hidden && !context.hiddenInherited) pages2.push(registered);
3707
4132
  }
3708
4133
  }
3709
4134
  return pages2;
@@ -3718,13 +4143,19 @@ function registerPage(object, parentSegments, context) {
3718
4143
  const label = typeof object.page === "string" && object.page.trim() ? object.page.trim() : "Untitled";
3719
4144
  const segment = segmentFor(object, label, context);
3720
4145
  const base = [...parentSegments, segment].filter(Boolean).join("/");
3721
- return registerPageAt(object.path, base, context) || null;
4146
+ return registerPageAt(object.path, base, context, label, object.hidden === true) || null;
3722
4147
  }
3723
4148
  function convertNode(node, parentSegments, context) {
3724
4149
  const object = objectValue3(node);
3725
4150
  if (!object) return null;
3726
- if (typeof object.page === "string") return registerPage(object, parentSegments, context);
3727
- if (typeof object.folder === "string") return registerFolder(object, parentSegments, context);
4151
+ if (typeof object.page === "string") {
4152
+ const page = registerPage(object, parentSegments, context);
4153
+ return object.hidden === true || context.hiddenInherited ? null : page;
4154
+ }
4155
+ if (typeof object.folder === "string") {
4156
+ const folder = registerFolder(object, parentSegments, object.hidden === true ? { ...context, hiddenInherited: true } : context);
4157
+ return object.hidden === true || context.hiddenInherited ? null : folder;
4158
+ }
3728
4159
  if (typeof object.section === "string") {
3729
4160
  const label = object.section;
3730
4161
  const segment = segmentFor(object, label, context);
@@ -3733,15 +4164,16 @@ function convertNode(node, parentSegments, context) {
3733
4164
  const pages = [];
3734
4165
  const hasOwnPath = typeof object.path === "string" && object.path.trim().length > 0;
3735
4166
  if (hasOwnPath) {
3736
- const registered = registerPageAt(object.path, segments.join("/"), context);
4167
+ const registered = registerPageAt(object.path, segments.join("/"), context, label, object.hidden === true);
3737
4168
  if (registered) pages.push(registered);
3738
4169
  }
3739
4170
  for (const child of contents) {
3740
- const converted = convertNode(child, segments, context);
4171
+ const converted = convertNode(child, segments, object.hidden === true ? { ...context, hiddenInherited: true } : context);
3741
4172
  if (converted) pages.push(converted);
3742
4173
  }
3743
4174
  if (pages.length === 0) return null;
3744
4175
  if (!hasOwnPath && segment) addBareRouteRedirect(segments, pages, context);
4176
+ if (object.hidden === true || context.hiddenInherited) return null;
3745
4177
  return {
3746
4178
  group: label,
3747
4179
  ...typeof object.icon === "string" ? { icon: object.icon } : {},
@@ -3750,11 +4182,23 @@ function convertNode(node, parentSegments, context) {
3750
4182
  };
3751
4183
  }
3752
4184
  if (typeof object.link === "string") {
3753
- warnOnce2(
3754
- context,
3755
- "fern-link",
3756
- 'Fern navigation "link" entries are external and cannot be nested inside a Thally group; add them to the navbar manually.'
3757
- );
4185
+ const label = object.link.trim();
4186
+ const rawHref = typeof object.href === "string" ? object.href.trim() : "";
4187
+ let href;
4188
+ if (/^https?:\/\//i.test(rawHref) && !/[\\\u0000-\u0020\u007f]/.test(rawHref)) {
4189
+ try {
4190
+ const url = new URL(rawHref);
4191
+ if (["http:", "https:"].includes(url.protocol) && !url.username && !url.password) href = url.toString();
4192
+ } catch {
4193
+ }
4194
+ }
4195
+ if (label && href && object.hidden !== true && !context.hiddenInherited) {
4196
+ if (!context.externalLinks.some((link) => link.label === label && link.href === href)) {
4197
+ context.externalLinks.push({ label, href });
4198
+ }
4199
+ } else if (!href && object.hidden !== true && !context.hiddenInherited) {
4200
+ warnOnce2(context, `fern-link-${rawHref}`, `Fern navigation link "${label}" has an unsafe or invalid external URL and was skipped.`);
4201
+ }
3758
4202
  return null;
3759
4203
  }
3760
4204
  if (typeof object.api === "string") {
@@ -3766,8 +4210,7 @@ function convertNode(node, parentSegments, context) {
3766
4210
  return null;
3767
4211
  }
3768
4212
  if (typeof object.changelog === "string") {
3769
- warnOnce2(context, "fern-changelog", "Fern changelog sections are not supported and were omitted.");
3770
- return null;
4213
+ return registerFolder({ folder: object.changelog, title: "Changelog", "skip-slug": true }, parentSegments, context);
3771
4214
  }
3772
4215
  return null;
3773
4216
  }
@@ -3840,6 +4283,43 @@ function buildTabsFromConfig(rawConfig, configDir, fernRoot, routePrefix, fallba
3840
4283
  for (let index = sectionsBefore; index < context.apiSections.length; index += 1) {
3841
4284
  context.apiSections[index].tabLabel ??= label;
3842
4285
  }
4286
+ if (groups.length === 0 && typeof meta.href === "string" && /^https?:\/\//i.test(meta.href)) {
4287
+ return [{ tab: label, href: meta.href }];
4288
+ }
4289
+ if (groups.length === 0 && typeof meta.changelog === "string") {
4290
+ const changelog = registerFolder({ folder: meta.changelog, title: label, "skip-slug": true }, segments, context);
4291
+ if (changelog) {
4292
+ const indexRoute = segments.join("/");
4293
+ const descriptors = new Map(context.descriptors.map((descriptor) => [descriptor.navigationId, descriptor]));
4294
+ const visiblePages = changelog.pages.filter((page) => typeof page === "string").filter((page) => !descriptors.get(page)?.hidden);
4295
+ const index = visiblePages.find((page) => {
4296
+ const descriptor = descriptors.get(page);
4297
+ if (!descriptor) return false;
4298
+ const stem = basename2(descriptor.sourcePath, extname4(descriptor.sourcePath));
4299
+ return page === indexRoute || stem === "overview" || stem === "index";
4300
+ });
4301
+ const entries = visiblePages.filter((page) => page !== index).sort((left, right) => {
4302
+ const date = (page) => descriptors.get(page)?.sourcePath.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? "";
4303
+ return date(right).localeCompare(date(left)) || left.localeCompare(right);
4304
+ });
4305
+ for (const entryId of entries) {
4306
+ const descriptor = descriptors.get(entryId);
4307
+ if (descriptor) descriptor.hidden = true;
4308
+ }
4309
+ changelog.pages = [index ?? indexRoute];
4310
+ context.changelogIndexes.push({ route: indexRoute, entries });
4311
+ const latest = index ?? firstLeafId(changelog.pages);
4312
+ const bareRouteIndex = context.bareRouteRedirects.findIndex((redirect) => redirect.source === `/${indexRoute}`);
4313
+ const indexDescriptor = index ? descriptors.get(index) : void 0;
4314
+ const indexSource = indexDescriptor && lstatSync4(resolveWithinRoot(context.fernRoot, indexDescriptor.sourcePath, context.repositoryRoot)).size <= MAX_CONFIG_BYTES2 ? readFileSync4(resolveWithinRoot(context.fernRoot, indexDescriptor.sourcePath, context.repositoryRoot), "utf8") : "";
4315
+ const indexFrontmatter = indexSource.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] ?? "";
4316
+ const indexSlug = indexFrontmatter.match(/^slug:\s*["']?([^\s"']+)["']?\s*$/m)?.[1]?.replace(/^\/+|\/+$/g, "");
4317
+ const hasBarePage = index === indexRoute || indexSlug === indexRoute;
4318
+ if (bareRouteIndex >= 0 && hasBarePage) context.bareRouteRedirects.splice(bareRouteIndex, 1);
4319
+ else if (latest && bareRouteIndex >= 0) context.bareRouteRedirects[bareRouteIndex].destination = `/${latest}`;
4320
+ return [{ tab: label, groups: [changelog] }];
4321
+ }
4322
+ }
3843
4323
  if (groups.length === 0) return [];
3844
4324
  if (segments.length > 0) addBareRouteRedirect(segments, groups, context);
3845
4325
  return [{
@@ -3916,8 +4396,10 @@ function projectFernNavigation(input) {
3916
4396
  descriptors: [],
3917
4397
  seenNavigationIds: /* @__PURE__ */ new Set(),
3918
4398
  apiSections: [],
4399
+ changelogIndexes: [],
3919
4400
  warnings: [],
3920
4401
  warningKeys: /* @__PURE__ */ new Set(),
4402
+ externalLinks: [],
3921
4403
  bareRouteRedirects: [],
3922
4404
  segmentAliases: /* @__PURE__ */ new Map(),
3923
4405
  pathPrefix: "",
@@ -3938,6 +4420,14 @@ function projectFernNavigation(input) {
3938
4420
  const label = typeof link?.text === "string" ? link.text : typeof link?.label === "string" ? link.label : link?.type === "github" ? "GitHub" : void 0;
3939
4421
  return href && label ? [{ label, href, ...link?.type === "github" ? { type: "github" } : {} }] : [];
3940
4422
  }) : [];
4423
+ for (const link of context.externalLinks) {
4424
+ if (!navbarLinks.some((existing) => existing.label === link.label && existing.href === link.href)) {
4425
+ navbarLinks.push(link);
4426
+ }
4427
+ }
4428
+ const announcement = objectValue3(config.announcement);
4429
+ const announcementMessage = typeof announcement?.message === "string" ? announcement.message.trim() : "";
4430
+ const bannerContent = announcementMarkdown(announcementMessage);
3941
4431
  const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
3942
4432
  const redirect = objectValue3(value);
3943
4433
  if (!redirect || typeof redirect.source !== "string" || typeof redirect.destination !== "string") return [];
@@ -3959,13 +4449,6 @@ function projectFernNavigation(input) {
3959
4449
  ...typeof redirect.permanent === "boolean" ? { permanent: redirect.permanent } : {}
3960
4450
  }];
3961
4451
  }) : [];
3962
- if (config.logo || config.favicon) {
3963
- warnOnce2(
3964
- context,
3965
- "fern-branding",
3966
- "Fern logo and favicon were not migrated; add them to public/brand/ (default-logo-light.svg, default-logo-dark.svg, default-favicon-light.svg, default-favicon-dark.svg)."
3967
- );
3968
- }
3969
4452
  const authoredSources = new Set(redirects.map((redirect) => redirect.source));
3970
4453
  const knownIds = new Set(context.descriptors.map((descriptor) => descriptor.navigationId));
3971
4454
  const keptNavigationId = (descriptor) => context.sourcePathToNavigationId.get(descriptor.sourcePath) ?? descriptor.navigationId;
@@ -4009,12 +4492,14 @@ function projectFernNavigation(input) {
4009
4492
  return {
4010
4493
  docsConfig: {
4011
4494
  tabs,
4495
+ ...bannerContent ? { banner: { content: bannerContent, dismissible: true } } : {},
4012
4496
  ...navbarLinks.length > 0 ? { navbar: { links: navbarLinks } } : {},
4013
4497
  ...allRedirects.length > 0 ? { redirects: allRedirects } : {}
4014
4498
  },
4015
4499
  descriptors: context.descriptors,
4016
4500
  warnings: context.warnings,
4017
- apiSections: disambiguatedApiSections
4501
+ apiSections: disambiguatedApiSections,
4502
+ changelogIndexes: context.changelogIndexes
4018
4503
  };
4019
4504
  }
4020
4505
 
@@ -4635,12 +5120,6 @@ function fernThemeColors(value) {
4635
5120
  if (lightMode) colors.dark = lightMode;
4636
5121
  return Object.keys(colors).length > 0 ? colors : void 0;
4637
5122
  }
4638
- function mintlifyBrandAssetPaths(value) {
4639
- if (typeof value === "string") return [value];
4640
- if (!value || typeof value !== "object") return [];
4641
- const source = value;
4642
- return ["light", "dark"].map((key) => source[key]).filter((entry) => typeof entry === "string");
4643
- }
4644
5123
  function normalizedReferenceKey(value) {
4645
5124
  return value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/").replace(/\.(?:mdx?|rst|txt)$/i, "").replace(/\/(?:index|readme)$/i, "").replace(/^(?:index|readme)$/i, "") || "introduction";
4646
5125
  }
@@ -4679,7 +5158,7 @@ function mintlifyTopLevelApiReferences(config) {
4679
5158
  }
4680
5159
  return references;
4681
5160
  }
4682
- function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings) {
5161
+ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
4683
5162
  if (!mintlifyConfig) return [];
4684
5163
  const references = [
4685
5164
  ...mintlifyTopLevelApiReferences(mintlifyConfig),
@@ -4699,13 +5178,19 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings) {
4699
5178
  });
4700
5179
  continue;
4701
5180
  }
4702
- if (/^https?:\/\//i.test(reference.value)) {
5181
+ if (/^https:\/\//i.test(reference.value)) {
5182
+ remoteSpecs.push({ url: reference.value, ...reference.tabLabel ? { tabLabel: reference.tabLabel } : {} });
4703
5183
  warnings.push({
4704
5184
  code: "unsupported-config",
4705
- 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.`
5185
+ message: `The remote OpenAPI spec "${reference.value}"${tabSuffix} requires a network download before this import is complete.`,
5186
+ source: reference.value
4706
5187
  });
4707
5188
  continue;
4708
5189
  }
5190
+ if (/^http:\/\//i.test(reference.value)) {
5191
+ warnings.push({ code: "unsupported-config", message: `The OpenAPI spec "${reference.value}"${tabSuffix} uses insecure HTTP and was not imported.`, source: reference.value });
5192
+ continue;
5193
+ }
4709
5194
  const key = reference.value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/");
4710
5195
  const match = files.find((file) => file.relativePath === key);
4711
5196
  if (!match) {
@@ -4947,7 +5432,7 @@ function fernSourceSiteUrl(config) {
4947
5432
  function externalizeMissingFernLinks(body, pageIds, sourceSiteUrl, externalized) {
4948
5433
  const rewrite = (target) => {
4949
5434
  const path = target.split(/[?#]/, 1)[0].replace(/^\//, "").replace(/\/$/, "");
4950
- if (!path || pageIds.has(path)) return target;
5435
+ if (!path || pageIds.has(path) || ASSET_EXTENSIONS.has(extname5(path).toLowerCase())) return target;
4951
5436
  try {
4952
5437
  const base = new URL(`${sourceSiteUrl}/`);
4953
5438
  const sourceLinkUrl = new URL(target.slice(1), base);
@@ -4983,15 +5468,20 @@ function rewriteMintlifyMountedLinks(body, pageIds) {
4983
5468
  };
4984
5469
  return replaceOutsideCode(body, (text) => text.replace(/(\]\()(\/[^\s)]+)(\))/g, (_match, before, target, after) => `${before}${rewrite(target)}${after}`).replace(/(\b(?:href|to)=(['"]))(\/[^'"\n]+)\2/g, (_match, before, quote, target) => `${before}${rewrite(target)}${quote}`));
4985
5470
  }
4986
- function preserveDocusaurusLinkedAnchors(pages) {
5471
+ function preserveLinkedAnchors(pages) {
4987
5472
  const pageById = new Map(pages.map((page) => [page.id, page]));
4988
5473
  const requested = /* @__PURE__ */ new Map();
4989
5474
  const crossPageRequests = /* @__PURE__ */ new Set();
4990
5475
  const addTarget = (target, currentId) => {
4991
5476
  const hash2 = target.indexOf("#");
4992
5477
  if (hash2 < 0) return;
4993
- const fragment = target.slice(hash2 + 1);
4994
- if (!/^[A-Za-z0-9_.:-]{1,120}$/.test(fragment)) return;
5478
+ let fragment;
5479
+ try {
5480
+ fragment = decodeURIComponent(target.slice(hash2 + 1));
5481
+ } catch {
5482
+ return;
5483
+ }
5484
+ if (!/^[\p{L}\p{N}_.:$-]{1,120}$/u.test(fragment)) return;
4995
5485
  const path = target.slice(0, hash2).split("?")[0];
4996
5486
  const id = target.startsWith("#") ? currentId : path.startsWith("/") && !path.startsWith("//") ? path.replace(/^\//, "").replace(/\/$/, "") || "introduction" : "";
4997
5487
  if (!pageById.has(id)) return;
@@ -5066,6 +5556,43 @@ function preserveDocusaurusLinkedAnchors(pages) {
5066
5556
  ]).join("\n");
5067
5557
  }
5068
5558
  }
5559
+ function closeOpenCodeFence(body) {
5560
+ let open = null;
5561
+ for (const line of body.split(/\r?\n/)) {
5562
+ const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
5563
+ if (!match) continue;
5564
+ const marker = match[1][0];
5565
+ if (!open) {
5566
+ open = { marker, length: match[1].length };
5567
+ } else if (marker === open.marker && match[1].length >= open.length && !match[2].trim()) {
5568
+ open = null;
5569
+ }
5570
+ }
5571
+ return open ? `${body}
5572
+
5573
+ ${open.marker.repeat(open.length)}` : body;
5574
+ }
5575
+ function addDatedHeadingAliases(body, date) {
5576
+ let open = null;
5577
+ const existingIds = new Set([...body.matchAll(/<a\s+id=["']([^"']+)["']/g)].map((match) => match[1]));
5578
+ return body.split(/\r?\n/).flatMap((line) => {
5579
+ const fence = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
5580
+ if (fence) {
5581
+ const marker = fence[1][0];
5582
+ if (!open) open = { marker, length: fence[1].length };
5583
+ else if (marker === open.marker && fence[1].length >= open.length && !fence[2].trim()) open = null;
5584
+ return [line];
5585
+ }
5586
+ if (open) return [line];
5587
+ const heading = line.match(/^#{2,6}\s+(.+?)\s*#*\s*$/);
5588
+ if (!heading) return [line];
5589
+ const slug2 = heading[1].replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_]/g, "").normalize("NFKD").toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/\s+/g, "-");
5590
+ const id = `${date}-${slug2}`;
5591
+ if (!slug2 || existingIds.has(id)) return [line];
5592
+ existingIds.add(id);
5593
+ return [`<a id="${id}" />`, "", line];
5594
+ }).join("\n");
5595
+ }
5069
5596
  function resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot) {
5070
5597
  sourcePath = sourcePath.replace(/\\(?=_)/g, "");
5071
5598
  const repositoryRelative = sourcePath.startsWith("@site/") ? relative5(repositoryRoot, resolveWithin(siteRoot, sourcePath.slice("@site/".length))).replace(/\\/g, "/") : sourcePath.startsWith("/") ? relative5(repositoryRoot, resolveWithin(siteRoot, sourcePath.replace(/^\/+/, ""))).replace(/\\/g, "/") : relative5(repositoryRoot, resolvePath(dirname4(currentFile), sourcePath)).replace(/\\/g, "/");
@@ -5113,7 +5640,11 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
5113
5640
  }
5114
5641
  if (!ASSET_EXTENSIONS.has(extname5(decodedPath).toLowerCase())) return null;
5115
5642
  try {
5116
- const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithin(dirname4(currentFile), decodedPath);
5643
+ const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithinRoot(dirname4(currentFile), decodedPath, siteRoot);
5644
+ const realRoot = realpathSync2(siteRoot);
5645
+ const realCandidate = realpathSync2(candidate);
5646
+ const realRelative = relative5(realRoot, realCandidate);
5647
+ if (realRelative === ".." || realRelative.startsWith(`..${sep2}`) || isAbsolute2(realRelative)) return null;
5117
5648
  const siteRelative = relative5(siteRoot, candidate).replace(/\\/g, "/");
5118
5649
  resolveWithin(siteRoot, siteRelative);
5119
5650
  if (!existsSync5(candidate) || !lstatSync5(candidate).isFile()) return null;
@@ -5348,8 +5879,11 @@ function migrateRepository(options) {
5348
5879
  let fernBasePath = "";
5349
5880
  let fernVersionPath = "";
5350
5881
  let fernApiSections = [];
5882
+ let fernChangelogIndexes = [];
5351
5883
  const fernReferencedPaths = /* @__PURE__ */ new Set();
5352
5884
  const fernSourceLinkAliases = /* @__PURE__ */ new Map();
5885
+ const fernNavTitles = /* @__PURE__ */ new Map();
5886
+ const fernHiddenIds = /* @__PURE__ */ new Set();
5353
5887
  if (platform === "mintlify") {
5354
5888
  try {
5355
5889
  const config = readMintlifyConfig(mintlifyProjectRoot ?? repositoryDir);
@@ -5384,8 +5918,13 @@ function migrateRepository(options) {
5384
5918
  docsConfig = projected.docsConfig;
5385
5919
  warnings.push(...projected.warnings);
5386
5920
  fernApiSections = projected.apiSections;
5921
+ fernChangelogIndexes = projected.changelogIndexes;
5387
5922
  for (const [index, descriptor] of projected.descriptors.entries()) {
5388
5923
  fernReferencedPaths.add(descriptor.sourcePath);
5924
+ if (descriptor.navTitle && !fernNavTitles.has(descriptor.navigationId)) {
5925
+ fernNavTitles.set(descriptor.navigationId, descriptor.navTitle);
5926
+ }
5927
+ if (descriptor.hidden) fernHiddenIds.add(descriptor.navigationId);
5389
5928
  const sourceTail = descriptor.sourcePath.match(/(?:^|\/)pages\/(.+)\.mdx?$/i)?.[1]?.replace(/\/index$/i, "");
5390
5929
  if (sourceTail) {
5391
5930
  const routeSegments = descriptor.navigationId.split("/");
@@ -5440,6 +5979,7 @@ function migrateRepository(options) {
5440
5979
  }
5441
5980
  const pages = [];
5442
5981
  const assets = [];
5982
+ const remoteApiSpecs = [];
5443
5983
  const referencedAssetPaths = /* @__PURE__ */ new Map();
5444
5984
  const addAssetReference = (assetPath, referencingPage) => {
5445
5985
  const referrers = referencedAssetPaths.get(assetPath);
@@ -5447,6 +5987,7 @@ function migrateRepository(options) {
5447
5987
  else referencedAssetPaths.set(assetPath, /* @__PURE__ */ new Set([referencingPage]));
5448
5988
  };
5449
5989
  const docusaurusDescriptors = [];
5990
+ const docusaurusDocCardRoutes = /* @__PURE__ */ new Set();
5450
5991
  const seenPageIds = /* @__PURE__ */ new Set();
5451
5992
  const fernIdRenames = /* @__PURE__ */ new Map();
5452
5993
  let skipped = 0;
@@ -5541,6 +6082,10 @@ function migrateRepository(options) {
5541
6082
  }
5542
6083
  raw = escapeFernLiteralBraces(protectedMath.body);
5543
6084
  }
6085
+ const hasDocusaurusDocCardList = platform === "docusaurus" && /<DocCardList\b[^>]*\/>/.test(raw);
6086
+ if (hasDocusaurusDocCardList) {
6087
+ raw = raw.replace(/<DocCardList\b[^>]*\/>/g, '<div data-thally-doc-card-list="" />');
6088
+ }
5544
6089
  let docusaurusDescriptor;
5545
6090
  const page = parseMarkdownPage({
5546
6091
  id,
@@ -5574,9 +6119,9 @@ function migrateRepository(options) {
5574
6119
  // hierarchy (never just its own segment) and wins over a docs.yml slug.
5575
6120
  ...platform === "fern" ? {
5576
6121
  resolveIdentity: (frontmatter, fallback) => {
5577
- const slug = typeof frontmatter.slug === "string" ? frontmatter.slug.trim() : "";
5578
- if (!slug) return fallback;
5579
- const overridden = pageIdFromReference(slug, true);
6122
+ const slug2 = typeof frontmatter.slug === "string" ? frontmatter.slug.trim() : "";
6123
+ if (!slug2) return fallback;
6124
+ const overridden = pageIdFromReference(slug2, true);
5580
6125
  if (!overridden) return fallback;
5581
6126
  fernIdRenames.set(fallback.navigationId, overridden);
5582
6127
  return { ...fallback, id: overridden, navigationId: overridden };
@@ -5587,6 +6132,11 @@ function migrateRepository(options) {
5587
6132
  skipped++;
5588
6133
  continue;
5589
6134
  }
6135
+ if (platform === "fern" && !page.navTitle) {
6136
+ const navTitle = fernNavTitles.get(navigationId);
6137
+ if (navTitle && navTitle !== page.title) page.navTitle = navTitle;
6138
+ }
6139
+ if (platform === "fern" && fernHiddenIds.has(navigationId)) page.hidden = true;
5590
6140
  if (platform === "mintlify" && mintlifyProjectRoot) {
5591
6141
  page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, mintlifyProjectRoot, (assetPath) => {
5592
6142
  addAssetReference(assetPath, file.relativePath);
@@ -5596,6 +6146,9 @@ function migrateRepository(options) {
5596
6146
  page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, fernProjectRoot, (assetPath) => {
5597
6147
  addAssetReference(assetPath, file.relativePath);
5598
6148
  });
6149
+ page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, repositoryDir, (assetPath) => {
6150
+ addAssetReference(assetPath, file.relativePath);
6151
+ });
5599
6152
  page.body = stripFernBasePathFromLinks(page.body, fernBasePath);
5600
6153
  page.body = stripFernBasePathFromLinks(page.body, fernVersionPath);
5601
6154
  }
@@ -5650,6 +6203,9 @@ function migrateRepository(options) {
5650
6203
  }
5651
6204
  seenPageIds.add(page.id);
5652
6205
  pages.push(page);
6206
+ if (hasDocusaurusDocCardList) {
6207
+ docusaurusDocCardRoutes.add(page.navigationId);
6208
+ }
5653
6209
  if (platform === "mintlify") {
5654
6210
  const sourcePath = exactReferenceKey(file.relativePath);
5655
6211
  if (sourcePath !== page.id && /^[A-Za-z0-9_./-]+$/.test(sourcePath)) {
@@ -5667,6 +6223,43 @@ function migrateRepository(options) {
5667
6223
  }
5668
6224
  }
5669
6225
  const docusaurusAssetRoot = docusaurusProjectRoot ?? repositoryDir;
6226
+ const fernBrandAssetPaths = /* @__PURE__ */ new Map();
6227
+ const fernReferencedAssets = [];
6228
+ if (platform === "fern" && fernProjectRoot) {
6229
+ const seen = /* @__PURE__ */ new Set();
6230
+ const addFernAsset = (candidate, source) => {
6231
+ try {
6232
+ const absolutePath = source ? resolveWithinRoot(fernProjectRoot, source.replace(/^\/+/, ""), repositoryDir) : resolveWithin(repositoryDir, candidate);
6233
+ if (!existsSync5(absolutePath) || !lstatSync5(absolutePath).isFile()) return;
6234
+ const realRoot = realpathSync2(repositoryDir);
6235
+ const realPath = realpathSync2(absolutePath);
6236
+ const confined = relative5(realRoot, realPath);
6237
+ if (confined === ".." || confined.startsWith(`..${sep2}`) || isAbsolute2(confined)) return;
6238
+ const withinFern = relative5(fernProjectRoot, absolutePath);
6239
+ const assetRelative = withinFern !== ".." && !withinFern.startsWith(`..${sep2}`) && !isAbsolute2(withinFern) ? withinFern : relative5(repositoryDir, absolutePath);
6240
+ const assetPath = normalizeAssetPath(assetRelative.replace(/\\/g, "/"));
6241
+ if (!assetPath || !ASSET_EXTENSIONS.has(extname5(assetPath).toLowerCase())) return;
6242
+ if (source) {
6243
+ fernBrandAssetPaths.set(source, assetPath);
6244
+ addAssetReference(assetPath, "fern/docs.yml");
6245
+ }
6246
+ if (seen.has(absolutePath)) return;
6247
+ seen.add(absolutePath);
6248
+ fernReferencedAssets.push({ absolutePath, relativePath: assetPath });
6249
+ } catch {
6250
+ }
6251
+ };
6252
+ for (const path of referencedAssetPaths.keys()) addFernAsset(path);
6253
+ for (const value of [fernRawConfig?.logo, fernRawConfig?.favicon]) {
6254
+ if (typeof value === "string") addFernAsset("", value);
6255
+ else if (value && typeof value === "object" && !Array.isArray(value)) {
6256
+ const variants = value;
6257
+ for (const candidate of [variants.light, variants.dark, variants.src, variants.srcDark]) {
6258
+ if (typeof candidate === "string") addFernAsset("", candidate);
6259
+ }
6260
+ }
6261
+ }
6262
+ }
5670
6263
  const repositoryAssets = platform === "docusaurus" && configuredDocsDir && !options.docusaurusSkipAssets ? ["static", "public"].flatMap((directory) => {
5671
6264
  const root = resolveWithin(docusaurusAssetRoot, directory);
5672
6265
  if (!existsSync5(root) || !lstatSync5(root).isDirectory()) return [];
@@ -5676,10 +6269,10 @@ function migrateRepository(options) {
5676
6269
  }));
5677
6270
  }) : [];
5678
6271
  const assetCandidates = [];
5679
- for (const file of [...files, ...repositoryAssets]) {
6272
+ for (const file of [...files, ...repositoryAssets, ...fernReferencedAssets]) {
5680
6273
  const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
5681
6274
  if (!ASSET_EXTENSIONS.has(extname5(file.relativePath).toLowerCase())) continue;
5682
- if (platform !== "mintlify" && !ASSET_DIRECTORIES.has(firstSegment)) continue;
6275
+ if (platform !== "mintlify" && !ASSET_DIRECTORIES.has(firstSegment) && !(platform === "fern" && referencedAssetPaths.has(file.relativePath))) continue;
5683
6276
  const isDocusaurusStatic = platform === "docusaurus" && firstSegment === "static";
5684
6277
  const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : isDocusaurusStatic ? file.relativePath.slice("static/".length) : file.relativePath);
5685
6278
  if (!assetPath) continue;
@@ -5725,6 +6318,7 @@ function migrateRepository(options) {
5725
6318
  routePrefix: options.docusaurusRoutePrefix
5726
6319
  });
5727
6320
  docsConfig = projected.docsConfig;
6321
+ expandDocusaurusDocCardLists(pages, docsConfig, docusaurusDocCardRoutes);
5728
6322
  warnings.push(...projected.warnings);
5729
6323
  for (const page of projected.generatedPages) {
5730
6324
  if (seenPageIds.has(page.id)) continue;
@@ -5740,7 +6334,8 @@ function migrateRepository(options) {
5740
6334
  const descriptor = descriptorByNavigationId.get(page.navigationId);
5741
6335
  if (descriptor) page.body = rewriteDocusaurusLinks(page.body, descriptor, docusaurusDescriptors, {
5742
6336
  sourceOrigin,
5743
- onExternalLink: (target) => externalizedLinks.add(target)
6337
+ onExternalLink: (target) => externalizedLinks.add(target),
6338
+ assetPaths: new Set(assets.map((asset) => asset.path))
5744
6339
  });
5745
6340
  }
5746
6341
  if (externalizedLinks.size > 0) warnings.push({
@@ -5831,7 +6426,7 @@ function migrateRepository(options) {
5831
6426
  }
5832
6427
  if (resolvedSpecs.length > 0) docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs);
5833
6428
  } else if (platform === "mintlify") {
5834
- const resolvedSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings);
6429
+ const resolvedSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs);
5835
6430
  for (const spec of resolvedSpecs) {
5836
6431
  if (!assets.some((asset) => asset.path === spec.filename)) {
5837
6432
  assets.push({ path: spec.filename, content: spec.content });
@@ -5895,10 +6490,10 @@ function migrateRepository(options) {
5895
6490
  seenPageIds.add(page.id);
5896
6491
  pages.push(page);
5897
6492
  }
5898
- const assetPaths = new Set(assets.map((asset) => asset.path));
6493
+ const assetPaths2 = new Set(assets.map((asset) => asset.path));
5899
6494
  for (const asset of pluginBundle.assets) {
5900
- if (assetPaths.has(asset.path)) continue;
5901
- assetPaths.add(asset.path);
6495
+ if (assetPaths2.has(asset.path)) continue;
6496
+ assetPaths2.add(asset.path);
5902
6497
  assets.push(asset);
5903
6498
  }
5904
6499
  const pluginLabel = plugin.label;
@@ -5911,8 +6506,97 @@ function migrateRepository(options) {
5911
6506
  skipped += pluginBundle.stats.skipped;
5912
6507
  }
5913
6508
  }
6509
+ if (platform === "docusaurus") {
6510
+ const copiedAssets = new Set(assets.map((asset) => asset.path));
6511
+ for (const page of pages) {
6512
+ page.body = rewriteDocusaurusLinks(page.body, {
6513
+ sourcePath: page.source ?? page.navigationId,
6514
+ docId: page.navigationId,
6515
+ navigationId: page.navigationId,
6516
+ title: page.title
6517
+ }, [], { assetPaths: copiedAssets });
6518
+ }
6519
+ const importedRoutes = new Set(pages.map((page) => page.navigationId));
6520
+ docsConfig.tabs = docsConfig.tabs.map((tab) => {
6521
+ if (tab.href) return tab;
6522
+ const root = tab.tab.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-");
6523
+ if (!root || !importedRoutes.has(root)) return tab;
6524
+ return {
6525
+ ...tab,
6526
+ href: `/${root}`,
6527
+ ...tab.pages ? { pages: tab.pages.map((node) => {
6528
+ if (typeof node === "string") return node;
6529
+ const index = node.pages.indexOf(root);
6530
+ return index > 0 ? { ...node, pages: [root, ...node.pages.filter((page) => page !== root)] } : node;
6531
+ }) } : {},
6532
+ ...tab.groups ? { groups: tab.groups.map((group) => {
6533
+ const index = group.pages.indexOf(root);
6534
+ return index > 0 ? { ...group, pages: [root, ...group.pages.filter((page) => page !== root)] } : group;
6535
+ }) } : {}
6536
+ };
6537
+ });
6538
+ }
6539
+ if (platform === "fern") {
6540
+ const byId = new Map(pages.map((page) => [page.navigationId, page]));
6541
+ for (const changelog of fernChangelogIndexes) {
6542
+ let index = byId.get(changelog.route);
6543
+ const entries = changelog.entries.map((id) => byId.get(fernIdRenames.get(id) ?? id)).filter((page) => Boolean(page)).slice(0, 10);
6544
+ if (entries.length === 0) continue;
6545
+ if (!index) {
6546
+ index = {
6547
+ id: changelog.route,
6548
+ navigationId: changelog.route,
6549
+ title: "Release Notes",
6550
+ description: "Latest release notes and product updates.",
6551
+ descriptionPlacement: "body",
6552
+ keywords: [],
6553
+ body: "",
6554
+ source: entries[0].source
6555
+ };
6556
+ pages.push(index);
6557
+ byId.set(index.navigationId, index);
6558
+ docsConfig.redirects = docsConfig.redirects?.filter((redirect) => redirect.source !== `/${changelog.route}`);
6559
+ }
6560
+ if (index.body.trim()) continue;
6561
+ index.description ||= "Latest release notes and product updates.";
6562
+ index.body = entries.map((entry) => {
6563
+ const date = entry.source.match(/\d{4}-\d{2}-\d{2}/)?.[0];
6564
+ const escapeText = (value) => value.replace(/([\\{}<>\[\]*_`])/g, "\\$1");
6565
+ const body = /^(?:import|export)\s.+(?:from\s|\{)/m.test(entry.body) ? escapeText(entry.description) : closeOpenCodeFence(date ? addDatedHeadingAliases(entry.body.trim(), date) : entry.body.trim()) || escapeText(entry.description);
6566
+ const dateAnchor = date ? `<a id="${date}T00:00:00.000Z" />
6567
+
6568
+ ` : "";
6569
+ return `${dateAnchor}## ${escapeText(entry.title)}
6570
+
6571
+ ${date ? `**${date}** \xB7 ` : ""}[Read release note](/${entry.id})
6572
+
6573
+ ${body}`;
6574
+ }).join("\n\n---\n\n");
6575
+ }
6576
+ }
5914
6577
  docsConfig = pruneMissingNavigationPages(docsConfig, new Set(pages.map((page) => page.navigationId)));
5915
- if (platform === "docusaurus") preserveDocusaurusLinkedAnchors(pages);
6578
+ if (platform === "docusaurus") addDocusaurusTranslatedHeadingAliases(pages);
6579
+ if (platform === "fern" && fernProjectRoot) {
6580
+ const sourcePath = (page) => {
6581
+ const fragment = page.source.split("#", 2)[1];
6582
+ if (!fragment) return null;
6583
+ try {
6584
+ return relative5(fernProjectRoot, resolveWithin(repositoryDir, decodeURIComponent(fragment))).replace(/\\/g, "/");
6585
+ } catch {
6586
+ return null;
6587
+ }
6588
+ };
6589
+ const routes = /* @__PURE__ */ new Map();
6590
+ for (const page of pages) {
6591
+ const source = sourcePath(page);
6592
+ if (source) routes.set(source, page.id);
6593
+ }
6594
+ for (const page of pages) {
6595
+ const source = sourcePath(page);
6596
+ if (source) page.body = rewriteFernRelativePageLinks(page.body, source, routes);
6597
+ }
6598
+ }
6599
+ preserveLinkedAnchors(pages);
5916
6600
  if (platform === "fern" && fernRawConfig) {
5917
6601
  const sourceSiteUrl = fernSourceSiteUrl(fernRawConfig);
5918
6602
  if (sourceSiteUrl) {
@@ -5953,20 +6637,168 @@ function migrateRepository(options) {
5953
6637
  ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown or MDX pages were found in the repository.");
5954
6638
  }
5955
6639
  const isTopLevelDocusaurus = platform === "docusaurus" && docusaurusProjectRoot && !options.docusaurusSkipRedirects;
5956
- const themeColors = mintlifyConfig ? mintlifyThemeColors(mintlifyConfig.colors) : fernRawConfig ? fernThemeColors(fernRawConfig.colors) : isTopLevelDocusaurus ? readDocusaurusThemeColor(docusaurusProjectRoot) : void 0;
5957
- const brandAssetPaths = mintlifyConfig ? [...mintlifyBrandAssetPaths(mintlifyConfig.logo), ...mintlifyBrandAssetPaths(mintlifyConfig.favicon)] : isTopLevelDocusaurus ? readDocusaurusBrandAssetPaths(docusaurusProjectRoot) : [];
5958
- if (brandAssetPaths.length > 0) {
5959
- warnings.push({
6640
+ const docusaurusSettings = isTopLevelDocusaurus ? readDocusaurusSiteSettings(docusaurusProjectRoot) : void 0;
6641
+ if (docusaurusSettings) {
6642
+ const importedRoutes = new Set(pages.map((page) => page.navigationId));
6643
+ const sourceOrigin = readDocusaurusSiteOrigin(docusaurusProjectRoot);
6644
+ const externalized = /* @__PURE__ */ new Set();
6645
+ const projectLink = (raw, sourceLiteral = false) => {
6646
+ if (/^https?:\/\//i.test(raw)) return raw;
6647
+ if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(raw) || raw.includes("\\")) return void 0;
6648
+ const suffixAt = raw.search(/[?#]/);
6649
+ const path = suffixAt < 0 ? raw : raw.slice(0, suffixAt);
6650
+ const suffix = suffixAt < 0 ? "" : raw.slice(suffixAt);
6651
+ const route = path.replace(/^\.?\//, "").replace(/\/+$/, "");
6652
+ if (route === ".." || route.startsWith("../") || route.includes("/../")) return void 0;
6653
+ const docsBase = docusaurusSettings.docsRouteBasePath.replace(/^\/+|\/+$/g, "");
6654
+ const isOutsideDocs = sourceLiteral && docsBase && route !== docsBase && !route.startsWith(`${docsBase}/`);
6655
+ if (isOutsideDocs && sourceOrigin) {
6656
+ externalized.add(raw);
6657
+ return new URL(raw, `${sourceOrigin}/`).toString();
6658
+ }
6659
+ const strippedDocsRoute = docsBase && route.startsWith(`${docsBase}/`) ? route.slice(docsBase.length + 1) : route;
6660
+ const candidates = [route, strippedDocsRoute, ...route === docsBase ? ["introduction"] : []];
6661
+ const imported = candidates.find((candidate) => importedRoutes.has(candidate));
6662
+ if (imported) return `/${imported}${suffix}`;
6663
+ if (sourceOrigin) {
6664
+ externalized.add(raw);
6665
+ return new URL(raw, `${sourceOrigin}/`).toString();
6666
+ }
6667
+ return path.startsWith("/") ? `${path}${suffix}` : `/${route}${suffix}`;
6668
+ };
6669
+ const firstNavigationPage = (nodes) => {
6670
+ for (const node of nodes) {
6671
+ if (typeof node === "string") return node;
6672
+ const nested = firstNavigationPage(node.pages);
6673
+ if (nested) return nested;
6674
+ }
6675
+ return void 0;
6676
+ };
6677
+ const navbarLinks = docusaurusSettings.navbarLinks.flatMap((link) => {
6678
+ const docRoute = link.docId && !link.docsPluginId && docusaurusDescriptors.find((page) => page.docId === link.docId)?.navigationId;
6679
+ const sidebar = link.sidebarId && docsConfig.tabs.find((tab) => tab.tab.toLowerCase() === link.sidebarId.toLowerCase());
6680
+ const sidebarRoute = sidebar && firstNavigationPage(sidebar.pages ?? sidebar.groups ?? []);
6681
+ const pluginRoot = link.docsPluginId && importedRoutes.has(link.docsPluginId) ? link.docsPluginId : void 0;
6682
+ const rawHref = link.href ?? (docRoute ? `/${docRoute}` : void 0) ?? (pluginRoot ? `/${pluginRoot}` : void 0) ?? (sidebarRoute ? `/${sidebarRoute}` : void 0) ?? (link.docsPluginId ? `/${link.docsPluginId}` : void 0);
6683
+ const href = rawHref && projectLink(rawHref, Boolean(link.href));
6684
+ return href ? [{ label: link.label, href }] : [];
6685
+ });
6686
+ const footerLinks = docusaurusSettings.footerLinks?.flatMap((column) => {
6687
+ const items = column.items.flatMap((item) => {
6688
+ const href = projectLink(item.href, true);
6689
+ return href ? [{ label: item.label, href }] : [];
6690
+ });
6691
+ return items.length > 0 ? [{ heading: column.heading, items }] : [];
6692
+ });
6693
+ docsConfig = {
6694
+ ...docsConfig,
6695
+ ...navbarLinks.length > 0 ? { navbar: { ...docsConfig.navbar, links: navbarLinks } } : {},
6696
+ ...footerLinks?.length || docusaurusSettings.copyright ? {
6697
+ footer: {
6698
+ ...docsConfig.footer,
6699
+ ...footerLinks?.length ? { links: footerLinks } : {},
6700
+ ...docusaurusSettings.copyright ? { copyright: docusaurusSettings.copyright } : {}
6701
+ }
6702
+ } : {}
6703
+ };
6704
+ if (externalized.size > 0) warnings.push({
5960
6705
  code: "unsupported-config",
5961
- message: `The site's logo/favicon (${brandAssetPaths.join(", ")}) were copied into public/ but are not wired into the migrated site's branding, which Thally manages from the admin dashboard rather than a static config field; set them there after the site is deployed.`
6706
+ message: `${externalized.size} site navigation/footer link(s) point outside the imported docs and were kept on ${sourceOrigin}.`
5962
6707
  });
5963
6708
  }
6709
+ if (!docsConfig.navigation?.display && docsConfig.tabs.filter((tab) => !tab.hidden).length > 6) {
6710
+ docsConfig = { ...docsConfig, navigation: { display: "dropdown" } };
6711
+ }
6712
+ const themeColors = mintlifyConfig ? mintlifyThemeColors(mintlifyConfig.colors) : fernRawConfig ? fernThemeColors(fernRawConfig.colors) : isTopLevelDocusaurus ? readDocusaurusThemeColor(docusaurusProjectRoot) : void 0;
6713
+ const assetPaths = new Set(assets.map((asset) => asset.path));
6714
+ const publicBrandPath = (value) => {
6715
+ if (typeof value !== "string" || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(value)) return void 0;
6716
+ const normalized = fernBrandAssetPaths.get(value) ?? normalizeAssetPath(value);
6717
+ return normalized && assetPaths.has(normalized) ? `/${normalized}` : void 0;
6718
+ };
6719
+ const brandVariants = (value) => {
6720
+ if (typeof value === "string") return { light: value };
6721
+ if (!value || typeof value !== "object") return {};
6722
+ const source = value;
6723
+ const light = source.light ?? source.src;
6724
+ const dark = source.dark ?? source.srcDark;
6725
+ return {
6726
+ ...typeof light === "string" ? { light } : {},
6727
+ ...typeof dark === "string" ? { dark } : {},
6728
+ ...typeof source.showTitle === "boolean" ? { showTitle: source.showTitle } : {}
6729
+ };
6730
+ };
6731
+ const sourceLogo = mintlifyConfig?.logo ?? fernRawConfig?.logo ?? docusaurusSettings?.logo;
6732
+ const sourceFavicon = mintlifyConfig?.favicon ?? fernRawConfig?.favicon ?? docusaurusSettings?.favicon;
6733
+ const logo = brandVariants(sourceLogo);
6734
+ const favicon = brandVariants(sourceFavicon);
6735
+ const logoLight = publicBrandPath(logo.light);
6736
+ const logoDark = publicBrandPath(logo.dark);
6737
+ const faviconLight = publicBrandPath(favicon.light);
6738
+ const faviconDark = publicBrandPath(favicon.dark);
6739
+ if (logoLight) docsConfig = {
6740
+ ...docsConfig,
6741
+ navbar: {
6742
+ ...docsConfig.navbar,
6743
+ logo: {
6744
+ light: logoLight,
6745
+ ...logoDark ? { dark: logoDark } : {},
6746
+ showTitle: mintlifyConfig || fernRawConfig ? false : logo.showTitle !== false,
6747
+ ...typeof fernRawConfig?.logo?.["right-text"] === "string" ? { rightText: String(fernRawConfig.logo["right-text"]).trim().slice(0, 40) } : {}
6748
+ }
6749
+ }
6750
+ };
6751
+ if (platform === "docusaurus" && docusaurusSettings?.navbarTitle && !logoLight) {
6752
+ docsConfig = { ...docsConfig, navbar: { ...docsConfig.navbar, logo: null } };
6753
+ }
6754
+ if (mintlifyConfig) {
6755
+ const appearance = mintlifyConfig.appearance && typeof mintlifyConfig.appearance === "object" ? mintlifyConfig.appearance : null;
6756
+ const defaultMode = appearance?.default;
6757
+ if (defaultMode === "light" || defaultMode === "dark" || defaultMode === "system") {
6758
+ docsConfig = { ...docsConfig, appearance: { ...docsConfig.appearance, default: defaultMode } };
6759
+ }
6760
+ const background = mintlifyConfig.background && typeof mintlifyConfig.background === "object" ? mintlifyConfig.background : null;
6761
+ if (background) {
6762
+ const decoration = background.decoration;
6763
+ const image = publicBrandPath(background.image);
6764
+ const imageDark = publicBrandPath(background.imageDark);
6765
+ docsConfig = {
6766
+ ...docsConfig,
6767
+ background: {
6768
+ ...docsConfig.background,
6769
+ ...decoration === "none" || decoration === "grid" || decoration === "gradient" ? { decoration } : {},
6770
+ ...image ? { image } : {},
6771
+ ...imageDark ? { imageDark } : {}
6772
+ }
6773
+ };
6774
+ }
6775
+ }
6776
+ if (platform === "docusaurus") {
6777
+ docsConfig = { ...docsConfig, appearance: { ...docsConfig.appearance, default: docusaurusSettings?.defaultColorMode ?? "light" } };
6778
+ }
6779
+ if (faviconLight) docsConfig = {
6780
+ ...docsConfig,
6781
+ favicon: { light: faviconLight, ...faviconDark ? { dark: faviconDark } : {} }
6782
+ };
6783
+ const missingBrand = [
6784
+ ...[logo.light, logo.dark].filter((value) => typeof value === "string" && !publicBrandPath(value)),
6785
+ ...[favicon.light, favicon.dark].filter((value) => typeof value === "string" && !publicBrandPath(value))
6786
+ ];
6787
+ if (missingBrand.length > 0) warnings.push({
6788
+ code: "unsupported-config",
6789
+ message: `Brand asset(s) were referenced but not imported: ${[...new Set(missingBrand)].join(", ")}. Add these files under public/ or update docs.json.`
6790
+ });
6791
+ if (fernRawConfig?.logo && !logoLight && !logo.light) warnings.push({
6792
+ code: "unsupported-config",
6793
+ message: "Fern supplied a logo through its global theme without a local asset; add a logo path to docs.json after import."
6794
+ });
5964
6795
  return {
5965
6796
  sourceUrl: options.sourceUrl,
5966
6797
  sourceKind: "repository",
5967
6798
  platform,
5968
6799
  pages,
5969
6800
  assets,
6801
+ ...remoteApiSpecs.length > 0 ? { remoteApiSpecs } : {},
5970
6802
  ...componentMigrator ? { componentFiles: componentMigrator.files() } : {},
5971
6803
  docsConfig,
5972
6804
  ...mintlifyConfig ? {
@@ -5980,6 +6812,12 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
5980
6812
  ...typeof fernRawConfig.title === "string" ? { name: fernRawConfig.title } : {},
5981
6813
  ...themeColors ? { colors: themeColors } : {}
5982
6814
  }
6815
+ } : docusaurusSettings && (docusaurusSettings.name || docusaurusSettings.description || themeColors) ? {
6816
+ site: {
6817
+ ...docusaurusSettings.navbarTitle || docusaurusSettings.name ? { name: docusaurusSettings.navbarTitle ?? docusaurusSettings.name } : {},
6818
+ ...docusaurusSettings.description ? { description: docusaurusSettings.description } : {},
6819
+ ...themeColors ? { colors: themeColors } : {}
6820
+ }
5983
6821
  } : themeColors ? {
5984
6822
  site: { colors: themeColors }
5985
6823
  } : {},
@@ -5998,6 +6836,7 @@ function renderPage(bundle, page) {
5998
6836
  `title: ${yamlString(page.title)}`,
5999
6837
  page.navTitle ? `navTitle: ${yamlString(page.navTitle)}` : null,
6000
6838
  `description: ${yamlString(page.description)}`,
6839
+ page.descriptionPlacement === "body" ? "descriptionPlacement: body" : null,
6001
6840
  page.badge ? `badge: ${yamlString(page.badge)}` : null,
6002
6841
  page.keywords.length > 0 ? `keywords: [${page.keywords.map(yamlString).join(", ")}]` : null,
6003
6842
  page.mode ? `mode: ${yamlString(page.mode)}` : null,
@@ -6099,7 +6938,7 @@ function renderMigrationFiles(bundle, options = {}) {
6099
6938
  }
6100
6939
 
6101
6940
  // src/url.ts
6102
- import { load } from "cheerio";
6941
+ import { load as load2 } from "cheerio";
6103
6942
  import remarkMdx3 from "remark-mdx";
6104
6943
  import remarkParse3 from "remark-parse";
6105
6944
  import TurndownService from "turndown";
@@ -6247,10 +7086,10 @@ async function readResponseBody(response) {
6247
7086
  }
6248
7087
  return new TextDecoder().decode(body);
6249
7088
  }
6250
- var defaultMigrationFetcher = async (url, request) => {
7089
+ var defaultMigrationFetcher = async (url, request2) => {
6251
7090
  const response = await fetch(url, {
6252
7091
  headers: {
6253
- Accept: request.accept,
7092
+ Accept: request2.accept,
6254
7093
  "User-Agent": "Thally-Migrate/1.0 (+https://thally.io)"
6255
7094
  },
6256
7095
  redirect: "follow",
@@ -6276,7 +7115,7 @@ function machineIndexScopePath(source, documents) {
6276
7115
  if (!document) continue;
6277
7116
  const references = [...headerLinks(document)];
6278
7117
  if (/html/i.test(document.contentType)) {
6279
- const $ = load(document.body);
7118
+ const $ = load2(document.body);
6280
7119
  references.push(...$("link[href], a[href]").map((_index, element) => $(element).attr("href") ?? "").get().filter((href) => /\/(?:llms(?:-full)?\.txt|sitemap\.xml)(?:[?#]|$)/i.test(href)));
6281
7120
  }
6282
7121
  for (const reference of references) {
@@ -6387,7 +7226,7 @@ function jsonObjectAfterKey(value, key) {
6387
7226
  return null;
6388
7227
  }
6389
7228
  function embeddedMintlifyConfig(body) {
6390
- const $ = load(body);
7229
+ const $ = load2(body);
6391
7230
  const flightPayloads = [];
6392
7231
  $("script").each((_index, element) => {
6393
7232
  const script = $(element).text();
@@ -6485,7 +7324,7 @@ function isSafePortableUrl(value) {
6485
7324
  }
6486
7325
  }
6487
7326
  function decodeHtmlAttribute(value) {
6488
- const document = load(`<i data-thally-value="${value.replace(/"/g, "&quot;")}"></i>`);
7327
+ const document = load2(`<i data-thally-value="${value.replace(/"/g, "&quot;")}"></i>`);
6489
7328
  return document("i").attr("data-thally-value") ?? "";
6490
7329
  }
6491
7330
  function escapeMarkdownText(value, escapeTableSeparator = false) {
@@ -6534,7 +7373,7 @@ function replacePortableHtmlImages(value) {
6534
7373
  if (start < cursor) continue;
6535
7374
  const end = htmlOpeningTagEnd(value, start + match[0].length);
6536
7375
  if (end === null) continue;
6537
- const fragment = load(value.slice(start, end));
7376
+ const fragment = load2(value.slice(start, end));
6538
7377
  const image = fragment("img").first();
6539
7378
  const source = image.attr("src");
6540
7379
  const destination = source && isSafePortableUrl(source) ? markdownLinkDestination(source) : null;
@@ -6559,7 +7398,7 @@ function replacePortableHtmlAnchors(value) {
6559
7398
  const closing = closingMatch.exec(value);
6560
7399
  if (!closing) continue;
6561
7400
  const end = closing.index + closing[0].length;
6562
- const fragment = load(value.slice(start, end));
7401
+ const fragment = load2(value.slice(start, end));
6563
7402
  const anchor = fragment("a").first();
6564
7403
  const href = anchor.attr("href");
6565
7404
  const label = escapeMarkdownText(anchor.text().replace(/\s+/g, " ").trim());
@@ -6940,7 +7779,7 @@ function mergeEmbeddedOpenApi(fragments) {
6940
7779
  return new TextEncoder().encode(stringifyYaml(combined));
6941
7780
  }
6942
7781
  function htmlPage(document, id) {
6943
- const $ = load(document.body);
7782
+ const $ = load2(document.body);
6944
7783
  const links = $('nav a[href], main a[href], article a[href], [role="main"] a[href]').map((_index, element) => $(element).attr("href") ?? "").get().filter(Boolean);
6945
7784
  const title = $("main h1, article h1, h1").first().text().trim() || $('meta[property="og:title"]').attr("content")?.trim() || $("title").text().split("|")[0].trim();
6946
7785
  const description = $('meta[name="description"]').attr("content")?.trim() || $('meta[property="og:description"]').attr("content")?.trim();
@@ -6984,7 +7823,7 @@ function htmlPage(document, id) {
6984
7823
  const element = node;
6985
7824
  const className = element.className;
6986
7825
  const tag = /admonition-(?:danger)/.test(className) ? "Error" : /admonition-(?:caution|warning)/.test(className) ? "Warning" : /admonition-info/.test(className) ? "Info" : "Note";
6987
- const fragment = load(element.outerHTML);
7826
+ const fragment = load2(element.outerHTML);
6988
7827
  const clone = fragment(".theme-admonition").first();
6989
7828
  const title2 = clone.find('[class*="admonitionHeading"],.admonition-heading').first();
6990
7829
  const titleText = title2.text().trim();
@@ -7005,7 +7844,7 @@ ${titleText ? `**${titleText}**
7005
7844
  filter: (node) => node.nodeName === "DIV" && node.classList.contains("tabs-container"),
7006
7845
  replacement: (_content, node) => {
7007
7846
  const element = node;
7008
- const fragment = load(element.outerHTML);
7847
+ const fragment = load2(element.outerHTML);
7009
7848
  const container = fragment(".tabs-container").first();
7010
7849
  const labels = container.children("ul.tabs").children("li").toArray().map((item) => fragment(item).text().trim() || "Tab");
7011
7850
  const panels = container.children('div[role="tabpanel"]').toArray();
@@ -7028,7 +7867,7 @@ ${tabs.join("\n\n")}
7028
7867
  turndown.addRule("gfm-table", {
7029
7868
  filter: "table",
7030
7869
  replacement: (_content, node) => {
7031
- const fragment = load(node.outerHTML);
7870
+ const fragment = load2(node.outerHTML);
7032
7871
  const rows = fragment("tr").toArray().map((row) => fragment(row).children("th,td").toArray().map((cell) => escapeMarkdownText(
7033
7872
  fragment(cell).text().replace(/\s+/g, " ").trim(),
7034
7873
  true
@@ -7049,7 +7888,7 @@ ${normalized.slice(1).map((row) => `| ${row.join(" | ")} |`).join("\n")}
7049
7888
  filter: (node) => node.nodeName === "PRE",
7050
7889
  replacement: (_content, node) => {
7051
7890
  const element = node;
7052
- const fragment = load(element.outerHTML);
7891
+ const fragment = load2(element.outerHTML);
7053
7892
  const tokenLines = fragment(".token-line").toArray();
7054
7893
  const code = (tokenLines.length > 0 ? tokenLines.map((line) => fragment(line).text()).join("\n") : fragment("pre").text()).replace(/\n$/, "");
7055
7894
  const language = fragment("code").attr("class")?.match(/(?:language-|lang-)([A-Za-z0-9_+-]+)/)?.[1] ?? "";
@@ -7082,7 +7921,7 @@ ${fence}
7082
7921
  };
7083
7922
  }
7084
7923
  function htmlNavigationLinks(document) {
7085
- const $ = load(document.body);
7924
+ const $ = load2(document.body);
7086
7925
  return $('.nav-tabs a[href], nav a[href], aside a[href], [role="navigation"] a[href]').map((_index, element) => $(element).attr("href") ?? "").get().filter(Boolean);
7087
7926
  }
7088
7927
  function normalizedLinkLabel(value) {
@@ -7090,7 +7929,7 @@ function normalizedLinkLabel(value) {
7090
7929
  }
7091
7930
  function readDocusaurusSidebar(document) {
7092
7931
  if (!/html/i.test(document.contentType)) return null;
7093
- const $ = load(document.body);
7932
+ const $ = load2(document.body);
7094
7933
  const sidebar = $("ul.theme-doc-sidebar-menu").first();
7095
7934
  if (sidebar.length === 0) return null;
7096
7935
  function readList(list) {
@@ -7126,7 +7965,7 @@ function docusaurusSidebarHrefs(snapshot) {
7126
7965
  return hrefs;
7127
7966
  }
7128
7967
  function deriveDocusaurusScope(source, document, submittedScopePath) {
7129
- const $ = load(document.body);
7968
+ const $ = load2(document.body);
7130
7969
  const sidebar = readDocusaurusSidebar(document);
7131
7970
  const internalPaths = docusaurusSidebarHrefs(sidebar).flatMap((href) => {
7132
7971
  try {
@@ -7256,7 +8095,7 @@ function docusaurusNavigationFromSnapshots(snapshots, pages, scopePath) {
7256
8095
  return { tabs };
7257
8096
  }
7258
8097
  function htmlTopLevelNavigation(document, source, scopePath) {
7259
- const $ = load(document.body);
8098
+ const $ = load2(document.body);
7260
8099
  const collectCandidates = (selector) => {
7261
8100
  const candidates2 = [];
7262
8101
  $(selector).each((_containerIndex, container) => {
@@ -7317,7 +8156,7 @@ function markdownLinks(body) {
7317
8156
  return [...body.matchAll(/\[[^\]]+\]\(([^)\s]+)(?:\s+['"][^'"]*['"])?\)/g)].map((match) => match[1]);
7318
8157
  }
7319
8158
  function sitemapLinks(body) {
7320
- const $ = load(body, { xmlMode: true });
8159
+ const $ = load2(body, { xmlMode: true });
7321
8160
  return $("url > loc, sitemap > loc").map((_index, element) => $(element).text().trim()).get().filter(Boolean);
7322
8161
  }
7323
8162
  function robotsSitemaps(body) {
@@ -7356,7 +8195,7 @@ async function migrateUrl(options) {
7356
8195
  const platform = options.platform ?? detectUrlPlatform(scopedHtmlProbe ?? initial);
7357
8196
  let docusaurusHtmlProbe = platform === "docusaurus" ? scopedHtmlProbe : null;
7358
8197
  if (docusaurusHtmlProbe && !readDocusaurusSidebar(docusaurusHtmlProbe)) {
7359
- const $ = load(docusaurusHtmlProbe.body);
8198
+ const $ = load2(docusaurusHtmlProbe.body);
7360
8199
  const docsHref = $("nav a[href]").toArray().map((element) => ({
7361
8200
  href: $(element).attr("href"),
7362
8201
  label: normalizedLinkLabel($(element).text())
@@ -7665,6 +8504,202 @@ async function migrateUrl(options) {
7665
8504
  }
7666
8505
  };
7667
8506
  }
8507
+
8508
+ // src/remote-api.ts
8509
+ import { createHash as createHash2 } from "crypto";
8510
+ import { lookup } from "dns/promises";
8511
+ import { request } from "https";
8512
+ import { isIP } from "net";
8513
+ import { parse as parseYaml6 } from "yaml";
8514
+ var MAX_SPEC_BYTES = 25e6;
8515
+ var MAX_REDIRECTS = 3;
8516
+ function isPublicAddress(address, family) {
8517
+ if (family === 4) {
8518
+ const parts = address.split(".").map(Number);
8519
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
8520
+ const [a, b] = parts;
8521
+ 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);
8522
+ }
8523
+ return family === 6 && /^[23][0-9a-f]{0,3}:/i.test(address) && !/^2001:db8:/i.test(address);
8524
+ }
8525
+ function validatedSpecUrl(value) {
8526
+ const url = new URL(value);
8527
+ if (url.protocol !== "https:" || url.username || url.password || isIP(url.hostname)) {
8528
+ throw new Error("Remote OpenAPI references must use an HTTPS domain without credentials or an IP literal.");
8529
+ }
8530
+ url.hash = "";
8531
+ return url;
8532
+ }
8533
+ async function getPinnedAddress(url) {
8534
+ const addresses = await lookup(url.hostname, { all: true });
8535
+ if (addresses.length === 0 || addresses.some((candidate) => !isPublicAddress(candidate.address, candidate.family))) {
8536
+ throw new Error("The OpenAPI host did not resolve exclusively to public addresses.");
8537
+ }
8538
+ return addresses[0];
8539
+ }
8540
+ async function requestPinnedSpec(url) {
8541
+ const pinned = await getPinnedAddress(url);
8542
+ return new Promise((resolve3, reject) => {
8543
+ const req = request(url, {
8544
+ headers: { Accept: "application/json, application/yaml, text/yaml, */*", "User-Agent": "Thally-Migrate/1.0 (+https://thally.io)" },
8545
+ signal: AbortSignal.timeout(3e4),
8546
+ // Node's dual-stack connector asks lookup for `all: true`; that
8547
+ // callback must receive address records, not a single address string.
8548
+ lookup: (_hostname, options, callback) => {
8549
+ if (typeof options === "object" && options.all) callback(null, [pinned]);
8550
+ else callback(null, pinned.address, pinned.family);
8551
+ }
8552
+ }, (response) => {
8553
+ const status = response.statusCode ?? 0;
8554
+ if (status >= 300 && status < 400) {
8555
+ response.resume();
8556
+ resolve3({ status, location: response.headers.location, body: Buffer.alloc(0) });
8557
+ return;
8558
+ }
8559
+ if (status < 200 || status >= 300) {
8560
+ response.resume();
8561
+ reject(new Error(`OpenAPI server returned ${status}.`));
8562
+ return;
8563
+ }
8564
+ const statedBytes = Number(response.headers["content-length"] ?? 0);
8565
+ if (statedBytes > MAX_SPEC_BYTES) {
8566
+ response.destroy();
8567
+ reject(new Error("Remote OpenAPI spec exceeds the 25 MB import limit."));
8568
+ return;
8569
+ }
8570
+ const chunks = [];
8571
+ let bytes = 0;
8572
+ response.on("data", (chunk) => {
8573
+ bytes += chunk.length;
8574
+ if (bytes > MAX_SPEC_BYTES) {
8575
+ response.destroy(new Error("Remote OpenAPI spec exceeds the 25 MB import limit."));
8576
+ return;
8577
+ }
8578
+ chunks.push(chunk);
8579
+ });
8580
+ response.on("end", () => resolve3({ status, body: Buffer.concat(chunks) }));
8581
+ response.on("error", reject);
8582
+ });
8583
+ req.on("error", reject);
8584
+ req.end();
8585
+ });
8586
+ }
8587
+ async function downloadSpec(value, fetcher) {
8588
+ let url = validatedSpecUrl(value);
8589
+ if (fetcher) {
8590
+ const response = await fetcher(url, { accept: "application/json, application/yaml, text/yaml" });
8591
+ validatedSpecUrl(response.finalUrl.toString());
8592
+ const body = Buffer.from(response.body);
8593
+ if (body.length > MAX_SPEC_BYTES) throw new Error("Remote OpenAPI spec exceeds the 25 MB import limit.");
8594
+ return body;
8595
+ }
8596
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
8597
+ const response = await requestPinnedSpec(url);
8598
+ if (response.status >= 300 && response.status < 400) {
8599
+ if (!response.location || hop === MAX_REDIRECTS) throw new Error("Remote OpenAPI spec redirected too many times.");
8600
+ url = validatedSpecUrl(new URL(response.location, url).toString());
8601
+ continue;
8602
+ }
8603
+ return response.body;
8604
+ }
8605
+ throw new Error("Remote OpenAPI spec redirected too many times.");
8606
+ }
8607
+ function parseOpenApi(content, extension) {
8608
+ const text = content.toString("utf8");
8609
+ const parsed = extension === "yaml" || !text.trimStart().startsWith("{") ? parseYaml6(text) : JSON.parse(text);
8610
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("OpenAPI spec is not an object.");
8611
+ const document = parsed;
8612
+ if (typeof document.openapi !== "string" || !/^3\./.test(document.openapi)) {
8613
+ throw new Error("Remote specification is not OpenAPI 3.x.");
8614
+ }
8615
+ if (!document.paths || typeof document.paths !== "object" || Array.isArray(document.paths)) {
8616
+ throw new Error("Remote OpenAPI spec has no paths object.");
8617
+ }
8618
+ const paths = Object.entries(document.paths);
8619
+ if (paths.length > 5e3) throw new Error("Remote OpenAPI spec has too many paths to import.");
8620
+ const operations = [];
8621
+ for (const [path, value] of paths) {
8622
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
8623
+ for (const [method, raw] of Object.entries(value)) {
8624
+ if (!/^(get|post|put|patch|delete|head|options|trace)$/i.test(method) || !raw || typeof raw !== "object" || Array.isArray(raw)) continue;
8625
+ const operation = raw;
8626
+ const tags = Array.isArray(operation.tags) ? operation.tags : [];
8627
+ operations.push({
8628
+ method: method.toLowerCase(),
8629
+ path,
8630
+ tag: typeof tags[0] === "string" ? tags[0] : "API Reference",
8631
+ summary: typeof operation.summary === "string" ? operation.summary : String(operation.operationId ?? "")
8632
+ });
8633
+ }
8634
+ }
8635
+ return operations;
8636
+ }
8637
+ function slug(value) {
8638
+ return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
8639
+ }
8640
+ function operationTarget(operation) {
8641
+ const path = operation.path.split("/").filter(Boolean).map((part) => part.replace(/[{}]/g, "").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase());
8642
+ return `/api/default/${[...path, operation.method].join("/")}`;
8643
+ }
8644
+ async function hydrateRemoteApiSpecs(bundle, fetcher) {
8645
+ if (!bundle.remoteApiSpecs?.length) return bundle;
8646
+ const assets = [...bundle.assets];
8647
+ const tabs = bundle.docsConfig.tabs.map((tab) => ({ ...tab }));
8648
+ const redirects = [...bundle.docsConfig.redirects ?? []];
8649
+ const warnings = [...bundle.warnings];
8650
+ const authoredLinks = /* @__PURE__ */ new Set();
8651
+ for (const page of bundle.pages) {
8652
+ for (const match of page.body.matchAll(/\/[a-z0-9_/-]+/gi)) {
8653
+ const segments = match[0].split("/").filter(Boolean);
8654
+ if (segments.length < 3) continue;
8655
+ const section = segments[0].toLowerCase();
8656
+ if (section.includes("api") || section.includes("reference")) authoredLinks.add(match[0]);
8657
+ }
8658
+ }
8659
+ for (const reference of bundle.remoteApiSpecs) {
8660
+ for (let index = warnings.length - 1; index >= 0; index--) {
8661
+ if (warnings[index].source === reference.url && warnings[index].message.includes("requires a network download")) warnings.splice(index, 1);
8662
+ }
8663
+ try {
8664
+ const url = validatedSpecUrl(reference.url);
8665
+ const extension = /\.ya?ml$/i.test(url.pathname) ? "yaml" : "json";
8666
+ const body = await downloadSpec(reference.url, fetcher);
8667
+ const operations = parseOpenApi(body, extension);
8668
+ const filename = `openapi-${createHash2("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
8669
+ assets.push({ path: filename, content: body });
8670
+ const tab = reference.tabLabel ? tabs.find((candidate) => candidate.tab === reference.tabLabel) : tabs.find((candidate) => candidate.tab.toLowerCase().includes("api"));
8671
+ if (tab) tab.api = { source: `/${filename}` };
8672
+ else tabs.push({ tab: reference.tabLabel ?? "API Reference", api: { source: `/${filename}` } });
8673
+ const routes = /* @__PURE__ */ new Map();
8674
+ for (const operation of operations) {
8675
+ const key = `${slug(operation.tag)}/${slug(operation.summary)}`;
8676
+ if (routes.has(key)) routes.set(key, null);
8677
+ else routes.set(key, operationTarget(operation));
8678
+ }
8679
+ const existingSources = new Set(redirects.map((redirect) => redirect.source));
8680
+ for (const source of authoredLinks) {
8681
+ const parts = source.split("/").filter(Boolean);
8682
+ const key = parts.slice(-2).join("/");
8683
+ const destination = routes.get(key);
8684
+ if (!destination || existingSources.has(source)) continue;
8685
+ redirects.push({ source, destination, permanent: false });
8686
+ existingSources.add(source);
8687
+ }
8688
+ } catch (error) {
8689
+ warnings.push({
8690
+ code: "fetch-failed",
8691
+ source: reference.url,
8692
+ message: `Remote OpenAPI spec could not be imported: ${error instanceof Error ? error.message : String(error)}`
8693
+ });
8694
+ }
8695
+ }
8696
+ return {
8697
+ ...bundle,
8698
+ assets,
8699
+ docsConfig: { ...bundle.docsConfig, tabs, ...redirects.length > 0 ? { redirects } : {} },
8700
+ warnings
8701
+ };
8702
+ }
7668
8703
  export {
7669
8704
  addMintlifyDirectoryRedirects,
7670
8705
  buildNavigationFromPages,
@@ -7673,6 +8708,7 @@ export {
7673
8708
  detectRepositoryDocsDir,
7674
8709
  detectRepositoryPlatform,
7675
8710
  gitmodulePaths,
8711
+ hydrateRemoteApiSpecs,
7676
8712
  mergeMigrationConfig,
7677
8713
  migrateRepository,
7678
8714
  migrateUrl,