@thallylabs/migrate 0.2.10 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -31,6 +31,20 @@ import { unified as unified2 } from "unified";
31
31
 
32
32
  // src/frontmatter.ts
33
33
  import { parse as parseYaml } from "yaml";
34
+ function salvageFrontmatterLines(matter) {
35
+ const data = {};
36
+ for (const line of matter.split(/\r?\n/)) {
37
+ if (!/^[A-Za-z_][\w-]*:\s?/.test(line)) continue;
38
+ try {
39
+ const parsedLine = parseYaml(line);
40
+ if (parsedLine && typeof parsedLine === "object" && !Array.isArray(parsedLine)) {
41
+ Object.assign(data, parsedLine);
42
+ }
43
+ } catch {
44
+ }
45
+ }
46
+ return data;
47
+ }
34
48
  function parseFrontmatter(raw) {
35
49
  const source = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
36
50
  const opening = /^---([^\r\n]*)\r?\n/.exec(source);
@@ -45,11 +59,19 @@ function parseFrontmatter(raw) {
45
59
  if (matter.trim() === "" || !["", "yaml", "yml"].includes(language)) {
46
60
  return { content, data: {} };
47
61
  }
48
- const parsed = parseYaml(matter);
49
- return {
50
- content,
51
- data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
52
- };
62
+ try {
63
+ const parsed = parseYaml(matter);
64
+ return {
65
+ content,
66
+ data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
67
+ };
68
+ } catch (err) {
69
+ return {
70
+ content,
71
+ data: salvageFrontmatterLines(matter),
72
+ error: err instanceof Error ? err.message : String(err)
73
+ };
74
+ }
53
75
  }
54
76
 
55
77
  // src/mdx.ts
@@ -233,8 +255,9 @@ function unwrapDocusaurusMdxCodeBlocks(body) {
233
255
  }
234
256
  return isMdxCodeBlock ? body : output.join("\n");
235
257
  }
236
- function normalizeExplicitHeadingIds(body) {
237
- return replaceOutsideCode(body, (segment) => segment.split("\n").map((line) => {
258
+ function normalizeExplicitHeadingIds(raw) {
259
+ const { front, body } = splitFrontmatterBlock(raw);
260
+ return front + replaceOutsideCode(body, (segment) => segment.split("\n").map((line) => {
238
261
  if (!/^[ \t]{0,3}#{1,6}[ \t]+/.test(line)) return line;
239
262
  const trimmed = line.trimEnd();
240
263
  if (trimmed.endsWith("}")) {
@@ -276,6 +299,7 @@ function removeGlobalDocusaurusImports(body) {
276
299
  }
277
300
  var JS_LITERAL_KEYWORDS = /* @__PURE__ */ new Set(["true", "false", "null", "undefined"]);
278
301
  var BARE_IDENTIFIER_PATH = /^[\p{ID_Start}$_][\p{ID_Continue}$‌‍]*(?:\.[\p{ID_Start}$_][\p{ID_Continue}$‌‍]*)*$/u;
302
+ var BARE_OBJECT_SHORTHAND = /^\{\s*([\p{ID_Start}$_][\p{ID_Continue}$‌‍]*)\s*\}$/u;
279
303
  var SAFE_BUILTIN_ROOTS = /* @__PURE__ */ new Set([
280
304
  "Math",
281
305
  "JSON",
@@ -500,12 +524,13 @@ function escapeFernLiteralBraces(raw) {
500
524
  const end = node.position?.end.offset;
501
525
  const root2 = value.split(".")[0];
502
526
  const safeBuiltinPath = value.includes(".") && SAFE_BUILTIN_ROOTS.has(root2);
503
- const isTemplatePlaceholder = /^\{\s*[A-Za-z_$][\w$.-]*\s*\}$/.test(value);
527
+ const isBareIdentifier = BARE_IDENTIFIER_PATH.test(value) && !JS_LITERAL_KEYWORDS.has(value) && !declared.has(root2) && !safeBuiltinPath;
528
+ const shorthandMatch = value.match(BARE_OBJECT_SHORTHAND);
529
+ const isDoubleBraceTemplate = shorthandMatch !== null && !declared.has(shorthandMatch[1]);
504
530
  const isIssueList = /^[A-Za-z][A-Za-z0-9]*-\d+(?:\s*,\s*[A-Za-z][A-Za-z0-9]*-\d+)*$/.test(value);
505
- if ((isTemplatePlaceholder || isIssueList) && start !== void 0 && end !== void 0) {
506
- edits.push({ start, end, value: body.slice(start, end).replace(/\\/g, "\\\\").replace(/[{}]/g, "\\$&") });
507
- } else if (BARE_IDENTIFIER_PATH.test(value) && !JS_LITERAL_KEYWORDS.has(value) && !declared.has(root2) && !safeBuiltinPath && start !== void 0 && end !== void 0) {
508
- edits.push({ start, end, value: `\\{${value}\\}` });
531
+ if ((isBareIdentifier || isDoubleBraceTemplate || isIssueList) && start !== void 0 && end !== void 0) {
532
+ const rawSpan = body.slice(start, end);
533
+ edits.push({ start, end, value: rawSpan.replace(/\\/g, "\\\\").replace(/[{}]/g, "\\$&") });
509
534
  }
510
535
  return;
511
536
  }
@@ -1153,7 +1178,8 @@ function parseMarkdownPage(input) {
1153
1178
  noindex: parsed.data.noindex === true || parsed.data.noindex === "true" ? true : void 0,
1154
1179
  openapi: typeof parsed.data.openapi === "string" ? parsed.data.openapi.trim() : void 0,
1155
1180
  body,
1156
- source: input.source
1181
+ source: input.source,
1182
+ ...parsed.error ? { frontmatterError: parsed.error } : {}
1157
1183
  };
1158
1184
  }
1159
1185
 
@@ -1206,6 +1232,11 @@ function mintlifyLocalizedReference(value, locale, defaultPageIds = /* @__PURE__
1206
1232
  const candidateId = pageIdFromReference(candidate, true);
1207
1233
  if (candidateId && defaultPageIds.has(candidateId)) return candidate;
1208
1234
  }
1235
+ const segments = reference.split("/");
1236
+ const localeIndex = segments.findIndex((segment) => segment.toLowerCase() === locale.toLowerCase());
1237
+ if (localeIndex > 0) {
1238
+ return [...segments.slice(0, localeIndex), ...segments.slice(localeIndex + 1)].join("/");
1239
+ }
1209
1240
  return value;
1210
1241
  }
1211
1242
  function resolveWithin(root, candidate) {
@@ -1664,6 +1695,19 @@ function declarationsReferenceBrowserGlobal(body) {
1664
1695
  }
1665
1696
  return found;
1666
1697
  }
1698
+ function neutralizeUnresolvableImportsWithoutAst(content, currentFile, warn) {
1699
+ const importPattern = /^import\s+(?:\{\s*(?:default\s+as\s+)?([A-Z][A-Za-z0-9_]*)\s*\}|([A-Z][A-Za-z0-9_]*))\s+from\s+(['"])((?:@site\/|\.\.?\/|\/)[^'"]+)\3\s*;?[ \t]*$/gm;
1700
+ const dropped = [];
1701
+ let result = content.replace(importPattern, (_match, namedComponent, defaultComponent, _quote, specifier) => {
1702
+ dropped.push({ name: namedComponent ?? defaultComponent, specifier });
1703
+ return "";
1704
+ });
1705
+ for (const { name, specifier } of dropped) {
1706
+ warn(`Component import ${JSON.stringify(specifier)} could not be resolved because this page has invalid MDX (see the previous warning), so the import and its usage were removed to keep the site building.`, currentFile);
1707
+ result = result.replace(new RegExp(`<${name}(?:\\s[^>]*)?/>`, "g"), `{/* Removed <${name}>: unresolved during fallback migration */}`).replace(new RegExp(`<${name}(?:\\s[^>]*)?>[\\s\\S]*?<\\/${name}>`, "g"), `{/* Removed <${name}>: unresolved during fallback migration */}`);
1708
+ }
1709
+ return result;
1710
+ }
1667
1711
  function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIdentity) {
1668
1712
  const root = resolve2(siteRoot);
1669
1713
  const confined = resolve2(confinementRoot);
@@ -1858,16 +1902,17 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1858
1902
  function transform(raw, currentFile) {
1859
1903
  const parsedFrontmatter = parseFrontmatter(raw).content;
1860
1904
  const frontmatter = raw.slice(0, raw.length - parsedFrontmatter.length);
1861
- const content = normalizeIndentedFences(normalizeHtmlComments(parsedFrontmatter));
1905
+ const content = normalizeExplicitHeadingIds(normalizeIndentedFences(normalizeHtmlComments(parsedFrontmatter)));
1862
1906
  let tree;
1863
1907
  try {
1864
1908
  tree = parser.parse(content);
1865
1909
  } catch {
1866
1910
  warn("Custom component analysis could not parse this MDX; its source was preserved for manual migration.", currentFile);
1867
- return raw;
1911
+ return frontmatter + neutralizeUnresolvableImportsWithoutAst(content, currentFile, warn);
1868
1912
  }
1869
1913
  const aliases = /* @__PURE__ */ new Map();
1870
1914
  const unsupportedImports = /* @__PURE__ */ new Map();
1915
+ const droppedMdxDataBindings = /* @__PURE__ */ new Set();
1871
1916
  const edits = [];
1872
1917
  const declarations = [];
1873
1918
  const moduleImports = [];
@@ -1967,14 +2012,31 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1967
2012
  }
1968
2013
  continue;
1969
2014
  }
1970
- if (/\.mdx?$/.test(specifier)) continue;
2015
+ if (/\.mdx?$/.test(specifier)) {
2016
+ if (bindings.length > 0 && bindings.every((binding) => !/^[A-Z]/.test(binding.local))) {
2017
+ warn(`MDX named import of ${JSON.stringify(rawSpecifier)} isn't a component and has no equivalent in the migrated site (for example Docusaurus's generated 'toc' export), so it was removed, along with any '...${bindings.map((binding) => binding.local).join(", '...")}' spread of it.`, currentFile);
2018
+ edits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
2019
+ for (const binding of bindings) droppedMdxDataBindings.add(binding.local);
2020
+ }
2021
+ continue;
2022
+ }
1971
2023
  if (bindings.length && RESCUABLE_ASSET_EXTENSIONS.has(extname(specifier).toLowerCase()) && !bindings.some((binding) => usedAsJsxTagName(binding.local))) {
1972
2024
  try {
1973
2025
  const href = copyPublicAsset(resolveDependency(specifier, currentFile));
1974
2026
  edits.push({
1975
2027
  start: node.position.start.offset + statement.getStart(ast),
1976
2028
  end: node.position.start.offset + statement.end,
1977
- value: bindings.map((binding) => `const ${binding.local} = ${JSON.stringify(href)};`).join("\n")
2029
+ // `export`, not a bare `const`: this replaces an `import`
2030
+ // statement in place, and the final MDX compile (a fresh
2031
+ // parse of this text, not a reuse of this pass's own AST)
2032
+ // only recognizes a top-level JS block as ESM — evaluated and
2033
+ // its bindings put in scope — when it starts with `import` or
2034
+ // `export`. A bare `const` there is just prose to that parser
2035
+ // and renders as literal paragraph text instead, leaving
2036
+ // every reference to this binding a `ReferenceError` at
2037
+ // render (reproduced against hasura/graphql-engine's
2038
+ // databases/overview.mdx, which binds asset URLs this way).
2039
+ value: bindings.map((binding) => `export const ${binding.local} = ${JSON.stringify(href)};`).join("\n")
1978
2040
  });
1979
2041
  warn(`Asset import ${JSON.stringify(rawSpecifier)} was copied to ${href} and bound to that URL instead of importing it as a component.`, currentFile);
1980
2042
  continue;
@@ -2042,7 +2104,9 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2042
2104
  edits.push({
2043
2105
  start: importStart,
2044
2106
  end: importEnd,
2045
- value: bindings.map((binding) => `const ${binding.local} = ${JSON.stringify(rescuedAssetHref)};`).join("\n")
2107
+ // See the identical `export const` note on the other asset
2108
+ // rescue above — same reason.
2109
+ value: bindings.map((binding) => `export const ${binding.local} = ${JSON.stringify(rescuedAssetHref)};`).join("\n")
2046
2110
  });
2047
2111
  warn(`Asset import ${JSON.stringify(rawSpecifier)} could not be copied as a component; it was copied to ${rescuedAssetHref} and bound to that URL instead.`, currentFile);
2048
2112
  } else {
@@ -2052,6 +2116,27 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2052
2116
  }
2053
2117
  }
2054
2118
  }
2119
+ if (droppedMdxDataBindings.size > 0) {
2120
+ for (const node of esmNodes) {
2121
+ let stripDroppedSpreads2 = function(inner) {
2122
+ if (ts.isSpreadElement(inner) && ts.isIdentifier(inner.expression) && droppedMdxDataBindings.has(inner.expression.text)) {
2123
+ const start = nodeStart + inner.getStart(ast);
2124
+ let end = nodeStart + inner.end;
2125
+ const following = nodeText.slice(inner.end).match(/^\s*,/);
2126
+ if (following) end += following[0].length;
2127
+ edits.push({ start, end, value: "" });
2128
+ return;
2129
+ }
2130
+ ts.forEachChild(inner, stripDroppedSpreads2);
2131
+ };
2132
+ var stripDroppedSpreads = stripDroppedSpreads2;
2133
+ if (node.value === void 0 || node.position?.start.offset === void 0) continue;
2134
+ const nodeStart = node.position.start.offset;
2135
+ const nodeText = node.value;
2136
+ const ast = sourceFile(nodeText, "inline.tsx");
2137
+ for (const statement of ast.statements) stripDroppedSpreads2(statement);
2138
+ }
2139
+ }
2055
2140
  walk(tree, (node) => {
2056
2141
  for (const attribute of node.attributes ?? []) {
2057
2142
  if (attribute.type !== "mdxJsxAttribute" || !attribute.value || typeof attribute.value !== "object") continue;
@@ -2315,7 +2400,7 @@ import { parse as parseYaml2 } from "yaml";
2315
2400
  // src/navigation.ts
2316
2401
  import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2, realpathSync } from "fs";
2317
2402
  import { dirname as dirname2, extname as extname2, relative as relative3 } from "path";
2318
- var MAX_MINTLIFY_CONFIG_BYTES = 2e6;
2403
+ var MAX_MINTLIFY_CONFIG_BYTES = 2e7;
2319
2404
  function addMintlifyDirectoryRedirects(config, pages) {
2320
2405
  const pageIds = new Set(pages.map((page) => page.id));
2321
2406
  const redirects = [...config.redirects ?? []];
@@ -2363,15 +2448,10 @@ function addMintlifyDirectoryRedirects(config, pages) {
2363
2448
  }
2364
2449
  }
2365
2450
  }
2366
- for (const redirect of config.redirects ?? []) {
2367
- const destination = redirect.destination.split(/[?#]/, 1)[0].replace(/\/:[A-Za-z_][A-Za-z0-9_]*\*\/?$/, "");
2368
- if (!destination.startsWith("/") || destination.startsWith("//") || /[:*()[\]{}]/.test(destination)) continue;
2369
- const directory = trimEdgeSlashes(destination);
2370
- if (!directory || pageIds.has(directory)) continue;
2451
+ for (const [directory, landing] of firstDescendantByDirectory) {
2452
+ if (pageIds.has(directory)) continue;
2371
2453
  const source = `/${directory}`;
2372
2454
  if (hasAuthoredRedirect(source)) continue;
2373
- const landing = firstDescendantByDirectory.get(directory);
2374
- if (!landing) continue;
2375
2455
  redirects.push({ source, destination: `/${landing.id}`, permanent: false });
2376
2456
  redirectSources.add(source);
2377
2457
  }
@@ -2422,6 +2502,18 @@ var LANGUAGE_LABELS = {
2422
2502
  function objectValue(value) {
2423
2503
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2424
2504
  }
2505
+ function withFlattenedProductGroups(container) {
2506
+ if (!Array.isArray(container.productGroups)) return container;
2507
+ const flattened = container.productGroups.flatMap((entry) => {
2508
+ const group = objectValue(entry);
2509
+ return group && Array.isArray(group.products) ? group.products : [];
2510
+ });
2511
+ if (flattened.length === 0) return container;
2512
+ return {
2513
+ ...container,
2514
+ products: [...Array.isArray(container.products) ? container.products : [], ...flattened]
2515
+ };
2516
+ }
2425
2517
  function projectedHref(value) {
2426
2518
  if (typeof value !== "string") return void 0;
2427
2519
  const href = value.trim();
@@ -2509,12 +2601,23 @@ function mintlifyNavigationApiReferences(config) {
2509
2601
  const references = [];
2510
2602
  const containerKeys = ["tabs", "anchors", "products", "dropdowns", "versions", "menus", "languages"];
2511
2603
  const nestedKeys = ["groups", "pages"];
2512
- function visit(node, tabLabel) {
2513
- if (typeof node.openapi === "string" && node.openapi.trim()) {
2514
- references.push({ value: node.openapi.trim(), kind: "openapi", tabLabel });
2515
- }
2516
- if (typeof node.asyncapi === "string" && node.asyncapi.trim()) {
2517
- references.push({ value: node.asyncapi.trim(), kind: "asyncapi", tabLabel });
2604
+ function visit(rawNode, tabLabel) {
2605
+ const node = withFlattenedProductGroups(rawNode);
2606
+ for (const kind of ["openapi", "asyncapi"]) {
2607
+ const value = node[kind];
2608
+ if (typeof value === "string" && value.trim()) {
2609
+ references.push({ value: value.trim(), kind, tabLabel });
2610
+ continue;
2611
+ }
2612
+ const object = objectValue(value);
2613
+ if (object && typeof object.source === "string" && object.source.trim()) {
2614
+ references.push({
2615
+ value: object.source.trim(),
2616
+ kind,
2617
+ tabLabel,
2618
+ ...typeof object.directory === "string" && object.directory.trim() ? { directory: object.directory.trim() } : {}
2619
+ });
2620
+ }
2518
2621
  }
2519
2622
  for (const key of containerKeys) {
2520
2623
  const entries = node[key];
@@ -2575,8 +2678,19 @@ function registerReference(value, context) {
2575
2678
  }
2576
2679
  return navigationId;
2577
2680
  }
2681
+ var OPENAPI_OPERATION_REF = /^(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+\S/;
2578
2682
  function convertPage(value, context) {
2579
- if (typeof value === "string") return registerReference(value, context);
2683
+ if (typeof value === "string") {
2684
+ if (OPENAPI_OPERATION_REF.test(value)) {
2685
+ warnOnce(
2686
+ context,
2687
+ "openapi-operation-list",
2688
+ 'One or more navigation groups hand-pick or reorder individual OpenAPI operations (e.g. "GET /users"); Thally always renders every operation of a bound spec and has no manual-selection equivalent, so that list was dropped. The operations themselves are still available through the bound API reference.'
2689
+ );
2690
+ return null;
2691
+ }
2692
+ return registerReference(value, context);
2693
+ }
2580
2694
  const object = objectValue(value);
2581
2695
  if (!object) return null;
2582
2696
  if (typeof object.page === "string") return registerReference(object.page, context);
@@ -2619,8 +2733,9 @@ function convertNavigationValues(values, context) {
2619
2733
  });
2620
2734
  }
2621
2735
  function convertContainerToTabs(containerValue, context, fallbackTab, trace, depth = 0) {
2622
- const container = objectValue(containerValue);
2623
- if (!container) return [];
2736
+ const rawContainer = objectValue(containerValue);
2737
+ if (!rawContainer) return [];
2738
+ const container = withFlattenedProductGroups(rawContainer);
2624
2739
  const containerKeys = ["tabs", "anchors", "products", "dropdowns", "versions", "menus"];
2625
2740
  for (const key of containerKeys) {
2626
2741
  if (!Array.isArray(container[key])) continue;
@@ -2842,6 +2957,47 @@ function isRedirectPathSafe(rawSource, rawDestination) {
2842
2957
  }
2843
2958
  return true;
2844
2959
  }
2960
+ function mintlifyDefaultVersionPrefixes(config) {
2961
+ const navigation = objectValue(config.navigation) ?? config;
2962
+ const prefixes = /* @__PURE__ */ new Set();
2963
+ function collect(container) {
2964
+ if (Array.isArray(container.versions)) {
2965
+ const entries = container.versions.map((value) => objectValue(value)).filter((value) => value !== null);
2966
+ const defaultEntry = entries.find((entry) => entry.default === true) ?? entries[0];
2967
+ if (defaultEntry && typeof defaultEntry.version === "string" && defaultEntry.version.trim()) {
2968
+ prefixes.add(defaultEntry.version.trim());
2969
+ }
2970
+ }
2971
+ if (Array.isArray(container.languages)) {
2972
+ for (const value of container.languages) {
2973
+ const language = objectValue(value);
2974
+ if (language) collect(language);
2975
+ }
2976
+ }
2977
+ }
2978
+ collect(navigation);
2979
+ return prefixes;
2980
+ }
2981
+ function mintlifyAllVersionPrefixes(config) {
2982
+ const navigation = objectValue(config.navigation) ?? config;
2983
+ const prefixes = /* @__PURE__ */ new Set();
2984
+ function collect(container) {
2985
+ if (Array.isArray(container.versions)) {
2986
+ for (const value of container.versions) {
2987
+ const entry = objectValue(value);
2988
+ if (entry && typeof entry.version === "string" && entry.version.trim()) prefixes.add(entry.version.trim());
2989
+ }
2990
+ }
2991
+ if (Array.isArray(container.languages)) {
2992
+ for (const value of container.languages) {
2993
+ const language = objectValue(value);
2994
+ if (language) collect(language);
2995
+ }
2996
+ }
2997
+ }
2998
+ collect(navigation);
2999
+ return prefixes;
3000
+ }
2845
3001
  function projectMintlifyNavigation(config, options = {}) {
2846
3002
  const warnings = [];
2847
3003
  const references = [];
@@ -3087,7 +3243,7 @@ function pruneMissingNavigationPages(config, availableIds) {
3087
3243
  }
3088
3244
 
3089
3245
  // src/docusaurus.ts
3090
- var MAX_CONFIG_BYTES = 1e6;
3246
+ var MAX_CONFIG_BYTES = 2e7;
3091
3247
  var CATEGORY_FILENAMES = ["_category_.json", "_category_.yml", "_category_.yaml"];
3092
3248
  var SIDEBAR_FILENAMES = [
3093
3249
  "sidebars.json",
@@ -3284,7 +3440,8 @@ function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
3284
3440
  return options.assetPaths?.has(path.slice(1)) ? target : null;
3285
3441
  }
3286
3442
  function rewriteTarget(target) {
3287
- if (!target || target.startsWith("#") || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(target)) return target;
3443
+ if (!target || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(target)) return target;
3444
+ if (target.startsWith("#")) return target;
3288
3445
  const suffixIndex = target.search(/[?#]/);
3289
3446
  const path = suffixIndex >= 0 ? target.slice(0, suffixIndex) : target;
3290
3447
  const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
@@ -3338,7 +3495,7 @@ function readDocusaurusSiteOrigin(repositoryRoot) {
3338
3495
  }
3339
3496
  function readBoundedText(path) {
3340
3497
  if (lstatSync3(path).size > MAX_CONFIG_BYTES) {
3341
- throw new Error("Docusaurus sidebar config exceeded the 1 MB static-parser limit.");
3498
+ throw new Error("The Docusaurus sidebar config is larger than 20 MB and could not be imported.");
3342
3499
  }
3343
3500
  return readFileSync3(path, "utf8");
3344
3501
  }
@@ -3970,7 +4127,7 @@ import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as rea
3970
4127
  import { basename as basename2, dirname as dirname3, extname as extname4, relative as relative4 } from "path";
3971
4128
  import { load } from "cheerio";
3972
4129
  import { parse as parseYaml3 } from "yaml";
3973
- var MAX_CONFIG_BYTES2 = 2e6;
4130
+ var MAX_CONFIG_BYTES2 = 2e7;
3974
4131
  function announcementMarkdown(message) {
3975
4132
  const document = load(message);
3976
4133
  document("script, style, iframe, object, embed, svg, math").remove();
@@ -4005,7 +4162,7 @@ function titleCase3(value) {
4005
4162
  }
4006
4163
  function readBoundedYaml(path) {
4007
4164
  if (lstatSync4(path).size > MAX_CONFIG_BYTES2) {
4008
- throw new Error("Fern config exceeded the 2 MB static-parser limit.");
4165
+ throw new Error("The Fern config is larger than 20 MB and could not be imported.");
4009
4166
  }
4010
4167
  return parseYaml3(readFileSync4(path, "utf8"));
4011
4168
  }
@@ -4067,7 +4224,7 @@ function registerPageAt(rawPath, base, context, navTitle, hidden = false) {
4067
4224
  let sourcePath;
4068
4225
  try {
4069
4226
  const absolute = resolveWithinRoot(
4070
- context.pathPrefix ? resolveWithin(context.fernRoot, context.pathPrefix) : context.fernRoot,
4227
+ context.pathPrefix ? resolveWithinRoot(context.fernRoot, context.pathPrefix, context.repositoryRoot) : context.fernRoot,
4071
4228
  rawPath.trim(),
4072
4229
  context.repositoryRoot
4073
4230
  );
@@ -4096,7 +4253,7 @@ function registerFolder(object, parentSegments, context) {
4096
4253
  let folder;
4097
4254
  try {
4098
4255
  folder = resolveWithinRoot(
4099
- context.pathPrefix ? resolveWithin(context.fernRoot, context.pathPrefix) : context.fernRoot,
4256
+ context.pathPrefix ? resolveWithinRoot(context.fernRoot, context.pathPrefix, context.repositoryRoot) : context.fernRoot,
4100
4257
  rawPath,
4101
4258
  context.repositoryRoot
4102
4259
  );
@@ -4202,10 +4359,11 @@ function convertNode(node, parentSegments, context) {
4202
4359
  return null;
4203
4360
  }
4204
4361
  if (typeof object.api === "string") {
4362
+ const ownSegment = segmentFor(object, object.api, context);
4205
4363
  context.apiSections.push({
4206
4364
  name: typeof object["api-name"] === "string" ? object["api-name"] : object.api,
4207
4365
  nameExplicit: typeof object["api-name"] === "string",
4208
- routeSegments: parentSegments
4366
+ routeSegments: ownSegment ? [...parentSegments, ownSegment] : parentSegments
4209
4367
  });
4210
4368
  return null;
4211
4369
  }
@@ -4247,7 +4405,7 @@ function resolveVersionedNavigation(config, configDir, fernRoot, context) {
4247
4405
  }
4248
4406
  if (!chosen || typeof chosen.path !== "string") return config;
4249
4407
  try {
4250
- const versionPath = resolveWithinRoot(configDir, chosen.path, fernRoot);
4408
+ const versionPath = resolveWithinRoot(configDir, chosen.path, context.repositoryRoot);
4251
4409
  if (!existsSync4(versionPath) || !lstatSync4(versionPath).isFile()) {
4252
4410
  warnOnce2(context, "fern-version-not-file", `Fern version file "${chosen.path}" is not a regular file and was skipped.`);
4253
4411
  return config;
@@ -4485,7 +4643,16 @@ function projectFernNavigation(input) {
4485
4643
  return section;
4486
4644
  }
4487
4645
  const distinguishingSegment = section.routeSegments.at(-1);
4488
- const tabLabel = distinguishingSegment ? `${section.tabLabel}: ${titleCase3(distinguishingSegment)}` : section.tabLabel;
4646
+ let tabLabel = distinguishingSegment ? `${section.tabLabel}: ${titleCase3(distinguishingSegment)}` : section.tabLabel;
4647
+ if (!tabLabel || tabLabel === `${section.tabLabel}: ${section.tabLabel}` || claimedTabLabels.has(tabLabel)) {
4648
+ let ordinal = 2;
4649
+ let candidate = `${section.tabLabel} (${ordinal})`;
4650
+ while (claimedTabLabels.has(candidate)) {
4651
+ ordinal += 1;
4652
+ candidate = `${section.tabLabel} (${ordinal})`;
4653
+ }
4654
+ tabLabel = candidate;
4655
+ }
4489
4656
  claimedTabLabels.add(tabLabel);
4490
4657
  return { ...section, tabLabel };
4491
4658
  });
@@ -4535,11 +4702,14 @@ var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
4535
4702
  ".ico",
4536
4703
  ".jpeg",
4537
4704
  ".jpg",
4705
+ ".m4a",
4538
4706
  ".mp3",
4539
4707
  ".mp4",
4708
+ ".ogg",
4540
4709
  ".pdf",
4541
4710
  ".png",
4542
4711
  ".svg",
4712
+ ".wav",
4543
4713
  ".webm",
4544
4714
  ".webp"
4545
4715
  ]);
@@ -4727,9 +4897,92 @@ function readDocusaurusConfigSource(projectRoot) {
4727
4897
  if (!path || lstatSync5(path).size > 1e6) return "";
4728
4898
  return readFileSync5(path, "utf8");
4729
4899
  }
4900
+ function matchingBraceIndex(source, openBraceIndex) {
4901
+ let depth = 0;
4902
+ for (let i = openBraceIndex; i < source.length; i++) {
4903
+ const ch = source[i];
4904
+ if (ch === '"' || ch === "'" || ch === "`") {
4905
+ const quote = ch;
4906
+ i++;
4907
+ while (i < source.length && source[i] !== quote) {
4908
+ if (source[i] === "\\") i++;
4909
+ i++;
4910
+ }
4911
+ continue;
4912
+ }
4913
+ if (ch === "/" && source[i + 1] === "/") {
4914
+ const newline = source.indexOf("\n", i);
4915
+ i = newline === -1 ? source.length : newline;
4916
+ continue;
4917
+ }
4918
+ if (ch === "/" && source[i + 1] === "*") {
4919
+ const end = source.indexOf("*/", i + 2);
4920
+ i = end === -1 ? source.length : end + 1;
4921
+ continue;
4922
+ }
4923
+ if (ch === "{") depth++;
4924
+ else if (ch === "}") {
4925
+ depth--;
4926
+ if (depth === 0) return i;
4927
+ }
4928
+ }
4929
+ return -1;
4930
+ }
4931
+ function topLevelStringProperty(objectBody, key) {
4932
+ let depth = 0;
4933
+ const keyPattern = new RegExp(`^${key}\\s*:\\s*(['"])`);
4934
+ for (let i = 0; i < objectBody.length; i++) {
4935
+ const ch = objectBody[i];
4936
+ if (ch === '"' || ch === "'" || ch === "`") {
4937
+ const quote = ch;
4938
+ i++;
4939
+ while (i < objectBody.length && objectBody[i] !== quote) {
4940
+ if (objectBody[i] === "\\") i++;
4941
+ i++;
4942
+ }
4943
+ continue;
4944
+ }
4945
+ if (ch === "/" && objectBody[i + 1] === "/") {
4946
+ const newline = objectBody.indexOf("\n", i);
4947
+ i = newline === -1 ? objectBody.length : newline;
4948
+ continue;
4949
+ }
4950
+ if (ch === "/" && objectBody[i + 1] === "*") {
4951
+ const end = objectBody.indexOf("*/", i + 2);
4952
+ i = end === -1 ? objectBody.length : end + 1;
4953
+ continue;
4954
+ }
4955
+ if (ch === "{" || ch === "[" || ch === "(") {
4956
+ depth++;
4957
+ continue;
4958
+ }
4959
+ if (ch === "}" || ch === "]" || ch === ")") {
4960
+ depth--;
4961
+ continue;
4962
+ }
4963
+ if (depth === 0) {
4964
+ const match = keyPattern.exec(objectBody.slice(i));
4965
+ if (match) {
4966
+ const quote = match[1];
4967
+ const valueStart = i + match[0].length;
4968
+ const valueEnd = objectBody.indexOf(quote, valueStart);
4969
+ if (valueEnd !== -1) return objectBody.slice(valueStart, valueEnd);
4970
+ }
4971
+ }
4972
+ }
4973
+ return void 0;
4974
+ }
4730
4975
  function primaryDocusaurusDocsDirectory(projectRoot) {
4731
4976
  const source = readDocusaurusConfigSource(projectRoot);
4732
- const configured = source.match(/\bdocs\s*:\s*\{[\s\S]{0,4000}?\bpath\s*:\s*(['"])([^'"]+)\1/)?.[2];
4977
+ const docsKeyMatch = source.match(/\bdocs\s*:\s*\{/);
4978
+ let configured;
4979
+ if (docsKeyMatch?.index !== void 0) {
4980
+ const openBrace = docsKeyMatch.index + docsKeyMatch[0].length - 1;
4981
+ const closeBrace = matchingBraceIndex(source, openBrace);
4982
+ if (closeBrace !== -1) {
4983
+ configured = topLevelStringProperty(source.slice(openBrace + 1, closeBrace), "path");
4984
+ }
4985
+ }
4733
4986
  return trimTrailingSlashes(configured?.replace(/^\.\//, "") ?? "") || "docs";
4734
4987
  }
4735
4988
  function docusaurusArchiveRoots(projectRoot, repositoryDir, warnings) {
@@ -4781,7 +5034,7 @@ function docusaurusArchiveRoots(projectRoot, repositoryDir, warnings) {
4781
5034
  function additionalDocusaurusPluginRoots(repositoryDir, projectRoot, warnings) {
4782
5035
  const source = readDocusaurusConfigSource(projectRoot);
4783
5036
  const plugins = [];
4784
- const matcher = /['"]@docusaurus\/plugin-content-docs['"][\s\S]{0,3000}?\bpath\s*:\s*(['"])([^'"]+)\1[\s\S]{0,1000}?\brouteBasePath\s*:\s*(['"])([^'"]+)\3/g;
5037
+ const matcher = /['"](?:@docusaurus\/plugin-)?content-docs['"][\s\S]{0,3000}?\bpath\s*:\s*(['"])([^'"]+)\1[\s\S]{0,1000}?\brouteBasePath\s*:\s*(['"])([^'"]+)\3/g;
4785
5038
  for (const match of source.matchAll(matcher)) {
4786
5039
  const localPath = trimTrailingSlashes(match[2].replace(/^\.\//, ""));
4787
5040
  const routePrefix = trimEdgeSlashes(match[4]);
@@ -5019,8 +5272,10 @@ function detectRepositoryDocsDir(repositoryDir) {
5019
5272
  }
5020
5273
  return "";
5021
5274
  }
5022
- function scanFiles(root, confinementRoot = root, warnings) {
5275
+ var MAX_RANKED_WALK_FILES = MAX_SOURCE_FILES * 20;
5276
+ function scanFiles(root, confinementRoot = root, warnings, rank) {
5023
5277
  const files = [];
5278
+ const walkCap = rank ? MAX_RANKED_WALK_FILES : MAX_SOURCE_FILES;
5024
5279
  let confinementReal;
5025
5280
  try {
5026
5281
  confinementReal = realpathSync2(confinementRoot);
@@ -5029,7 +5284,7 @@ function scanFiles(root, confinementRoot = root, warnings) {
5029
5284
  }
5030
5285
  const visitedRealPaths = /* @__PURE__ */ new Set();
5031
5286
  function visit(directory, logicalDirectory = directory) {
5032
- if (files.length >= MAX_SOURCE_FILES) return;
5287
+ if (files.length >= walkCap) return;
5033
5288
  let directoryReal;
5034
5289
  try {
5035
5290
  directoryReal = realpathSync2(directory);
@@ -5045,7 +5300,7 @@ function scanFiles(root, confinementRoot = root, warnings) {
5045
5300
  return;
5046
5301
  }
5047
5302
  for (const entry of entries) {
5048
- if (files.length >= MAX_SOURCE_FILES) return;
5303
+ if (files.length >= walkCap) return;
5049
5304
  if (isIgnoredContentDirectory(entry.name)) {
5050
5305
  if (warnings && entry.isDirectory() && !entry.isSymbolicLink() && containsMarkdown(resolveWithin(directory, entry.name))) {
5051
5306
  warnings.push({
@@ -5082,8 +5337,44 @@ function scanFiles(root, confinementRoot = root, warnings) {
5082
5337
  }
5083
5338
  }
5084
5339
  visit(root);
5340
+ if (rank && files.length >= walkCap) {
5341
+ warnings?.push({
5342
+ code: "limit-reached",
5343
+ message: `Stopped scanning after ${walkCap} files, so the rest of the repository was not looked at. Run the migration on a smaller part of the repository with --docs-dir.`
5344
+ });
5345
+ }
5085
5346
  return files;
5086
5347
  }
5348
+ function selectGroupWithinBudget(scanned, rank, warnings, allVersionPrefixes, label) {
5349
+ if (scanned.length <= MAX_SOURCE_FILES) return scanned;
5350
+ const ranked = scanned.map((file) => ({ file, priority: rank(file.relativePath) })).sort((left, right) => left.priority - right.priority || (left.file.relativePath < right.file.relativePath ? -1 : left.file.relativePath > right.file.relativePath ? 1 : 0));
5351
+ const dropped = ranked.slice(MAX_SOURCE_FILES).map(({ file }) => file);
5352
+ if (warnings) {
5353
+ const droppedPages = label === "file" ? dropped.filter((file) => isDocumentationExtension(file.relativePath)) : dropped;
5354
+ const noun = label === "file" ? "page" : "image or media file";
5355
+ const droppedVersions = /* @__PURE__ */ new Set();
5356
+ for (const file of dropped) {
5357
+ const firstSegment = file.relativePath.split("/", 1)[0];
5358
+ if (allVersionPrefixes.has(firstSegment)) droppedVersions.add(firstSegment);
5359
+ }
5360
+ const examples = droppedPages.slice(0, 3).map((file) => file.relativePath);
5361
+ const rest = droppedPages.length - examples.length;
5362
+ warnings.push({
5363
+ code: "limit-reached",
5364
+ message: `This repository has more than ${MAX_SOURCE_FILES} ${label === "file" ? "files" : "assets"}, so only the first ${MAX_SOURCE_FILES} (in navigation order, default version first) were migrated. ${droppedPages.length} ${noun}(s) were left out` + (examples.length > 0 ? `: ${examples.join(", ")}${rest > 0 ? `, and ${rest} more` : ""}` : "") + (droppedVersions.size > 0 ? ` (versions: ${[...droppedVersions].slice(0, 5).join(", ")}${droppedVersions.size > 5 ? `, and ${droppedVersions.size - 5} more` : ""})` : "") + (label === "file" ? ". To include them, run the migration on a smaller part of the repository with --docs-dir." : ". Copy them into public/ manually if your pages use them.")
5365
+ });
5366
+ }
5367
+ return ranked.slice(0, MAX_SOURCE_FILES).map(({ file }) => file);
5368
+ }
5369
+ function selectFilesWithinBudget(scanned, rank, warnings, allVersionPrefixes) {
5370
+ const isAsset = (file) => ASSET_EXTENSIONS.has(extname5(file.relativePath).toLowerCase());
5371
+ const assetFiles = scanned.filter(isAsset);
5372
+ const otherFiles = scanned.filter((file) => !isAsset(file));
5373
+ return [
5374
+ ...selectGroupWithinBudget(otherFiles, rank, warnings, allVersionPrefixes, "file"),
5375
+ ...selectGroupWithinBudget(assetFiles, rank, warnings, allVersionPrefixes, "asset")
5376
+ ];
5377
+ }
5087
5378
  function readMintignoreMatcher(mintlifyRoot) {
5088
5379
  const path = resolveWithin(mintlifyRoot, MINTIGNORE_FILENAME);
5089
5380
  if (!existsSync5(path) || !lstatSync5(path).isFile()) return null;
@@ -5200,10 +5491,159 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
5200
5491
  });
5201
5492
  continue;
5202
5493
  }
5203
- specs.push({ filename: basename3(match.relativePath), content: readFileSync5(match.absolutePath), tabLabel: reference.tabLabel });
5494
+ if (reference.directory) {
5495
+ warnings.push({
5496
+ code: "unsupported-config",
5497
+ message: `The OpenAPI spec "${reference.value}"${tabSuffix} was limited to pages under "${reference.directory}" in the source, but Thally's API reference always covers a whole tab, so it was migrated as the tab's full API reference. Update any links to "${reference.directory}/..." pages manually.`
5498
+ });
5499
+ }
5500
+ specs.push({
5501
+ filename: basename3(match.relativePath),
5502
+ content: readFileSync5(match.absolutePath),
5503
+ tabLabel: reference.tabLabel,
5504
+ sourcePath: match.relativePath,
5505
+ directory: reference.directory
5506
+ });
5507
+ }
5508
+ const filenameOwners = /* @__PURE__ */ new Map();
5509
+ for (const spec of specs) {
5510
+ const groupKey = spec.tabLabel ?? "\0default";
5511
+ const owner = filenameOwners.get(spec.filename);
5512
+ if (owner !== void 0 && owner !== groupKey) {
5513
+ let disambiguated = spec.sourcePath.replace(/\//g, "-");
5514
+ while (filenameOwners.has(disambiguated) && filenameOwners.get(disambiguated) !== groupKey) {
5515
+ disambiguated = `${groupKey.replace(/\W+/g, "-")}-${disambiguated}`;
5516
+ }
5517
+ spec.filename = disambiguated;
5518
+ }
5519
+ filenameOwners.set(spec.filename, groupKey);
5204
5520
  }
5205
5521
  return specs;
5206
5522
  }
5523
+ var OPENAPI_HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
5524
+ var FERN_CRUD_METHOD_NAMES = {
5525
+ findall: "list",
5526
+ findone: "get",
5527
+ remove: "delete"
5528
+ };
5529
+ function mintlifyOperationSlugSegment(value) {
5530
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
5531
+ }
5532
+ function thallyOperationSlugSegments(path, method) {
5533
+ const cleaned = path.split("/").filter(Boolean).map((segment) => segment.replace(/[{}]/g, "").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase());
5534
+ if (!cleaned.length) cleaned.push("root");
5535
+ cleaned.push(method.toLowerCase());
5536
+ return cleaned;
5537
+ }
5538
+ function slugifyApiSpecId(value) {
5539
+ return value.toLowerCase().replace(/[^a-z0-9/]+/g, "-").replace(/(^-|-$)+/g, "").replace(/\//g, "-") || value.toLowerCase();
5540
+ }
5541
+ function parseOpenApiOperationsForLinking(content) {
5542
+ let document;
5543
+ try {
5544
+ document = parseYaml4(content.toString("utf8"));
5545
+ } catch {
5546
+ return [];
5547
+ }
5548
+ const paths = document && typeof document === "object" ? document.paths : null;
5549
+ if (!paths || typeof paths !== "object") return [];
5550
+ const operations = [];
5551
+ for (const [path, pathItem] of Object.entries(paths)) {
5552
+ if (!pathItem || typeof pathItem !== "object") continue;
5553
+ for (const method of OPENAPI_HTTP_METHODS) {
5554
+ const operation = pathItem[method];
5555
+ if (!operation || typeof operation !== "object") continue;
5556
+ const op = operation;
5557
+ const tags = Array.isArray(op.tags) ? op.tags.filter((tag) => typeof tag === "string") : [];
5558
+ operations.push({
5559
+ method,
5560
+ path,
5561
+ tag: tags[0],
5562
+ summary: typeof op.summary === "string" ? op.summary : void 0,
5563
+ operationId: typeof op.operationId === "string" ? op.operationId : void 0
5564
+ });
5565
+ }
5566
+ }
5567
+ return operations;
5568
+ }
5569
+ function apiOperationLinkMap(sources, docsConfig) {
5570
+ const map = /* @__PURE__ */ new Map();
5571
+ const prefixLandings = /* @__PURE__ */ new Map();
5572
+ const apiTabs = docsConfig.tabs.filter((tab) => !tab.hidden && tab.api);
5573
+ apiTabs.forEach((tab, index) => {
5574
+ const source = sources.find((entry) => tab.api?.source === `/${entry.filename}`);
5575
+ if (!source || !source.prefix) return;
5576
+ const specId = index === 0 ? "default" : slugifyApiSpecId(tab.tab);
5577
+ const prefix = source.prefix.replace(/^\/+|\/+$/g, "").toLowerCase();
5578
+ if (prefix) prefixLandings.set(prefix, specId === "default" ? "/api" : `/api/${specId}`);
5579
+ for (const operation of parseOpenApiOperationsForLinking(source.content)) {
5580
+ const thallyHref = `/api/${specId}/${thallyOperationSlugSegments(operation.path, operation.method).join("/")}`;
5581
+ const leafCandidates = /* @__PURE__ */ new Set();
5582
+ if (operation.operationId) {
5583
+ const parts = operation.operationId.split(/[_.]/).filter(Boolean);
5584
+ const lastPart = parts.at(-1) ?? operation.operationId;
5585
+ const conventional = FERN_CRUD_METHOD_NAMES[lastPart.toLowerCase()];
5586
+ if (conventional) leafCandidates.add(conventional);
5587
+ leafCandidates.add(mintlifyOperationSlugSegment(lastPart));
5588
+ }
5589
+ if (operation.summary) leafCandidates.add(mintlifyOperationSlugSegment(operation.summary));
5590
+ if (operation.operationId) leafCandidates.add(mintlifyOperationSlugSegment(operation.operationId));
5591
+ leafCandidates.add(mintlifyOperationSlugSegment(`${operation.method} ${operation.path}`));
5592
+ const tagSegment = mintlifyOperationSlugSegment(operation.tag ?? "default");
5593
+ for (const leaf of leafCandidates) {
5594
+ if (!leaf) continue;
5595
+ const tagged = `${source.prefix}/${tagSegment}/${leaf}`.replace(/^\/+|\/+$/g, "").toLowerCase();
5596
+ const flat = `${source.prefix}/${leaf}`.replace(/^\/+|\/+$/g, "").toLowerCase();
5597
+ if (!map.has(tagged)) map.set(tagged, thallyHref);
5598
+ if (!map.has(flat)) map.set(flat, thallyHref);
5599
+ }
5600
+ }
5601
+ });
5602
+ return { operationLinks: map, prefixLandings };
5603
+ }
5604
+ function rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings) {
5605
+ if (operationLinks.size === 0 && prefixLandings.size === 0) return;
5606
+ const unmatched = /* @__PURE__ */ new Set();
5607
+ for (const page of pages) {
5608
+ page.body = rewriteApiOperationLinks(page.body, operationLinks, prefixLandings, unmatched);
5609
+ }
5610
+ if (unmatched.size === 0) return;
5611
+ const sorted = [...unmatched].sort();
5612
+ warnings.push({
5613
+ code: "unsupported-config",
5614
+ message: `${sorted.length} link(s) to API endpoints matched no endpoint in the OpenAPI spec, so they now point to the API section's overview page instead: ${sorted.slice(0, 10).join(", ")}${sorted.length > 10 ? `, and ${sorted.length - 10} more` : ""}. Update those links if you want them to reach a specific endpoint.`
5615
+ });
5616
+ }
5617
+ function rewriteApiOperationLinks(body, linkMap, prefixLandings, unmatched) {
5618
+ if (linkMap.size === 0 && prefixLandings.size === 0) return body;
5619
+ const orderedPrefixes = [...prefixLandings.entries()].sort((a, b) => b[0].length - a[0].length);
5620
+ let codeFence = null;
5621
+ return body.split("\n").map((line) => {
5622
+ const fence = line.match(/^\s*(`{3,}|~{3,})/);
5623
+ if (fence) {
5624
+ if (!codeFence) codeFence = fence[1][0];
5625
+ else if (fence[1][0] === codeFence) codeFence = null;
5626
+ return line;
5627
+ }
5628
+ if (codeFence) return line;
5629
+ const rewriteTarget = (target) => {
5630
+ const suffixIndex = target.search(/[?#]/);
5631
+ const path = (suffixIndex >= 0 ? target.slice(0, suffixIndex) : target).replace(/^\/+/, "").toLowerCase();
5632
+ const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
5633
+ const mapped = linkMap.get(path);
5634
+ if (mapped) return `${mapped}${suffix}`;
5635
+ for (const [prefix, landing] of orderedPrefixes) {
5636
+ if (path === prefix) return `${landing}${suffix}`;
5637
+ if (path.startsWith(`${prefix}/`)) {
5638
+ unmatched.add(target);
5639
+ return `${landing}${suffix}`;
5640
+ }
5641
+ }
5642
+ return target;
5643
+ };
5644
+ return line.replace(/(\]\()\/([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(`/${target}`)}`).replace(/(\bhref=")\/([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(`/${target}`)}${suffix}`);
5645
+ }).join("\n");
5646
+ }
5207
5647
  var MAX_FERN_GENERATORS_BYTES = 2e6;
5208
5648
  function fernGeneratorsOpenApiPaths(config) {
5209
5649
  const api = config.api;
@@ -5820,17 +6260,27 @@ ${result.slice(prefix.length)}`;
5820
6260
  }
5821
6261
  return depth === 0 ? hoistMdxImports(result) : result;
5822
6262
  }
5823
- function injectOpenApiSpecs(config, specs) {
6263
+ function injectOpenApiSpecs(config, specs, warnings) {
5824
6264
  let tabs = config.tabs.map((tab) => ({ ...tab }));
5825
6265
  for (const spec of specs) {
5826
6266
  const apiTab = spec.tabLabel ? tabs.find((tab) => tab.tab === spec.tabLabel) : tabs.find((tab) => tab.tab.toLowerCase().includes("api"));
5827
- if (apiTab) apiTab.api = {
5828
- source: `/${spec.filename}`,
5829
- // An API-only tab needs generated endpoint navigation; an authored
5830
- // page tab keeps its own groups alongside the bound spec.
5831
- ...apiTab.groups?.length || apiTab.pages?.length ? { navigation: false } : {}
5832
- };
5833
- else tabs = [...tabs, { tab: spec.tabLabel ?? "API Reference", api: { source: `/${spec.filename}` } }];
6267
+ if (apiTab) {
6268
+ if (apiTab.api && warnings) {
6269
+ warnings.push({
6270
+ code: "unsupported-config",
6271
+ message: `Tab "${apiTab.tab}" already uses another OpenAPI spec, and Thally supports one API spec per tab, so "/${spec.filename}" was not added. Put it in its own tab to include it.`
6272
+ });
6273
+ continue;
6274
+ }
6275
+ apiTab.api = {
6276
+ source: `/${spec.filename}`,
6277
+ // An API-only tab needs generated endpoint navigation; an authored
6278
+ // page tab keeps its own groups alongside the bound spec.
6279
+ ...apiTab.groups?.length || apiTab.pages?.length ? { navigation: false } : {}
6280
+ };
6281
+ } else {
6282
+ tabs = [...tabs, { tab: spec.tabLabel ?? "API Reference", api: { source: `/${spec.filename}` } }];
6283
+ }
5834
6284
  }
5835
6285
  return { ...config, tabs };
5836
6286
  }
@@ -5870,6 +6320,8 @@ function migrateRepository(options) {
5870
6320
  const componentRoot = mintlifyProjectRoot ?? docusaurusProjectRoot ?? fernProjectRoot ?? repositoryDir;
5871
6321
  const componentMigrator = platform === "mintlify" || platform === "docusaurus" || platform === "fern" ? createComponentMigrator(componentRoot, repositoryDir, warnings, componentSourceIdentity(options.sourceUrl, repositoryDir, componentRoot)) : void 0;
5872
6322
  let docsConfig = { tabs: [] };
6323
+ let defaultVersionPrefixes = /* @__PURE__ */ new Set();
6324
+ let allVersionPrefixes = /* @__PURE__ */ new Set();
5873
6325
  const referenceMap = /* @__PURE__ */ new Map();
5874
6326
  const exactReferenceMap = /* @__PURE__ */ new Map();
5875
6327
  const referenceOrder = /* @__PURE__ */ new Map();
@@ -5879,6 +6331,7 @@ function migrateRepository(options) {
5879
6331
  let fernBasePath = "";
5880
6332
  let fernVersionPath = "";
5881
6333
  let fernApiSections = [];
6334
+ const fernExternalSourcePaths = /* @__PURE__ */ new Set();
5882
6335
  let fernChangelogIndexes = [];
5883
6336
  const fernReferencedPaths = /* @__PURE__ */ new Set();
5884
6337
  const fernSourceLinkAliases = /* @__PURE__ */ new Map();
@@ -5889,6 +6342,8 @@ function migrateRepository(options) {
5889
6342
  const config = readMintlifyConfig(mintlifyProjectRoot ?? repositoryDir);
5890
6343
  if (config) {
5891
6344
  mintlifyConfig = config;
6345
+ defaultVersionPrefixes = mintlifyDefaultVersionPrefixes(config);
6346
+ allVersionPrefixes = mintlifyAllVersionPrefixes(config);
5892
6347
  const projected = projectMintlifyNavigation(config);
5893
6348
  docsConfig = projected.docsConfig;
5894
6349
  warnings.push(...projected.warnings);
@@ -5939,6 +6394,7 @@ function migrateRepository(options) {
5939
6394
  const exactKey = exactReferenceKey(descriptor.sourcePath);
5940
6395
  if (!exactReferenceMap.has(exactKey)) exactReferenceMap.set(exactKey, { navigationId: descriptor.navigationId });
5941
6396
  if (!referenceOrder.has(key)) referenceOrder.set(key, index);
6397
+ if (descriptor.sourcePath.startsWith("../")) fernExternalSourcePaths.add(descriptor.sourcePath);
5942
6398
  }
5943
6399
  }
5944
6400
  } catch (error) {
@@ -5963,15 +6419,35 @@ function migrateRepository(options) {
5963
6419
  throw new Error(`Documentation directory does not exist: ${configuredDocsDir || "."}`);
5964
6420
  }
5965
6421
  const mintignoreMatcher = platform === "mintlify" && mintlifyProjectRoot ? readMintignoreMatcher(mintlifyProjectRoot) : null;
5966
- const files = mintignoreMatcher ? scanFiles(contentRoot, repositoryDir, warnings).filter((file) => !mintignoreMatcher.ignores(file.relativePath)) : scanFiles(contentRoot, repositoryDir, warnings);
6422
+ const lastReferencedIndexByVersionLocale = /* @__PURE__ */ new Map();
6423
+ for (const [key, index] of referenceOrder) {
6424
+ const blockKey = key.split("/").slice(0, 2).join("/");
6425
+ const current = lastReferencedIndexByVersionLocale.get(blockKey);
6426
+ if (current === void 0 || index > current) lastReferencedIndexByVersionLocale.set(blockKey, index);
6427
+ }
6428
+ const discoveryRank = referenceOrder.size > 0 ? (relativePath) => {
6429
+ const referencedIndex = referenceOrder.get(normalizedReferenceKey(relativePath));
6430
+ if (referencedIndex !== void 0) return referencedIndex;
6431
+ const blockKey = relativePath.split("/").slice(0, 2).join("/");
6432
+ const versionCeiling = lastReferencedIndexByVersionLocale.get(blockKey);
6433
+ return versionCeiling !== void 0 ? versionCeiling + 1 : Number.MAX_SAFE_INTEGER;
6434
+ } : void 0;
6435
+ const scannedFiles = scanFiles(contentRoot, repositoryDir, warnings, discoveryRank);
6436
+ const mintignoreFilteredFiles = mintignoreMatcher ? scannedFiles.filter((file) => !mintignoreMatcher.ignores(file.relativePath)) : scannedFiles;
6437
+ const discoveryBudgetApplied = discoveryRank !== void 0 && mintignoreFilteredFiles.length > MAX_SOURCE_FILES;
6438
+ const files = discoveryRank ? selectFilesWithinBudget(mintignoreFilteredFiles, discoveryRank, warnings, allVersionPrefixes) : mintignoreFilteredFiles;
5967
6439
  if (platform === "fern" && fernProjectRoot) {
5968
- const scanned = new Set(files.map((file) => file.absolutePath));
5969
- for (const sourcePath of fernReferencedPaths) {
6440
+ const discovered2 = new Set(files.map((file) => file.relativePath));
6441
+ for (const sourcePath of fernExternalSourcePaths) {
6442
+ if (discovered2.has(sourcePath)) continue;
5970
6443
  try {
5971
6444
  const absolutePath = resolveWithinRoot(fernProjectRoot, sourcePath, repositoryDir);
5972
- if (scanned.has(absolutePath) || !existsSync5(absolutePath) || !lstatSync5(absolutePath).isFile()) continue;
5973
- files.push({ absolutePath, relativePath: sourcePath });
5974
- scanned.add(absolutePath);
6445
+ if (existsSync5(absolutePath) && lstatSync5(absolutePath).isFile()) {
6446
+ files.push({ absolutePath, relativePath: sourcePath });
6447
+ discovered2.add(sourcePath);
6448
+ } else {
6449
+ warnings.push({ code: "missing-page", message: "A Fern navigation path could not be read within this repository.", source: sourcePath });
6450
+ }
5975
6451
  } catch {
5976
6452
  warnings.push({ code: "missing-page", message: "A Fern navigation path could not be read within this repository.", source: sourcePath });
5977
6453
  }
@@ -6057,6 +6533,9 @@ function migrateRepository(options) {
6057
6533
  mintlifyProjectRoot ?? docusaurusProjectRoot ?? fernProjectRoot ?? repositoryDir,
6058
6534
  snippetAliases
6059
6535
  );
6536
+ if (platform === "fern" || platform === "mintlify" || platform === "docusaurus") {
6537
+ raw = normalizeExplicitHeadingIds(raw);
6538
+ }
6060
6539
  if (componentMigrator) {
6061
6540
  const warningsBeforeTransform = warnings.length;
6062
6541
  raw = componentMigrator.transform(raw, file.absolutePath);
@@ -6132,6 +6611,13 @@ function migrateRepository(options) {
6132
6611
  skipped++;
6133
6612
  continue;
6134
6613
  }
6614
+ if (page.frontmatterError) {
6615
+ warnings.push({
6616
+ code: "unsupported-config",
6617
+ message: `The page's frontmatter is not valid YAML (${page.frontmatterError}); the page was kept and as much of its metadata as could be read was used.`,
6618
+ source: relative5(repositoryDir, file.absolutePath).replace(/\\/g, "/")
6619
+ });
6620
+ }
6135
6621
  if (platform === "fern" && !page.navTitle) {
6136
6622
  const navTitle = fernNavTitles.get(navigationId);
6137
6623
  if (navTitle && navTitle !== page.title) page.navTitle = navTitle;
@@ -6211,6 +6697,13 @@ function migrateRepository(options) {
6211
6697
  if (sourcePath !== page.id && /^[A-Za-z0-9_./-]+$/.test(sourcePath)) {
6212
6698
  routeAliases.push({ source: `/${sourcePath}`, destination: `/${page.id}`, permanent: false });
6213
6699
  }
6700
+ const versionPrefix = [...defaultVersionPrefixes].find((prefix) => sourcePath === prefix || sourcePath.startsWith(`${prefix}/`));
6701
+ if (versionPrefix) {
6702
+ const unversioned = sourcePath.slice(versionPrefix.length).replace(/^\/+/, "");
6703
+ if (unversioned && unversioned !== sourcePath && unversioned !== page.id && /^[A-Za-z0-9_./-]+$/.test(unversioned) && !routeAliases.some((redirect) => redirect.source === `/${unversioned}`)) {
6704
+ routeAliases.push({ source: `/${unversioned}`, destination: `/${page.id}`, permanent: false });
6705
+ }
6706
+ }
6214
6707
  }
6215
6708
  if (docusaurusDescriptor) {
6216
6709
  docusaurusDescriptors.push({ ...docusaurusDescriptor, title: page.title });
@@ -6289,7 +6782,7 @@ function migrateRepository(options) {
6289
6782
  const referencingPages = referencedAssetPaths.get(assetPath);
6290
6783
  warnings.push({
6291
6784
  code: "limit-reached",
6292
- message: referencingPages ? `Asset was skipped because the migration asset budget was exhausted, but it is referenced by: ${[...referencingPages].join(", ")}.` : "An asset was skipped because the migration asset budget was exhausted.",
6785
+ message: "This file was not copied because it is larger than 25 MB or the copied files would exceed 500 MB in total. Add it to public/ manually" + (referencingPages ? `; it is used by ${[...referencingPages].slice(0, 3).join(", ")}${referencingPages.size > 3 ? `, and ${referencingPages.size - 3} more` : ""}.` : "."),
6293
6786
  source: file.relativePath
6294
6787
  });
6295
6788
  continue;
@@ -6306,8 +6799,8 @@ function migrateRepository(options) {
6306
6799
  assets.push({ path: assetPath, content });
6307
6800
  totalAssetBytes += size;
6308
6801
  }
6309
- if (files.length >= MAX_SOURCE_FILES) {
6310
- warnings.push({ code: "limit-reached", message: `Repository discovery stopped at ${MAX_SOURCE_FILES} files.` });
6802
+ if (!discoveryBudgetApplied && files.length >= MAX_SOURCE_FILES) {
6803
+ warnings.push({ code: "limit-reached", message: `Stopped scanning after ${MAX_SOURCE_FILES} files, so the rest of the repository was not looked at. Run the migration on a smaller part of the repository with --docs-dir.` });
6311
6804
  }
6312
6805
  if (platform === "docusaurus") {
6313
6806
  const projected = projectDocusaurusNavigation({
@@ -6393,10 +6886,11 @@ function migrateRepository(options) {
6393
6886
  filename = `${prefix}${filename}`;
6394
6887
  }
6395
6888
  specFilenameSources.set(filename, spec.absolutePath);
6889
+ const specContent = readFileSync5(spec.absolutePath);
6396
6890
  if (!assets.some((asset) => asset.path === filename)) {
6397
- assets.push({ path: filename, content: readFileSync5(spec.absolutePath) });
6891
+ assets.push({ path: filename, content: specContent });
6398
6892
  }
6399
- resolvedSpecs.push({ filename, tabLabel: section.tabLabel });
6893
+ resolvedSpecs.push({ filename, tabLabel: section.tabLabel, content: specContent, routeSegments: section.routeSegments ?? [] });
6400
6894
  continue;
6401
6895
  }
6402
6896
  if (resolution.unsupported.length > 0) {
@@ -6424,7 +6918,14 @@ function migrateRepository(options) {
6424
6918
  message: checked.length ? `No OpenAPI/AsyncAPI spec could be found for the "${section.name}" API. Checked: ${checked.join(", ")}. Point generators.yml at a spec file that exists.` : `No OpenAPI/AsyncAPI spec could be found for the "${section.name}" API, because its api-name is not a valid folder name. Add the spec manually.`
6425
6919
  });
6426
6920
  }
6427
- if (resolvedSpecs.length > 0) docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs);
6921
+ if (resolvedSpecs.length > 0) {
6922
+ docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs, warnings);
6923
+ const { operationLinks, prefixLandings } = apiOperationLinkMap(
6924
+ resolvedSpecs.map((spec) => ({ filename: spec.filename, content: spec.content, prefix: spec.routeSegments.join("/") })),
6925
+ docsConfig
6926
+ );
6927
+ rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings);
6928
+ }
6428
6929
  } else if (platform === "mintlify") {
6429
6930
  const resolvedSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs);
6430
6931
  for (const spec of resolvedSpecs) {
@@ -6433,7 +6934,12 @@ function migrateRepository(options) {
6433
6934
  }
6434
6935
  }
6435
6936
  if (resolvedSpecs.length > 0) {
6436
- docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs);
6937
+ docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs, warnings);
6938
+ const { operationLinks, prefixLandings } = apiOperationLinkMap(
6939
+ resolvedSpecs.map((spec) => ({ filename: spec.filename, content: spec.content, prefix: spec.directory })),
6940
+ docsConfig
6941
+ );
6942
+ rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings);
6437
6943
  } else {
6438
6944
  const fallback = findOpenApi(files);
6439
6945
  if (fallback) {
@@ -6466,7 +6972,7 @@ function migrateRepository(options) {
6466
6972
  ];
6467
6973
  for (const plugin of additionalRoots) {
6468
6974
  if (discovered >= MAX_SOURCE_FILES) {
6469
- warnings.push({ code: "limit-reached", message: `Docusaurus docs import stopped at the ${MAX_SOURCE_FILES}-file discovery budget.` });
6975
+ warnings.push({ code: "limit-reached", message: `Stopped importing Docusaurus docs plugins after ${MAX_SOURCE_FILES} files; the remaining plugin docs were not migrated.` });
6470
6976
  break;
6471
6977
  }
6472
6978
  if (plugin.docsDir === configuredDocsDir) continue;