@thallylabs/migrate 0.2.9 → 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
@@ -53,6 +53,7 @@ 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";
57
58
  import { nameToEmoji } from "gemoji";
58
59
  import remarkGfm from "remark-gfm";
@@ -644,6 +645,66 @@ function replaceOutsideCode(body, transform) {
644
645
  const { masked, unmask } = maskCode(body);
645
646
  return unmask(transform(masked));
646
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
+ }
647
708
  function normalizeHtmlComments(body) {
648
709
  return replaceOutsideCode(body, (whole) => whole.replace(/<!--([\s\S]*?)-->/g, (_match, comment) => `{/*${comment}*/}`));
649
710
  }
@@ -1013,7 +1074,7 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1013
1074
  const runMintlify = platform === "mintlify";
1014
1075
  const runDocusaurus = platform === "docusaurus";
1015
1076
  const sourceBody = runDocusaurus && unwrapMdxCodeBlocks ? unwrapDocusaurusMdxCodeBlocks(body) : body;
1016
- let rewritten = normalizeDocusaurusAdmonitions(normalizeExplicitHeadingIds(runFern ? normalizeFernCallouts(sourceBody) : sourceBody));
1077
+ let rewritten = normalizeDocusaurusAdmonitions(normalizeExplicitHeadingIds(runFern ? normalizeFernFileTrees(normalizeFernCallouts(sourceBody)) : sourceBody));
1017
1078
  if (runDocusaurus) {
1018
1079
  rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1019
1080
  /:([+\w-]+):/g,
@@ -1493,9 +1554,14 @@ function implicitReactImports(source) {
1493
1554
  visit(source);
1494
1555
  const hooks = [...REACT_GLOBALS].filter((name) => references.has(name) && !bindings.has(name)).sort();
1495
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);
1496
1561
  return [
1497
1562
  references.has("React") && !bindings.has("React") ? "import * as React from 'react';" : "",
1498
1563
  hooks.length ? `import { ${hooks.join(", ")} } from 'react';` : "",
1564
+ ...implicitComponents,
1499
1565
  usesMintlifyComponents ? mintlifyComponentsShim() : ""
1500
1566
  ].filter(Boolean).join("\n");
1501
1567
  }
@@ -2242,7 +2308,7 @@ import { customComponents as ${name} } from ${JSON.stringify(registryPath)}
2242
2308
 
2243
2309
  // src/docusaurus.ts
2244
2310
  import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3 } from "fs";
2245
- import { extname as extname3, posix } from "path";
2311
+ import { extname as extname3, posix as posix2 } from "path";
2246
2312
  import JSON5 from "json5";
2247
2313
  import { parse as parseYaml2 } from "yaml";
2248
2314
 
@@ -3063,19 +3129,19 @@ function storageIdForRoute(routeId) {
3063
3129
  function titleCase2(value) {
3064
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(" ");
3065
3131
  }
3066
- function routeFromSlug(slug, fallback, sourceDirectory) {
3067
- if (slug === "/") return "introduction";
3068
- if (slug.startsWith("/")) return docusaurusRouteId(slug);
3069
- const fallbackDirectory = sourceDirectory ?? (fallback === "introduction" ? "" : posix.dirname(fallback));
3070
- const resolved = posix.normalize(posix.join(fallbackDirectory, slug));
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));
3071
3137
  if (resolved === ".." || resolved.startsWith("../")) return null;
3072
3138
  return docusaurusRouteId(resolved);
3073
3139
  }
3074
3140
  function resolveDocusaurusPageIdentity(sourcePath, frontmatter, fallback) {
3075
3141
  const sourceDocId = normalizeDocId(sourcePath);
3076
- const sourceDirectory = posix.dirname(sourceDocId);
3142
+ const sourceDirectory = posix2.dirname(sourceDocId);
3077
3143
  const configuredId = typeof frontmatter.id === "string" && frontmatter.id.trim() ? stripNumberPrefix(frontmatter.id.trim()) : null;
3078
- const docId = configuredId ? normalizeDocId(sourceDirectory === "." ? configuredId : posix.join(sourceDirectory, configuredId)) : sourceDocId;
3144
+ const docId = configuredId ? normalizeDocId(sourceDirectory === "." ? configuredId : posix2.join(sourceDirectory, configuredId)) : sourceDocId;
3079
3145
  const defaultNavigationId = defaultDocusaurusRouteId(docId) ?? fallback.navigationId;
3080
3146
  const configuredSlug = typeof frontmatter.slug === "string" ? frontmatter.slug.trim() : "";
3081
3147
  const navigationId = configuredSlug ? routeFromSlug(
@@ -3097,6 +3163,105 @@ function resolveDocusaurusPageIdentity(sourcePath, frontmatter, fallback) {
3097
3163
  function sourceReferenceKey(value) {
3098
3164
  return value.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\.(?:mdx?)$/i, "").replace(/\/$/, "");
3099
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
+ }
3100
3265
  function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
3101
3266
  const routes = /* @__PURE__ */ new Map();
3102
3267
  for (const descriptor of descriptors) {
@@ -3107,18 +3272,30 @@ function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
3107
3272
  const sourceRoute = pageIdFromReference(sourceKey);
3108
3273
  if (sourceRoute) routes.set(sourceRoute, descriptor.navigationId);
3109
3274
  }
3110
- 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
+ }
3111
3286
  function rewriteTarget(target) {
3112
3287
  if (!target || target.startsWith("#") || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(target)) return target;
3113
3288
  const suffixIndex = target.search(/[?#]/);
3114
3289
  const path = suffixIndex >= 0 ? target.slice(0, suffixIndex) : target;
3115
3290
  const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
3291
+ const copiedAsset = copiedAssetTarget(target);
3292
+ if (copiedAsset) return copiedAsset;
3116
3293
  const candidates = path.startsWith("/") ? [
3117
3294
  sourceReferenceKey(path),
3118
3295
  sourceReferenceKey(path).replace(/^docs\//, ""),
3119
3296
  sourceReferenceKey(path).replace(/^docs\/(?:next|current|latest)\//, "")
3120
3297
  ] : [
3121
- sourceReferenceKey(posix.normalize(posix.join(currentDirectory, path))),
3298
+ sourceReferenceKey(posix2.normalize(posix2.join(currentDirectory, path))),
3122
3299
  sourceReferenceKey(path)
3123
3300
  ];
3124
3301
  const route = candidates.map((candidate) => routes.get(candidate)).find(Boolean);
@@ -3140,7 +3317,7 @@ function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
3140
3317
  return line;
3141
3318
  }
3142
3319
  if (codeFence) return line;
3143
- return line.replace(/(\]\()([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(target)}`).replace(/(\bhref=")([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(target)}${suffix}`).replace(/^(\s{0,3}\[[^\]]+\]:\s*)(<[^>]+>|\S+)/, (_match, prefix, destination) => {
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) => {
3144
3321
  const bracketed = destination.startsWith("<") && destination.endsWith(">");
3145
3322
  const target = bracketed ? destination.slice(1, -1) : destination;
3146
3323
  const rewritten = rewriteTarget(target);
@@ -3418,7 +3595,7 @@ function readDocusaurusRedirects(repositoryRoot, warnings) {
3418
3595
  }
3419
3596
  function readCategoryMetadata(contentRoot, directory) {
3420
3597
  for (const filename of CATEGORY_FILENAMES) {
3421
- const path = resolveWithin(contentRoot, posix.join(directory, filename));
3598
+ const path = resolveWithin(contentRoot, posix2.join(directory, filename));
3422
3599
  if (!existsSync3(path) || !lstatSync3(path).isFile()) continue;
3423
3600
  try {
3424
3601
  const raw = readBoundedText(path);
@@ -3463,7 +3640,7 @@ function generatedIndexPage(label, linkValue, context) {
3463
3640
  if (link.type !== "generated-index") return null;
3464
3641
  const fallback = `category/${slugifySegment(label)}`;
3465
3642
  const unprefixedNavigationId = typeof link.slug === "string" ? routeFromSlug(link.slug, fallback) ?? fallback : fallback;
3466
- const navigationId = context.routePrefix && unprefixedNavigationId !== context.routePrefix && !unprefixedNavigationId.startsWith(`${context.routePrefix}/`) ? posix.join(context.routePrefix, unprefixedNavigationId) : unprefixedNavigationId;
3643
+ const navigationId = context.routePrefix && unprefixedNavigationId !== context.routePrefix && !unprefixedNavigationId.startsWith(`${context.routePrefix}/`) ? posix2.join(context.routePrefix, unprefixedNavigationId) : unprefixedNavigationId;
3467
3644
  if (!context.generatedIds.has(navigationId) && !context.descriptors.some((page) => page.navigationId === navigationId)) {
3468
3645
  const description = typeof link.description === "string" ? link.description : `Browse the ${label} documentation.`;
3469
3646
  context.generatedPages.push({
@@ -3601,7 +3778,7 @@ function staticStringField(source, name, bindings) {
3601
3778
  }
3602
3779
  }
3603
3780
  function readDocusaurusSiteSettings(repositoryRoot) {
3604
- const empty = { docsRouteBasePath: "docs", navbarLinks: [], footerLinks: [] };
3781
+ const empty = { docsRouteBasePath: "docs", defaultColorMode: "light", navbarLinks: [], footerLinks: [] };
3605
3782
  const configPath = findDocusaurusConfigPath(repositoryRoot);
3606
3783
  if (!configPath) return empty;
3607
3784
  const source = readBoundedText(configPath);
@@ -3611,6 +3788,10 @@ function readDocusaurusSiteSettings(repositoryRoot) {
3611
3788
  const docsRouteBasePath = classicDocs && staticStringField(classicDocs, "routeBasePath") || "docs";
3612
3789
  const themeStart = propertyStart(source, "themeConfig");
3613
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";
3614
3795
  const navbarStart = theme ? propertyStart(theme, "navbar") : -1;
3615
3796
  const navbar = navbarStart < 0 ? null : matchingObjectLiteral(theme, navbarStart);
3616
3797
  const logoStart = navbar ? propertyStart(navbar, "logo") : -1;
@@ -3646,6 +3827,8 @@ function readDocusaurusSiteSettings(repositoryRoot) {
3646
3827
  return {
3647
3828
  name: staticStringField(source, "title"),
3648
3829
  description: staticStringField(source, "tagline"),
3830
+ ...navbar && staticStringField(navbar, "title") ? { navbarTitle: staticStringField(navbar, "title") } : {},
3831
+ defaultColorMode,
3649
3832
  docsRouteBasePath,
3650
3833
  ...logo && staticStringField(logo, "src") ? {
3651
3834
  logo: {
@@ -3670,32 +3853,47 @@ function autogeneratedItems(dirName, context) {
3670
3853
  const childDirectories = /* @__PURE__ */ new Set();
3671
3854
  for (const descriptor of inDirectory) {
3672
3855
  const source = descriptor.sourcePath.replace(/\.(?:mdx?)$/i, "");
3673
- const relativePath = normalizedDir ? posix.relative(normalizedDir, source) : source;
3856
+ const relativePath = normalizedDir ? posix2.relative(normalizedDir, source) : source;
3674
3857
  const segments = relativePath.split("/").filter(Boolean);
3675
3858
  if (segments.length <= 1) directPages.push(descriptor);
3676
3859
  else childDirectories.add(segments[0]);
3677
3860
  }
3678
- const items = directPages.sort(descriptorSort).map((descriptor) => {
3679
- context.referencedNavigationIds.add(descriptor.navigationId);
3680
- return descriptor.navigationId;
3681
- });
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
+ }));
3682
3868
  const groups = [...childDirectories].map((segment) => {
3683
- const directory = normalizedDir ? posix.join(normalizedDir, segment) : segment;
3869
+ const directory = normalizedDir ? posix2.join(normalizedDir, segment) : segment;
3684
3870
  const metadata = readCategoryMetadata(context.contentRoot, directory);
3685
- 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
+ }
3686
3882
  const landing = generatedIndexPage(metadata.label ?? titleCase2(segment), metadata.link, context);
3687
- if (landing && !pages.includes(landing)) pages.unshift(landing);
3883
+ if (landing && !pages2.includes(landing)) pages2.unshift(landing);
3688
3884
  return {
3689
3885
  position: metadata.position ?? Number.MAX_SAFE_INTEGER,
3690
- segment,
3691
- group: {
3886
+ sortKey: segment,
3887
+ item: {
3692
3888
  group: metadata.label ?? titleCase2(segment),
3693
- pages
3889
+ pages: pages2
3694
3890
  }
3695
3891
  };
3696
- }).sort((left, right) => left.position - right.position || left.segment.localeCompare(right.segment, void 0, { numeric: true }));
3697
- items.push(...groups.map((entry) => entry.group));
3698
- return items;
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
+ });
3699
3897
  }
3700
3898
  function convertItems(value, context) {
3701
3899
  if (typeof value === "string") {
@@ -3984,11 +4182,23 @@ function convertNode(node, parentSegments, context) {
3984
4182
  };
3985
4183
  }
3986
4184
  if (typeof object.link === "string") {
3987
- warnOnce2(
3988
- context,
3989
- "fern-link",
3990
- 'Fern navigation "link" entries are external and cannot be nested inside a Thally group; add them to the navbar manually.'
3991
- );
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
+ }
3992
4202
  return null;
3993
4203
  }
3994
4204
  if (typeof object.api === "string") {
@@ -4189,6 +4399,7 @@ function projectFernNavigation(input) {
4189
4399
  changelogIndexes: [],
4190
4400
  warnings: [],
4191
4401
  warningKeys: /* @__PURE__ */ new Set(),
4402
+ externalLinks: [],
4192
4403
  bareRouteRedirects: [],
4193
4404
  segmentAliases: /* @__PURE__ */ new Map(),
4194
4405
  pathPrefix: "",
@@ -4209,6 +4420,11 @@ function projectFernNavigation(input) {
4209
4420
  const label = typeof link?.text === "string" ? link.text : typeof link?.label === "string" ? link.label : link?.type === "github" ? "GitHub" : void 0;
4210
4421
  return href && label ? [{ label, href, ...link?.type === "github" ? { type: "github" } : {} }] : [];
4211
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
+ }
4212
4428
  const announcement = objectValue3(config.announcement);
4213
4429
  const announcementMessage = typeof announcement?.message === "string" ? announcement.message.trim() : "";
4214
4430
  const bannerContent = announcementMarkdown(announcementMessage);
@@ -4942,7 +5158,7 @@ function mintlifyTopLevelApiReferences(config) {
4942
5158
  }
4943
5159
  return references;
4944
5160
  }
4945
- function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings) {
5161
+ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
4946
5162
  if (!mintlifyConfig) return [];
4947
5163
  const references = [
4948
5164
  ...mintlifyTopLevelApiReferences(mintlifyConfig),
@@ -4962,13 +5178,19 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings) {
4962
5178
  });
4963
5179
  continue;
4964
5180
  }
4965
- if (/^https?:\/\//i.test(reference.value)) {
5181
+ if (/^https:\/\//i.test(reference.value)) {
5182
+ remoteSpecs.push({ url: reference.value, ...reference.tabLabel ? { tabLabel: reference.tabLabel } : {} });
4966
5183
  warnings.push({
4967
5184
  code: "unsupported-config",
4968
- message: `The remote OpenAPI spec "${reference.value}"${tabSuffix} was not downloaded. Download it manually, add it to public/, and set its API source in docs.json to the local path.`
5185
+ message: `The remote OpenAPI spec "${reference.value}"${tabSuffix} requires a network download before this import is complete.`,
5186
+ source: reference.value
4969
5187
  });
4970
5188
  continue;
4971
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
+ }
4972
5194
  const key = reference.value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/");
4973
5195
  const match = files.find((file) => file.relativePath === key);
4974
5196
  if (!match) {
@@ -5364,9 +5586,9 @@ function addDatedHeadingAliases(body, date) {
5364
5586
  if (open) return [line];
5365
5587
  const heading = line.match(/^#{2,6}\s+(.+?)\s*#*\s*$/);
5366
5588
  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];
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];
5370
5592
  existingIds.add(id);
5371
5593
  return [`<a id="${id}" />`, "", line];
5372
5594
  }).join("\n");
@@ -5757,6 +5979,7 @@ function migrateRepository(options) {
5757
5979
  }
5758
5980
  const pages = [];
5759
5981
  const assets = [];
5982
+ const remoteApiSpecs = [];
5760
5983
  const referencedAssetPaths = /* @__PURE__ */ new Map();
5761
5984
  const addAssetReference = (assetPath, referencingPage) => {
5762
5985
  const referrers = referencedAssetPaths.get(assetPath);
@@ -5764,6 +5987,7 @@ function migrateRepository(options) {
5764
5987
  else referencedAssetPaths.set(assetPath, /* @__PURE__ */ new Set([referencingPage]));
5765
5988
  };
5766
5989
  const docusaurusDescriptors = [];
5990
+ const docusaurusDocCardRoutes = /* @__PURE__ */ new Set();
5767
5991
  const seenPageIds = /* @__PURE__ */ new Set();
5768
5992
  const fernIdRenames = /* @__PURE__ */ new Map();
5769
5993
  let skipped = 0;
@@ -5858,6 +6082,10 @@ function migrateRepository(options) {
5858
6082
  }
5859
6083
  raw = escapeFernLiteralBraces(protectedMath.body);
5860
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
+ }
5861
6089
  let docusaurusDescriptor;
5862
6090
  const page = parseMarkdownPage({
5863
6091
  id,
@@ -5891,9 +6119,9 @@ function migrateRepository(options) {
5891
6119
  // hierarchy (never just its own segment) and wins over a docs.yml slug.
5892
6120
  ...platform === "fern" ? {
5893
6121
  resolveIdentity: (frontmatter, fallback) => {
5894
- const slug = typeof frontmatter.slug === "string" ? frontmatter.slug.trim() : "";
5895
- if (!slug) return fallback;
5896
- 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);
5897
6125
  if (!overridden) return fallback;
5898
6126
  fernIdRenames.set(fallback.navigationId, overridden);
5899
6127
  return { ...fallback, id: overridden, navigationId: overridden };
@@ -5918,6 +6146,9 @@ function migrateRepository(options) {
5918
6146
  page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, fernProjectRoot, (assetPath) => {
5919
6147
  addAssetReference(assetPath, file.relativePath);
5920
6148
  });
6149
+ page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, repositoryDir, (assetPath) => {
6150
+ addAssetReference(assetPath, file.relativePath);
6151
+ });
5921
6152
  page.body = stripFernBasePathFromLinks(page.body, fernBasePath);
5922
6153
  page.body = stripFernBasePathFromLinks(page.body, fernVersionPath);
5923
6154
  }
@@ -5972,6 +6203,9 @@ function migrateRepository(options) {
5972
6203
  }
5973
6204
  seenPageIds.add(page.id);
5974
6205
  pages.push(page);
6206
+ if (hasDocusaurusDocCardList) {
6207
+ docusaurusDocCardRoutes.add(page.navigationId);
6208
+ }
5975
6209
  if (platform === "mintlify") {
5976
6210
  const sourcePath = exactReferenceKey(file.relativePath);
5977
6211
  if (sourcePath !== page.id && /^[A-Za-z0-9_./-]+$/.test(sourcePath)) {
@@ -5989,6 +6223,43 @@ function migrateRepository(options) {
5989
6223
  }
5990
6224
  }
5991
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
+ }
5992
6263
  const repositoryAssets = platform === "docusaurus" && configuredDocsDir && !options.docusaurusSkipAssets ? ["static", "public"].flatMap((directory) => {
5993
6264
  const root = resolveWithin(docusaurusAssetRoot, directory);
5994
6265
  if (!existsSync5(root) || !lstatSync5(root).isDirectory()) return [];
@@ -5998,10 +6269,10 @@ function migrateRepository(options) {
5998
6269
  }));
5999
6270
  }) : [];
6000
6271
  const assetCandidates = [];
6001
- for (const file of [...files, ...repositoryAssets]) {
6272
+ for (const file of [...files, ...repositoryAssets, ...fernReferencedAssets]) {
6002
6273
  const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
6003
6274
  if (!ASSET_EXTENSIONS.has(extname5(file.relativePath).toLowerCase())) continue;
6004
- if (platform !== "mintlify" && !ASSET_DIRECTORIES.has(firstSegment)) continue;
6275
+ if (platform !== "mintlify" && !ASSET_DIRECTORIES.has(firstSegment) && !(platform === "fern" && referencedAssetPaths.has(file.relativePath))) continue;
6005
6276
  const isDocusaurusStatic = platform === "docusaurus" && firstSegment === "static";
6006
6277
  const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : isDocusaurusStatic ? file.relativePath.slice("static/".length) : file.relativePath);
6007
6278
  if (!assetPath) continue;
@@ -6047,6 +6318,7 @@ function migrateRepository(options) {
6047
6318
  routePrefix: options.docusaurusRoutePrefix
6048
6319
  });
6049
6320
  docsConfig = projected.docsConfig;
6321
+ expandDocusaurusDocCardLists(pages, docsConfig, docusaurusDocCardRoutes);
6050
6322
  warnings.push(...projected.warnings);
6051
6323
  for (const page of projected.generatedPages) {
6052
6324
  if (seenPageIds.has(page.id)) continue;
@@ -6062,7 +6334,8 @@ function migrateRepository(options) {
6062
6334
  const descriptor = descriptorByNavigationId.get(page.navigationId);
6063
6335
  if (descriptor) page.body = rewriteDocusaurusLinks(page.body, descriptor, docusaurusDescriptors, {
6064
6336
  sourceOrigin,
6065
- onExternalLink: (target) => externalizedLinks.add(target)
6337
+ onExternalLink: (target) => externalizedLinks.add(target),
6338
+ assetPaths: new Set(assets.map((asset) => asset.path))
6066
6339
  });
6067
6340
  }
6068
6341
  if (externalizedLinks.size > 0) warnings.push({
@@ -6153,7 +6426,7 @@ function migrateRepository(options) {
6153
6426
  }
6154
6427
  if (resolvedSpecs.length > 0) docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs);
6155
6428
  } else if (platform === "mintlify") {
6156
- const resolvedSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings);
6429
+ const resolvedSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs);
6157
6430
  for (const spec of resolvedSpecs) {
6158
6431
  if (!assets.some((asset) => asset.path === spec.filename)) {
6159
6432
  assets.push({ path: spec.filename, content: spec.content });
@@ -6234,6 +6507,15 @@ function migrateRepository(options) {
6234
6507
  }
6235
6508
  }
6236
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
+ }
6237
6519
  const importedRoutes = new Set(pages.map((page) => page.navigationId));
6238
6520
  docsConfig.tabs = docsConfig.tabs.map((tab) => {
6239
6521
  if (tab.href) return tab;
@@ -6293,6 +6575,27 @@ ${body}`;
6293
6575
  }
6294
6576
  }
6295
6577
  docsConfig = pruneMissingNavigationPages(docsConfig, new Set(pages.map((page) => page.navigationId)));
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
+ }
6296
6599
  preserveLinkedAnchors(pages);
6297
6600
  if (platform === "fern" && fernRawConfig) {
6298
6601
  const sourceSiteUrl = fernSourceSiteUrl(fernRawConfig);
@@ -6410,7 +6713,7 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
6410
6713
  const assetPaths = new Set(assets.map((asset) => asset.path));
6411
6714
  const publicBrandPath = (value) => {
6412
6715
  if (typeof value !== "string" || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(value)) return void 0;
6413
- const normalized = normalizeAssetPath(value);
6716
+ const normalized = fernBrandAssetPaths.get(value) ?? normalizeAssetPath(value);
6414
6717
  return normalized && assetPaths.has(normalized) ? `/${normalized}` : void 0;
6415
6718
  };
6416
6719
  const brandVariants = (value) => {
@@ -6445,6 +6748,34 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
6445
6748
  }
6446
6749
  }
6447
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
+ }
6448
6779
  if (faviconLight) docsConfig = {
6449
6780
  ...docsConfig,
6450
6781
  favicon: { light: faviconLight, ...faviconDark ? { dark: faviconDark } : {} }
@@ -6467,6 +6798,7 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
6467
6798
  platform,
6468
6799
  pages,
6469
6800
  assets,
6801
+ ...remoteApiSpecs.length > 0 ? { remoteApiSpecs } : {},
6470
6802
  ...componentMigrator ? { componentFiles: componentMigrator.files() } : {},
6471
6803
  docsConfig,
6472
6804
  ...mintlifyConfig ? {
@@ -6482,7 +6814,7 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
6482
6814
  }
6483
6815
  } : docusaurusSettings && (docusaurusSettings.name || docusaurusSettings.description || themeColors) ? {
6484
6816
  site: {
6485
- ...docusaurusSettings.name ? { name: docusaurusSettings.name } : {},
6817
+ ...docusaurusSettings.navbarTitle || docusaurusSettings.name ? { name: docusaurusSettings.navbarTitle ?? docusaurusSettings.name } : {},
6486
6818
  ...docusaurusSettings.description ? { description: docusaurusSettings.description } : {},
6487
6819
  ...themeColors ? { colors: themeColors } : {}
6488
6820
  }
@@ -6754,10 +7086,10 @@ async function readResponseBody(response) {
6754
7086
  }
6755
7087
  return new TextDecoder().decode(body);
6756
7088
  }
6757
- var defaultMigrationFetcher = async (url, request) => {
7089
+ var defaultMigrationFetcher = async (url, request2) => {
6758
7090
  const response = await fetch(url, {
6759
7091
  headers: {
6760
- Accept: request.accept,
7092
+ Accept: request2.accept,
6761
7093
  "User-Agent": "Thally-Migrate/1.0 (+https://thally.io)"
6762
7094
  },
6763
7095
  redirect: "follow",
@@ -8172,6 +8504,202 @@ async function migrateUrl(options) {
8172
8504
  }
8173
8505
  };
8174
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
+ }
8175
8703
  export {
8176
8704
  addMintlifyDirectoryRedirects,
8177
8705
  buildNavigationFromPages,
@@ -8180,6 +8708,7 @@ export {
8180
8708
  detectRepositoryDocsDir,
8181
8709
  detectRepositoryPlatform,
8182
8710
  gitmodulePaths,
8711
+ hydrateRemoteApiSpecs,
8183
8712
  mergeMigrationConfig,
8184
8713
  migrateRepository,
8185
8714
  migrateUrl,