@thallylabs/migrate 0.2.8 → 0.2.9

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
 
@@ -54,6 +54,8 @@ function parseFrontmatter(raw) {
54
54
 
55
55
  // src/mdx.ts
56
56
  import { compileSync } from "@mdx-js/mdx";
57
+ import { nameToEmoji } from "gemoji";
58
+ import remarkGfm from "remark-gfm";
57
59
  import remarkMdx from "remark-mdx";
58
60
  import remarkParse from "remark-parse";
59
61
  import { unified } from "unified";
@@ -129,7 +131,7 @@ function titleFromId(id) {
129
131
  if (value === "introduction") return "Introduction";
130
132
  return value.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
131
133
  }
132
- var descriptionParser = unified().use(remarkParse).use(remarkMdx);
134
+ var descriptionParser = unified().use(remarkParse).use(remarkGfm).use(remarkMdx);
133
135
  function firstParagraph(content) {
134
136
  function plainText(node) {
135
137
  if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
@@ -286,6 +288,20 @@ var SAFE_BUILTIN_ROOTS = /* @__PURE__ */ new Set([
286
288
  "BigInt",
287
289
  "Reflect"
288
290
  ]);
291
+ function leadingPageHeading(body) {
292
+ try {
293
+ const root = descriptionParser.parse(body);
294
+ const first = root.children?.find((node) => node.type !== "mdxjsEsm" && node.type !== "html");
295
+ if (first?.type !== "heading" || first.depth !== 1) return void 0;
296
+ const text = (node) => node.type === "mdxTextExpression" || node.type === "mdxFlowExpression" ? "" : node.value ?? (node.children ?? []).map(text).join("");
297
+ const title = text(first).replace(/\s+/g, " ").trim();
298
+ const start = first.position?.start.offset;
299
+ const end = first.position?.end.offset;
300
+ return title && start !== void 0 && end !== void 0 ? { title, start, end } : void 0;
301
+ } catch {
302
+ return void 0;
303
+ }
304
+ }
289
305
  function collectPatternNames(pattern, names) {
290
306
  switch (pattern.type) {
291
307
  case "Identifier":
@@ -999,6 +1015,10 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
999
1015
  const sourceBody = runDocusaurus && unwrapMdxCodeBlocks ? unwrapDocusaurusMdxCodeBlocks(body) : body;
1000
1016
  let rewritten = normalizeDocusaurusAdmonitions(normalizeExplicitHeadingIds(runFern ? normalizeFernCallouts(sourceBody) : sourceBody));
1001
1017
  if (runDocusaurus) {
1018
+ rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1019
+ /:([+\w-]+):/g,
1020
+ (shortcode, name) => nameToEmoji[name] ?? shortcode
1021
+ ));
1002
1022
  rewritten = normalizeDocusaurusTabBlocks(normalizeDocusaurusTabs(removeGlobalDocusaurusImports(rewritten)));
1003
1023
  rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1004
1024
  /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s+(['"])(@site\/[^'"]+)\2\s*;?/g,
@@ -1045,9 +1065,13 @@ function parseMarkdownPage(input) {
1045
1065
  ...input.locale ? { locale: input.locale } : {}
1046
1066
  };
1047
1067
  const identity = input.resolveIdentity?.(parsed.data, fallbackIdentity) ?? fallbackIdentity;
1048
- const body = normalizeMdx(parsed.content, input.platform).trim();
1068
+ let body = normalizeMdx(parsed.content, input.platform).trim();
1049
1069
  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);
1070
+ const heading = leadingPageHeading(body);
1071
+ const title = typeof parsed.data.title === "string" && parsed.data.title.trim() ? parsed.data.title.trim() : heading?.title ?? titleFromId(identity.navigationId);
1072
+ if (heading && heading.title === title) {
1073
+ body = `${body.slice(0, heading.start)}${body.slice(heading.end)}`.trim();
1074
+ }
1051
1075
  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
1076
  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
1077
  const sourceMode = typeof parsed.data.mode === "string" ? parsed.data.mode : void 0;
@@ -1060,6 +1084,7 @@ function parseMarkdownPage(input) {
1060
1084
  title,
1061
1085
  navTitle,
1062
1086
  description,
1087
+ ...input.platform === "docusaurus" || input.platform === "fern" ? { descriptionPlacement: "body" } : {},
1063
1088
  badge,
1064
1089
  keywords,
1065
1090
  mode,
@@ -2770,6 +2795,7 @@ function projectMintlifyNavigation(config, options = {}) {
2770
2795
  }) : [];
2771
2796
  let tabs = [];
2772
2797
  let i18n;
2798
+ const localizedNavigation = {};
2773
2799
  const projectionTrace = {};
2774
2800
  if (languages.length > 0) {
2775
2801
  const defaultLanguage = languages.find((entry) => entry.default === true) ?? languages[0];
@@ -2805,8 +2831,11 @@ function projectMintlifyNavigation(config, options = {}) {
2805
2831
  if (language === defaultLanguage) {
2806
2832
  tabs = languageTabs;
2807
2833
  for (const reference of references) defaultPageIds.add(reference.navigationId);
2834
+ } else if (languageTabs.length > 0) {
2835
+ localizedNavigation[locale] = languageTabs;
2808
2836
  }
2809
2837
  }
2838
+ if (Object.keys(localizedNavigation).length > 0) i18n.navigation = localizedNavigation;
2810
2839
  } else {
2811
2840
  const context = { references, seenReferences, warnings, warningKeys, pathPrefix: options.pathPrefix };
2812
2841
  tabs = convertContainerToTabs(navigation, context, "Documentation", projectionTrace);
@@ -2964,11 +2993,18 @@ function isDocumentationExtension(filename) {
2964
2993
  return [".md", ".mdx", ".rst", ".txt"].includes(extname2(filename).toLowerCase());
2965
2994
  }
2966
2995
  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
- });
2996
+ const pruneNodes = (nodes) => {
2997
+ const seenSiblings = /* @__PURE__ */ new Set();
2998
+ return nodes.flatMap((node) => {
2999
+ if (typeof node === "string") {
3000
+ if (!availableIds.has(node) || seenSiblings.has(node)) return [];
3001
+ seenSiblings.add(node);
3002
+ return [node];
3003
+ }
3004
+ const pages = pruneNodes(node.pages);
3005
+ return pages.length > 0 ? [{ ...node, pages }] : [];
3006
+ });
3007
+ };
2972
3008
  const tabs = config.tabs.flatMap((tab) => {
2973
3009
  const hadPages = (tab.pages?.length ?? 0) > 0 || (tab.groups?.length ?? 0) > 0;
2974
3010
  const pages = tab.pages ? pruneNodes(tab.pages) : void 0;
@@ -3104,7 +3140,12 @@ function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
3104
3140
  return line;
3105
3141
  }
3106
3142
  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}`);
3143
+ return line.replace(/(\]\()([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(target)}`).replace(/(\bhref=")([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(target)}${suffix}`).replace(/^(\s{0,3}\[[^\]]+\]:\s*)(<[^>]+>|\S+)/, (_match, prefix, destination) => {
3144
+ const bracketed = destination.startsWith("<") && destination.endsWith(">");
3145
+ const target = bracketed ? destination.slice(1, -1) : destination;
3146
+ const rewritten = rewriteTarget(target);
3147
+ return `${prefix}${bracketed ? `<${rewritten}>` : rewritten}`;
3148
+ });
3108
3149
  }).join("\n");
3109
3150
  }
3110
3151
  function readDocusaurusSiteOrigin(repositoryRoot) {
@@ -3279,6 +3320,14 @@ function configuredSidebarPath(repositoryRoot) {
3279
3320
  const path = resolveWithin(repositoryRoot, filename);
3280
3321
  if (!existsSync3(path)) continue;
3281
3322
  const source = readBoundedText(path);
3323
+ for (const docs of source.matchAll(/\bdocs\s*:\s*/g)) {
3324
+ const object = matchingObjectLiteral(source, (docs.index ?? 0) + docs[0].length);
3325
+ const candidate2 = object && staticStringField(object, "sidebarPath")?.replace(/^\.\//, "");
3326
+ if (candidate2) {
3327
+ const resolved2 = resolveWithin(repositoryRoot, candidate2);
3328
+ if (existsSync3(resolved2) && lstatSync3(resolved2).isFile()) return candidate2;
3329
+ }
3330
+ }
3282
3331
  const match = source.match(/\bsidebarPath\s*:\s*(?:require\.resolve\(\s*)?(['"])([^'"]+)\1/);
3283
3332
  if (!match) continue;
3284
3333
  const candidate = match[2].replace(/^\.\//, "");
@@ -3460,15 +3509,156 @@ function readDocusaurusThemeColor(repositoryRoot) {
3460
3509
  if (lightMode) colors.dark = lightMode;
3461
3510
  return Object.keys(colors).length > 0 ? colors : void 0;
3462
3511
  }
3463
- function readDocusaurusBrandAssetPaths(repositoryRoot) {
3512
+ function propertyStart(source, name) {
3513
+ const pattern = new RegExp(`(?:^|[,{\\n])\\s*["']?${name}["']?\\s*:\\s*`, "m");
3514
+ const match = pattern.exec(source);
3515
+ return match ? match.index + match[0].length : -1;
3516
+ }
3517
+ function staticStringBindings(source) {
3518
+ const bindings = /* @__PURE__ */ new Map();
3519
+ for (const match of source.matchAll(/\bconst\s+([A-Z][A-Z0-9_]*)\s*=\s*((?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'))/g)) {
3520
+ try {
3521
+ const value = JSON5.parse(match[2]);
3522
+ if (typeof value === "string") bindings.set(match[1], value);
3523
+ } catch {
3524
+ }
3525
+ }
3526
+ return bindings;
3527
+ }
3528
+ function staticArrayField(source, name) {
3529
+ const start = propertyStart(source, name);
3530
+ return start < 0 ? null : matchingArrayLiteral(source, start);
3531
+ }
3532
+ function staticObjectEntries(array) {
3533
+ if (!array) return [];
3534
+ const entries = [];
3535
+ let afterDelimiter = 1;
3536
+ let quote = "";
3537
+ let lineComment = false;
3538
+ let blockComment = false;
3539
+ for (let index = 1; index < array.length - 1; index++) {
3540
+ const char = array[index];
3541
+ const next = array[index + 1];
3542
+ if (lineComment) {
3543
+ if (char === "\n") lineComment = false;
3544
+ continue;
3545
+ }
3546
+ if (blockComment) {
3547
+ if (char === "*" && next === "/") {
3548
+ blockComment = false;
3549
+ index++;
3550
+ }
3551
+ continue;
3552
+ }
3553
+ if (quote) {
3554
+ if (char === "\\") {
3555
+ index++;
3556
+ continue;
3557
+ }
3558
+ if (char === quote) quote = "";
3559
+ continue;
3560
+ }
3561
+ if (char === "/" && next === "/") {
3562
+ lineComment = true;
3563
+ index++;
3564
+ continue;
3565
+ }
3566
+ if (char === "/" && next === "*") {
3567
+ blockComment = true;
3568
+ index++;
3569
+ continue;
3570
+ }
3571
+ if (char === '"' || char === "'" || char === "`") {
3572
+ quote = char;
3573
+ continue;
3574
+ }
3575
+ if (char === ",") {
3576
+ afterDelimiter = index + 1;
3577
+ continue;
3578
+ }
3579
+ if (char !== "{") continue;
3580
+ const literal = matchingObjectLiteral(array, index);
3581
+ if (!literal) continue;
3582
+ const lead = array.slice(afterDelimiter, index).replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, "").trim();
3583
+ if (!lead) entries.push(literal);
3584
+ index += literal.length - 1;
3585
+ }
3586
+ return entries;
3587
+ }
3588
+ function staticStringField(source, name, bindings) {
3589
+ const start = propertyStart(source, name);
3590
+ if (start < 0) return void 0;
3591
+ const quoted = source.slice(start).match(/^("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/);
3592
+ if (!quoted) {
3593
+ const binding = source.slice(start).match(/^([A-Z][A-Z0-9_]*)\b/)?.[1];
3594
+ return binding ? bindings?.get(binding) : void 0;
3595
+ }
3596
+ try {
3597
+ const parsed = JSON5.parse(quoted[1]);
3598
+ return typeof parsed === "string" ? parsed : void 0;
3599
+ } catch {
3600
+ return void 0;
3601
+ }
3602
+ }
3603
+ function readDocusaurusSiteSettings(repositoryRoot) {
3604
+ const empty = { docsRouteBasePath: "docs", navbarLinks: [], footerLinks: [] };
3464
3605
  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");
3606
+ if (!configPath) return empty;
3607
+ const source = readBoundedText(configPath);
3608
+ const bindings = staticStringBindings(source);
3609
+ const presets = staticArrayField(source, "presets");
3610
+ const classicDocs = presets && [...presets.matchAll(/["']?docs["']?\s*:\s*/g)].map((match) => matchingObjectLiteral(presets, (match.index ?? 0) + match[0].length)).find((object) => Boolean(object));
3611
+ const docsRouteBasePath = classicDocs && staticStringField(classicDocs, "routeBasePath") || "docs";
3612
+ const themeStart = propertyStart(source, "themeConfig");
3613
+ const theme = themeStart < 0 ? null : matchingObjectLiteral(source, themeStart);
3614
+ const navbarStart = theme ? propertyStart(theme, "navbar") : -1;
3615
+ const navbar = navbarStart < 0 ? null : matchingObjectLiteral(theme, navbarStart);
3616
+ const logoStart = navbar ? propertyStart(navbar, "logo") : -1;
3617
+ const logo = logoStart < 0 ? null : matchingObjectLiteral(navbar, logoStart);
3618
+ const footerStart = theme ? propertyStart(theme, "footer") : -1;
3619
+ const footer = footerStart < 0 ? null : matchingObjectLiteral(theme, footerStart);
3620
+ const navbarLinks = navbar ? staticObjectEntries(staticArrayField(navbar, "items")).flatMap((item) => {
3621
+ if (propertyStart(item, "items") >= 0 || propertyStart(item, "dropdownItemsAfter") >= 0) return [];
3622
+ const docsPluginId = staticStringField(item, "docsPluginId", bindings);
3623
+ const href = staticStringField(item, "to", bindings) ?? staticStringField(item, "href", bindings);
3624
+ const docId = staticStringField(item, "docId", bindings);
3625
+ const sidebarId = staticStringField(item, "sidebarId", bindings);
3626
+ const label = staticStringField(item, "label", bindings) ?? staticStringField(item, "aria-label", bindings);
3627
+ return label && (href || docId || sidebarId || docsPluginId) ? [{
3628
+ label,
3629
+ ...href ? { href } : {},
3630
+ ...docId ? { docId } : {},
3631
+ ...sidebarId ? { sidebarId } : {},
3632
+ ...docsPluginId ? { docsPluginId } : {}
3633
+ }] : [];
3634
+ }) : [];
3635
+ const footerLinks = footer ? staticObjectEntries(staticArrayField(footer, "links")).flatMap((column) => {
3636
+ const heading = staticStringField(column, "title", bindings);
3637
+ if (!heading) return [];
3638
+ const items = staticObjectEntries(staticArrayField(column, "items")).flatMap((item) => {
3639
+ const href = staticStringField(item, "to", bindings) ?? staticStringField(item, "href", bindings);
3640
+ const label = staticStringField(item, "label", bindings);
3641
+ return label && href ? [{ label, href }] : [];
3642
+ });
3643
+ return items.length > 0 ? [{ heading, items }] : [];
3644
+ }) : [];
3645
+ 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;
3646
+ return {
3647
+ name: staticStringField(source, "title"),
3648
+ description: staticStringField(source, "tagline"),
3649
+ docsRouteBasePath,
3650
+ ...logo && staticStringField(logo, "src") ? {
3651
+ logo: {
3652
+ light: staticStringField(logo, "src"),
3653
+ ...staticStringField(logo, "srcDark") ? { dark: staticStringField(logo, "srcDark") } : {},
3654
+ showTitle: Boolean(navbar && staticStringField(navbar, "title"))
3655
+ }
3656
+ } : {},
3657
+ ...staticStringField(source, "favicon") ? { favicon: staticStringField(source, "favicon") } : {},
3658
+ navbarLinks,
3659
+ footerLinks,
3660
+ ...copyright ? { copyright } : {}
3661
+ };
3472
3662
  }
3473
3663
  function autogeneratedItems(dirName, context) {
3474
3664
  const normalizedDir = dirName === "." ? "" : normalizeDocId(dirName);
@@ -3555,19 +3745,19 @@ function projectDocusaurusNavigation(input) {
3555
3745
  ]]];
3556
3746
  const tabs = sidebarEntries.flatMap(([sidebarId, value]) => {
3557
3747
  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 [];
3748
+ if (converted.length === 0) return [];
3562
3749
  return [{
3563
3750
  tab: sidebarEntries.length === 1 ? "Documentation" : titleCase2(sidebarId),
3564
- groups
3751
+ // Docusaurus categories are disclosures, including at the sidebar
3752
+ // root. Keeping them as Thally root nodes preserves their collapsed
3753
+ // state and the authored interleaving of documents and categories.
3754
+ pages: converted
3565
3755
  }];
3566
3756
  });
3567
3757
  const unreferenced = input.descriptors.filter((page) => !context.referencedNavigationIds.has(page.navigationId)).sort(descriptorSort).map((page) => page.navigationId);
3568
3758
  if (unreferenced.length > 0) {
3569
- if (tabs.length === 0) tabs.push({ tab: "Documentation", groups: [] });
3570
- tabs[0].groups?.push({ group: "Additional", pages: unreferenced });
3759
+ if (tabs.length === 0) tabs.push({ tab: "Documentation", pages: [] });
3760
+ tabs[0].pages?.push({ group: "Additional", pages: unreferenced });
3571
3761
  }
3572
3762
  return {
3573
3763
  docsConfig: { tabs },
@@ -3580,8 +3770,35 @@ function projectDocusaurusNavigation(input) {
3580
3770
  // src/fern.ts
3581
3771
  import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as readFileSync4, readdirSync } from "fs";
3582
3772
  import { basename as basename2, dirname as dirname3, extname as extname4, relative as relative4 } from "path";
3773
+ import { load } from "cheerio";
3583
3774
  import { parse as parseYaml3 } from "yaml";
3584
3775
  var MAX_CONFIG_BYTES2 = 2e6;
3776
+ function announcementMarkdown(message) {
3777
+ const document = load(message);
3778
+ document("script, style, iframe, object, embed, svg, math").remove();
3779
+ const escapeText = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/([\\`*_\[\]{}()!])/g, "\\$1");
3780
+ const links = /* @__PURE__ */ new Map();
3781
+ document("a").each((index, element) => {
3782
+ const marker = `THALLYANNOUNCEMENTLINK${index}TOKEN`;
3783
+ const label = document(element).text().trim();
3784
+ const rawHref = document(element).attr("href")?.trim() ?? "";
3785
+ let href;
3786
+ if (/^https?:\/\//i.test(rawHref)) {
3787
+ try {
3788
+ href = new URL(rawHref).toString();
3789
+ } catch {
3790
+ }
3791
+ } else if (rawHref.startsWith("/") && !rawHref.startsWith("//") && !/[\\<>\s]/.test(rawHref)) {
3792
+ href = rawHref;
3793
+ }
3794
+ const destination = href?.replace(/\(/g, "%28").replace(/\)/g, "%29");
3795
+ links.set(marker, destination && label ? `[${escapeText(label)}](${destination})` : escapeText(label));
3796
+ document(element).replaceWith(marker);
3797
+ });
3798
+ let text = escapeText(document("body").text());
3799
+ for (const [marker, link] of links) text = text.replaceAll(marker, link);
3800
+ return text.trim();
3801
+ }
3585
3802
  function objectValue3(value) {
3586
3803
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3587
3804
  }
@@ -3648,7 +3865,7 @@ function uniqueNavigationId(base, context) {
3648
3865
  while (context.seenNavigationIds.has(`${base}-${index}`)) index++;
3649
3866
  return `${base}-${index}`;
3650
3867
  }
3651
- function registerPageAt(rawPath, base, context) {
3868
+ function registerPageAt(rawPath, base, context, navTitle, hidden = false) {
3652
3869
  let sourcePath;
3653
3870
  try {
3654
3871
  const absolute = resolveWithinRoot(
@@ -3663,9 +3880,16 @@ function registerPageAt(rawPath, base, context) {
3663
3880
  }
3664
3881
  const navigationId = uniqueNavigationId(base || "introduction", context);
3665
3882
  context.seenNavigationIds.add(navigationId);
3666
- context.descriptors.push({ sourcePath, navigationId });
3883
+ const isHidden = hidden || context.hiddenInherited === true;
3884
+ context.descriptors.push({ sourcePath, navigationId, ...navTitle ? { navTitle } : {}, ...isHidden ? { hidden: true } : {} });
3667
3885
  const kept = context.sourcePathToNavigationId.get(sourcePath);
3668
- if (kept) return kept;
3886
+ if (kept) {
3887
+ if (!isHidden) {
3888
+ const owner = context.descriptors.find((descriptor) => descriptor.navigationId === kept);
3889
+ if (owner) owner.hidden = false;
3890
+ }
3891
+ return kept;
3892
+ }
3669
3893
  context.sourcePathToNavigationId.set(sourcePath, navigationId);
3670
3894
  return navigationId;
3671
3895
  }
@@ -3702,8 +3926,11 @@ function registerFolder(object, parentSegments, context) {
3702
3926
  const stem = basename2(entry.name, extname4(entry.name));
3703
3927
  const pageSegment = stem.toLowerCase() === "index" ? null : fernBasicSlug(stem);
3704
3928
  const route = [...routeSegments, ...pageSegment ? [pageSegment] : []].join("/");
3705
- const registered = registerPageAt(relative4(context.fernRoot, path), route, { ...context, pathPrefix: "" });
3706
- if (registered) pages2.push(registered);
3929
+ const source = lstatSync4(path).size <= MAX_CONFIG_BYTES2 ? readFileSync4(path, "utf8") : "";
3930
+ const frontmatter = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] ?? "";
3931
+ const hidden = /^hidden:\s*true\s*$/m.test(frontmatter);
3932
+ const registered = registerPageAt(relative4(context.fernRoot, path), route, { ...context, pathPrefix: "" }, void 0, hidden);
3933
+ if (registered && !hidden && !context.hiddenInherited) pages2.push(registered);
3707
3934
  }
3708
3935
  }
3709
3936
  return pages2;
@@ -3718,13 +3945,19 @@ function registerPage(object, parentSegments, context) {
3718
3945
  const label = typeof object.page === "string" && object.page.trim() ? object.page.trim() : "Untitled";
3719
3946
  const segment = segmentFor(object, label, context);
3720
3947
  const base = [...parentSegments, segment].filter(Boolean).join("/");
3721
- return registerPageAt(object.path, base, context) || null;
3948
+ return registerPageAt(object.path, base, context, label, object.hidden === true) || null;
3722
3949
  }
3723
3950
  function convertNode(node, parentSegments, context) {
3724
3951
  const object = objectValue3(node);
3725
3952
  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);
3953
+ if (typeof object.page === "string") {
3954
+ const page = registerPage(object, parentSegments, context);
3955
+ return object.hidden === true || context.hiddenInherited ? null : page;
3956
+ }
3957
+ if (typeof object.folder === "string") {
3958
+ const folder = registerFolder(object, parentSegments, object.hidden === true ? { ...context, hiddenInherited: true } : context);
3959
+ return object.hidden === true || context.hiddenInherited ? null : folder;
3960
+ }
3728
3961
  if (typeof object.section === "string") {
3729
3962
  const label = object.section;
3730
3963
  const segment = segmentFor(object, label, context);
@@ -3733,15 +3966,16 @@ function convertNode(node, parentSegments, context) {
3733
3966
  const pages = [];
3734
3967
  const hasOwnPath = typeof object.path === "string" && object.path.trim().length > 0;
3735
3968
  if (hasOwnPath) {
3736
- const registered = registerPageAt(object.path, segments.join("/"), context);
3969
+ const registered = registerPageAt(object.path, segments.join("/"), context, label, object.hidden === true);
3737
3970
  if (registered) pages.push(registered);
3738
3971
  }
3739
3972
  for (const child of contents) {
3740
- const converted = convertNode(child, segments, context);
3973
+ const converted = convertNode(child, segments, object.hidden === true ? { ...context, hiddenInherited: true } : context);
3741
3974
  if (converted) pages.push(converted);
3742
3975
  }
3743
3976
  if (pages.length === 0) return null;
3744
3977
  if (!hasOwnPath && segment) addBareRouteRedirect(segments, pages, context);
3978
+ if (object.hidden === true || context.hiddenInherited) return null;
3745
3979
  return {
3746
3980
  group: label,
3747
3981
  ...typeof object.icon === "string" ? { icon: object.icon } : {},
@@ -3766,8 +4000,7 @@ function convertNode(node, parentSegments, context) {
3766
4000
  return null;
3767
4001
  }
3768
4002
  if (typeof object.changelog === "string") {
3769
- warnOnce2(context, "fern-changelog", "Fern changelog sections are not supported and were omitted.");
3770
- return null;
4003
+ return registerFolder({ folder: object.changelog, title: "Changelog", "skip-slug": true }, parentSegments, context);
3771
4004
  }
3772
4005
  return null;
3773
4006
  }
@@ -3840,6 +4073,43 @@ function buildTabsFromConfig(rawConfig, configDir, fernRoot, routePrefix, fallba
3840
4073
  for (let index = sectionsBefore; index < context.apiSections.length; index += 1) {
3841
4074
  context.apiSections[index].tabLabel ??= label;
3842
4075
  }
4076
+ if (groups.length === 0 && typeof meta.href === "string" && /^https?:\/\//i.test(meta.href)) {
4077
+ return [{ tab: label, href: meta.href }];
4078
+ }
4079
+ if (groups.length === 0 && typeof meta.changelog === "string") {
4080
+ const changelog = registerFolder({ folder: meta.changelog, title: label, "skip-slug": true }, segments, context);
4081
+ if (changelog) {
4082
+ const indexRoute = segments.join("/");
4083
+ const descriptors = new Map(context.descriptors.map((descriptor) => [descriptor.navigationId, descriptor]));
4084
+ const visiblePages = changelog.pages.filter((page) => typeof page === "string").filter((page) => !descriptors.get(page)?.hidden);
4085
+ const index = visiblePages.find((page) => {
4086
+ const descriptor = descriptors.get(page);
4087
+ if (!descriptor) return false;
4088
+ const stem = basename2(descriptor.sourcePath, extname4(descriptor.sourcePath));
4089
+ return page === indexRoute || stem === "overview" || stem === "index";
4090
+ });
4091
+ const entries = visiblePages.filter((page) => page !== index).sort((left, right) => {
4092
+ const date = (page) => descriptors.get(page)?.sourcePath.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? "";
4093
+ return date(right).localeCompare(date(left)) || left.localeCompare(right);
4094
+ });
4095
+ for (const entryId of entries) {
4096
+ const descriptor = descriptors.get(entryId);
4097
+ if (descriptor) descriptor.hidden = true;
4098
+ }
4099
+ changelog.pages = [index ?? indexRoute];
4100
+ context.changelogIndexes.push({ route: indexRoute, entries });
4101
+ const latest = index ?? firstLeafId(changelog.pages);
4102
+ const bareRouteIndex = context.bareRouteRedirects.findIndex((redirect) => redirect.source === `/${indexRoute}`);
4103
+ const indexDescriptor = index ? descriptors.get(index) : void 0;
4104
+ const indexSource = indexDescriptor && lstatSync4(resolveWithinRoot(context.fernRoot, indexDescriptor.sourcePath, context.repositoryRoot)).size <= MAX_CONFIG_BYTES2 ? readFileSync4(resolveWithinRoot(context.fernRoot, indexDescriptor.sourcePath, context.repositoryRoot), "utf8") : "";
4105
+ const indexFrontmatter = indexSource.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] ?? "";
4106
+ const indexSlug = indexFrontmatter.match(/^slug:\s*["']?([^\s"']+)["']?\s*$/m)?.[1]?.replace(/^\/+|\/+$/g, "");
4107
+ const hasBarePage = index === indexRoute || indexSlug === indexRoute;
4108
+ if (bareRouteIndex >= 0 && hasBarePage) context.bareRouteRedirects.splice(bareRouteIndex, 1);
4109
+ else if (latest && bareRouteIndex >= 0) context.bareRouteRedirects[bareRouteIndex].destination = `/${latest}`;
4110
+ return [{ tab: label, groups: [changelog] }];
4111
+ }
4112
+ }
3843
4113
  if (groups.length === 0) return [];
3844
4114
  if (segments.length > 0) addBareRouteRedirect(segments, groups, context);
3845
4115
  return [{
@@ -3916,6 +4186,7 @@ function projectFernNavigation(input) {
3916
4186
  descriptors: [],
3917
4187
  seenNavigationIds: /* @__PURE__ */ new Set(),
3918
4188
  apiSections: [],
4189
+ changelogIndexes: [],
3919
4190
  warnings: [],
3920
4191
  warningKeys: /* @__PURE__ */ new Set(),
3921
4192
  bareRouteRedirects: [],
@@ -3938,6 +4209,9 @@ function projectFernNavigation(input) {
3938
4209
  const label = typeof link?.text === "string" ? link.text : typeof link?.label === "string" ? link.label : link?.type === "github" ? "GitHub" : void 0;
3939
4210
  return href && label ? [{ label, href, ...link?.type === "github" ? { type: "github" } : {} }] : [];
3940
4211
  }) : [];
4212
+ const announcement = objectValue3(config.announcement);
4213
+ const announcementMessage = typeof announcement?.message === "string" ? announcement.message.trim() : "";
4214
+ const bannerContent = announcementMarkdown(announcementMessage);
3941
4215
  const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
3942
4216
  const redirect = objectValue3(value);
3943
4217
  if (!redirect || typeof redirect.source !== "string" || typeof redirect.destination !== "string") return [];
@@ -3959,13 +4233,6 @@ function projectFernNavigation(input) {
3959
4233
  ...typeof redirect.permanent === "boolean" ? { permanent: redirect.permanent } : {}
3960
4234
  }];
3961
4235
  }) : [];
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
4236
  const authoredSources = new Set(redirects.map((redirect) => redirect.source));
3970
4237
  const knownIds = new Set(context.descriptors.map((descriptor) => descriptor.navigationId));
3971
4238
  const keptNavigationId = (descriptor) => context.sourcePathToNavigationId.get(descriptor.sourcePath) ?? descriptor.navigationId;
@@ -4009,12 +4276,14 @@ function projectFernNavigation(input) {
4009
4276
  return {
4010
4277
  docsConfig: {
4011
4278
  tabs,
4279
+ ...bannerContent ? { banner: { content: bannerContent, dismissible: true } } : {},
4012
4280
  ...navbarLinks.length > 0 ? { navbar: { links: navbarLinks } } : {},
4013
4281
  ...allRedirects.length > 0 ? { redirects: allRedirects } : {}
4014
4282
  },
4015
4283
  descriptors: context.descriptors,
4016
4284
  warnings: context.warnings,
4017
- apiSections: disambiguatedApiSections
4285
+ apiSections: disambiguatedApiSections,
4286
+ changelogIndexes: context.changelogIndexes
4018
4287
  };
4019
4288
  }
4020
4289
 
@@ -4635,12 +4904,6 @@ function fernThemeColors(value) {
4635
4904
  if (lightMode) colors.dark = lightMode;
4636
4905
  return Object.keys(colors).length > 0 ? colors : void 0;
4637
4906
  }
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
4907
  function normalizedReferenceKey(value) {
4645
4908
  return value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/").replace(/\.(?:mdx?|rst|txt)$/i, "").replace(/\/(?:index|readme)$/i, "").replace(/^(?:index|readme)$/i, "") || "introduction";
4646
4909
  }
@@ -4947,7 +5210,7 @@ function fernSourceSiteUrl(config) {
4947
5210
  function externalizeMissingFernLinks(body, pageIds, sourceSiteUrl, externalized) {
4948
5211
  const rewrite = (target) => {
4949
5212
  const path = target.split(/[?#]/, 1)[0].replace(/^\//, "").replace(/\/$/, "");
4950
- if (!path || pageIds.has(path)) return target;
5213
+ if (!path || pageIds.has(path) || ASSET_EXTENSIONS.has(extname5(path).toLowerCase())) return target;
4951
5214
  try {
4952
5215
  const base = new URL(`${sourceSiteUrl}/`);
4953
5216
  const sourceLinkUrl = new URL(target.slice(1), base);
@@ -4983,15 +5246,20 @@ function rewriteMintlifyMountedLinks(body, pageIds) {
4983
5246
  };
4984
5247
  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
5248
  }
4986
- function preserveDocusaurusLinkedAnchors(pages) {
5249
+ function preserveLinkedAnchors(pages) {
4987
5250
  const pageById = new Map(pages.map((page) => [page.id, page]));
4988
5251
  const requested = /* @__PURE__ */ new Map();
4989
5252
  const crossPageRequests = /* @__PURE__ */ new Set();
4990
5253
  const addTarget = (target, currentId) => {
4991
5254
  const hash2 = target.indexOf("#");
4992
5255
  if (hash2 < 0) return;
4993
- const fragment = target.slice(hash2 + 1);
4994
- if (!/^[A-Za-z0-9_.:-]{1,120}$/.test(fragment)) return;
5256
+ let fragment;
5257
+ try {
5258
+ fragment = decodeURIComponent(target.slice(hash2 + 1));
5259
+ } catch {
5260
+ return;
5261
+ }
5262
+ if (!/^[\p{L}\p{N}_.:$-]{1,120}$/u.test(fragment)) return;
4995
5263
  const path = target.slice(0, hash2).split("?")[0];
4996
5264
  const id = target.startsWith("#") ? currentId : path.startsWith("/") && !path.startsWith("//") ? path.replace(/^\//, "").replace(/\/$/, "") || "introduction" : "";
4997
5265
  if (!pageById.has(id)) return;
@@ -5066,6 +5334,43 @@ function preserveDocusaurusLinkedAnchors(pages) {
5066
5334
  ]).join("\n");
5067
5335
  }
5068
5336
  }
5337
+ function closeOpenCodeFence(body) {
5338
+ let open = null;
5339
+ for (const line of body.split(/\r?\n/)) {
5340
+ const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
5341
+ if (!match) continue;
5342
+ const marker = match[1][0];
5343
+ if (!open) {
5344
+ open = { marker, length: match[1].length };
5345
+ } else if (marker === open.marker && match[1].length >= open.length && !match[2].trim()) {
5346
+ open = null;
5347
+ }
5348
+ }
5349
+ return open ? `${body}
5350
+
5351
+ ${open.marker.repeat(open.length)}` : body;
5352
+ }
5353
+ function addDatedHeadingAliases(body, date) {
5354
+ let open = null;
5355
+ const existingIds = new Set([...body.matchAll(/<a\s+id=["']([^"']+)["']/g)].map((match) => match[1]));
5356
+ return body.split(/\r?\n/).flatMap((line) => {
5357
+ const fence = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
5358
+ if (fence) {
5359
+ const marker = fence[1][0];
5360
+ if (!open) open = { marker, length: fence[1].length };
5361
+ else if (marker === open.marker && fence[1].length >= open.length && !fence[2].trim()) open = null;
5362
+ return [line];
5363
+ }
5364
+ if (open) return [line];
5365
+ const heading = line.match(/^#{2,6}\s+(.+?)\s*#*\s*$/);
5366
+ if (!heading) return [line];
5367
+ const slug = heading[1].replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_]/g, "").normalize("NFKD").toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/\s+/g, "-");
5368
+ const id = `${date}-${slug}`;
5369
+ if (!slug || existingIds.has(id)) return [line];
5370
+ existingIds.add(id);
5371
+ return [`<a id="${id}" />`, "", line];
5372
+ }).join("\n");
5373
+ }
5069
5374
  function resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot) {
5070
5375
  sourcePath = sourcePath.replace(/\\(?=_)/g, "");
5071
5376
  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 +5418,11 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
5113
5418
  }
5114
5419
  if (!ASSET_EXTENSIONS.has(extname5(decodedPath).toLowerCase())) return null;
5115
5420
  try {
5116
- const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithin(dirname4(currentFile), decodedPath);
5421
+ const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithinRoot(dirname4(currentFile), decodedPath, siteRoot);
5422
+ const realRoot = realpathSync2(siteRoot);
5423
+ const realCandidate = realpathSync2(candidate);
5424
+ const realRelative = relative5(realRoot, realCandidate);
5425
+ if (realRelative === ".." || realRelative.startsWith(`..${sep2}`) || isAbsolute2(realRelative)) return null;
5117
5426
  const siteRelative = relative5(siteRoot, candidate).replace(/\\/g, "/");
5118
5427
  resolveWithin(siteRoot, siteRelative);
5119
5428
  if (!existsSync5(candidate) || !lstatSync5(candidate).isFile()) return null;
@@ -5348,8 +5657,11 @@ function migrateRepository(options) {
5348
5657
  let fernBasePath = "";
5349
5658
  let fernVersionPath = "";
5350
5659
  let fernApiSections = [];
5660
+ let fernChangelogIndexes = [];
5351
5661
  const fernReferencedPaths = /* @__PURE__ */ new Set();
5352
5662
  const fernSourceLinkAliases = /* @__PURE__ */ new Map();
5663
+ const fernNavTitles = /* @__PURE__ */ new Map();
5664
+ const fernHiddenIds = /* @__PURE__ */ new Set();
5353
5665
  if (platform === "mintlify") {
5354
5666
  try {
5355
5667
  const config = readMintlifyConfig(mintlifyProjectRoot ?? repositoryDir);
@@ -5384,8 +5696,13 @@ function migrateRepository(options) {
5384
5696
  docsConfig = projected.docsConfig;
5385
5697
  warnings.push(...projected.warnings);
5386
5698
  fernApiSections = projected.apiSections;
5699
+ fernChangelogIndexes = projected.changelogIndexes;
5387
5700
  for (const [index, descriptor] of projected.descriptors.entries()) {
5388
5701
  fernReferencedPaths.add(descriptor.sourcePath);
5702
+ if (descriptor.navTitle && !fernNavTitles.has(descriptor.navigationId)) {
5703
+ fernNavTitles.set(descriptor.navigationId, descriptor.navTitle);
5704
+ }
5705
+ if (descriptor.hidden) fernHiddenIds.add(descriptor.navigationId);
5389
5706
  const sourceTail = descriptor.sourcePath.match(/(?:^|\/)pages\/(.+)\.mdx?$/i)?.[1]?.replace(/\/index$/i, "");
5390
5707
  if (sourceTail) {
5391
5708
  const routeSegments = descriptor.navigationId.split("/");
@@ -5587,6 +5904,11 @@ function migrateRepository(options) {
5587
5904
  skipped++;
5588
5905
  continue;
5589
5906
  }
5907
+ if (platform === "fern" && !page.navTitle) {
5908
+ const navTitle = fernNavTitles.get(navigationId);
5909
+ if (navTitle && navTitle !== page.title) page.navTitle = navTitle;
5910
+ }
5911
+ if (platform === "fern" && fernHiddenIds.has(navigationId)) page.hidden = true;
5590
5912
  if (platform === "mintlify" && mintlifyProjectRoot) {
5591
5913
  page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, mintlifyProjectRoot, (assetPath) => {
5592
5914
  addAssetReference(assetPath, file.relativePath);
@@ -5895,10 +6217,10 @@ function migrateRepository(options) {
5895
6217
  seenPageIds.add(page.id);
5896
6218
  pages.push(page);
5897
6219
  }
5898
- const assetPaths = new Set(assets.map((asset) => asset.path));
6220
+ const assetPaths2 = new Set(assets.map((asset) => asset.path));
5899
6221
  for (const asset of pluginBundle.assets) {
5900
- if (assetPaths.has(asset.path)) continue;
5901
- assetPaths.add(asset.path);
6222
+ if (assetPaths2.has(asset.path)) continue;
6223
+ assetPaths2.add(asset.path);
5902
6224
  assets.push(asset);
5903
6225
  }
5904
6226
  const pluginLabel = plugin.label;
@@ -5911,8 +6233,67 @@ function migrateRepository(options) {
5911
6233
  skipped += pluginBundle.stats.skipped;
5912
6234
  }
5913
6235
  }
6236
+ if (platform === "docusaurus") {
6237
+ const importedRoutes = new Set(pages.map((page) => page.navigationId));
6238
+ docsConfig.tabs = docsConfig.tabs.map((tab) => {
6239
+ if (tab.href) return tab;
6240
+ const root = tab.tab.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-");
6241
+ if (!root || !importedRoutes.has(root)) return tab;
6242
+ return {
6243
+ ...tab,
6244
+ href: `/${root}`,
6245
+ ...tab.pages ? { pages: tab.pages.map((node) => {
6246
+ if (typeof node === "string") return node;
6247
+ const index = node.pages.indexOf(root);
6248
+ return index > 0 ? { ...node, pages: [root, ...node.pages.filter((page) => page !== root)] } : node;
6249
+ }) } : {},
6250
+ ...tab.groups ? { groups: tab.groups.map((group) => {
6251
+ const index = group.pages.indexOf(root);
6252
+ return index > 0 ? { ...group, pages: [root, ...group.pages.filter((page) => page !== root)] } : group;
6253
+ }) } : {}
6254
+ };
6255
+ });
6256
+ }
6257
+ if (platform === "fern") {
6258
+ const byId = new Map(pages.map((page) => [page.navigationId, page]));
6259
+ for (const changelog of fernChangelogIndexes) {
6260
+ let index = byId.get(changelog.route);
6261
+ const entries = changelog.entries.map((id) => byId.get(fernIdRenames.get(id) ?? id)).filter((page) => Boolean(page)).slice(0, 10);
6262
+ if (entries.length === 0) continue;
6263
+ if (!index) {
6264
+ index = {
6265
+ id: changelog.route,
6266
+ navigationId: changelog.route,
6267
+ title: "Release Notes",
6268
+ description: "Latest release notes and product updates.",
6269
+ descriptionPlacement: "body",
6270
+ keywords: [],
6271
+ body: "",
6272
+ source: entries[0].source
6273
+ };
6274
+ pages.push(index);
6275
+ byId.set(index.navigationId, index);
6276
+ docsConfig.redirects = docsConfig.redirects?.filter((redirect) => redirect.source !== `/${changelog.route}`);
6277
+ }
6278
+ if (index.body.trim()) continue;
6279
+ index.description ||= "Latest release notes and product updates.";
6280
+ index.body = entries.map((entry) => {
6281
+ const date = entry.source.match(/\d{4}-\d{2}-\d{2}/)?.[0];
6282
+ const escapeText = (value) => value.replace(/([\\{}<>\[\]*_`])/g, "\\$1");
6283
+ 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);
6284
+ const dateAnchor = date ? `<a id="${date}T00:00:00.000Z" />
6285
+
6286
+ ` : "";
6287
+ return `${dateAnchor}## ${escapeText(entry.title)}
6288
+
6289
+ ${date ? `**${date}** \xB7 ` : ""}[Read release note](/${entry.id})
6290
+
6291
+ ${body}`;
6292
+ }).join("\n\n---\n\n");
6293
+ }
6294
+ }
5914
6295
  docsConfig = pruneMissingNavigationPages(docsConfig, new Set(pages.map((page) => page.navigationId)));
5915
- if (platform === "docusaurus") preserveDocusaurusLinkedAnchors(pages);
6296
+ preserveLinkedAnchors(pages);
5916
6297
  if (platform === "fern" && fernRawConfig) {
5917
6298
  const sourceSiteUrl = fernSourceSiteUrl(fernRawConfig);
5918
6299
  if (sourceSiteUrl) {
@@ -5953,14 +6334,133 @@ function migrateRepository(options) {
5953
6334
  ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown or MDX pages were found in the repository.");
5954
6335
  }
5955
6336
  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({
6337
+ const docusaurusSettings = isTopLevelDocusaurus ? readDocusaurusSiteSettings(docusaurusProjectRoot) : void 0;
6338
+ if (docusaurusSettings) {
6339
+ const importedRoutes = new Set(pages.map((page) => page.navigationId));
6340
+ const sourceOrigin = readDocusaurusSiteOrigin(docusaurusProjectRoot);
6341
+ const externalized = /* @__PURE__ */ new Set();
6342
+ const projectLink = (raw, sourceLiteral = false) => {
6343
+ if (/^https?:\/\//i.test(raw)) return raw;
6344
+ if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(raw) || raw.includes("\\")) return void 0;
6345
+ const suffixAt = raw.search(/[?#]/);
6346
+ const path = suffixAt < 0 ? raw : raw.slice(0, suffixAt);
6347
+ const suffix = suffixAt < 0 ? "" : raw.slice(suffixAt);
6348
+ const route = path.replace(/^\.?\//, "").replace(/\/+$/, "");
6349
+ if (route === ".." || route.startsWith("../") || route.includes("/../")) return void 0;
6350
+ const docsBase = docusaurusSettings.docsRouteBasePath.replace(/^\/+|\/+$/g, "");
6351
+ const isOutsideDocs = sourceLiteral && docsBase && route !== docsBase && !route.startsWith(`${docsBase}/`);
6352
+ if (isOutsideDocs && sourceOrigin) {
6353
+ externalized.add(raw);
6354
+ return new URL(raw, `${sourceOrigin}/`).toString();
6355
+ }
6356
+ const strippedDocsRoute = docsBase && route.startsWith(`${docsBase}/`) ? route.slice(docsBase.length + 1) : route;
6357
+ const candidates = [route, strippedDocsRoute, ...route === docsBase ? ["introduction"] : []];
6358
+ const imported = candidates.find((candidate) => importedRoutes.has(candidate));
6359
+ if (imported) return `/${imported}${suffix}`;
6360
+ if (sourceOrigin) {
6361
+ externalized.add(raw);
6362
+ return new URL(raw, `${sourceOrigin}/`).toString();
6363
+ }
6364
+ return path.startsWith("/") ? `${path}${suffix}` : `/${route}${suffix}`;
6365
+ };
6366
+ const firstNavigationPage = (nodes) => {
6367
+ for (const node of nodes) {
6368
+ if (typeof node === "string") return node;
6369
+ const nested = firstNavigationPage(node.pages);
6370
+ if (nested) return nested;
6371
+ }
6372
+ return void 0;
6373
+ };
6374
+ const navbarLinks = docusaurusSettings.navbarLinks.flatMap((link) => {
6375
+ const docRoute = link.docId && !link.docsPluginId && docusaurusDescriptors.find((page) => page.docId === link.docId)?.navigationId;
6376
+ const sidebar = link.sidebarId && docsConfig.tabs.find((tab) => tab.tab.toLowerCase() === link.sidebarId.toLowerCase());
6377
+ const sidebarRoute = sidebar && firstNavigationPage(sidebar.pages ?? sidebar.groups ?? []);
6378
+ const pluginRoot = link.docsPluginId && importedRoutes.has(link.docsPluginId) ? link.docsPluginId : void 0;
6379
+ const rawHref = link.href ?? (docRoute ? `/${docRoute}` : void 0) ?? (pluginRoot ? `/${pluginRoot}` : void 0) ?? (sidebarRoute ? `/${sidebarRoute}` : void 0) ?? (link.docsPluginId ? `/${link.docsPluginId}` : void 0);
6380
+ const href = rawHref && projectLink(rawHref, Boolean(link.href));
6381
+ return href ? [{ label: link.label, href }] : [];
6382
+ });
6383
+ const footerLinks = docusaurusSettings.footerLinks?.flatMap((column) => {
6384
+ const items = column.items.flatMap((item) => {
6385
+ const href = projectLink(item.href, true);
6386
+ return href ? [{ label: item.label, href }] : [];
6387
+ });
6388
+ return items.length > 0 ? [{ heading: column.heading, items }] : [];
6389
+ });
6390
+ docsConfig = {
6391
+ ...docsConfig,
6392
+ ...navbarLinks.length > 0 ? { navbar: { ...docsConfig.navbar, links: navbarLinks } } : {},
6393
+ ...footerLinks?.length || docusaurusSettings.copyright ? {
6394
+ footer: {
6395
+ ...docsConfig.footer,
6396
+ ...footerLinks?.length ? { links: footerLinks } : {},
6397
+ ...docusaurusSettings.copyright ? { copyright: docusaurusSettings.copyright } : {}
6398
+ }
6399
+ } : {}
6400
+ };
6401
+ if (externalized.size > 0) warnings.push({
5960
6402
  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.`
6403
+ message: `${externalized.size} site navigation/footer link(s) point outside the imported docs and were kept on ${sourceOrigin}.`
5962
6404
  });
5963
6405
  }
6406
+ if (!docsConfig.navigation?.display && docsConfig.tabs.filter((tab) => !tab.hidden).length > 6) {
6407
+ docsConfig = { ...docsConfig, navigation: { display: "dropdown" } };
6408
+ }
6409
+ const themeColors = mintlifyConfig ? mintlifyThemeColors(mintlifyConfig.colors) : fernRawConfig ? fernThemeColors(fernRawConfig.colors) : isTopLevelDocusaurus ? readDocusaurusThemeColor(docusaurusProjectRoot) : void 0;
6410
+ const assetPaths = new Set(assets.map((asset) => asset.path));
6411
+ const publicBrandPath = (value) => {
6412
+ if (typeof value !== "string" || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(value)) return void 0;
6413
+ const normalized = normalizeAssetPath(value);
6414
+ return normalized && assetPaths.has(normalized) ? `/${normalized}` : void 0;
6415
+ };
6416
+ const brandVariants = (value) => {
6417
+ if (typeof value === "string") return { light: value };
6418
+ if (!value || typeof value !== "object") return {};
6419
+ const source = value;
6420
+ const light = source.light ?? source.src;
6421
+ const dark = source.dark ?? source.srcDark;
6422
+ return {
6423
+ ...typeof light === "string" ? { light } : {},
6424
+ ...typeof dark === "string" ? { dark } : {},
6425
+ ...typeof source.showTitle === "boolean" ? { showTitle: source.showTitle } : {}
6426
+ };
6427
+ };
6428
+ const sourceLogo = mintlifyConfig?.logo ?? fernRawConfig?.logo ?? docusaurusSettings?.logo;
6429
+ const sourceFavicon = mintlifyConfig?.favicon ?? fernRawConfig?.favicon ?? docusaurusSettings?.favicon;
6430
+ const logo = brandVariants(sourceLogo);
6431
+ const favicon = brandVariants(sourceFavicon);
6432
+ const logoLight = publicBrandPath(logo.light);
6433
+ const logoDark = publicBrandPath(logo.dark);
6434
+ const faviconLight = publicBrandPath(favicon.light);
6435
+ const faviconDark = publicBrandPath(favicon.dark);
6436
+ if (logoLight) docsConfig = {
6437
+ ...docsConfig,
6438
+ navbar: {
6439
+ ...docsConfig.navbar,
6440
+ logo: {
6441
+ light: logoLight,
6442
+ ...logoDark ? { dark: logoDark } : {},
6443
+ showTitle: mintlifyConfig || fernRawConfig ? false : logo.showTitle !== false,
6444
+ ...typeof fernRawConfig?.logo?.["right-text"] === "string" ? { rightText: String(fernRawConfig.logo["right-text"]).trim().slice(0, 40) } : {}
6445
+ }
6446
+ }
6447
+ };
6448
+ if (faviconLight) docsConfig = {
6449
+ ...docsConfig,
6450
+ favicon: { light: faviconLight, ...faviconDark ? { dark: faviconDark } : {} }
6451
+ };
6452
+ const missingBrand = [
6453
+ ...[logo.light, logo.dark].filter((value) => typeof value === "string" && !publicBrandPath(value)),
6454
+ ...[favicon.light, favicon.dark].filter((value) => typeof value === "string" && !publicBrandPath(value))
6455
+ ];
6456
+ if (missingBrand.length > 0) warnings.push({
6457
+ code: "unsupported-config",
6458
+ message: `Brand asset(s) were referenced but not imported: ${[...new Set(missingBrand)].join(", ")}. Add these files under public/ or update docs.json.`
6459
+ });
6460
+ if (fernRawConfig?.logo && !logoLight && !logo.light) warnings.push({
6461
+ code: "unsupported-config",
6462
+ message: "Fern supplied a logo through its global theme without a local asset; add a logo path to docs.json after import."
6463
+ });
5964
6464
  return {
5965
6465
  sourceUrl: options.sourceUrl,
5966
6466
  sourceKind: "repository",
@@ -5980,6 +6480,12 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
5980
6480
  ...typeof fernRawConfig.title === "string" ? { name: fernRawConfig.title } : {},
5981
6481
  ...themeColors ? { colors: themeColors } : {}
5982
6482
  }
6483
+ } : docusaurusSettings && (docusaurusSettings.name || docusaurusSettings.description || themeColors) ? {
6484
+ site: {
6485
+ ...docusaurusSettings.name ? { name: docusaurusSettings.name } : {},
6486
+ ...docusaurusSettings.description ? { description: docusaurusSettings.description } : {},
6487
+ ...themeColors ? { colors: themeColors } : {}
6488
+ }
5983
6489
  } : themeColors ? {
5984
6490
  site: { colors: themeColors }
5985
6491
  } : {},
@@ -5998,6 +6504,7 @@ function renderPage(bundle, page) {
5998
6504
  `title: ${yamlString(page.title)}`,
5999
6505
  page.navTitle ? `navTitle: ${yamlString(page.navTitle)}` : null,
6000
6506
  `description: ${yamlString(page.description)}`,
6507
+ page.descriptionPlacement === "body" ? "descriptionPlacement: body" : null,
6001
6508
  page.badge ? `badge: ${yamlString(page.badge)}` : null,
6002
6509
  page.keywords.length > 0 ? `keywords: [${page.keywords.map(yamlString).join(", ")}]` : null,
6003
6510
  page.mode ? `mode: ${yamlString(page.mode)}` : null,
@@ -6099,7 +6606,7 @@ function renderMigrationFiles(bundle, options = {}) {
6099
6606
  }
6100
6607
 
6101
6608
  // src/url.ts
6102
- import { load } from "cheerio";
6609
+ import { load as load2 } from "cheerio";
6103
6610
  import remarkMdx3 from "remark-mdx";
6104
6611
  import remarkParse3 from "remark-parse";
6105
6612
  import TurndownService from "turndown";
@@ -6276,7 +6783,7 @@ function machineIndexScopePath(source, documents) {
6276
6783
  if (!document) continue;
6277
6784
  const references = [...headerLinks(document)];
6278
6785
  if (/html/i.test(document.contentType)) {
6279
- const $ = load(document.body);
6786
+ const $ = load2(document.body);
6280
6787
  references.push(...$("link[href], a[href]").map((_index, element) => $(element).attr("href") ?? "").get().filter((href) => /\/(?:llms(?:-full)?\.txt|sitemap\.xml)(?:[?#]|$)/i.test(href)));
6281
6788
  }
6282
6789
  for (const reference of references) {
@@ -6387,7 +6894,7 @@ function jsonObjectAfterKey(value, key) {
6387
6894
  return null;
6388
6895
  }
6389
6896
  function embeddedMintlifyConfig(body) {
6390
- const $ = load(body);
6897
+ const $ = load2(body);
6391
6898
  const flightPayloads = [];
6392
6899
  $("script").each((_index, element) => {
6393
6900
  const script = $(element).text();
@@ -6485,7 +6992,7 @@ function isSafePortableUrl(value) {
6485
6992
  }
6486
6993
  }
6487
6994
  function decodeHtmlAttribute(value) {
6488
- const document = load(`<i data-thally-value="${value.replace(/"/g, "&quot;")}"></i>`);
6995
+ const document = load2(`<i data-thally-value="${value.replace(/"/g, "&quot;")}"></i>`);
6489
6996
  return document("i").attr("data-thally-value") ?? "";
6490
6997
  }
6491
6998
  function escapeMarkdownText(value, escapeTableSeparator = false) {
@@ -6534,7 +7041,7 @@ function replacePortableHtmlImages(value) {
6534
7041
  if (start < cursor) continue;
6535
7042
  const end = htmlOpeningTagEnd(value, start + match[0].length);
6536
7043
  if (end === null) continue;
6537
- const fragment = load(value.slice(start, end));
7044
+ const fragment = load2(value.slice(start, end));
6538
7045
  const image = fragment("img").first();
6539
7046
  const source = image.attr("src");
6540
7047
  const destination = source && isSafePortableUrl(source) ? markdownLinkDestination(source) : null;
@@ -6559,7 +7066,7 @@ function replacePortableHtmlAnchors(value) {
6559
7066
  const closing = closingMatch.exec(value);
6560
7067
  if (!closing) continue;
6561
7068
  const end = closing.index + closing[0].length;
6562
- const fragment = load(value.slice(start, end));
7069
+ const fragment = load2(value.slice(start, end));
6563
7070
  const anchor = fragment("a").first();
6564
7071
  const href = anchor.attr("href");
6565
7072
  const label = escapeMarkdownText(anchor.text().replace(/\s+/g, " ").trim());
@@ -6940,7 +7447,7 @@ function mergeEmbeddedOpenApi(fragments) {
6940
7447
  return new TextEncoder().encode(stringifyYaml(combined));
6941
7448
  }
6942
7449
  function htmlPage(document, id) {
6943
- const $ = load(document.body);
7450
+ const $ = load2(document.body);
6944
7451
  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
7452
  const title = $("main h1, article h1, h1").first().text().trim() || $('meta[property="og:title"]').attr("content")?.trim() || $("title").text().split("|")[0].trim();
6946
7453
  const description = $('meta[name="description"]').attr("content")?.trim() || $('meta[property="og:description"]').attr("content")?.trim();
@@ -6984,7 +7491,7 @@ function htmlPage(document, id) {
6984
7491
  const element = node;
6985
7492
  const className = element.className;
6986
7493
  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);
7494
+ const fragment = load2(element.outerHTML);
6988
7495
  const clone = fragment(".theme-admonition").first();
6989
7496
  const title2 = clone.find('[class*="admonitionHeading"],.admonition-heading').first();
6990
7497
  const titleText = title2.text().trim();
@@ -7005,7 +7512,7 @@ ${titleText ? `**${titleText}**
7005
7512
  filter: (node) => node.nodeName === "DIV" && node.classList.contains("tabs-container"),
7006
7513
  replacement: (_content, node) => {
7007
7514
  const element = node;
7008
- const fragment = load(element.outerHTML);
7515
+ const fragment = load2(element.outerHTML);
7009
7516
  const container = fragment(".tabs-container").first();
7010
7517
  const labels = container.children("ul.tabs").children("li").toArray().map((item) => fragment(item).text().trim() || "Tab");
7011
7518
  const panels = container.children('div[role="tabpanel"]').toArray();
@@ -7028,7 +7535,7 @@ ${tabs.join("\n\n")}
7028
7535
  turndown.addRule("gfm-table", {
7029
7536
  filter: "table",
7030
7537
  replacement: (_content, node) => {
7031
- const fragment = load(node.outerHTML);
7538
+ const fragment = load2(node.outerHTML);
7032
7539
  const rows = fragment("tr").toArray().map((row) => fragment(row).children("th,td").toArray().map((cell) => escapeMarkdownText(
7033
7540
  fragment(cell).text().replace(/\s+/g, " ").trim(),
7034
7541
  true
@@ -7049,7 +7556,7 @@ ${normalized.slice(1).map((row) => `| ${row.join(" | ")} |`).join("\n")}
7049
7556
  filter: (node) => node.nodeName === "PRE",
7050
7557
  replacement: (_content, node) => {
7051
7558
  const element = node;
7052
- const fragment = load(element.outerHTML);
7559
+ const fragment = load2(element.outerHTML);
7053
7560
  const tokenLines = fragment(".token-line").toArray();
7054
7561
  const code = (tokenLines.length > 0 ? tokenLines.map((line) => fragment(line).text()).join("\n") : fragment("pre").text()).replace(/\n$/, "");
7055
7562
  const language = fragment("code").attr("class")?.match(/(?:language-|lang-)([A-Za-z0-9_+-]+)/)?.[1] ?? "";
@@ -7082,7 +7589,7 @@ ${fence}
7082
7589
  };
7083
7590
  }
7084
7591
  function htmlNavigationLinks(document) {
7085
- const $ = load(document.body);
7592
+ const $ = load2(document.body);
7086
7593
  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
7594
  }
7088
7595
  function normalizedLinkLabel(value) {
@@ -7090,7 +7597,7 @@ function normalizedLinkLabel(value) {
7090
7597
  }
7091
7598
  function readDocusaurusSidebar(document) {
7092
7599
  if (!/html/i.test(document.contentType)) return null;
7093
- const $ = load(document.body);
7600
+ const $ = load2(document.body);
7094
7601
  const sidebar = $("ul.theme-doc-sidebar-menu").first();
7095
7602
  if (sidebar.length === 0) return null;
7096
7603
  function readList(list) {
@@ -7126,7 +7633,7 @@ function docusaurusSidebarHrefs(snapshot) {
7126
7633
  return hrefs;
7127
7634
  }
7128
7635
  function deriveDocusaurusScope(source, document, submittedScopePath) {
7129
- const $ = load(document.body);
7636
+ const $ = load2(document.body);
7130
7637
  const sidebar = readDocusaurusSidebar(document);
7131
7638
  const internalPaths = docusaurusSidebarHrefs(sidebar).flatMap((href) => {
7132
7639
  try {
@@ -7256,7 +7763,7 @@ function docusaurusNavigationFromSnapshots(snapshots, pages, scopePath) {
7256
7763
  return { tabs };
7257
7764
  }
7258
7765
  function htmlTopLevelNavigation(document, source, scopePath) {
7259
- const $ = load(document.body);
7766
+ const $ = load2(document.body);
7260
7767
  const collectCandidates = (selector) => {
7261
7768
  const candidates2 = [];
7262
7769
  $(selector).each((_containerIndex, container) => {
@@ -7317,7 +7824,7 @@ function markdownLinks(body) {
7317
7824
  return [...body.matchAll(/\[[^\]]+\]\(([^)\s]+)(?:\s+['"][^'"]*['"])?\)/g)].map((match) => match[1]);
7318
7825
  }
7319
7826
  function sitemapLinks(body) {
7320
- const $ = load(body, { xmlMode: true });
7827
+ const $ = load2(body, { xmlMode: true });
7321
7828
  return $("url > loc, sitemap > loc").map((_index, element) => $(element).text().trim()).get().filter(Boolean);
7322
7829
  }
7323
7830
  function robotsSitemaps(body) {
@@ -7356,7 +7863,7 @@ async function migrateUrl(options) {
7356
7863
  const platform = options.platform ?? detectUrlPlatform(scopedHtmlProbe ?? initial);
7357
7864
  let docusaurusHtmlProbe = platform === "docusaurus" ? scopedHtmlProbe : null;
7358
7865
  if (docusaurusHtmlProbe && !readDocusaurusSidebar(docusaurusHtmlProbe)) {
7359
- const $ = load(docusaurusHtmlProbe.body);
7866
+ const $ = load2(docusaurusHtmlProbe.body);
7360
7867
  const docsHref = $("nav a[href]").toArray().map((element) => ({
7361
7868
  href: $(element).attr("href"),
7362
7869
  label: normalizedLinkLabel($(element).text())