@thallylabs/migrate 0.2.7 → 0.2.8

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
@@ -2,17 +2,21 @@
2
2
  import { compileSync as compileSync2 } from "@mdx-js/mdx";
3
3
  import { spawn } from "child_process";
4
4
  import {
5
+ closeSync,
5
6
  existsSync as existsSync5,
6
7
  lstatSync as lstatSync5,
8
+ openSync,
7
9
  readFileSync as readFileSync5,
8
- readdirSync,
10
+ readSync,
11
+ readdirSync as readdirSync2,
9
12
  realpathSync as realpathSync2,
10
13
  rmSync,
11
14
  statSync
12
15
  } from "fs";
13
16
  import { createRequire } from "module";
14
- import { basename as basename2, dirname as dirname4, extname as extname4, relative as relative5, resolve as resolvePath, sep as sep2 } from "path";
17
+ import { basename as basename3, dirname as dirname4, extname as extname5, relative as relative5, resolve as resolvePath, sep as sep2 } from "path";
15
18
  import { parse as parseYaml4 } from "yaml";
19
+ import * as ts2 from "typescript";
16
20
 
17
21
  // src/components.ts
18
22
  import { createHash } from "crypto";
@@ -159,19 +163,21 @@ function docusaurusAdmonitionTag(kind) {
159
163
  function normalizeDocusaurusAdmonitions(body) {
160
164
  const lines = body.split("\n");
161
165
  const openAdmonitions = [];
166
+ const headingLines = [];
162
167
  let codeFence = null;
163
- return lines.map((line) => {
164
- const codeMatch = line.match(/^\s*(`{3,}|~{3,})/);
168
+ const output = lines.map((line, index) => {
169
+ const codeMatch = line.match(/^\s{0,3}(`{3,}|~{3,})/);
165
170
  if (codeMatch) {
166
- if (!codeFence) codeFence = codeMatch[1][0];
167
- else if (codeMatch[1][0] === codeFence) codeFence = null;
171
+ if (!codeFence) codeFence = codeMatch[1];
172
+ else if (codeMatch[1][0] === codeFence[0] && codeMatch[1].length >= codeFence.length && /^\s*$/.test(line.slice(codeMatch[0].length))) codeFence = null;
168
173
  return line;
169
174
  }
170
175
  if (codeFence) return line;
176
+ if (/^#{1,6}\s/.test(line)) headingLines.push(index);
171
177
  const opening = line.match(/^\s*(:{3,})(note|tip|info|warning|caution|danger)(?:\[([^\]]+)\]|\s+(.+))?\s*$/i);
172
178
  if (opening) {
173
179
  const tag = docusaurusAdmonitionTag(opening[2].toLowerCase());
174
- openAdmonitions.push({ delimiter: opening[1], tag });
180
+ openAdmonitions.push({ delimiter: opening[1], tag, openedAt: index });
175
181
  const title = (opening[3] ?? opening[4])?.trim();
176
182
  return title ? `<${tag}>
177
183
  **${title}**` : `<${tag}>`;
@@ -183,7 +189,74 @@ function normalizeDocusaurusAdmonitions(body) {
183
189
  return `</${current.tag}>`;
184
190
  }
185
191
  return line;
186
- }).join("\n");
192
+ });
193
+ const closingByLine = /* @__PURE__ */ new Map();
194
+ for (const open of openAdmonitions.reverse()) {
195
+ const at = headingLines.find((line) => line > open.openedAt) ?? output.length;
196
+ closingByLine.set(at, [...closingByLine.get(at) ?? [], `</${open.tag}>`]);
197
+ }
198
+ for (const [at, closing] of [...closingByLine].sort(([left], [right]) => right - left)) output.splice(at, 0, ...closing);
199
+ return output.join("\n");
200
+ }
201
+ function unwrapDocusaurusMdxCodeBlocks(body) {
202
+ const output = [];
203
+ let fence;
204
+ let isMdxCodeBlock = false;
205
+ for (const line of body.split("\n")) {
206
+ const opening = !fence && line.match(/^ {0,3}(`{3,}|~{3,})mdx-code-block\s*$/);
207
+ if (opening) {
208
+ fence = opening[1];
209
+ isMdxCodeBlock = true;
210
+ continue;
211
+ }
212
+ if (fence) {
213
+ const closing = line.match(/^ {0,3}(`{3,}|~{3,})\s*$/);
214
+ if (closing && closing[1][0] === fence[0] && closing[1].length >= fence.length) {
215
+ if (!isMdxCodeBlock) output.push(line);
216
+ fence = void 0;
217
+ isMdxCodeBlock = false;
218
+ continue;
219
+ }
220
+ if (isMdxCodeBlock && /^\s*(?:import|export)\b/.test(line)) return body;
221
+ output.push(line);
222
+ continue;
223
+ }
224
+ const ordinaryFence = line.match(/^ {0,3}(`{3,}|~{3,})/);
225
+ if (ordinaryFence) {
226
+ fence = ordinaryFence[1];
227
+ isMdxCodeBlock = false;
228
+ }
229
+ output.push(line);
230
+ }
231
+ return isMdxCodeBlock ? body : output.join("\n");
232
+ }
233
+ function normalizeExplicitHeadingIds(body) {
234
+ return replaceOutsideCode(body, (segment) => segment.split("\n").map((line) => {
235
+ if (!/^[ \t]{0,3}#{1,6}[ \t]+/.test(line)) return line;
236
+ const trimmed = line.trimEnd();
237
+ if (trimmed.endsWith("}")) {
238
+ const marker = trimmed.lastIndexOf(" {#");
239
+ if (marker >= 0) {
240
+ const id = trimmed.slice(marker + 3, -1);
241
+ if (/^[A-Za-z][A-Za-z0-9_-]*$/.test(id)) {
242
+ return `<a id="${id}"></a>
243
+ ${trimmed.slice(0, marker).trimEnd()}`;
244
+ }
245
+ }
246
+ }
247
+ if (trimmed.endsWith("*/}")) {
248
+ const marker = trimmed.lastIndexOf(" {/*");
249
+ if (marker >= 0) {
250
+ const comment = trimmed.slice(marker + 4, -3).trim();
251
+ const id = comment.startsWith("#") ? comment.slice(1) : "";
252
+ if (/^[A-Za-z][A-Za-z0-9_.:-]*$/.test(id)) {
253
+ return `<a id="${id}"></a>
254
+ ${trimmed.slice(0, marker).trimEnd()}`;
255
+ }
256
+ }
257
+ }
258
+ return line;
259
+ }).join("\n"));
187
260
  }
188
261
  var GLOBAL_DOCUSARUS_COMPONENTS = /* @__PURE__ */ new Set([
189
262
  "Tabs",
@@ -192,19 +265,11 @@ var GLOBAL_DOCUSARUS_COMPONENTS = /* @__PURE__ */ new Set([
192
265
  "DocCardList",
193
266
  "TOCInline"
194
267
  ]);
195
- function isGlobalDocusaurusImport(line) {
196
- const trimmed = line.trim().replace(/;$/, "").trimEnd();
197
- if (!trimmed.startsWith("import ")) return false;
198
- const separator = trimmed.indexOf(" from ", "import ".length);
199
- if (separator < 0) return false;
200
- const component = trimmed.slice("import ".length, separator).trim();
201
- if (!GLOBAL_DOCUSARUS_COMPONENTS.has(component)) return false;
202
- const source = trimmed.slice(separator + " from ".length);
203
- if (source.length < 3) return false;
204
- const quote = source[0];
205
- if (quote !== "'" && quote !== '"' || source.at(-1) !== quote) return false;
206
- const moduleName = source.slice(1, -1);
207
- return moduleName.startsWith("@theme/") || moduleName.startsWith("@docusaurus/");
268
+ function removeGlobalDocusaurusImports(body) {
269
+ return replaceOutsideCode(body, (segment) => segment.replace(
270
+ /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s+(['"])(@(?:theme|docusaurus)\/[^'"]+)\2\s*;?/g,
271
+ (statement, component) => GLOBAL_DOCUSARUS_COMPONENTS.has(component) ? "" : statement
272
+ ));
208
273
  }
209
274
  var JS_LITERAL_KEYWORDS = /* @__PURE__ */ new Set(["true", "false", "null", "undefined"]);
210
275
  var BARE_IDENTIFIER_PATH = /^[\p{ID_Start}$_][\p{ID_Continue}$‌‍]*(?:\.[\p{ID_Start}$_][\p{ID_Continue}$‌‍]*)*$/u;
@@ -418,7 +483,11 @@ function escapeFernLiteralBraces(raw) {
418
483
  const end = node.position?.end.offset;
419
484
  const root2 = value.split(".")[0];
420
485
  const safeBuiltinPath = value.includes(".") && SAFE_BUILTIN_ROOTS.has(root2);
421
- if (BARE_IDENTIFIER_PATH.test(value) && !JS_LITERAL_KEYWORDS.has(value) && !declared.has(root2) && !safeBuiltinPath && start !== void 0 && end !== void 0) {
486
+ const isTemplatePlaceholder = /^\{\s*[A-Za-z_$][\w$.-]*\s*\}$/.test(value);
487
+ const isIssueList = /^[A-Za-z][A-Za-z0-9]*-\d+(?:\s*,\s*[A-Za-z][A-Za-z0-9]*-\d+)*$/.test(value);
488
+ if ((isTemplatePlaceholder || isIssueList) && start !== void 0 && end !== void 0) {
489
+ edits.push({ start, end, value: body.slice(start, end).replace(/\\/g, "\\\\").replace(/[{}]/g, "\\$&") });
490
+ } else if (BARE_IDENTIFIER_PATH.test(value) && !JS_LITERAL_KEYWORDS.has(value) && !declared.has(root2) && !safeBuiltinPath && start !== void 0 && end !== void 0) {
422
491
  edits.push({ start, end, value: `\\{${value}\\}` });
423
492
  }
424
493
  return;
@@ -532,10 +601,10 @@ function maskCode(body) {
532
601
  let codeFence = null;
533
602
  let fenceBuffer = [];
534
603
  for (const line of source.split("\n")) {
535
- const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/);
604
+ const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})/);
536
605
  if (codeFence) {
537
606
  fenceBuffer.push(line);
538
- if (fenceMatch && fenceMatch[1][0] === codeFence) {
607
+ if (fenceMatch && fenceMatch[1][0] === codeFence[0] && fenceMatch[1].length >= codeFence.length && /^\s*$/.test(line.slice(fenceMatch[0].length))) {
539
608
  lines.push(stash(fenceBuffer.join("\n")));
540
609
  codeFence = null;
541
610
  fenceBuffer = [];
@@ -543,7 +612,7 @@ function maskCode(body) {
543
612
  continue;
544
613
  }
545
614
  if (fenceMatch) {
546
- codeFence = fenceMatch[1][0];
615
+ codeFence = fenceMatch[1];
547
616
  fenceBuffer = [line];
548
617
  continue;
549
618
  }
@@ -565,14 +634,104 @@ function normalizeHtmlComments(body) {
565
634
  function normalizeFernCallouts(body) {
566
635
  return replaceOutsideCode(body, (whole) => {
567
636
  const openTags = [];
568
- return whole.replace(/<Callout\s+intent=(?:"([^"]*)"|'([^']*)')([^>]*?)(\/?)>/g, (_match, doubleQuoted, singleQuoted, rest, selfClose) => {
569
- const intent = (doubleQuoted ?? singleQuoted ?? "").toLowerCase();
570
- const tag = intent === "warning" ? "Warning" : intent === "success" || intent === "tip" ? "Tip" : intent === "error" || intent === "danger" ? "Error" : "Note";
571
- if (!selfClose) openTags.push(tag);
572
- return `<${tag}${rest}${selfClose}>`;
573
- }).replace(/<\/Callout>/g, (match) => openTags.length ? `</${openTags.pop()}>` : match);
637
+ const output = [];
638
+ let cursor = 0;
639
+ while (cursor < whole.length) {
640
+ const opening = whole.indexOf("<Callout", cursor);
641
+ const closing = whole.indexOf("</Callout>", cursor);
642
+ const next = opening < 0 ? closing : closing < 0 ? opening : Math.min(opening, closing);
643
+ if (next < 0) break;
644
+ output.push(whole.slice(cursor, next));
645
+ if (next === closing) {
646
+ const tag = openTags.pop();
647
+ output.push(tag ? `</${tag}>` : "</Callout>");
648
+ cursor = next + "</Callout>".length;
649
+ continue;
650
+ }
651
+ const attributeStart = next + "<Callout".length;
652
+ if (!/[\s/>]/.test(whole[attributeStart] ?? "")) {
653
+ output.push("<Callout");
654
+ cursor = attributeStart;
655
+ continue;
656
+ }
657
+ let end = attributeStart;
658
+ let quote = null;
659
+ let expressionDepth = 0;
660
+ for (; end < whole.length; end++) {
661
+ const character = whole[end];
662
+ if (quote) {
663
+ if (character === quote && whole[end - 1] !== "\\") quote = null;
664
+ } else if (character === '"' || character === "'") {
665
+ quote = character;
666
+ } else if (character === "{") {
667
+ expressionDepth++;
668
+ } else if (character === "}" && expressionDepth > 0) {
669
+ expressionDepth--;
670
+ } else if (character === ">" && expressionDepth === 0) {
671
+ break;
672
+ }
673
+ }
674
+ if (end === whole.length) {
675
+ output.push(whole.slice(next));
676
+ cursor = whole.length;
677
+ break;
678
+ }
679
+ const attributes = whole.slice(attributeStart, end);
680
+ const intent = findStaticCalloutIntent(attributes);
681
+ const isSelfClosing = attributes.trimEnd().endsWith("/");
682
+ if (!intent) {
683
+ output.push(whole.slice(next, end + 1));
684
+ if (!isSelfClosing) openTags.push(null);
685
+ } else {
686
+ const value = intent.value.toLowerCase();
687
+ const tag = value === "warning" ? "Warning" : value === "success" || value === "tip" ? "Tip" : value === "error" || value === "danger" ? "Error" : "Note";
688
+ output.push(`<${tag}${attributes.slice(0, intent.start)}${attributes.slice(intent.end)}>`);
689
+ if (!isSelfClosing) openTags.push(tag);
690
+ }
691
+ cursor = end + 1;
692
+ }
693
+ output.push(whole.slice(cursor));
694
+ return output.join("");
574
695
  });
575
696
  }
697
+ function findStaticCalloutIntent(attributes) {
698
+ let cursor = 0;
699
+ while (cursor < attributes.length) {
700
+ const start = cursor;
701
+ while (/\s/.test(attributes[cursor] ?? "")) cursor++;
702
+ if (cursor === start) {
703
+ cursor++;
704
+ continue;
705
+ }
706
+ const nameStart = cursor;
707
+ while (/[A-Za-z0-9_-]/.test(attributes[cursor] ?? "")) cursor++;
708
+ const name = attributes.slice(nameStart, cursor);
709
+ while (/\s/.test(attributes[cursor] ?? "")) cursor++;
710
+ if (attributes[cursor] !== "=") continue;
711
+ cursor++;
712
+ while (/\s/.test(attributes[cursor] ?? "")) cursor++;
713
+ const quote = attributes[cursor];
714
+ if (quote !== '"' && quote !== "'") {
715
+ if (quote === "{") {
716
+ let depth = 0;
717
+ do {
718
+ if (attributes[cursor] === "{") depth++;
719
+ if (attributes[cursor] === "}") depth--;
720
+ cursor++;
721
+ } while (cursor < attributes.length && depth > 0);
722
+ } else {
723
+ while (cursor < attributes.length && !/\s/.test(attributes[cursor])) cursor++;
724
+ }
725
+ continue;
726
+ }
727
+ const valueStart = ++cursor;
728
+ while (cursor < attributes.length && attributes[cursor] !== quote) cursor++;
729
+ const value = attributes.slice(valueStart, cursor);
730
+ if (cursor < attributes.length) cursor++;
731
+ if (name === "intent") return { start, end: cursor, value };
732
+ }
733
+ return null;
734
+ }
576
735
  function unwrapFernCodeBlockTabs(body) {
577
736
  return replaceOutsideCode(body, (whole) => {
578
737
  let groupDepth = 0;
@@ -679,25 +838,31 @@ ${result}`;
679
838
  return result;
680
839
  }
681
840
  function normalizeDocusaurusTabs(body) {
682
- return body.replace(/<Tabs\b([^>]*)>([\s\S]*?)<\/Tabs>/g, (_match, attributes, inner) => {
683
- const labelByValue = /* @__PURE__ */ new Map();
684
- const valuesSource = attributes.match(/\bvalues=\{(\[[\s\S]*?\])\s*\}/)?.[1];
685
- if (valuesSource) {
686
- for (const entry of valuesSource.matchAll(/\{([^{}]*)\}/g)) {
687
- const label = entry[1].match(/\blabel:\s*(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
688
- const value = entry[1].match(/\bvalue:\s*(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
689
- if (value) labelByValue.set(value, label ?? value);
841
+ let result = body;
842
+ for (let depth = 0; depth < 20; depth += 1) {
843
+ const updated = result.replace(/<Tabs\b([^>]*)>((?:(?!<Tabs\b)[\s\S])*?)<\/Tabs>/g, (_match, attributes, inner) => {
844
+ const labelByValue = /* @__PURE__ */ new Map();
845
+ const valuesSource = attributes.match(/\bvalues=\{(\[[\s\S]*?\])\s*\}/)?.[1];
846
+ if (valuesSource) {
847
+ for (const entry of valuesSource.matchAll(/\{([^{}]*)\}/g)) {
848
+ const label = entry[1].match(/\blabel:\s*(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
849
+ const value = entry[1].match(/\bvalue:\s*(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
850
+ if (value) labelByValue.set(value, label ?? value);
851
+ }
690
852
  }
691
- }
692
- const cleanedAttributes = attributes.replace(/\s*defaultValue=(?:\{[^}]*\}|"[^"]*"|'[^']*')/g, "").replace(/\s*values=\{[\s\S]*?\]\s*\}/g, "").replace(/\s*groupId=(?:"[^"]*"|'[^']*')/g, "").replace(/\s*queryString(?:=(?:\{[^}]*\}|"[^"]*"|'[^']*'))?/g, "");
693
- const rewrittenInner = inner.replace(/<TabItem\b([^>]*)>/g, (_itemMatch, itemAttributes) => {
694
- const label = itemAttributes.match(/\blabel=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
695
- const value = itemAttributes.match(/\bvalue=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
696
- const title = label ?? (value && labelByValue.get(value)) ?? value ?? "Tab";
697
- return `<Tab title="${title.replace(/"/g, "&quot;")}">`;
698
- }).replace(/<\/TabItem>/g, "</Tab>");
699
- return `<Tabs${cleanedAttributes}>${rewrittenInner}</Tabs>`;
700
- });
853
+ const cleanedAttributes = attributes.replace(/\s*defaultValue=(?:\{[^}]*\}|"[^"]*"|'[^']*')/g, "").replace(/\s*values=\{[\s\S]*?\]\s*\}/g, "").replace(/\s*values=\{[^{}]*\}/g, "").replace(/\s*groupId=(?:"[^"]*"|'[^']*')/g, "").replace(/\s*queryString(?:=(?:\{[^}]*\}|"[^"]*"|'[^']*'))?/g, "");
854
+ const rewrittenInner = inner.replace(/<TabItem\b([^>]*)>/g, (_itemMatch, itemAttributes) => {
855
+ const label = itemAttributes.match(/\blabel=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
856
+ const value = itemAttributes.match(/\bvalue=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
857
+ const title = label ?? (value && labelByValue.get(value)) ?? value ?? "Tab";
858
+ return `<Tab title="${title.replace(/"/g, "&quot;")}">`;
859
+ }).replace(/<\/TabItem>/g, "</Tab>");
860
+ return `<ThallyTabs${cleanedAttributes}>${rewrittenInner}</ThallyTabs>`;
861
+ });
862
+ if (updated === result) break;
863
+ result = updated;
864
+ }
865
+ return result.replace(/<ThallyTabs\b/g, "<Tabs").replace(/<\/ThallyTabs>/g, "</Tabs>");
701
866
  }
702
867
  var TAB_FENCE_OPEN = /^(`{3,}|~{3,})(\S*)\s+tab\b(.*)$/;
703
868
  function normalizeDocusaurusTabBlocks(body) {
@@ -783,15 +948,68 @@ function convertHtmlStyleAttributes(body) {
783
948
  (_match, tag, before, _quote, styleText, after, selfClose) => `<${tag}${before} style={${cssTextToStyleObjectLiteral(styleText)}}${after}${selfClose ? " /" : ""}>`
784
949
  );
785
950
  }
786
- function normalizeMdx(body, platform) {
951
+ function normalizeMintlifyUpdateLabels(body) {
952
+ return body.replace(/(<Update\b[^>]*\blabel=)\{(<\>[\s\S]*?<\/\>)\}/g, (match, prefix, fragment) => {
953
+ let root;
954
+ try {
955
+ root = descriptionParser.parse(fragment);
956
+ } catch {
957
+ return match;
958
+ }
959
+ const plainText = (node) => {
960
+ if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
961
+ if (node.type === "mdxTextExpression" || node.type === "mdxFlowExpression" || node.type === "mdxjsEsm") return null;
962
+ const parts = (node.children ?? []).map(plainText);
963
+ return parts.some((part) => part === null) ? null : parts.join("");
964
+ };
965
+ const label = plainText(root)?.replace(/\s+/g, " ").trim();
966
+ return label && !/[{}]/.test(label) ? `${prefix}{${JSON.stringify(label)}}` : match;
967
+ });
968
+ }
969
+ function normalizeMintlifyParameterAnchors(body) {
970
+ const seen = /* @__PURE__ */ new Set();
971
+ const anchorFor = (name) => {
972
+ const id = `param-${name.toLowerCase().replace(/[._]/g, "-")}`;
973
+ if (seen.has(id)) return "";
974
+ seen.add(id);
975
+ return `<a id="${id}"></a>
976
+ `;
977
+ };
978
+ let fence;
979
+ const withHeadings = body.split("\n").map((line) => {
980
+ const marker = line.match(/^ {0,3}(`{3,}|~{3,})/);
981
+ if (marker) {
982
+ if (!fence) fence = marker[1];
983
+ else if (marker[1][0] === fence[0] && marker[1].length >= fence.length && /^\s*$/.test(line.slice(marker[0].length))) fence = void 0;
984
+ return line;
985
+ }
986
+ if (fence) return line;
987
+ const heading = line.match(/^ {0,3}#{1,6}\s+`([A-Za-z][A-Za-z0-9._-]*)`(?:\s|$)/);
988
+ return heading ? `${anchorFor(heading[1])}${line}` : line;
989
+ }).join("\n");
990
+ return replaceOutsideCode(withHeadings, (segment) => segment.replace(
991
+ /<(ResponseField|ParamField)\b([^>]*?)\bname=(?:"([A-Za-z][A-Za-z0-9._-]*)"|'([A-Za-z][A-Za-z0-9._-]*)')([^>]*)>/g,
992
+ (match, _tag, _before, doubleQuoted, singleQuoted) => `${anchorFor(doubleQuoted ?? singleQuoted)}${match}`
993
+ ));
994
+ }
995
+ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
787
996
  const runFern = platform === "fern";
788
997
  const runMintlify = platform === "mintlify";
789
998
  const runDocusaurus = platform === "docusaurus";
790
- let rewritten = normalizeDocusaurusAdmonitions(runFern ? normalizeFernCallouts(body) : body);
999
+ const sourceBody = runDocusaurus && unwrapMdxCodeBlocks ? unwrapDocusaurusMdxCodeBlocks(body) : body;
1000
+ let rewritten = normalizeDocusaurusAdmonitions(normalizeExplicitHeadingIds(runFern ? normalizeFernCallouts(sourceBody) : sourceBody));
791
1001
  if (runDocusaurus) {
792
- const withoutGlobalImports = rewritten.split("\n").filter((line) => !isGlobalDocusaurusImport(line)).join("\n");
793
- rewritten = normalizeDocusaurusTabBlocks(normalizeDocusaurusTabs(withoutGlobalImports));
1002
+ rewritten = normalizeDocusaurusTabBlocks(normalizeDocusaurusTabs(removeGlobalDocusaurusImports(rewritten)));
1003
+ rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1004
+ /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s+(['"])(@site\/[^'"]+)\2\s*;?/g,
1005
+ (statement, binding, _quote, _source, offset) => {
1006
+ const withoutStatement = segment.slice(0, offset) + segment.slice(offset + statement.length);
1007
+ return new RegExp(`\\b${binding}\\s*(?:\\.|\\[|\\})`).test(withoutStatement) ? statement : "";
1008
+ }
1009
+ ));
1010
+ rewritten = rewritten.replace(/^\n{2,}/, "\n");
794
1011
  }
1012
+ if (runMintlify) rewritten = normalizeMintlifyParameterAnchors(rewritten);
795
1013
  rewritten = replaceOutsideCode(rewritten, (segment) => {
796
1014
  let result = convertHtmlStyleAttributes(segment).replace(/<!--([\s\S]*?)-->/g, (_match, content) => `{/*${content}*/}`).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Warn(\s[^>]*)?>/g, "<Warning$1>").replace(/<\/Warn>/g, "</Warning>").replace(/<Check(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Check>/g, "</Note>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
797
1015
  if (runDocusaurus) {
@@ -801,7 +1019,7 @@ function normalizeMdx(body, platform) {
801
1019
  }).replace(/<\/TabItem>/g, "</Tab>").replace(/<Link\b([^>]*)\bto=(?:"([^"]*)"|'([^']*)')([^>]*)>/g, (_match, before, doubleQuoted, singleQuoted, after) => `<a${before}href="${doubleQuoted ?? singleQuoted}"${after}>`).replace(/<\/Link>/g, "</a>").replace(/<(?:DocCardList|TOCInline)\b[^>]*\/>/g, "");
802
1020
  }
803
1021
  if (runMintlify) {
804
- result = result.replace(/<FileTree(\s[^>]*)?>/g, "<Tree$1>").replace(/<\/FileTree>/g, "</Tree>").replace(/<GitHub\.Repo/g, "<GitHub").replace(/<\/GitHub\.Repo>/g, "</GitHub>").replace(/<Column(\s[^>]*)?>/g, "<div$1>").replace(/<\/Column>/g, "</div>");
1022
+ result = normalizeMintlifyUpdateLabels(result).replace(/<FileTree(\s[^>]*)?>/g, "<Tree$1>").replace(/<\/FileTree>/g, "</Tree>").replace(/<GitHub\.Repo/g, "<GitHub").replace(/<\/GitHub\.Repo>/g, "</GitHub>").replace(/<Column(\s[^>]*)?>/g, "<div$1>").replace(/<\/Column>/g, "</div>");
805
1023
  }
806
1024
  if (runFern) {
807
1025
  result = result.replace(/<CodeBlocks(\s[^>]*)?>/g, "<CodeGroup$1>").replace(/<\/CodeBlocks>/g, "</CodeGroup>").replace(/<ParameterField\b/g, "<ParamField").replace(/<\/ParameterField>/g, "</ParamField>").replace(/<Cards(\s[^>]*)?>/g, "<CardGroup$1>").replace(/<\/Cards>/g, "</CardGroup>").replace(/<Success(\s[^>]*)?>/g, "<Tip$1>").replace(/<\/Success>/g, "</Tip>").replace(/<Launch(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Launch>/g, "</Note>").replace(/<Files(\s[^>]*)?>\n?/g, "").replace(/<\/Files>/g, "");
@@ -809,7 +1027,15 @@ function normalizeMdx(body, platform) {
809
1027
  return result;
810
1028
  });
811
1029
  if (runFern) rewritten = unwrapFernCodeBlockTabs(rewritten);
812
- return provideScopedGlobalReferences(rewritten);
1030
+ const normalized = provideScopedGlobalReferences(rewritten);
1031
+ if (runDocusaurus && sourceBody !== body) {
1032
+ try {
1033
+ compileSync(normalized, { outputFormat: "program" });
1034
+ } catch {
1035
+ return normalizeMdx(body, platform, false);
1036
+ }
1037
+ }
1038
+ return normalized;
813
1039
  }
814
1040
  function parseMarkdownPage(input) {
815
1041
  const parsed = parseFrontmatter(input.raw);
@@ -919,6 +1145,73 @@ function normalizeAssetPath(value) {
919
1145
  }
920
1146
 
921
1147
  // src/components.ts
1148
+ function normalizeIndentedFences(content) {
1149
+ const lines = content.split("\n");
1150
+ let active;
1151
+ let ordinaryFence;
1152
+ let indentedJsx;
1153
+ let listIndent;
1154
+ return lines.map((line) => {
1155
+ const trimmed = line.trimStart();
1156
+ if (indentedJsx) {
1157
+ const normalized2 = line.startsWith(indentedJsx.indent) ? line.slice(indentedJsx.indent.length) : line;
1158
+ if (normalized2.trimStart().startsWith(`</${indentedJsx.tag}>`)) indentedJsx = void 0;
1159
+ return normalized2;
1160
+ }
1161
+ if (ordinaryFence) {
1162
+ const closing2 = trimmed.match(/^(`{3,}|~{3,})\s*$/)?.[1];
1163
+ if (closing2 && closing2[0] === ordinaryFence[0] && closing2.length >= ordinaryFence.length) ordinaryFence = void 0;
1164
+ return line;
1165
+ }
1166
+ if (!active) {
1167
+ const listItem = line.match(/^([ \t]*)(?:[-*+]|\d+[.)])\s/);
1168
+ const indentation = line.length - trimmed.length;
1169
+ if (listItem) listIndent = listItem[1].length;
1170
+ else if (trimmed && listIndent !== void 0 && indentation <= listIndent) listIndent = void 0;
1171
+ const jsx = line.match(/^([ \t]{2,})<([A-Z][\w.]*)\b[^>]*>\s*$/);
1172
+ if (jsx && listIndent !== void 0 && jsx[1].length > listIndent && !trimmed.endsWith("/>")) {
1173
+ indentedJsx = { tag: jsx[2], indent: jsx[1] };
1174
+ return line.slice(jsx[1].length);
1175
+ }
1176
+ const match = line.match(/^([ \t]{4,})(`{3,}|~{3,})/);
1177
+ if (match) {
1178
+ active = { indent: match[1], marker: match[2] };
1179
+ return line.slice(match[1].length);
1180
+ }
1181
+ const ordinary = line.match(/^[ \t]{0,3}(`{3,}|~{3,})/);
1182
+ if (ordinary) ordinaryFence = ordinary[1];
1183
+ return line;
1184
+ }
1185
+ const normalized = line.startsWith(active.indent) ? line.slice(active.indent.length) : line;
1186
+ const closing = normalized.trimStart().match(/^(`{3,}|~{3,})\s*$/)?.[1];
1187
+ if (closing && closing[0] === active.marker[0] && closing.length >= active.marker.length) active = void 0;
1188
+ return normalized;
1189
+ }).join("\n");
1190
+ }
1191
+ function staticServerHelper(path, imported, local) {
1192
+ if (imported === "default") return null;
1193
+ const parsed = ts.createSourceFile(path, readFileSync(path, "utf8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1194
+ const isLiteral = (node) => {
1195
+ if (ts.isParenthesizedExpression(node)) return isLiteral(node.expression);
1196
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) return true;
1197
+ if ([ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword, ts.SyntaxKind.NullKeyword].includes(node.kind)) return true;
1198
+ if (ts.isPrefixUnaryExpression(node)) return [ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken].includes(node.operator) && ts.isNumericLiteral(node.operand);
1199
+ if (ts.isArrayLiteralExpression(node)) return node.elements.every((element) => ts.isExpression(element) && isLiteral(element));
1200
+ if (ts.isObjectLiteralExpression(node)) return node.properties.every((property) => ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name) || ts.isNumericLiteral(property.name)) && isLiteral(property.initializer));
1201
+ return false;
1202
+ };
1203
+ for (const statement of parsed.statements) {
1204
+ if (!ts.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue;
1205
+ for (const declaration of statement.declarationList.declarations) {
1206
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== imported || !declaration.initializer || !ts.isArrowFunction(declaration.initializer)) continue;
1207
+ const arrow = declaration.initializer;
1208
+ if (arrow.parameters.length > 0) continue;
1209
+ const result = ts.isBlock(arrow.body) ? arrow.body.statements.length === 1 && ts.isReturnStatement(arrow.body.statements[0]) ? arrow.body.statements[0].expression : void 0 : arrow.body;
1210
+ if (result && isLiteral(result)) return `export const ${local} = () => (${result.getText(parsed)});`;
1211
+ }
1212
+ }
1213
+ return null;
1214
+ }
922
1215
  var YOUTUBE_VIDEO_ID = /^[\w-]{1,64}$/;
923
1216
  var YOUTUBE_ID_PACKAGES = /* @__PURE__ */ new Set(["react-lite-youtube-embed", "react-youtube", "@justinribeiro/lite-youtube"]);
924
1217
  var YOUTUBE_URL = /(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{1,64})/i;
@@ -1474,7 +1767,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1474
1767
  function transform(raw, currentFile) {
1475
1768
  const parsedFrontmatter = parseFrontmatter(raw).content;
1476
1769
  const frontmatter = raw.slice(0, raw.length - parsedFrontmatter.length);
1477
- const content = normalizeHtmlComments(parsedFrontmatter);
1770
+ const content = normalizeIndentedFences(normalizeHtmlComments(parsedFrontmatter));
1478
1771
  let tree;
1479
1772
  try {
1480
1773
  tree = parser.parse(content);
@@ -1488,6 +1781,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1488
1781
  const declarations = [];
1489
1782
  const moduleImports = [];
1490
1783
  const realPageImports = [];
1784
+ const serverPageDeclarations = [];
1491
1785
  const sharedImportEdits = [];
1492
1786
  let hasUnsupportedImports = false;
1493
1787
  function hasExpressionReference(names, options = {}) {
@@ -1553,13 +1847,19 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1553
1847
  const rawSpecifier = ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : "";
1554
1848
  const specifier = rawSpecifier.startsWith("@site/") ? `/${rawSpecifier.slice("@site/".length)}` : rawSpecifier;
1555
1849
  if (isScaffoldProvidedImport(specifier)) {
1850
+ if (specifier === "react" && bindings.length > 0 && bindings.every((binding) => REACT_GLOBALS.has(binding.imported)) && !hasExpressionReference(new Set(bindings.map((binding) => binding.local)))) {
1851
+ edits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
1852
+ continue;
1853
+ }
1556
1854
  moduleImports.push(statement.getText(ast));
1557
1855
  continue;
1558
1856
  }
1559
1857
  if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
1560
1858
  if (SHARED_IMPORTS.has(specifier)) {
1561
- moduleImports.push(statement.getText(ast));
1562
- sharedImportEdits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
1859
+ const isUnusedReactHookImport = specifier === "react" && bindings.length > 0 && bindings.every((binding) => REACT_GLOBALS.has(binding.imported)) && !hasExpressionReference(new Set(bindings.map((binding) => binding.local)));
1860
+ if (!isUnusedReactHookImport) moduleImports.push(statement.getText(ast));
1861
+ if (isUnusedReactHookImport) edits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
1862
+ else sharedImportEdits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
1563
1863
  } else if (specifier.startsWith("@theme/") || specifier.startsWith("@docusaurus/")) {
1564
1864
  edits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
1565
1865
  } else if (hasExpressionReference(new Set(bindings.map((binding) => binding.local)))) {
@@ -1595,18 +1895,19 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1595
1895
  hasUnsupportedImports = true;
1596
1896
  continue;
1597
1897
  }
1598
- if (bindings.some((binding) => !/^[A-Z]/.test(binding.local))) {
1599
- warn("MDX imports containing non-component values require manual migration; the import was preserved.", currentFile);
1600
- hasUnsupportedImports = true;
1601
- continue;
1602
- }
1603
1898
  try {
1604
- const path = copyGraph(resolveDependency(specifier, currentFile));
1899
+ const sourcePath = resolveDependency(specifier, currentFile);
1900
+ const path = copyGraph(sourcePath);
1605
1901
  const isSvgUsedAsTag = extname(path).toLowerCase() === ".svg" && bindings.some((binding) => usedAsJsxTagName(binding.local));
1606
1902
  for (const binding of bindings) {
1607
1903
  const registerPath = isSvgUsedAsTag ? wrapSvgAsComponent(path) : path;
1608
1904
  const registerImported = isSvgUsedAsTag ? "default" : binding.imported;
1609
1905
  if (!isSvgUsedAsTag && hasExpressionReference(/* @__PURE__ */ new Set([binding.local]))) {
1906
+ const helper = staticServerHelper(sourcePath, binding.imported, binding.local);
1907
+ if (helper) {
1908
+ serverPageDeclarations.push(helper);
1909
+ continue;
1910
+ }
1610
1911
  const aliasSpecifier = portableSpecifier(`@/${registerPath.replace(/^src\//, "").replace(/\\/g, "/")}`);
1611
1912
  realPageImports.push(`import { ${registerImported} as ${binding.local} } from ${JSON.stringify(aliasSpecifier)};`);
1612
1913
  continue;
@@ -1617,6 +1918,15 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
1617
1918
  }
1618
1919
  edits.push({ start: node.position.start.offset + statement.getStart(ast), end: node.position.start.offset + statement.end, value: "" });
1619
1920
  } catch (error) {
1921
+ if (hasExpressionReference(new Set(bindings.map((binding) => binding.local)))) {
1922
+ warnings.push({
1923
+ code: "skipped-file",
1924
+ message: `This page was excluded because local import ${JSON.stringify(rawSpecifier)} could not be copied and is used in an expression: ${error instanceof Error ? error.message : "unsupported dependency"}.`,
1925
+ source: relative2(root, currentFile).replace(/\\/g, "/")
1926
+ });
1927
+ hasUnsupportedImports = true;
1928
+ continue;
1929
+ }
1620
1930
  const failedPackage = error instanceof Error ? error.message.match(/^external package (\S+) requires manual installation and review$/)?.[1] : void 0;
1621
1931
  if (failedPackage && YOUTUBE_ID_PACKAGES.has(failedPackage)) {
1622
1932
  for (const binding of bindings) unsupportedImports.set(binding.local, failedPackage);
@@ -1758,9 +2068,64 @@ ${inlineSource}` });
1758
2068
  }
1759
2069
  edits.push(...declarations.map(({ start, end }) => ({ start, end, value: "" })), ...sharedImportEdits);
1760
2070
  }
2071
+ const pageFunctions = functionDeclaredNames(content);
2072
+ const passiveRoots = [];
2073
+ const wrappedRoots = /* @__PURE__ */ new Set();
2074
+ walk(tree, (node) => {
2075
+ if (!node.name || !aliases.has(node.name) || !functionValuedAttribute(node, pageFunctions) || node.position?.start.offset === void 0 || node.position.end.offset === void 0) return;
2076
+ if (passiveRoots.some((root2) => node.position.start.offset >= root2.position.start.offset && node.position.end.offset <= root2.position.end.offset)) return;
2077
+ const functionAttributes = (node.attributes ?? []).filter((attribute) => {
2078
+ const value = attribute.value && typeof attribute.value === "object" ? attribute.value.value?.trim() : void 0;
2079
+ return !!value && pageFunctions.has(value);
2080
+ });
2081
+ if (functionAttributes.length === 1 && functionAttributes[0].name && functionAttributes[0].position?.start.offset !== void 0 && functionAttributes[0].position.end.offset !== void 0) {
2082
+ const functionName = functionAttributes[0].value.value.trim();
2083
+ const declaration = declarations.find((entry) => new RegExp(`^export const ${functionName}\\s*=`).test(entry.source));
2084
+ const safeDeclaration = declaration?.source.match(/^export const \w+\s*=\s*\(\s*\{\s*children(?:\s*,\s*\.\.\.props)?\s*\}\s*\)\s*=>\s*\([\s\S]*\)\s*;?$/) && !/\b(?:window|document|process|fetch|use[A-Z]\w*)\b/.test(declaration.source) && [...declaration.source.matchAll(/<([A-Z]\w*)\b/g)].every((match) => match[1] === "CodeBlock") && [...declaration.source.matchAll(/\{([^{}]*)\}/g)].every((match) => ["children", "...props", "children, ...props"].includes(match[1].trim()));
2085
+ const binding = registrations.get(aliases.get(node.name));
2086
+ if (safeDeclaration && binding) {
2087
+ const fallbackDeclaration = declaration.source.replace(/<CodeBlock\b[^>]*>/g, "<pre><code>").replace(/<\/CodeBlock>/g, "</code></pre>");
2088
+ const wrapperPath = `${destinationRoot}/callback-${hash(`${currentFile}:${node.position.start.offset}`)}.jsx`;
2089
+ const imported = binding.imported === "default" ? "default" : binding.imported;
2090
+ const importClause = imported === "default" ? "import Original" : `import { ${imported} as Original }`;
2091
+ const specifier = portableSpecifier(`./${relative2(destinationRoot, binding.path).replace(/\\/g, "/")}`);
2092
+ const wrapperName = register(wrapperPath, "CallbackWrapper");
2093
+ copied.set(wrapperPath, { path: wrapperPath, content: [
2094
+ "'use client';",
2095
+ `${importClause} from ${JSON.stringify(specifier)};`,
2096
+ fallbackDeclaration,
2097
+ `export function CallbackWrapper({ children, ...props }) { return <Original {...props} ${functionAttributes[0].name}={${functionName}}>{children}</Original>; }`
2098
+ ].join("\n") });
2099
+ const start = node.position.start.offset;
2100
+ const end = node.position.end.offset;
2101
+ const source2 = content.slice(start, end);
2102
+ const opening = source2.indexOf(`<${node.name}`);
2103
+ const closing = source2.lastIndexOf(`</${node.name}`);
2104
+ if (opening >= 0) edits.push({ start: start + opening + 1, end: start + opening + 1 + node.name.length, value: wrapperName });
2105
+ if (closing >= 0) edits.push({ start: start + closing + 2, end: start + closing + 2 + node.name.length, value: wrapperName });
2106
+ edits.push({ start: functionAttributes[0].position.start.offset, end: functionAttributes[0].position.end.offset, value: "" });
2107
+ wrappedRoots.add(node);
2108
+ if (declaration.source.includes("<CodeBlock")) warn(`Interactive <${node.name}> was retained with a basic code block in place of Mintlify's CodeBlock.`, currentFile);
2109
+ return;
2110
+ }
2111
+ }
2112
+ const source = content.slice(node.position.start.offset, node.position.end.offset);
2113
+ const openingEnd = source.indexOf(">");
2114
+ const closingStart = source.lastIndexOf(`</${node.name}`);
2115
+ const children = openingEnd >= 0 && closingStart > openingEnd ? source.slice(openingEnd + 1, closingStart) : "";
2116
+ edits.push({
2117
+ start: node.position.start.offset,
2118
+ end: node.position.end.offset,
2119
+ value: `<div data-migration-interactive-fallback="${node.name}">${children}</div>`
2120
+ });
2121
+ passiveRoots.push(node);
2122
+ warn(`Interactive <${node.name}> passes a page-local function into a client component; its child content was retained without the interactive control. Move the callback into a client component to restore it.`, currentFile);
2123
+ });
1761
2124
  walk(tree, (node) => {
1762
2125
  const replacement = node.name ? aliases.get(node.name) : void 0;
1763
2126
  if (!replacement || node.position?.start.offset === void 0 || node.position.end.offset === void 0) return;
2127
+ if (wrappedRoots.has(node)) return;
2128
+ if (passiveRoots.some((root2) => node.position.start.offset >= root2.position.start.offset && node.position.end.offset <= root2.position.end.offset)) return;
1764
2129
  const start = node.position.start.offset;
1765
2130
  const end = node.position.end.offset;
1766
2131
  if (extracted.some((item) => start >= item.node.position.start.offset && end <= item.node.position.end.offset)) return;
@@ -1780,7 +2145,8 @@ ${inlineSource}` });
1780
2145
  edits.push({ start, end, value });
1781
2146
  });
1782
2147
  const rendered = applyReplacements(content, edits);
1783
- const realImportsBlock = realPageImports.length ? `${[...new Set(realPageImports)].join("\n")}
2148
+ const pageStatements = [.../* @__PURE__ */ new Set([...realPageImports, ...serverPageDeclarations])];
2149
+ const realImportsBlock = pageStatements.length ? `${pageStatements.join("\n")}
1784
2150
 
1785
2151
  ` : "";
1786
2152
  return frontmatter + realImportsBlock + rendered;
@@ -2695,7 +3061,7 @@ function resolveDocusaurusPageIdentity(sourcePath, frontmatter, fallback) {
2695
3061
  function sourceReferenceKey(value) {
2696
3062
  return value.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\.(?:mdx?)$/i, "").replace(/\/$/, "");
2697
3063
  }
2698
- function rewriteDocusaurusLinks(body, current, descriptors) {
3064
+ function rewriteDocusaurusLinks(body, current, descriptors, options = {}) {
2699
3065
  const routes = /* @__PURE__ */ new Map();
2700
3066
  for (const descriptor of descriptors) {
2701
3067
  const sourceKey = sourceReferenceKey(descriptor.sourcePath);
@@ -2711,26 +3077,47 @@ function rewriteDocusaurusLinks(body, current, descriptors) {
2711
3077
  const suffixIndex = target.search(/[?#]/);
2712
3078
  const path = suffixIndex >= 0 ? target.slice(0, suffixIndex) : target;
2713
3079
  const suffix = suffixIndex >= 0 ? target.slice(suffixIndex) : "";
2714
- const candidates = path.startsWith("/") ? [sourceReferenceKey(path), sourceReferenceKey(path).replace(/^docs\//, "")] : [
3080
+ const candidates = path.startsWith("/") ? [
3081
+ sourceReferenceKey(path),
3082
+ sourceReferenceKey(path).replace(/^docs\//, ""),
3083
+ sourceReferenceKey(path).replace(/^docs\/(?:next|current|latest)\//, "")
3084
+ ] : [
2715
3085
  sourceReferenceKey(posix.normalize(posix.join(currentDirectory, path))),
2716
3086
  sourceReferenceKey(path)
2717
3087
  ];
2718
3088
  const route = candidates.map((candidate) => routes.get(candidate)).find(Boolean);
2719
- if (!route) return target;
3089
+ if (!route) {
3090
+ if (path.startsWith("/") && path !== "/" && options.sourceOrigin) {
3091
+ options.onExternalLink?.(target);
3092
+ return new URL(target, options.sourceOrigin).toString();
3093
+ }
3094
+ return target;
3095
+ }
2720
3096
  return `${route === "introduction" ? "/" : `/${route}`}${suffix}`;
2721
3097
  }
2722
3098
  let codeFence = null;
2723
3099
  return body.split("\n").map((line) => {
2724
- const fence = line.match(/^\s*(`{3,}|~{3,})/);
3100
+ const fence = line.match(/^\s{0,3}(`{3,}|~{3,})/);
2725
3101
  if (fence) {
2726
- if (!codeFence) codeFence = fence[1][0];
2727
- else if (fence[1][0] === codeFence) codeFence = null;
3102
+ if (!codeFence) codeFence = fence[1];
3103
+ else if (fence[1][0] === codeFence[0] && fence[1].length >= codeFence.length && /^\s*$/.test(line.slice(fence[0].length))) codeFence = null;
2728
3104
  return line;
2729
3105
  }
2730
3106
  if (codeFence) return line;
2731
3107
  return line.replace(/(\]\()([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(target)}`).replace(/(\bhref=")([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(target)}${suffix}`);
2732
3108
  }).join("\n");
2733
3109
  }
3110
+ function readDocusaurusSiteOrigin(repositoryRoot) {
3111
+ const configPath = findDocusaurusConfigPath(repositoryRoot);
3112
+ if (!configPath) return void 0;
3113
+ const address = readBoundedText(configPath).match(/\burl['"]?\s*:\s*(['"])(https?:\/\/[^'"]+)\1/)?.[2];
3114
+ if (!address) return void 0;
3115
+ try {
3116
+ return new URL(address).origin;
3117
+ } catch {
3118
+ return void 0;
3119
+ }
3120
+ }
2734
3121
  function readBoundedText(path) {
2735
3122
  if (lstatSync3(path).size > MAX_CONFIG_BYTES) {
2736
3123
  throw new Error("Docusaurus sidebar config exceeded the 1 MB static-parser limit.");
@@ -2900,9 +3287,12 @@ function configuredSidebarPath(repositoryRoot) {
2900
3287
  }
2901
3288
  return null;
2902
3289
  }
2903
- function readDocusaurusSidebars(repositoryRoot) {
2904
- const configured = configuredSidebarPath(repositoryRoot);
2905
- const sourcePath = [configured, ...SIDEBAR_FILENAMES].filter((value) => Boolean(value)).find((candidate) => existsSync3(resolveWithin(repositoryRoot, candidate)));
3290
+ function readDocusaurusSidebars(repositoryRoot, versionedSidebarPath) {
3291
+ const configured = versionedSidebarPath ?? configuredSidebarPath(repositoryRoot);
3292
+ const sourcePath = [configured, ...versionedSidebarPath ? [] : SIDEBAR_FILENAMES].filter((value) => Boolean(value)).find((candidate) => {
3293
+ const path = resolveWithin(repositoryRoot, candidate);
3294
+ return existsSync3(path) && lstatSync3(path).isFile();
3295
+ });
2906
3296
  if (!sourcePath) return null;
2907
3297
  const absolutePath = resolveWithin(repositoryRoot, sourcePath);
2908
3298
  const source = readBoundedText(absolutePath);
@@ -3188,8 +3578,8 @@ function projectDocusaurusNavigation(input) {
3188
3578
  }
3189
3579
 
3190
3580
  // src/fern.ts
3191
- import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as readFileSync4 } from "fs";
3192
- import { dirname as dirname3, relative as relative4 } from "path";
3581
+ import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as readFileSync4, readdirSync } from "fs";
3582
+ import { basename as basename2, dirname as dirname3, extname as extname4, relative as relative4 } from "path";
3193
3583
  import { parse as parseYaml3 } from "yaml";
3194
3584
  var MAX_CONFIG_BYTES2 = 2e6;
3195
3585
  function objectValue3(value) {
@@ -3243,8 +3633,9 @@ function fernLabelSlug(label) {
3243
3633
  }
3244
3634
  function segmentFor(value, label, context) {
3245
3635
  if (value["skip-slug"] === true) return null;
3246
- const explicit = typeof value.slug === "string" ? value.slug.trim() : "";
3247
- if (!explicit) return fernLabelSlug(label) || null;
3636
+ if (typeof value.slug !== "string") return fernLabelSlug(label) || null;
3637
+ const explicit = value.slug.trim();
3638
+ if (!explicit) return null;
3248
3639
  const slugified = fernBasicSlug(explicit);
3249
3640
  if (context && explicit !== slugified && FERN_SEGMENT_ALIAS.test(explicit)) {
3250
3641
  context.segmentAliases.set(slugified, explicit);
@@ -3258,8 +3649,18 @@ function uniqueNavigationId(base, context) {
3258
3649
  return `${base}-${index}`;
3259
3650
  }
3260
3651
  function registerPageAt(rawPath, base, context) {
3261
- const relativePath = rawPath.trim().replace(/^\.\//, "");
3262
- const sourcePath = context.pathPrefix ? `${context.pathPrefix}/${relativePath}` : relativePath;
3652
+ let sourcePath;
3653
+ try {
3654
+ const absolute = resolveWithinRoot(
3655
+ context.pathPrefix ? resolveWithin(context.fernRoot, context.pathPrefix) : context.fernRoot,
3656
+ rawPath.trim(),
3657
+ context.repositoryRoot
3658
+ );
3659
+ sourcePath = relative4(context.fernRoot, absolute).replace(/\\/g, "/");
3660
+ } catch {
3661
+ warnOnce2(context, `fern-unsafe-page-${rawPath}`, `Fern page path "${rawPath}" escapes the repository and was skipped.`);
3662
+ return "";
3663
+ }
3263
3664
  const navigationId = uniqueNavigationId(base || "introduction", context);
3264
3665
  context.seenNavigationIds.add(navigationId);
3265
3666
  context.descriptors.push({ sourcePath, navigationId });
@@ -3268,17 +3669,62 @@ function registerPageAt(rawPath, base, context) {
3268
3669
  context.sourcePathToNavigationId.set(sourcePath, navigationId);
3269
3670
  return navigationId;
3270
3671
  }
3672
+ function registerFolder(object, parentSegments, context) {
3673
+ const rawPath = String(object.folder).trim();
3674
+ let folder;
3675
+ try {
3676
+ folder = resolveWithinRoot(
3677
+ context.pathPrefix ? resolveWithin(context.fernRoot, context.pathPrefix) : context.fernRoot,
3678
+ rawPath,
3679
+ context.repositoryRoot
3680
+ );
3681
+ } catch {
3682
+ warnOnce2(context, `fern-unsafe-folder-${rawPath}`, `Fern folder "${rawPath}" escapes the repository and was skipped.`);
3683
+ return null;
3684
+ }
3685
+ if (!existsSync4(folder) || !lstatSync4(folder).isDirectory()) {
3686
+ warnOnce2(context, `fern-missing-folder-${rawPath}`, `Fern folder "${rawPath}" does not exist and was skipped.`);
3687
+ return null;
3688
+ }
3689
+ const label = typeof object.title === "string" && object.title.trim() ? object.title.trim() : titleCase3(basename2(folder));
3690
+ const segment = typeof object.slug === "string" || object["skip-slug"] === true ? segmentFor(object, label, context) : fernBasicSlug(basename2(folder));
3691
+ const segments = segment ? [...parentSegments, segment] : parentSegments;
3692
+ const visit = (directory, routeSegments, depth) => {
3693
+ if (depth > 12 || context.descriptors.length >= 5e3) return [];
3694
+ const pages2 = [];
3695
+ const entries = readdirSync(directory, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".") && !entry.isSymbolicLink()).sort((left, right) => Number(left.name !== "index.mdx" && left.name !== "index.md") - Number(right.name !== "index.mdx" && right.name !== "index.md") || left.name.localeCompare(right.name));
3696
+ for (const entry of entries) {
3697
+ const path = resolveWithin(directory, entry.name);
3698
+ if (entry.isDirectory()) {
3699
+ const nested = visit(path, [...routeSegments, fernBasicSlug(entry.name)], depth + 1);
3700
+ if (nested.length) pages2.push({ group: titleCase3(entry.name), pages: nested });
3701
+ } else if (entry.isFile() && [".md", ".mdx"].includes(extname4(entry.name).toLowerCase())) {
3702
+ const stem = basename2(entry.name, extname4(entry.name));
3703
+ const pageSegment = stem.toLowerCase() === "index" ? null : fernBasicSlug(stem);
3704
+ const route = [...routeSegments, ...pageSegment ? [pageSegment] : []].join("/");
3705
+ const registered = registerPageAt(relative4(context.fernRoot, path), route, { ...context, pathPrefix: "" });
3706
+ if (registered) pages2.push(registered);
3707
+ }
3708
+ }
3709
+ return pages2;
3710
+ };
3711
+ const pages = visit(folder, segments, 0);
3712
+ if (!pages.length) return null;
3713
+ addBareRouteRedirect(segments, pages, context);
3714
+ return { group: label, pages };
3715
+ }
3271
3716
  function registerPage(object, parentSegments, context) {
3272
3717
  if (typeof object.path !== "string" || !object.path.trim()) return null;
3273
3718
  const label = typeof object.page === "string" && object.page.trim() ? object.page.trim() : "Untitled";
3274
3719
  const segment = segmentFor(object, label, context);
3275
3720
  const base = [...parentSegments, segment].filter(Boolean).join("/");
3276
- return registerPageAt(object.path, base, context);
3721
+ return registerPageAt(object.path, base, context) || null;
3277
3722
  }
3278
3723
  function convertNode(node, parentSegments, context) {
3279
3724
  const object = objectValue3(node);
3280
3725
  if (!object) return null;
3281
3726
  if (typeof object.page === "string") return registerPage(object, parentSegments, context);
3727
+ if (typeof object.folder === "string") return registerFolder(object, parentSegments, context);
3282
3728
  if (typeof object.section === "string") {
3283
3729
  const label = object.section;
3284
3730
  const segment = segmentFor(object, label, context);
@@ -3287,7 +3733,8 @@ function convertNode(node, parentSegments, context) {
3287
3733
  const pages = [];
3288
3734
  const hasOwnPath = typeof object.path === "string" && object.path.trim().length > 0;
3289
3735
  if (hasOwnPath) {
3290
- pages.push(registerPageAt(object.path, segments.join("/"), context));
3736
+ const registered = registerPageAt(object.path, segments.join("/"), context);
3737
+ if (registered) pages.push(registered);
3291
3738
  }
3292
3739
  for (const child of contents) {
3293
3740
  const converted = convertNode(child, segments, context);
@@ -3364,6 +3811,7 @@ function resolveVersionedNavigation(config, configDir, fernRoot, context) {
3364
3811
  }
3365
3812
  const versionConfig = objectValue3(readBoundedYaml(versionPath));
3366
3813
  if (!versionConfig) return config;
3814
+ context.pathPrefix = relative4(fernRoot, dirname3(versionPath)).replace(/\\/g, "/");
3367
3815
  return { ...config, navigation: versionConfig.navigation, tabs: versionConfig.tabs ?? config.tabs };
3368
3816
  } catch (error) {
3369
3817
  warnOnce2(
@@ -3463,6 +3911,8 @@ function projectFernProducts(rawProducts, fernRoot, context) {
3463
3911
  }
3464
3912
  function projectFernNavigation(input) {
3465
3913
  const context = {
3914
+ fernRoot: input.fernRoot,
3915
+ repositoryRoot: input.repositoryRoot ?? input.fernRoot,
3466
3916
  descriptors: [],
3467
3917
  seenNavigationIds: /* @__PURE__ */ new Set(),
3468
3918
  apiSections: [],
@@ -3618,7 +4068,8 @@ var REPOSITORY_ONLY_DOCUMENTS = /* @__PURE__ */ new Set([
3618
4068
  "security.md"
3619
4069
  ]);
3620
4070
  var SNIPPET_DIRECTORIES = /* @__PURE__ */ new Set(["snippets", "_snippets", "partials", "_partials"]);
3621
- var SNIPPET_IMPORT_PATTERN = /^import\s+(?:\{\s*(?:default\s+as\s+)?([A-Z][A-Za-z0-9_]*)\s*\}|([A-Z][A-Za-z0-9_]*))\s+from\s+['"]([^'"]+\.mdx?)['"]\s*;?(?:\s*\/\/.*)?$/gm;
4071
+ var SNIPPET_IMPORT_PATTERN = /\bimport\s+(?:\{\s*(?:default\s+as\s+)?([A-Z][A-Za-z0-9_]*)\s*\}|([A-Z][A-Za-z0-9_]*))\s+from\s+['"]([^'"]+\.mdx?)['"]\s*;?/g;
4072
+ var SNIPPET_VALUE_IMPORT_PATTERN = /\bimport\s*\{([^{}]+)\}\s*from\s+['"]([^'"]+\.mdx?)['"]\s*;?/g;
3622
4073
  var SNIPPET_TAG_PATTERN = /<Snippet\s+file=(?:"([^"]+)"|'([^']+)')\s*(?:\/>|>[\s\S]*?<\/Snippet>)/g;
3623
4074
  var MINTIGNORE_FILENAME = ".mintignore";
3624
4075
  var createIgnoreMatcher = createRequire(import.meta.url)("ignore");
@@ -3664,7 +4115,7 @@ function countMarkdownPages(directory, limit = 2e4) {
3664
4115
  const current = stack.pop();
3665
4116
  let entries;
3666
4117
  try {
3667
- entries = readdirSync(current, { withFileTypes: true });
4118
+ entries = readdirSync2(current, { withFileTypes: true });
3668
4119
  } catch {
3669
4120
  continue;
3670
4121
  }
@@ -3683,7 +4134,7 @@ function pickPreferredDocsRoot(candidates, repositoryDir, platformLabel, warning
3683
4134
  const ranked = [...candidates].sort((left, right) => {
3684
4135
  const byRank = (rank?.(right) ?? 0) - (rank?.(left) ?? 0);
3685
4136
  if (byRank !== 0) return byRank;
3686
- const byName = (basename2(right) === "docs" ? 1 : 0) - (basename2(left) === "docs" ? 1 : 0);
4137
+ const byName = (basename3(right) === "docs" ? 1 : 0) - (basename3(left) === "docs" ? 1 : 0);
3687
4138
  if (byName !== 0) return byName;
3688
4139
  return countMarkdownPages(right) - countMarkdownPages(left);
3689
4140
  });
@@ -3717,7 +4168,7 @@ function findMintlifyProjectRoot(repositoryDir, docsDir, warnings) {
3717
4168
  visited++;
3718
4169
  if (hasMintlifyConfig(current.directory)) candidates.push(current.directory);
3719
4170
  if (current.depth >= 4) continue;
3720
- for (const entry of readdirSync(current.directory, { withFileTypes: true })) {
4171
+ for (const entry of readdirSync2(current.directory, { withFileTypes: true })) {
3721
4172
  if (!entry.isDirectory() || entry.isSymbolicLink() || isIgnoredDirectory(entry.name)) continue;
3722
4173
  queue.push({ directory: resolveWithin(current.directory, entry.name), depth: current.depth + 1 });
3723
4174
  }
@@ -3749,7 +4200,7 @@ function findFernProjectRoot(repositoryDir, docsDir) {
3749
4200
  visited++;
3750
4201
  if (hasFernConfig(current.directory)) return current.directory;
3751
4202
  if (current.depth >= 4) continue;
3752
- for (const entry of readdirSync(current.directory, { withFileTypes: true })) {
4203
+ for (const entry of readdirSync2(current.directory, { withFileTypes: true })) {
3753
4204
  if (!entry.isDirectory() || entry.isSymbolicLink() || isIgnoredDirectory(entry.name)) continue;
3754
4205
  queue.push({ directory: resolveWithin(current.directory, entry.name), depth: current.depth + 1 });
3755
4206
  }
@@ -3775,7 +4226,7 @@ function findDocusaurusProjectRoot(repositoryDir, docsDir, warnings) {
3775
4226
  visited++;
3776
4227
  if (hasDocusaurusConfig(current.directory)) candidates.push(current.directory);
3777
4228
  if (current.depth >= 4) continue;
3778
- for (const entry of readdirSync(current.directory, { withFileTypes: true })) {
4229
+ for (const entry of readdirSync2(current.directory, { withFileTypes: true })) {
3779
4230
  if (!entry.isDirectory() || entry.isSymbolicLink() || isIgnoredDirectory(entry.name)) continue;
3780
4231
  queue.push({
3781
4232
  directory: resolveWithin(current.directory, entry.name),
@@ -3796,6 +4247,52 @@ function primaryDocusaurusDocsDirectory(projectRoot) {
3796
4247
  const configured = source.match(/\bdocs\s*:\s*\{[\s\S]{0,4000}?\bpath\s*:\s*(['"])([^'"]+)\1/)?.[2];
3797
4248
  return trimTrailingSlashes(configured?.replace(/^\.\//, "") ?? "") || "docs";
3798
4249
  }
4250
+ function docusaurusArchiveRoots(projectRoot, repositoryDir, warnings) {
4251
+ const archives = [];
4252
+ const relativeRoot = relative5(repositoryDir, projectRoot).replace(/\\/g, "/");
4253
+ const directoryNames = (directory) => {
4254
+ const path = resolveWithin(projectRoot, directory);
4255
+ if (!existsSync5(path) || !lstatSync5(path).isDirectory()) return [];
4256
+ return readdirSync2(path, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).map((entry) => entry.name).sort();
4257
+ };
4258
+ let versions = directoryNames("versioned_docs").filter((name) => /^version-[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name));
4259
+ const versionsPath = resolveWithin(projectRoot, "versions.json");
4260
+ if (existsSync5(versionsPath) && lstatSync5(versionsPath).isFile()) {
4261
+ try {
4262
+ if (lstatSync5(versionsPath).size > 1e5) throw new Error("file exceeds the 100 KB limit");
4263
+ const declared = JSON.parse(readFileSync5(versionsPath, "utf8"));
4264
+ if (!Array.isArray(declared) || !declared.every((value) => typeof value === "string")) throw new Error("expected an array of version names");
4265
+ const declaredOrder = declared.map((value) => `version-${value}`);
4266
+ versions = [...declaredOrder.filter((name) => versions.includes(name)), ...versions.filter((name) => !declaredOrder.includes(name))];
4267
+ } catch (error) {
4268
+ warnings.push({ code: "unsupported-config", message: `Docusaurus versions.json could not be read safely: ${error instanceof Error ? error.message : String(error)}` });
4269
+ }
4270
+ }
4271
+ const sidebarPath = (version) => {
4272
+ const candidates = ["json", "js", "cjs", "mjs", "ts"].map((extension) => `versioned_sidebars/${version}-sidebars.${extension}`);
4273
+ return candidates.find((candidate) => {
4274
+ const path = resolveWithin(projectRoot, candidate);
4275
+ return existsSync5(path) && lstatSync5(path).isFile();
4276
+ });
4277
+ };
4278
+ const add = (directory, routePrefix, label, sidebar) => {
4279
+ const docsDir = [relativeRoot, directory].filter(Boolean).join("/");
4280
+ archives.push({ docsDir, routePrefix, label, ...sidebar ? { sidebarPath: sidebar } : {} });
4281
+ };
4282
+ for (const version of versions) add(`versioned_docs/${version}`, version.slice("version-".length), `Version ${version.slice("version-".length)}`, sidebarPath(version));
4283
+ for (const locale of directoryNames("i18n").filter((name) => /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{2,8})*$/.test(name))) {
4284
+ const localizedRoot = `i18n/${locale}/docusaurus-plugin-content-docs`;
4285
+ const current = resolveWithin(projectRoot, `${localizedRoot}/current`);
4286
+ if (existsSync5(current) && lstatSync5(current).isDirectory()) add(`${localizedRoot}/current`, locale, `${locale} Documentation`);
4287
+ const localizedVersions = directoryNames(localizedRoot).filter((name) => /^version-[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name));
4288
+ for (const version of localizedVersions) {
4289
+ const path = resolveWithin(projectRoot, `${localizedRoot}/${version}`);
4290
+ if (existsSync5(path) && lstatSync5(path).isDirectory()) add(`${localizedRoot}/${version}`, `${locale}/${version.slice("version-".length)}`, `${locale} Version ${version.slice("version-".length)}`);
4291
+ }
4292
+ }
4293
+ if (archives.length > 48) warnings.push({ code: "limit-reached", message: `Only the first 48 Docusaurus archived or localized docs roots were imported (${archives.length} found).` });
4294
+ return archives.slice(0, 48);
4295
+ }
3799
4296
  function additionalDocusaurusPluginRoots(repositoryDir, projectRoot, warnings) {
3800
4297
  const source = readDocusaurusConfigSource(projectRoot);
3801
4298
  const plugins = [];
@@ -4022,10 +4519,10 @@ function detectRepositoryPlatform(repositoryDir, docsDir) {
4022
4519
  }
4023
4520
  function containsMarkdown(directory, depth = 0) {
4024
4521
  if (depth > 4) return false;
4025
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
4522
+ for (const entry of readdirSync2(directory, { withFileTypes: true })) {
4026
4523
  if (isIgnoredDirectory(entry.name) || entry.isSymbolicLink()) continue;
4027
4524
  const path = resolveWithin(directory, entry.name);
4028
- if (entry.isFile() && [".md", ".mdx"].includes(extname4(entry.name).toLowerCase())) return true;
4525
+ if (entry.isFile() && [".md", ".mdx"].includes(extname5(entry.name).toLowerCase())) return true;
4029
4526
  if (entry.isDirectory() && containsMarkdown(path, depth + 1)) return true;
4030
4527
  }
4031
4528
  return false;
@@ -4058,7 +4555,7 @@ function scanFiles(root, confinementRoot = root, warnings) {
4058
4555
  visitedRealPaths.add(directoryReal);
4059
4556
  let entries;
4060
4557
  try {
4061
- entries = readdirSync(directory, { withFileTypes: true });
4558
+ entries = readdirSync2(directory, { withFileTypes: true });
4062
4559
  } catch {
4063
4560
  return;
4064
4561
  }
@@ -4151,7 +4648,25 @@ function exactReferenceKey(value) {
4151
4648
  return value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/").replace(/\.(?:mdx?|rst|txt)$/i, "");
4152
4649
  }
4153
4650
  function findOpenApi(files) {
4154
- return files.find((file) => OPENAPI_FILENAMES.has(basename2(file.relativePath).toLowerCase())) ?? null;
4651
+ return files.find((file) => OPENAPI_FILENAMES.has(basename3(file.relativePath).toLowerCase()) && classifyApiSpec(file.absolutePath) === "openapi") ?? null;
4652
+ }
4653
+ function classifyApiSpec(path) {
4654
+ const buffer = Buffer.alloc(64e3);
4655
+ let bytesRead = 0;
4656
+ try {
4657
+ const descriptor = openSync(path, "r");
4658
+ try {
4659
+ bytesRead = readSync(descriptor, buffer, 0, buffer.length, 0);
4660
+ } finally {
4661
+ closeSync(descriptor);
4662
+ }
4663
+ } catch {
4664
+ return "unknown";
4665
+ }
4666
+ const text = buffer.toString("utf8", 0, bytesRead);
4667
+ const topLevel = text.match(/^(?:openapi|swagger|asyncapi)\s*:/m)?.[0] ?? text.match(/^\s*\{\s*"(?:openapi|swagger|asyncapi)"\s*:/m)?.[0];
4668
+ if (!topLevel) return "unknown";
4669
+ return /asyncapi/i.test(topLevel) ? "asyncapi" : "openapi";
4155
4670
  }
4156
4671
  function mintlifyTopLevelApiReferences(config) {
4157
4672
  const api = config?.api && typeof config.api === "object" && !Array.isArray(config.api) ? config.api : null;
@@ -4200,7 +4715,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings) {
4200
4715
  });
4201
4716
  continue;
4202
4717
  }
4203
- specs.push({ filename: basename2(match.relativePath), content: readFileSync5(match.absolutePath), tabLabel: reference.tabLabel });
4718
+ specs.push({ filename: basename3(match.relativePath), content: readFileSync5(match.absolutePath), tabLabel: reference.tabLabel });
4204
4719
  }
4205
4720
  return specs;
4206
4721
  }
@@ -4267,6 +4782,12 @@ function findFernConfiguredOpenApi(fernRoot, repositoryDir, apiName, apiNameExpl
4267
4782
  try {
4268
4783
  const absolute = resolveWithinRoot(dir, specPath, repositoryDir);
4269
4784
  if (!existsSync5(absolute) || !lstatSync5(absolute).isFile()) continue;
4785
+ const kind = classifyApiSpec(absolute);
4786
+ if (kind === "asyncapi") {
4787
+ unsupported.push({ kind: "asyncapi", path: specPath });
4788
+ continue;
4789
+ }
4790
+ if (kind !== "openapi") continue;
4270
4791
  return { spec: { absolutePath: absolute, relativePath: relative5(repositoryDir, absolute).replace(/\\/g, "/") }, unsupported };
4271
4792
  } catch {
4272
4793
  warnings.push({
@@ -4304,9 +4825,11 @@ function staticSnippetProperties(attributes) {
4304
4825
  }
4305
4826
  return properties;
4306
4827
  }
4307
- function interpolateSnippet(snippet, attributes) {
4828
+ function interpolateSnippet(snippet, attributes, children) {
4308
4829
  const properties = staticSnippetProperties(attributes);
4830
+ if (children !== void 0) properties.set("children", children.trim());
4309
4831
  if (properties.size === 0) return snippet;
4832
+ const isJsxSnippet = /<[A-Za-z][A-Za-z0-9]*/.test(snippet);
4310
4833
  let fence = "";
4311
4834
  return snippet.split("\n").map((line) => {
4312
4835
  const marker = line.match(/^\s*(`{3,}|~{3,})/)?.[1];
@@ -4317,11 +4840,234 @@ function interpolateSnippet(snippet, attributes) {
4317
4840
  }
4318
4841
  if (fence) return line;
4319
4842
  return line.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (original, name) => {
4320
- return properties.get(name) ?? original;
4843
+ const value = properties.get(name);
4844
+ if (value === void 0) return original;
4845
+ if (name === "children") return value;
4846
+ return isJsxSnippet ? `{${JSON.stringify(value)}}` : value;
4321
4847
  });
4322
4848
  }).join("\n");
4323
4849
  }
4850
+ function snippetComponentBody(source, componentName) {
4851
+ const parsed = ts2.createSourceFile("snippet.tsx", source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TSX);
4852
+ for (const statement of parsed.statements) {
4853
+ if (!ts2.isVariableStatement(statement)) continue;
4854
+ for (const declaration of statement.declarationList.declarations) {
4855
+ if (!ts2.isIdentifier(declaration.name) || declaration.name.text !== componentName) continue;
4856
+ const initializer = declaration.initializer;
4857
+ if (!initializer || !ts2.isArrowFunction(initializer)) continue;
4858
+ const returned = ts2.isBlock(initializer.body) ? initializer.body.statements.find(ts2.isReturnStatement)?.expression : initializer.body;
4859
+ if (!returned) continue;
4860
+ let expression = returned;
4861
+ while (ts2.isParenthesizedExpression(expression)) expression = expression.expression;
4862
+ if (ts2.isJsxElement(expression) || ts2.isJsxSelfClosingElement(expression) || ts2.isJsxFragment(expression)) {
4863
+ return expression.getText(parsed);
4864
+ }
4865
+ }
4866
+ }
4867
+ return source;
4868
+ }
4869
+ function statefulSnippetDeclaration(source, componentName) {
4870
+ const parsed = ts2.createSourceFile("snippet.tsx", source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TSX);
4871
+ if (parsed.statements.length !== 1) return null;
4872
+ const statement = parsed.statements[0];
4873
+ if (!ts2.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword) || statement.declarationList.declarations.length !== 1) return null;
4874
+ const declaration = statement.declarationList.declarations[0];
4875
+ if (!ts2.isIdentifier(declaration.name) || declaration.name.text !== componentName || !declaration.initializer || !ts2.isArrowFunction(declaration.initializer) || !ts2.isBlock(declaration.initializer.body)) return null;
4876
+ const body = declaration.initializer.body.statements;
4877
+ if (body.length < 2 || !body.some(ts2.isReturnStatement)) return null;
4878
+ return source.trim();
4879
+ }
4880
+ function staticNamedSnippetValues(source) {
4881
+ const values = /* @__PURE__ */ new Map();
4882
+ const parsed = ts2.createSourceFile("snippet.tsx", source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TSX);
4883
+ for (const statement of parsed.statements) {
4884
+ if (!ts2.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword)) continue;
4885
+ for (const declaration of statement.declarationList.declarations) {
4886
+ if (!ts2.isIdentifier(declaration.name) || !declaration.initializer) continue;
4887
+ const expression = declaration.initializer;
4888
+ if (ts2.isStringLiteral(expression) || ts2.isNoSubstitutionTemplateLiteral(expression)) {
4889
+ values.set(declaration.name.text, JSON.stringify(expression.text));
4890
+ } else if (ts2.isNumericLiteral(expression)) {
4891
+ values.set(declaration.name.text, expression.text);
4892
+ } else if (expression.kind === ts2.SyntaxKind.TrueKeyword || expression.kind === ts2.SyntaxKind.FalseKeyword) {
4893
+ values.set(declaration.name.text, expression.kind === ts2.SyntaxKind.TrueKeyword ? "true" : "false");
4894
+ }
4895
+ }
4896
+ }
4897
+ return values;
4898
+ }
4899
+ function hoistMdxImports(source) {
4900
+ const imports2 = /* @__PURE__ */ new Set();
4901
+ const withoutImports = replaceOutsideCode(source, (body) => body.replace(
4902
+ /^[ \t]*import\s+(?:\{[^}\n]+\}|[A-Za-z_$][\w$]*)\s+from\s+(['"])[^'"\n]+\1[ \t]*;?/gm,
4903
+ (statement) => {
4904
+ imports2.add(statement.trim().replace(/;?$/, ";"));
4905
+ return "";
4906
+ }
4907
+ ));
4908
+ if (imports2.size === 0) return source;
4909
+ const frontmatter = withoutImports.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
4910
+ const prefix = frontmatter?.[0] ?? "";
4911
+ return `${prefix}${[...imports2].join("\n")}
4912
+
4913
+ ${withoutImports.slice(prefix.length).replace(/^(?:\r?\n)*/, "")}`;
4914
+ }
4915
+ function fernSourceBasePath(config) {
4916
+ const instance = Array.isArray(config.instances) ? config.instances[0] : null;
4917
+ if (!instance || typeof instance !== "object" || Array.isArray(instance)) return "";
4918
+ const value = instance;
4919
+ const address = typeof value["custom-domain"] === "string" ? value["custom-domain"] : value.url;
4920
+ if (typeof address !== "string") return "";
4921
+ try {
4922
+ const path = new URL(/^https?:\/\//i.test(address) ? address : `https://${address}`).pathname.replace(/\/+$/, "");
4923
+ return path === "/" ? "" : path;
4924
+ } catch {
4925
+ return "";
4926
+ }
4927
+ }
4928
+ function fernDefaultVersionPath(config) {
4929
+ if (!Array.isArray(config.versions)) return "";
4930
+ const versions = config.versions.filter((entry) => !!entry && typeof entry === "object" && !Array.isArray(entry));
4931
+ const chosen = versions.find((version) => version.default === true) ?? versions[0];
4932
+ return typeof chosen?.slug === "string" && /^[A-Za-z0-9_-]+$/.test(chosen.slug) ? `/${chosen.slug}` : "";
4933
+ }
4934
+ function fernSourceSiteUrl(config) {
4935
+ const instance = Array.isArray(config.instances) ? config.instances[0] : null;
4936
+ if (!instance || typeof instance !== "object" || Array.isArray(instance)) return void 0;
4937
+ const value = instance;
4938
+ const address = typeof value["custom-domain"] === "string" ? value["custom-domain"] : value.url;
4939
+ if (typeof address !== "string") return void 0;
4940
+ try {
4941
+ const url = new URL(/^https?:\/\//i.test(address) ? address : `https://${address}`);
4942
+ return `${url.origin}${fernSourceBasePath(config)}${fernDefaultVersionPath(config)}`;
4943
+ } catch {
4944
+ return void 0;
4945
+ }
4946
+ }
4947
+ function externalizeMissingFernLinks(body, pageIds, sourceSiteUrl, externalized) {
4948
+ const rewrite = (target) => {
4949
+ const path = target.split(/[?#]/, 1)[0].replace(/^\//, "").replace(/\/$/, "");
4950
+ if (!path || pageIds.has(path)) return target;
4951
+ try {
4952
+ const base = new URL(`${sourceSiteUrl}/`);
4953
+ const sourceLinkUrl = new URL(target.slice(1), base);
4954
+ if (sourceLinkUrl.origin !== base.origin) return target;
4955
+ const sourceLink = sourceLinkUrl.toString();
4956
+ externalized.add(target);
4957
+ return sourceLink;
4958
+ } catch {
4959
+ return target;
4960
+ }
4961
+ };
4962
+ return replaceOutsideCode(body, (text) => text.replace(/(\]\()\/(?!\/)([^\s)]+)(\))/g, (_match, before, target, after) => `${before}${rewrite(`/${target}`)}${after}`).replace(/(\b(?:href|to)=(['"]))\/(?!\/)([^'"\n]+)\2/g, (_match, before, quote, target) => `${before}${rewrite(`/${target}`)}${quote}`));
4963
+ }
4964
+ function stripFernBasePathFromLinks(body, basePath) {
4965
+ if (!basePath) return body;
4966
+ const escaped = basePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4967
+ return replaceOutsideCode(body, (text) => text.replace(new RegExp(`(\\]\\()${escaped}(?=\\/|[)#?])`, "g"), (match, prefix, offset, source) => prefix + (source[offset + match.length] === "/" ? "" : "/")).replace(new RegExp(`(\\b(?:href|to)=["'])${escaped}(?=\\/|["'#?])`, "g"), (match, prefix, offset, source) => prefix + (source[offset + match.length] === "/" ? "" : "/")));
4968
+ }
4969
+ function rewriteMintlifyMountedLinks(body, pageIds) {
4970
+ const rewrite = (target) => {
4971
+ const suffixAt = target.search(/[?#]/);
4972
+ const pathname = suffixAt < 0 ? target : target.slice(0, suffixAt);
4973
+ const suffix = suffixAt < 0 ? "" : target.slice(suffixAt);
4974
+ if (!pathname.startsWith("/") || pathname.startsWith("//") || pathname.includes("\\")) return target;
4975
+ const segments = pathname.slice(1).split("/");
4976
+ if (segments.some((segment) => !segment || segment === "." || segment === "..")) return target;
4977
+ if (pageIds.has(segments.join("/"))) return target;
4978
+ for (let count = 1; count < segments.length && count <= 3; count++) {
4979
+ const candidate = segments.slice(count).join("/");
4980
+ if (pageIds.has(candidate)) return `/${candidate}${suffix}`;
4981
+ }
4982
+ return target;
4983
+ };
4984
+ return replaceOutsideCode(body, (text) => text.replace(/(\]\()(\/[^\s)]+)(\))/g, (_match, before, target, after) => `${before}${rewrite(target)}${after}`).replace(/(\b(?:href|to)=(['"]))(\/[^'"\n]+)\2/g, (_match, before, quote, target) => `${before}${rewrite(target)}${quote}`));
4985
+ }
4986
+ function preserveDocusaurusLinkedAnchors(pages) {
4987
+ const pageById = new Map(pages.map((page) => [page.id, page]));
4988
+ const requested = /* @__PURE__ */ new Map();
4989
+ const crossPageRequests = /* @__PURE__ */ new Set();
4990
+ const addTarget = (target, currentId) => {
4991
+ const hash2 = target.indexOf("#");
4992
+ if (hash2 < 0) return;
4993
+ const fragment = target.slice(hash2 + 1);
4994
+ if (!/^[A-Za-z0-9_.:-]{1,120}$/.test(fragment)) return;
4995
+ const path = target.slice(0, hash2).split("?")[0];
4996
+ const id = target.startsWith("#") ? currentId : path.startsWith("/") && !path.startsWith("//") ? path.replace(/^\//, "").replace(/\/$/, "") || "introduction" : "";
4997
+ if (!pageById.has(id)) return;
4998
+ if (!requested.has(id)) requested.set(id, /* @__PURE__ */ new Set());
4999
+ requested.get(id).add(fragment);
5000
+ if (id !== currentId) crossPageRequests.add(`${id}#${fragment}`);
5001
+ };
5002
+ for (const page of pages) {
5003
+ replaceOutsideCode(page.body, (text) => {
5004
+ for (const match of text.matchAll(/\]\(([^)\s]+)\)/g)) addTarget(match[1], page.id);
5005
+ for (const match of text.matchAll(/\b(?:href|to)=(['"])([^'"\n]+)\1/g)) addTarget(match[2], page.id);
5006
+ return text;
5007
+ });
5008
+ }
5009
+ const comparable = (value) => value.replace(/&(?:lt|gt|amp|quot);/g, " ").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
5010
+ for (const page of pages) {
5011
+ const fragments = requested.get(page.id);
5012
+ if (!fragments?.size) continue;
5013
+ const lines = page.body.split("\n");
5014
+ let fence;
5015
+ const tableCells = [];
5016
+ const headings = lines.map((line, index) => {
5017
+ const marker = line.match(/^ {0,3}(`{3,}|~{3,})/);
5018
+ if (marker) {
5019
+ if (!fence) fence = marker[1];
5020
+ else if (marker[1][0] === fence[0] && marker[1].length >= fence.length && /^\s*$/.test(line.slice(marker[0].length))) fence = void 0;
5021
+ return null;
5022
+ }
5023
+ if (fence) return null;
5024
+ const tableCell = line.match(/^ {0,3}\|\s*([^|]+?)\s*\|/);
5025
+ if (tableCell && !/^[:\s-]+$/.test(tableCell[1])) tableCells.push({ index, text: tableCell[1] });
5026
+ const match = line.match(/^ {0,3}#{1,6}\s+(.+?)\s*#*\s*$/);
5027
+ return match ? { index, text: match[1] } : null;
5028
+ }).filter((entry) => entry !== null);
5029
+ const existing = /* @__PURE__ */ new Set();
5030
+ replaceOutsideCode(page.body, (text) => {
5031
+ for (const match of text.matchAll(/\bid=(?:"([^"]+)"|'([^']+)')/g)) existing.add(match[1] ?? match[2]);
5032
+ return text;
5033
+ });
5034
+ const candidateIndexes = /* @__PURE__ */ new Map();
5035
+ for (const heading of headings) {
5036
+ const key = comparable(heading.text);
5037
+ candidateIndexes.set(key, [...candidateIndexes.get(key) ?? [], { index: heading.index, kind: "heading" }]);
5038
+ }
5039
+ for (const cell of tableCells) {
5040
+ const key = comparable(cell.text);
5041
+ candidateIndexes.set(key, [...candidateIndexes.get(key) ?? [], { index: cell.index, kind: "table" }]);
5042
+ }
5043
+ for (const heading of headings) existing.add(heading.text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""));
5044
+ const headingAliases = /* @__PURE__ */ new Map();
5045
+ const tableAliases = /* @__PURE__ */ new Map();
5046
+ for (const fragment of fragments) {
5047
+ if (existing.has(fragment)) continue;
5048
+ const matches = candidateIndexes.get(comparable(fragment)) ?? [];
5049
+ let match = matches.length === 1 ? matches[0] : void 0;
5050
+ if (!match && matches.length > 1 && !crossPageRequests.has(`${page.id}#${fragment}`)) {
5051
+ const escaped = fragment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5052
+ const linkPattern = new RegExp(`\\]\\(#${escaped}\\)|\\b(?:href|to)=["']#${escaped}["']`);
5053
+ const linkIndexes = lines.flatMap((line, index2) => linkPattern.test(line) ? [index2] : []);
5054
+ const selections = linkIndexes.map((index2) => matches.filter((candidate) => candidate.index < index2).at(-1));
5055
+ if (selections.length && selections.every((candidate) => candidate?.index === selections[0]?.index)) match = selections[0];
5056
+ }
5057
+ if (!match) continue;
5058
+ const { index, kind } = match;
5059
+ const aliases = kind === "heading" ? headingAliases : tableAliases;
5060
+ aliases.set(index, [...aliases.get(index) ?? [], fragment]);
5061
+ existing.add(fragment);
5062
+ }
5063
+ if (headingAliases.size || tableAliases.size) page.body = lines.flatMap((line, index) => [
5064
+ ...(headingAliases.get(index) ?? []).map((fragment) => `<a id="${fragment}"></a>`),
5065
+ tableAliases.has(index) ? line.replace(/^( {0,3}\|\s*)/, (prefix) => `${prefix}${tableAliases.get(index).map((fragment) => `<a id="${fragment}"></a>`).join("")}`) : line
5066
+ ]).join("\n");
5067
+ }
5068
+ }
4324
5069
  function resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot) {
5070
+ sourcePath = sourcePath.replace(/\\(?=_)/g, "");
4325
5071
  const repositoryRelative = sourcePath.startsWith("@site/") ? relative5(repositoryRoot, resolveWithin(siteRoot, sourcePath.slice("@site/".length))).replace(/\\/g, "/") : sourcePath.startsWith("/") ? relative5(repositoryRoot, resolveWithin(siteRoot, sourcePath.replace(/^\/+/, ""))).replace(/\\/g, "/") : relative5(repositoryRoot, resolvePath(dirname4(currentFile), sourcePath)).replace(/\\/g, "/");
4326
5072
  const candidate = resolveWithin(repositoryRoot, repositoryRelative);
4327
5073
  resolveWithin(repositoryRoot, relative5(repositoryRoot, candidate));
@@ -4332,12 +5078,12 @@ function globalSnippetAliases(files, repositoryRoot, siteRoot) {
4332
5078
  for (const file of files) {
4333
5079
  const segments = file.relativePath.split("/");
4334
5080
  if (!segments.some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase()))) continue;
4335
- const basenameWithoutExtension = basename2(file.relativePath).replace(/\.mdx?$/i, "");
5081
+ const basenameWithoutExtension = basename3(file.relativePath).replace(/\.mdx?$/i, "");
4336
5082
  const conventionalAlias = basenameWithoutExtension.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
4337
5083
  if (conventionalAlias) aliases.set(conventionalAlias, file.absolutePath);
4338
5084
  }
4339
5085
  for (const file of files) {
4340
- if (![".md", ".mdx"].includes(extname4(file.relativePath).toLowerCase())) continue;
5086
+ if (![".md", ".mdx"].includes(extname5(file.relativePath).toLowerCase())) continue;
4341
5087
  const raw = readFileSync5(file.absolutePath, "utf8");
4342
5088
  for (const match of raw.matchAll(SNIPPET_IMPORT_PATTERN)) {
4343
5089
  try {
@@ -4365,7 +5111,7 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
4365
5111
  } catch {
4366
5112
  return null;
4367
5113
  }
4368
- if (!ASSET_EXTENSIONS.has(extname4(decodedPath).toLowerCase())) return null;
5114
+ if (!ASSET_EXTENSIONS.has(extname5(decodedPath).toLowerCase())) return null;
4369
5115
  try {
4370
5116
  const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithin(dirname4(currentFile), decodedPath);
4371
5117
  const siteRelative = relative5(siteRoot, candidate).replace(/\\/g, "/");
@@ -4381,15 +5127,7 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
4381
5127
  }
4382
5128
  }
4383
5129
  function rewriteFernIdRenameLinks(body, renames) {
4384
- let codeFence = null;
4385
- return body.split("\n").map((line) => {
4386
- const fence = line.match(/^\s*(`{3,}|~{3,})/);
4387
- if (fence) {
4388
- if (!codeFence) codeFence = fence[1][0];
4389
- else if (fence[1][0] === codeFence) codeFence = null;
4390
- return line;
4391
- }
4392
- if (codeFence) return line;
5130
+ return replaceOutsideCode(body, (text) => {
4393
5131
  const rewriteTarget = (target) => {
4394
5132
  const suffixIndex = target.search(/[?#]/);
4395
5133
  const path = (suffixIndex >= 0 ? target.slice(0, suffixIndex) : target).replace(/^\/+/, "");
@@ -4397,8 +5135,8 @@ function rewriteFernIdRenameLinks(body, renames) {
4397
5135
  const renamed = renames.get(path);
4398
5136
  return renamed ? `/${renamed}${suffix}` : target;
4399
5137
  };
4400
- return line.replace(/(\]\()\/([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(`/${target}`)}`).replace(/(\bhref=")\/([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(`/${target}`)}${suffix}`);
4401
- }).join("\n");
5138
+ return text.replace(/(\]\()\/([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(`/${target}`)}`).replace(/(\bhref=")\/([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(`/${target}`)}${suffix}`);
5139
+ });
4402
5140
  }
4403
5141
  function rewriteRepositoryAssetLinks(body, currentFile, siteRoot, onReferenced) {
4404
5142
  return body.replace(/(!?\[[^\]]*\]\()(<[^>]+>|[^)\s]+)([^)]*\))/g, (original, opening, destination, closing) => {
@@ -4428,8 +5166,10 @@ function locallyDeclaredNames(raw) {
4428
5166
  }
4429
5167
  function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0, siteRoot = repositoryRoot, globalAliases = /* @__PURE__ */ new Map()) {
4430
5168
  if (depth >= 8) return raw;
5169
+ raw = normalizeIndentedFences(raw);
4431
5170
  const snippets = /* @__PURE__ */ new Map();
4432
- const withoutImports = raw.replace(
5171
+ const preservedDeclarations = /* @__PURE__ */ new Map();
5172
+ let withoutImports = replaceOutsideCode(raw, (source) => source.replace(
4433
5173
  SNIPPET_IMPORT_PATTERN,
4434
5174
  (_statement, namedComponent, defaultComponent, sourcePath) => {
4435
5175
  const componentName = namedComponent ?? defaultComponent;
@@ -4445,7 +5185,9 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
4445
5185
  siteRoot,
4446
5186
  globalAliases
4447
5187
  );
4448
- snippets.set(componentName, nested);
5188
+ const declaration = statefulSnippetDeclaration(nested, componentName);
5189
+ if (declaration) preservedDeclarations.set(componentName, declaration);
5190
+ else snippets.set(componentName, snippetComponentBody(nested, componentName));
4449
5191
  return "";
4450
5192
  } catch {
4451
5193
  warnings.push({
@@ -4457,7 +5199,40 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
4457
5199
  return "";
4458
5200
  }
4459
5201
  }
4460
- );
5202
+ ));
5203
+ withoutImports = replaceOutsideCode(withoutImports, (source) => source.replace(
5204
+ SNIPPET_VALUE_IMPORT_PATTERN,
5205
+ (statement, names, sourcePath) => {
5206
+ const bindings = names.split(",").map((name) => name.trim()).filter(Boolean).map((name) => {
5207
+ const parsed = name.match(/^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
5208
+ return parsed ? { exported: parsed[1], local: parsed[2] ?? parsed[1] } : null;
5209
+ });
5210
+ if (bindings.length === 0 || bindings.some((binding) => !binding)) return statement;
5211
+ try {
5212
+ const candidate = resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot);
5213
+ if (!existsSync5(candidate) || !lstatSync5(candidate).isFile()) return statement;
5214
+ const snippetSource = readFileSync5(candidate, "utf8");
5215
+ const values = staticNamedSnippetValues(snippetSource);
5216
+ const declarations = [];
5217
+ const components = [];
5218
+ for (const binding of bindings) {
5219
+ const value = values.get(binding.exported);
5220
+ if (value !== void 0) {
5221
+ declarations.push(`export const ${binding.local} = ${value};`);
5222
+ continue;
5223
+ }
5224
+ if (!/^[A-Z]/.test(binding.exported)) return statement;
5225
+ const body = snippetComponentBody(snippetSource, binding.exported);
5226
+ if (body === snippetSource) return statement;
5227
+ components.push([binding.local, body]);
5228
+ }
5229
+ for (const [name, body] of components) snippets.set(name, body);
5230
+ return declarations.join("\n");
5231
+ } catch {
5232
+ return statement;
5233
+ }
5234
+ }
5235
+ ));
4461
5236
  const localNames = locallyDeclaredNames(withoutImports);
4462
5237
  for (const [componentName, candidate] of globalAliases) {
4463
5238
  if (snippets.has(componentName) || localNames.has(componentName) || !new RegExp(`<${componentName}(?:\\s|/?>)`).test(withoutImports)) continue;
@@ -4470,14 +5245,16 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
4470
5245
  siteRoot,
4471
5246
  globalAliases
4472
5247
  );
4473
- snippets.set(componentName, nested);
5248
+ const declaration = statefulSnippetDeclaration(nested, componentName);
5249
+ if (declaration) preservedDeclarations.set(componentName, declaration);
5250
+ else snippets.set(componentName, snippetComponentBody(nested, componentName));
4474
5251
  }
4475
5252
  let result = withoutImports;
4476
5253
  for (const [componentName, snippet] of snippets) {
4477
5254
  result = result.replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)\\s*/>`, "g"), (_tag, attributes) => {
4478
5255
  return interpolateSnippet(snippet, attributes);
4479
- }).replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)>(?:[\\s\\S]*?)<\\/${componentName}>`, "g"), (_tag, attributes) => {
4480
- return interpolateSnippet(snippet, attributes);
5256
+ }).replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)>([\\s\\S]*?)<\\/${componentName}>`, "g"), (_tag, attributes, children) => {
5257
+ return interpolateSnippet(snippet, attributes, children);
4481
5258
  });
4482
5259
  }
4483
5260
  result = result.replace(SNIPPET_TAG_PATTERN, (_tag, doubleQuoted, singleQuoted) => {
@@ -4503,13 +5280,25 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
4503
5280
  return `{/* Missing snippet: ${filePath} */}`;
4504
5281
  }
4505
5282
  });
4506
- return result;
5283
+ if (preservedDeclarations.size > 0) {
5284
+ const frontmatter = result.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
5285
+ const prefix = frontmatter?.[0] ?? "";
5286
+ result = `${prefix}${[...preservedDeclarations.values()].join("\n\n")}
5287
+
5288
+ ${result.slice(prefix.length)}`;
5289
+ }
5290
+ return depth === 0 ? hoistMdxImports(result) : result;
4507
5291
  }
4508
5292
  function injectOpenApiSpecs(config, specs) {
4509
5293
  let tabs = config.tabs.map((tab) => ({ ...tab }));
4510
5294
  for (const spec of specs) {
4511
5295
  const apiTab = spec.tabLabel ? tabs.find((tab) => tab.tab === spec.tabLabel) : tabs.find((tab) => tab.tab.toLowerCase().includes("api"));
4512
- if (apiTab) apiTab.api = { source: `/${spec.filename}`, navigation: false };
5296
+ if (apiTab) apiTab.api = {
5297
+ source: `/${spec.filename}`,
5298
+ // An API-only tab needs generated endpoint navigation; an authored
5299
+ // page tab keeps its own groups alongside the bound spec.
5300
+ ...apiTab.groups?.length || apiTab.pages?.length ? { navigation: false } : {}
5301
+ };
4513
5302
  else tabs = [...tabs, { tab: spec.tabLabel ?? "API Reference", api: { source: `/${spec.filename}` } }];
4514
5303
  }
4515
5304
  return { ...config, tabs };
@@ -4547,30 +5336,6 @@ function migrateRepository(options) {
4547
5336
  }
4548
5337
  return platform === "mintlify" ? "" : detectRepositoryDocsDir(repositoryDir);
4549
5338
  })();
4550
- if (platform === "docusaurus" && docusaurusProjectRoot) {
4551
- for (const [directory, label] of [
4552
- ["versioned_docs", "versioned docs"],
4553
- ["versioned_sidebars", "versioned sidebars"]
4554
- ]) {
4555
- const path = resolveWithin(docusaurusProjectRoot, directory);
4556
- if (!existsSync5(path) || !lstatSync5(path).isDirectory()) continue;
4557
- const versions = readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
4558
- if (versions.length) warnings.push({
4559
- code: "unsupported-config",
4560
- message: `Docusaurus ${label} were skipped; only current docs were imported. Skipped: ${versions.join(", ")}.`,
4561
- source: directory
4562
- });
4563
- }
4564
- const i18nPath = resolveWithin(docusaurusProjectRoot, "i18n");
4565
- if (existsSync5(i18nPath) && lstatSync5(i18nPath).isDirectory()) {
4566
- const locales = readdirSync(i18nPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
4567
- if (locales.length) warnings.push({
4568
- code: "unsupported-config",
4569
- message: `Docusaurus localized content was skipped; only the default locale was imported. Skipped locales: ${locales.join(", ")}.`,
4570
- source: "i18n"
4571
- });
4572
- }
4573
- }
4574
5339
  const componentRoot = mintlifyProjectRoot ?? docusaurusProjectRoot ?? fernProjectRoot ?? repositoryDir;
4575
5340
  const componentMigrator = platform === "mintlify" || platform === "docusaurus" || platform === "fern" ? createComponentMigrator(componentRoot, repositoryDir, warnings, componentSourceIdentity(options.sourceUrl, repositoryDir, componentRoot)) : void 0;
4576
5341
  let docsConfig = { tabs: [] };
@@ -4580,7 +5345,11 @@ function migrateRepository(options) {
4580
5345
  let docusaurusSidebars = null;
4581
5346
  let mintlifyConfig = null;
4582
5347
  let fernRawConfig = null;
5348
+ let fernBasePath = "";
5349
+ let fernVersionPath = "";
4583
5350
  let fernApiSections = [];
5351
+ const fernReferencedPaths = /* @__PURE__ */ new Set();
5352
+ const fernSourceLinkAliases = /* @__PURE__ */ new Map();
4584
5353
  if (platform === "mintlify") {
4585
5354
  try {
4586
5355
  const config = readMintlifyConfig(mintlifyProjectRoot ?? repositoryDir);
@@ -4609,11 +5378,23 @@ function migrateRepository(options) {
4609
5378
  const fernConfig = readFernConfig(fernProjectRoot);
4610
5379
  if (fernConfig) {
4611
5380
  fernRawConfig = fernConfig.config;
4612
- const projected = projectFernNavigation({ config: fernConfig.config, fernRoot: fernProjectRoot });
5381
+ fernBasePath = fernSourceBasePath(fernConfig.config);
5382
+ fernVersionPath = fernDefaultVersionPath(fernConfig.config);
5383
+ const projected = projectFernNavigation({ config: fernConfig.config, fernRoot: fernProjectRoot, repositoryRoot: repositoryDir });
4613
5384
  docsConfig = projected.docsConfig;
4614
5385
  warnings.push(...projected.warnings);
4615
5386
  fernApiSections = projected.apiSections;
4616
5387
  for (const [index, descriptor] of projected.descriptors.entries()) {
5388
+ fernReferencedPaths.add(descriptor.sourcePath);
5389
+ const sourceTail = descriptor.sourcePath.match(/(?:^|\/)pages\/(.+)\.mdx?$/i)?.[1]?.replace(/\/index$/i, "");
5390
+ if (sourceTail) {
5391
+ const routeSegments = descriptor.navigationId.split("/");
5392
+ const sourceSegments = sourceTail.split("/");
5393
+ if (routeSegments.length === sourceSegments.length + 1) {
5394
+ const alias = `${routeSegments[0]}/${sourceTail}`;
5395
+ if (alias !== descriptor.navigationId && !fernSourceLinkAliases.has(alias)) fernSourceLinkAliases.set(alias, descriptor.navigationId);
5396
+ }
5397
+ }
4617
5398
  const key = normalizedReferenceKey(descriptor.sourcePath);
4618
5399
  if (!referenceMap.has(key)) referenceMap.set(key, { navigationId: descriptor.navigationId });
4619
5400
  const exactKey = exactReferenceKey(descriptor.sourcePath);
@@ -4630,7 +5411,7 @@ function migrateRepository(options) {
4630
5411
  }
4631
5412
  if (platform === "docusaurus") {
4632
5413
  try {
4633
- docusaurusSidebars = options.docusaurusSkipSidebar || !docusaurusProjectRoot ? null : readDocusaurusSidebars(docusaurusProjectRoot);
5414
+ docusaurusSidebars = options.docusaurusSkipSidebar || !docusaurusProjectRoot ? null : readDocusaurusSidebars(docusaurusProjectRoot, options.docusaurusSidebarPath);
4634
5415
  } catch (error) {
4635
5416
  warnings.push({
4636
5417
  code: "unsupported-config",
@@ -4644,6 +5425,19 @@ function migrateRepository(options) {
4644
5425
  }
4645
5426
  const mintignoreMatcher = platform === "mintlify" && mintlifyProjectRoot ? readMintignoreMatcher(mintlifyProjectRoot) : null;
4646
5427
  const files = mintignoreMatcher ? scanFiles(contentRoot, repositoryDir, warnings).filter((file) => !mintignoreMatcher.ignores(file.relativePath)) : scanFiles(contentRoot, repositoryDir, warnings);
5428
+ if (platform === "fern" && fernProjectRoot) {
5429
+ const scanned = new Set(files.map((file) => file.absolutePath));
5430
+ for (const sourcePath of fernReferencedPaths) {
5431
+ try {
5432
+ const absolutePath = resolveWithinRoot(fernProjectRoot, sourcePath, repositoryDir);
5433
+ if (scanned.has(absolutePath) || !existsSync5(absolutePath) || !lstatSync5(absolutePath).isFile()) continue;
5434
+ files.push({ absolutePath, relativePath: sourcePath });
5435
+ scanned.add(absolutePath);
5436
+ } catch {
5437
+ warnings.push({ code: "missing-page", message: "A Fern navigation path could not be read within this repository.", source: sourcePath });
5438
+ }
5439
+ }
5440
+ }
4647
5441
  const pages = [];
4648
5442
  const assets = [];
4649
5443
  const referencedAssetPaths = /* @__PURE__ */ new Map();
@@ -4673,16 +5467,16 @@ function migrateRepository(options) {
4673
5467
  continue;
4674
5468
  }
4675
5469
  const sourceSegments = file.relativePath.split("/");
4676
- if (sourceSegments.some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase())) || platform === "docusaurus" && basename2(file.relativePath).startsWith("_")) {
5470
+ if (sourceSegments.some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase())) || platform === "docusaurus" && basename3(file.relativePath).startsWith("_")) {
4677
5471
  skipped++;
4678
5472
  continue;
4679
5473
  }
4680
- const rootFilename = !file.relativePath.includes("/") ? basename2(file.relativePath).toLowerCase() : "";
5474
+ const rootFilename = !file.relativePath.includes("/") ? basename3(file.relativePath).toLowerCase() : "";
4681
5475
  if (REPOSITORY_ONLY_DOCUMENTS.has(rootFilename) && !exactReferenceMap.has(exactReferenceKey(file.relativePath))) {
4682
5476
  skipped++;
4683
5477
  continue;
4684
5478
  }
4685
- if (![".md", ".mdx"].includes(extname4(file.relativePath).toLowerCase())) {
5479
+ if (![".md", ".mdx"].includes(extname5(file.relativePath).toLowerCase())) {
4686
5480
  skipped++;
4687
5481
  warnings.push({ code: "skipped-file", message: "Only Markdown and MDX are imported without an explicit converter.", source: file.relativePath });
4688
5482
  continue;
@@ -4802,6 +5596,8 @@ function migrateRepository(options) {
4802
5596
  page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, fernProjectRoot, (assetPath) => {
4803
5597
  addAssetReference(assetPath, file.relativePath);
4804
5598
  });
5599
+ page.body = stripFernBasePathFromLinks(page.body, fernBasePath);
5600
+ page.body = stripFernBasePathFromLinks(page.body, fernVersionPath);
4805
5601
  }
4806
5602
  if (platform === "mintlify" || platform === "docusaurus" || platform === "fern") {
4807
5603
  page.body = replaceLinkWithAnchor(page.body);
@@ -4882,7 +5678,7 @@ function migrateRepository(options) {
4882
5678
  const assetCandidates = [];
4883
5679
  for (const file of [...files, ...repositoryAssets]) {
4884
5680
  const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
4885
- if (!ASSET_EXTENSIONS.has(extname4(file.relativePath).toLowerCase())) continue;
5681
+ if (!ASSET_EXTENSIONS.has(extname5(file.relativePath).toLowerCase())) continue;
4886
5682
  if (platform !== "mintlify" && !ASSET_DIRECTORIES.has(firstSegment)) continue;
4887
5683
  const isDocusaurusStatic = platform === "docusaurus" && firstSegment === "static";
4888
5684
  const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : isDocusaurusStatic ? file.relativePath.slice("static/".length) : file.relativePath);
@@ -4938,10 +5734,19 @@ function migrateRepository(options) {
4938
5734
  const descriptorByNavigationId = new Map(
4939
5735
  docusaurusDescriptors.map((descriptor) => [descriptor.navigationId, descriptor])
4940
5736
  );
5737
+ const sourceOrigin = docusaurusProjectRoot ? readDocusaurusSiteOrigin(docusaurusProjectRoot) : void 0;
5738
+ const externalizedLinks = /* @__PURE__ */ new Set();
4941
5739
  for (const page of pages) {
4942
5740
  const descriptor = descriptorByNavigationId.get(page.navigationId);
4943
- if (descriptor) page.body = rewriteDocusaurusLinks(page.body, descriptor, docusaurusDescriptors);
5741
+ if (descriptor) page.body = rewriteDocusaurusLinks(page.body, descriptor, docusaurusDescriptors, {
5742
+ sourceOrigin,
5743
+ onExternalLink: (target) => externalizedLinks.add(target)
5744
+ });
4944
5745
  }
5746
+ if (externalizedLinks.size > 0) warnings.push({
5747
+ code: "unsupported-config",
5748
+ message: `${externalizedLinks.size} link(s) outside the imported Docusaurus docs still point to ${sourceOrigin}; migrate those sections separately if this site must be fully independent.`
5749
+ });
4945
5750
  if (docusaurusProjectRoot && !options.docusaurusSkipRedirects) {
4946
5751
  const redirects = readDocusaurusRedirects(docusaurusProjectRoot, warnings);
4947
5752
  if (redirects.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...redirects] };
@@ -4972,6 +5777,11 @@ function migrateRepository(options) {
4972
5777
  } : {}
4973
5778
  };
4974
5779
  }
5780
+ if (platform === "fern" && fernSourceLinkAliases.size > 0) {
5781
+ const imported = new Set(pages.map((page) => page.id));
5782
+ const aliases = new Map([...fernSourceLinkAliases].map(([source, target]) => [source, fernIdRenames.get(target) ?? target]).filter(([, target]) => imported.has(target)));
5783
+ for (const page of pages) page.body = rewriteFernIdRenameLinks(page.body, aliases);
5784
+ }
4975
5785
  if (docsConfig.tabs.length === 0) docsConfig = buildNavigationFromPages(pages);
4976
5786
  if (platform === "fern" && fernProjectRoot) {
4977
5787
  const sections = fernApiSections.length > 0 ? fernApiSections : [{ nameExplicit: false }];
@@ -4981,7 +5791,7 @@ function migrateRepository(options) {
4981
5791
  const resolution = findFernConfiguredOpenApi(fernProjectRoot, repositoryDir, section.name, section.nameExplicit, warnings);
4982
5792
  const spec = resolution.spec ?? (!section.nameExplicit ? findOpenApi(files) : null);
4983
5793
  if (spec) {
4984
- let filename = basename2(spec.relativePath);
5794
+ let filename = basename3(spec.relativePath);
4985
5795
  const existingSource = specFilenameSources.get(filename);
4986
5796
  if (existingSource && existingSource !== spec.absolutePath) {
4987
5797
  const prefix = section.name ? `${section.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "")}-` : `${specFilenameSources.size + 1}-`;
@@ -5032,7 +5842,7 @@ function migrateRepository(options) {
5032
5842
  } else {
5033
5843
  const fallback = findOpenApi(files);
5034
5844
  if (fallback) {
5035
- const filename = basename2(fallback.relativePath);
5845
+ const filename = basename3(fallback.relativePath);
5036
5846
  if (!assets.some((asset) => asset.path === filename)) {
5037
5847
  assets.push({ path: filename, content: readFileSync5(fallback.absolutePath) });
5038
5848
  }
@@ -5045,9 +5855,25 @@ function migrateRepository(options) {
5045
5855
  const aliases = routeAliases.filter((redirect) => !sources.has(redirect.source));
5046
5856
  if (aliases.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...aliases] };
5047
5857
  docsConfig = addMintlifyHomepageRedirects(addMintlifyDirectoryRedirects(docsConfig, pages), pages);
5858
+ const reachablePaths = /* @__PURE__ */ new Set([
5859
+ ...pages.map((page) => page.id),
5860
+ ...(docsConfig.redirects ?? []).map((redirect) => redirect.source.replace(/^\//, ""))
5861
+ ]);
5862
+ for (const page of pages) page.body = rewriteMintlifyMountedLinks(page.body, reachablePaths);
5048
5863
  }
5049
5864
  if (platform === "docusaurus" && docusaurusProjectRoot && !options.docusaurusSkipPlugins) {
5050
- for (const plugin of additionalDocusaurusPluginRoots(repositoryDir, docusaurusProjectRoot, warnings)) {
5865
+ const additionalRoots = [
5866
+ ...additionalDocusaurusPluginRoots(repositoryDir, docusaurusProjectRoot, warnings).map((plugin) => ({
5867
+ ...plugin,
5868
+ label: plugin.routePrefix.split(/[-_/]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ")
5869
+ })),
5870
+ ...docusaurusArchiveRoots(docusaurusProjectRoot, repositoryDir, warnings)
5871
+ ];
5872
+ for (const plugin of additionalRoots) {
5873
+ if (discovered >= MAX_SOURCE_FILES) {
5874
+ warnings.push({ code: "limit-reached", message: `Docusaurus docs import stopped at the ${MAX_SOURCE_FILES}-file discovery budget.` });
5875
+ break;
5876
+ }
5051
5877
  if (plugin.docsDir === configuredDocsDir) continue;
5052
5878
  const pluginBundle = migrateRepository({
5053
5879
  ...options,
@@ -5055,12 +5881,17 @@ function migrateRepository(options) {
5055
5881
  platform: "docusaurus",
5056
5882
  docusaurusRoutePrefix: plugin.routePrefix,
5057
5883
  docusaurusSkipPlugins: true,
5058
- docusaurusSkipSidebar: true,
5884
+ docusaurusSkipSidebar: !plugin.sidebarPath,
5885
+ docusaurusSidebarPath: plugin.sidebarPath,
5059
5886
  docusaurusSkipAssets: true,
5060
5887
  docusaurusSkipRedirects: true
5061
5888
  });
5062
5889
  for (const page of pluginBundle.pages) {
5063
- if (seenPageIds.has(page.id)) continue;
5890
+ if (seenPageIds.has(page.id)) {
5891
+ skipped++;
5892
+ warnings.push({ code: "collision", message: `Docusaurus docs roots both map to ${page.id}; the first page was kept.`, source: page.source });
5893
+ continue;
5894
+ }
5064
5895
  seenPageIds.add(page.id);
5065
5896
  pages.push(page);
5066
5897
  }
@@ -5070,7 +5901,7 @@ function migrateRepository(options) {
5070
5901
  assetPaths.add(asset.path);
5071
5902
  assets.push(asset);
5072
5903
  }
5073
- const pluginLabel = plugin.routePrefix.split(/[-_/]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
5904
+ const pluginLabel = plugin.label;
5074
5905
  docsConfig.tabs.push(...pluginBundle.docsConfig.tabs.map((tab, index) => ({
5075
5906
  ...tab,
5076
5907
  tab: index === 0 ? pluginLabel : `${pluginLabel}: ${tab.tab}`
@@ -5081,6 +5912,41 @@ function migrateRepository(options) {
5081
5912
  }
5082
5913
  }
5083
5914
  docsConfig = pruneMissingNavigationPages(docsConfig, new Set(pages.map((page) => page.navigationId)));
5915
+ if (platform === "docusaurus") preserveDocusaurusLinkedAnchors(pages);
5916
+ if (platform === "fern" && fernRawConfig) {
5917
+ const sourceSiteUrl = fernSourceSiteUrl(fernRawConfig);
5918
+ if (sourceSiteUrl) {
5919
+ const externalized = /* @__PURE__ */ new Set();
5920
+ const pageIds = new Set(pages.map((page) => page.id));
5921
+ for (const page of pages) page.body = externalizeMissingFernLinks(page.body, pageIds, sourceSiteUrl, externalized);
5922
+ if (externalized.size > 0) warnings.push({
5923
+ code: "unsupported-config",
5924
+ message: `${externalized.size} unresolved Fern link(s) still point to ${sourceSiteUrl}; import those sections separately for a fully independent site.`
5925
+ });
5926
+ }
5927
+ }
5928
+ if (docsConfig.redirects) {
5929
+ docsConfig = {
5930
+ ...docsConfig,
5931
+ redirects: docsConfig.redirects.filter((redirect) => {
5932
+ const literalRoute = (route) => route.startsWith("/") && !/[?:*#()[\]{}]/.test(route) ? route.replace(/\/+$/, "") || "/" : null;
5933
+ const sourceRoute = literalRoute(redirect.source);
5934
+ if (sourceRoute && sourceRoute === literalRoute(redirect.destination)) {
5935
+ warnings.push({
5936
+ code: "unsupported-config",
5937
+ message: `Self-redirect from ${redirect.source} to ${redirect.destination} was omitted.`
5938
+ });
5939
+ return false;
5940
+ }
5941
+ if (!/[+?()[\]{}]/.test(redirect.source)) return true;
5942
+ warnings.push({
5943
+ code: "unsupported-config",
5944
+ message: `Redirect from ${redirect.source} contains route-pattern syntax Next.js cannot express literally and was omitted.`
5945
+ });
5946
+ return false;
5947
+ })
5948
+ };
5949
+ }
5084
5950
  if (pages.length === 0) {
5085
5951
  const reasons = warnings.map((warning) => warning.message);
5086
5952
  throw new Error(reasons.length ? `No importable Markdown or MDX pages were found in the repository. Warnings encountered during migration: