@thallylabs/migrate 0.2.41 → 0.2.43

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
@@ -1,6 +1,6 @@
1
1
  // src/repository.ts
2
2
  import { compileSync as compileSync2 } from "@mdx-js/mdx";
3
- import { createHash as createHash4 } from "crypto";
3
+ import { createHash as createHash5 } from "crypto";
4
4
  import { spawn } from "child_process";
5
5
  import {
6
6
  closeSync,
@@ -801,9 +801,9 @@ import { basename, dirname, extname as extname2, posix as posix2, relative as re
801
801
  import postcss from "postcss";
802
802
  import selectorParser from "postcss-selector-parser";
803
803
  import remarkMdx2 from "remark-mdx";
804
- import remarkParse2 from "remark-parse";
804
+ import remarkParse3 from "remark-parse";
805
805
  import ts2 from "typescript";
806
- import { unified as unified2 } from "unified";
806
+ import { unified as unified3 } from "unified";
807
807
 
808
808
  // src/frontmatter.ts
809
809
  import { parse as parseYaml } from "yaml";
@@ -856,8 +856,8 @@ import { compileSync } from "@mdx-js/mdx";
856
856
  import { nameToEmoji } from "gemoji";
857
857
  import remarkGfm from "remark-gfm";
858
858
  import remarkMdx from "remark-mdx";
859
- import remarkParse from "remark-parse";
860
- import { unified } from "unified";
859
+ import remarkParse2 from "remark-parse";
860
+ import { unified as unified2 } from "unified";
861
861
 
862
862
  // src/mintlify-extras.ts
863
863
  import { extname } from "path";
@@ -885,8 +885,8 @@ function slugifySegment(value, preserveCase = false) {
885
885
  } catch {
886
886
  }
887
887
  const normalized = preserveCase ? decoded : decoded.toLowerCase();
888
- const slug2 = normalized.replace(/\.(?:html?|mdx?)$/i, "").replace(SAFE_SEGMENT, "-").replace(/(^-|-$)/g, "");
889
- return slug2 === "." || slug2 === ".." ? "" : slug2;
888
+ const slug3 = normalized.replace(/\.(?:html?|mdx?)$/i, "").replace(SAFE_SEGMENT, "-").replace(/(^-|-$)/g, "");
889
+ return slug3 === "." || slug3 === ".." ? "" : slug3;
890
890
  }
891
891
  function pageIdFromReference(value, preserveCase = false) {
892
892
  const withoutQuery = value.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^\/+/, "");
@@ -1125,13 +1125,103 @@ function playgroundDisplay(value, where, warn) {
1125
1125
  return void 0;
1126
1126
  }
1127
1127
 
1128
+ // src/fence-meta.ts
1129
+ import remarkParse from "remark-parse";
1130
+ import { unified } from "unified";
1131
+ function delimitedEnd(meta, start) {
1132
+ const opening = meta[start];
1133
+ if (opening === '"' || opening === "'") {
1134
+ let close = start + 1;
1135
+ while (close < meta.length && meta[close] !== opening) close += meta[close] === "\\" && meta[close + 1] === opening ? 2 : 1;
1136
+ return close < meta.length ? close + 1 : close;
1137
+ }
1138
+ if (opening !== "{") return void 0;
1139
+ let depth = 0;
1140
+ let quote = "";
1141
+ for (let index = start; index < meta.length; index++) {
1142
+ const char = meta[index];
1143
+ if (quote) {
1144
+ if (char === quote) quote = "";
1145
+ } else if (char === '"' || char === "'") quote = char;
1146
+ else if (char === "{") depth++;
1147
+ else if (char === "}" && --depth === 0) return index + 1;
1148
+ }
1149
+ return meta.length;
1150
+ }
1151
+ function tokens(meta) {
1152
+ const result = [];
1153
+ let index = 0;
1154
+ while (index < meta.length) {
1155
+ if (/\s/.test(meta[index])) {
1156
+ index++;
1157
+ continue;
1158
+ }
1159
+ const start = index;
1160
+ const delimited = delimitedEnd(meta, index);
1161
+ if (delimited !== void 0) {
1162
+ result.push({ start, end: delimited, value: meta.slice(start, delimited), quoted: meta[start] !== "{", group: meta[start] === "{" });
1163
+ index = delimited;
1164
+ continue;
1165
+ }
1166
+ const key = /^([A-Za-z][\w-]*)=/.exec(meta.slice(index));
1167
+ if (key) {
1168
+ index += key[0].length;
1169
+ const groupEnd = delimitedEnd(meta, index);
1170
+ if (groupEnd !== void 0) index = groupEnd;
1171
+ else while (index < meta.length && !/\s/.test(meta[index])) index++;
1172
+ result.push({ start, end: index, key: key[1], value: meta.slice(start, index) });
1173
+ continue;
1174
+ }
1175
+ while (index < meta.length && !/\s/.test(meta[index])) {
1176
+ if (index > start && meta[index] === "{" && /^\{[\d,\s-]*\}/.test(meta.slice(index))) break;
1177
+ index++;
1178
+ }
1179
+ result.push({ start, end: index, value: meta.slice(start, index) });
1180
+ }
1181
+ return result;
1182
+ }
1183
+ function normalizeMintlifyCodeFences(body, warn) {
1184
+ const tree = unified().use(remarkParse).parse(body);
1185
+ const edits = [];
1186
+ const removed = /* @__PURE__ */ new Set();
1187
+ const unknown = /* @__PURE__ */ new Set();
1188
+ const supported = /* @__PURE__ */ new Set(["title", "filename", "framework", "tag", "icon", "highlight", "focus", "wrap", "lines", "expandable", "nocopy"]);
1189
+ function visit(node) {
1190
+ if (node.type === "code" && node.position?.start.offset !== void 0) {
1191
+ const start = node.position.start.offset;
1192
+ const lineEnd = body.indexOf("\n", start);
1193
+ const end = lineEnd < 0 ? body.length : lineEnd;
1194
+ const line = body.slice(start, end);
1195
+ const opening = /^([`~]{3,}[^\s]*)([ \t]+)(.*?)(\r?)$/.exec(line);
1196
+ if (opening) {
1197
+ const meta = opening[3];
1198
+ const kept = tokens(meta).filter((token) => {
1199
+ if (token.key === "theme" || !token.key && !token.quoted && !token.group && token.value === "twoslash") {
1200
+ removed.add(token.key ?? token.value);
1201
+ return false;
1202
+ }
1203
+ if (token.key && !supported.has(token.key)) unknown.add(token.key);
1204
+ return true;
1205
+ });
1206
+ const normalized = kept.map((token) => meta.slice(token.start, token.end)).join(" ");
1207
+ if (normalized !== meta && kept.length !== tokens(meta).length) edits.push({ start, end, value: `${opening[1]}${normalized ? ` ${normalized}` : ""}${opening[4]}` });
1208
+ }
1209
+ }
1210
+ for (const child of node.children ?? []) visit(child);
1211
+ }
1212
+ visit(tree);
1213
+ if (removed.size) warn?.(`Removed unsupported Mintlify code-fence options: ${[...removed].join(", ")}. Code examples and supported display options were preserved.`);
1214
+ if (unknown.size) warn?.(`Unsupported Mintlify code-fence options are ignored: ${[...unknown].join(", ")}. Review these renderer-specific options.`);
1215
+ return edits.sort((a, b) => b.start - a.start).reduce((text3, edit) => text3.slice(0, edit.start) + edit.value + text3.slice(edit.end), body);
1216
+ }
1217
+
1128
1218
  // src/mdx.ts
1129
1219
  function titleFromId(id) {
1130
1220
  const value = id.split("/").at(-1) ?? id;
1131
1221
  if (value === "introduction") return "Introduction";
1132
1222
  return value.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1133
1223
  }
1134
- var descriptionParser = unified().use(remarkParse).use(remarkGfm).use(remarkMdx);
1224
+ var descriptionParser = unified2().use(remarkParse2).use(remarkGfm).use(remarkMdx);
1135
1225
  function plainText(node) {
1136
1226
  if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
1137
1227
  if (node.type === "image") return node.alt ?? "";
@@ -2648,13 +2738,41 @@ function externalPageUrl(value) {
2648
2738
  }
2649
2739
  function parseMarkdownPage(input) {
2650
2740
  const parsed = parseFrontmatter(input.raw);
2741
+ if (input.platform === "mintlify") {
2742
+ const supported = /* @__PURE__ */ new Set([
2743
+ "title",
2744
+ "description",
2745
+ "sidebarTitle",
2746
+ "navTitle",
2747
+ "icon",
2748
+ "iconType",
2749
+ "tag",
2750
+ "badge",
2751
+ "keywords",
2752
+ "mode",
2753
+ "hidden",
2754
+ "noindex",
2755
+ "url",
2756
+ "openapi",
2757
+ "api",
2758
+ "authMethod",
2759
+ "playground",
2760
+ // Access settings are enforced by the repository classifier, never lost here.
2761
+ "groups",
2762
+ "public",
2763
+ ...PAGE_META_FIELDS.map(([field]) => field)
2764
+ ]);
2765
+ const dropped = Object.keys(parsed.data).filter((key) => !supported.has(key));
2766
+ if (dropped.length) input.warn?.(`Unsupported Mintlify frontmatter fields were omitted: ${dropped.join(", ")}. Review their behavior before publishing.`);
2767
+ }
2651
2768
  const fallbackIdentity = {
2652
2769
  id: input.id,
2653
2770
  navigationId: input.navigationId ?? input.id,
2654
2771
  ...input.locale ? { locale: input.locale } : {}
2655
2772
  };
2656
2773
  const identity = input.resolveIdentity?.(parsed.data, fallbackIdentity) ?? fallbackIdentity;
2657
- const body = removeUndefinedExpressions(injectReactHookImports(normalizeMdx(parsed.content, input.platform)), input.warn).trim();
2774
+ const normalized = normalizeMdx(parsed.content, input.platform);
2775
+ const body = removeUndefinedExpressions(injectReactHookImports(input.platform === "mintlify" ? normalizeMintlifyCodeFences(normalized, input.warn) : normalized), input.warn).trim();
2658
2776
  const keywords = Array.isArray(parsed.data.keywords) ? parsed.data.keywords.filter((value) => typeof value === "string") : [];
2659
2777
  const heading = leadingPageHeading(body);
2660
2778
  const title = typeof parsed.data.title === "string" && parsed.data.title.trim() ? parsed.data.title.trim() : heading?.title ?? titleFromId(identity.navigationId);
@@ -2817,7 +2935,7 @@ function youtubeEmbedVideoId(node, specifier) {
2817
2935
  }
2818
2936
  return void 0;
2819
2937
  }
2820
- var parser = unified2().use(remarkParse2).use(remarkMdx2);
2938
+ var parser = unified3().use(remarkParse3).use(remarkMdx2);
2821
2939
  var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".mjs"]);
2822
2940
  var DATA_EXTENSIONS = /* @__PURE__ */ new Set([".json", ".css", ".svg", ".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif", ".woff", ".woff2"]);
2823
2941
  var RESCUABLE_ASSET_EXTENSIONS = /* @__PURE__ */ new Set([".svg", ".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif", ".ico", ".docx", ".pdf"]);
@@ -4495,8 +4613,8 @@ function localReference(value) {
4495
4613
  }
4496
4614
  function jsonPointer(root, pointer) {
4497
4615
  if (!pointer || pointer === "#") return root;
4498
- const tokens = pointer.replace(/^#\/?/, "").split("/").filter(Boolean);
4499
- return tokens.reduce((current, token) => {
4616
+ const tokens2 = pointer.replace(/^#\/?/, "").split("/").filter(Boolean);
4617
+ return tokens2.reduce((current, token) => {
4500
4618
  if (Array.isArray(current)) {
4501
4619
  const index = Number(token);
4502
4620
  return Number.isSafeInteger(index) && index >= 0 ? current[index] : void 0;
@@ -6416,8 +6534,8 @@ function docusaurusSlugifySegment(value) {
6416
6534
  decoded = decodeURIComponent(value);
6417
6535
  } catch {
6418
6536
  }
6419
- const slug2 = decoded.replace(/\.(?:html?|mdx?)$/i, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-|-$)/g, "");
6420
- return slug2 === "." || slug2 === ".." ? "" : slug2;
6537
+ const slug3 = decoded.replace(/\.(?:html?|mdx?)$/i, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-|-$)/g, "");
6538
+ return slug3 === "." || slug3 === ".." ? "" : slug3;
6421
6539
  }
6422
6540
  function docusaurusRouteId(value) {
6423
6541
  const segments = normalizeDocId(value).split("/").map(docusaurusSlugifySegment).filter(Boolean);
@@ -6435,11 +6553,11 @@ function storageIdForRoute(routeId) {
6435
6553
  function titleCase3(value) {
6436
6554
  return stripNumberPrefix(value).replace(/sidebar$/i, "").split(/[-_]/).filter(Boolean).map((word) => ["api", "cli", "sdk", "ui"].includes(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
6437
6555
  }
6438
- function routeFromSlug(slug2, fallback, sourceDirectory) {
6439
- if (slug2 === "/") return "introduction";
6440
- if (slug2.startsWith("/")) return docusaurusRouteId(slug2);
6556
+ function routeFromSlug(slug3, fallback, sourceDirectory) {
6557
+ if (slug3 === "/") return "introduction";
6558
+ if (slug3.startsWith("/")) return docusaurusRouteId(slug3);
6441
6559
  const fallbackDirectory = sourceDirectory ?? (fallback === "introduction" ? "" : posix3.dirname(fallback));
6442
- const resolved = posix3.normalize(posix3.join(fallbackDirectory, slug2));
6560
+ const resolved = posix3.normalize(posix3.join(fallbackDirectory, slug3));
6443
6561
  if (resolved === ".." || resolved.startsWith("../")) return null;
6444
6562
  return docusaurusRouteId(resolved);
6445
6563
  }
@@ -6560,9 +6678,9 @@ function expandDocusaurusDocCardLists(pages, config, sourceRoutes) {
6560
6678
  const target2 = pageByRoute.get(item);
6561
6679
  return [{ title: target2?.title ?? item.split("/").at(-1) ?? item, route: item }];
6562
6680
  }
6563
- const slug2 = slugifySegment(item.group);
6681
+ const slug3 = slugifySegment(item.group);
6564
6682
  const routes = [...item.pages].flatMap((child) => typeof child === "string" ? [child] : []);
6565
- const route = routes.find((candidate) => candidate === slug2 || candidate.endsWith(`/${slug2}`)) ?? firstRoute(item.pages);
6683
+ const route = routes.find((candidate) => candidate === slug3 || candidate.endsWith(`/${slug3}`)) ?? firstRoute(item.pages);
6566
6684
  return route ? [{ title: item.group, route }] : [];
6567
6685
  });
6568
6686
  const markup = cards.length ? `<CardGroup>
@@ -7395,7 +7513,7 @@ function readDocusaurusPresentation(repositoryRoot) {
7395
7513
  }
7396
7514
  return styles.join(";");
7397
7515
  };
7398
- let tokens = "";
7516
+ let tokens2 = "";
7399
7517
  const prismStart = propertyStart(config, "prism");
7400
7518
  const prism = prismStart < 0 ? null : matchingObjectLiteral(config, prismStart);
7401
7519
  const themeStart = prism ? propertyStart(prism, "theme") : -1;
@@ -7421,7 +7539,7 @@ function readDocusaurusPresentation(repositoryRoot) {
7421
7539
  const hex = typeof style.style?.color === "string" ? cssColorToHex(style.style.color) : void 0;
7422
7540
  if (tokenNames[type] && hex) colors.set(`--shiki-token-${tokenNames[type]}`, hex);
7423
7541
  }
7424
- tokens = [...colors].map(([name, hex]) => `${name}:${hex}`).join(";");
7542
+ tokens2 = [...colors].map(([name, hex]) => `${name}:${hex}`).join(";");
7425
7543
  }
7426
7544
  } catch {
7427
7545
  }
@@ -7449,7 +7567,7 @@ body{font-family:${safeBodyFont}}
7449
7567
  .thally-docs-prose{line-height:1.5;color:var(--docs-ink)}
7450
7568
  .thally-docs-prose p{margin-block:0 1rem}
7451
7569
  .thally-docs-prose a{color:var(--docs-accent)}
7452
- .thally-docs-code{${tokens};background:var(--docs-code-bg)}
7570
+ .thally-docs-code{${tokens2};background:var(--docs-code-bg)}
7453
7571
  @media(min-width:1024px){
7454
7572
  .thally-docs-shell{max-width:none;padding-inline:0}
7455
7573
  .thally-docs-sidebar,.thally-docs-sidebar>div{width:300px}
@@ -7471,10 +7589,45 @@ function unquote(value) {
7471
7589
  }
7472
7590
  function splitOpenApiRef(raw) {
7473
7591
  const value = unquote(raw);
7474
- const match = /^(?:(.+?)\s+)?((?:webhook\s+[^\s/]\S*)|(?:[A-Za-z]+\s+\/.*))$/i.exec(value);
7475
- if (!match) return null;
7476
- const specRef = match[1] ? unquote(match[1]) : void 0;
7477
- return { ...specRef ? { specRef } : {}, operation: match[2] };
7592
+ let firstLineBreak = -1;
7593
+ let lastLineBreak = -1;
7594
+ let lastWhitespace = -1;
7595
+ for (let index = 0; index < value.length; index++) {
7596
+ const character = value[index];
7597
+ if (/\s/.test(character)) lastWhitespace = index;
7598
+ if (character === "\n" || character === "\r" || character === "\u2028" || character === "\u2029") {
7599
+ if (firstLineBreak < 0) firstLineBreak = index;
7600
+ lastLineBreak = index;
7601
+ }
7602
+ }
7603
+ let bare = null;
7604
+ let previousEnd = 0;
7605
+ let start = 0;
7606
+ while (start < value.length) {
7607
+ let end = start;
7608
+ let letters = true;
7609
+ while (end < value.length && !/\s/.test(value[end])) {
7610
+ const code = value.charCodeAt(end);
7611
+ if (!(code >= 65 && code <= 90) && !(code >= 97 && code <= 122)) letters = false;
7612
+ end++;
7613
+ }
7614
+ let argument = end;
7615
+ while (argument < value.length && /\s/.test(value[argument])) argument++;
7616
+ const hasArgument = argument > end && argument < value.length;
7617
+ const method = letters && value[argument] === "/" && lastLineBreak < argument;
7618
+ const webhook = end - start === 7 && value.slice(start, end).toLowerCase() === "webhook" && value[argument] !== "/" && lastWhitespace < argument;
7619
+ if (hasArgument && (method || webhook)) {
7620
+ if (start === 0) bare = { operation: value };
7621
+ else {
7622
+ const specRef = unquote(value.slice(0, previousEnd));
7623
+ return { ...specRef ? { specRef } : {}, operation: value.slice(start) };
7624
+ }
7625
+ }
7626
+ previousEnd = end;
7627
+ if (firstLineBreak >= 0 && firstLineBreak < previousEnd) break;
7628
+ start = argument;
7629
+ }
7630
+ return bare;
7478
7631
  }
7479
7632
  function canonicalSpecRef(specRef) {
7480
7633
  return specRef.trim().split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^(?:\.?\/+)+/, "");
@@ -7628,11 +7781,246 @@ import { isAbsolute as isAbsolute2, join, relative as relative5, resolve as reso
7628
7781
  // src/url.ts
7629
7782
  import { load as load4 } from "cheerio";
7630
7783
  import remarkMdx4 from "remark-mdx";
7631
- import remarkParse4 from "remark-parse";
7784
+ import remarkParse5 from "remark-parse";
7632
7785
  import TurndownService from "turndown";
7633
- import { unified as unified4 } from "unified";
7786
+ import { unified as unified5 } from "unified";
7634
7787
  import { parse as parseYaml5, stringify as stringifyYaml2 } from "yaml";
7635
7788
 
7789
+ // src/url-openapi.ts
7790
+ import { createHash as createHash2 } from "crypto";
7791
+
7792
+ // src/remote-spec.ts
7793
+ import { lookup } from "dns/promises";
7794
+ import { request } from "https";
7795
+ import { isIP } from "net";
7796
+ var isWithheld = (node) => [node["x-hidden"], node["x-excluded"]].some((value) => value === true || value === "true");
7797
+ var MAX_SPEC_BYTES = 25e6;
7798
+ var MAX_REDIRECTS = 3;
7799
+ function isPublicAddress(address, family) {
7800
+ if (family === 4) {
7801
+ const parts = address.split(".").map(Number);
7802
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
7803
+ const [a, b] = parts;
7804
+ return !(a === 0 || a === 10 || a === 127 || a >= 224 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && (b === 0 || b === 168) || a === 198 && (b === 18 || b === 19) || a === 203 && b === 0 || a === 198 && b === 51);
7805
+ }
7806
+ return family === 6 && /^[23][0-9a-f]{0,3}:/i.test(address) && !/^2001:db8:/i.test(address);
7807
+ }
7808
+ function validatedSpecUrl(value) {
7809
+ const url = new URL(value);
7810
+ if (url.protocol !== "https:" || url.username || url.password || isIP(url.hostname)) {
7811
+ throw new Error("Remote OpenAPI references must use an HTTPS domain without credentials or an IP literal.");
7812
+ }
7813
+ url.hash = "";
7814
+ return url;
7815
+ }
7816
+ async function getPinnedAddress(url) {
7817
+ const addresses = await lookup(url.hostname, { all: true });
7818
+ if (addresses.length === 0 || addresses.some((candidate) => !isPublicAddress(candidate.address, candidate.family))) {
7819
+ throw new Error("The OpenAPI host did not resolve exclusively to public addresses.");
7820
+ }
7821
+ return addresses[0];
7822
+ }
7823
+ async function requestPinnedSpec(url) {
7824
+ const pinned = await getPinnedAddress(url);
7825
+ return new Promise((resolve5, reject) => {
7826
+ const req = request(url, {
7827
+ headers: { Accept: "application/json, application/yaml, text/yaml, */*", "User-Agent": "Thally-Migrate/1.0 (+https://thally.io)" },
7828
+ signal: AbortSignal.timeout(3e4),
7829
+ // Node's dual-stack connector asks lookup for `all: true`; that
7830
+ // callback must receive address records, not a single address string.
7831
+ lookup: (_hostname, options, callback) => {
7832
+ if (typeof options === "object" && options.all) callback(null, [pinned]);
7833
+ else callback(null, pinned.address, pinned.family);
7834
+ }
7835
+ }, (response) => {
7836
+ const status = response.statusCode ?? 0;
7837
+ if (status >= 300 && status < 400) {
7838
+ response.resume();
7839
+ resolve5({ status, location: response.headers.location, body: Buffer.alloc(0) });
7840
+ return;
7841
+ }
7842
+ if (status < 200 || status >= 300) {
7843
+ response.resume();
7844
+ reject(new Error(`OpenAPI server returned ${status}.`));
7845
+ return;
7846
+ }
7847
+ const statedBytes = Number(response.headers["content-length"] ?? 0);
7848
+ if (statedBytes > MAX_SPEC_BYTES) {
7849
+ response.destroy();
7850
+ reject(new Error("Remote OpenAPI spec exceeds the 25 MB import limit."));
7851
+ return;
7852
+ }
7853
+ const chunks = [];
7854
+ let bytes = 0;
7855
+ response.on("data", (chunk) => {
7856
+ bytes += chunk.length;
7857
+ if (bytes > MAX_SPEC_BYTES) {
7858
+ response.destroy(new Error("Remote OpenAPI spec exceeds the 25 MB import limit."));
7859
+ return;
7860
+ }
7861
+ chunks.push(chunk);
7862
+ });
7863
+ response.on("end", () => resolve5({ status, body: Buffer.concat(chunks) }));
7864
+ response.on("error", reject);
7865
+ });
7866
+ req.on("error", reject);
7867
+ req.end();
7868
+ });
7869
+ }
7870
+ async function downloadSpec(value, fetcher) {
7871
+ let url = validatedSpecUrl(value);
7872
+ if (fetcher) {
7873
+ const response = await fetcher(url, { accept: "application/json, application/yaml, text/yaml" });
7874
+ validatedSpecUrl(response.finalUrl.toString());
7875
+ const body = Buffer.from(response.body);
7876
+ if (body.length > MAX_SPEC_BYTES) throw new Error("Remote OpenAPI spec exceeds the 25 MB import limit.");
7877
+ return body;
7878
+ }
7879
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
7880
+ const response = await requestPinnedSpec(url);
7881
+ if (response.status >= 300 && response.status < 400) {
7882
+ if (!response.location || hop === MAX_REDIRECTS) throw new Error("Remote OpenAPI spec redirected too many times.");
7883
+ url = validatedSpecUrl(new URL(response.location, url).toString());
7884
+ continue;
7885
+ }
7886
+ return response.body;
7887
+ }
7888
+ throw new Error("Remote OpenAPI spec redirected too many times.");
7889
+ }
7890
+ function parseOpenApi(parsed) {
7891
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("OpenAPI spec is not an object.");
7892
+ const document = parsed;
7893
+ if (typeof document.openapi !== "string" || !/^3\./.test(document.openapi)) {
7894
+ throw new Error("Remote specification is not OpenAPI 3.x.");
7895
+ }
7896
+ if (!document.paths || typeof document.paths !== "object" || Array.isArray(document.paths)) {
7897
+ throw new Error("Remote OpenAPI spec has no paths object.");
7898
+ }
7899
+ const paths = Object.entries(document.paths);
7900
+ if (paths.length > 5e3) throw new Error("Remote OpenAPI spec has too many paths to import.");
7901
+ const operations = [];
7902
+ for (const [path, value] of paths) {
7903
+ if (!value || typeof value !== "object" || Array.isArray(value) || isWithheld(value)) continue;
7904
+ for (const [method, raw] of Object.entries(value)) {
7905
+ if (!/^(get|post|put|patch|delete|head|options|trace)$/i.test(method) || !raw || typeof raw !== "object" || Array.isArray(raw)) continue;
7906
+ const operation = raw;
7907
+ if (isWithheld(operation)) continue;
7908
+ const tags = Array.isArray(operation.tags) ? operation.tags : [];
7909
+ operations.push({
7910
+ method: method.toLowerCase(),
7911
+ path,
7912
+ tag: typeof tags[0] === "string" ? tags[0] : "API Reference",
7913
+ summary: typeof operation.summary === "string" ? operation.summary : String(operation.operationId ?? "")
7914
+ });
7915
+ }
7916
+ }
7917
+ return operations;
7918
+ }
7919
+
7920
+ // src/url-openapi.ts
7921
+ var slug = (value) => value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7922
+ async function hydrateUrlOpenApi(bundle, specUrl, fetcher) {
7923
+ try {
7924
+ const content = withoutByteOrderMark(await downloadSpec(specUrl, fetcher));
7925
+ const { document, format } = parseSpecDocument(content.toString("utf8"));
7926
+ const operations = parseOpenApi(document);
7927
+ const path = `openapi/openapi-${createHash2("sha256").update(specUrl).digest("hex").slice(0, 12)}.${format}`;
7928
+ const byRoute = /* @__PURE__ */ new Map();
7929
+ for (const operation of operations) {
7930
+ const key = `${slug(operation.tag)}/${slug(operation.summary)}`;
7931
+ if (!slug(operation.summary)) continue;
7932
+ byRoute.set(key, byRoute.has(key) ? null : operation);
7933
+ }
7934
+ const represented = /* @__PURE__ */ new Set();
7935
+ const visible = new Set(operations.map((operation) => operationKey2(`${operation.method} ${operation.path}`)));
7936
+ const assets = bundle.assets.filter((asset) => /\.(?:json|ya?ml)$/i.test(asset.path)).map((asset) => ({ ...asset, sourcePath: canonicalSpecRef(asset.path), filename: specRefBaseName(asset.path) }));
7937
+ const published = /* @__PURE__ */ new Map();
7938
+ const defaultSource = [...bundle.docsConfig.tabs.filter((tab) => !tab.hidden), ...bundle.docsConfig.tabs.filter((tab) => tab.hidden)].find((tab) => tab.api)?.api?.source;
7939
+ const isExistingBinding = (raw) => {
7940
+ const ref = splitOpenApiRef(raw);
7941
+ if (!ref) return void 0;
7942
+ const key = operationKey2(ref.operation);
7943
+ if (!visible.has(key)) return void 0;
7944
+ const source = ref.specRef ?? defaultSource;
7945
+ if (!source) return void 0;
7946
+ const resolved = resolveSpecRef(source, assets, false);
7947
+ if (resolved.kind !== "match") return void 0;
7948
+ const asset = resolved.spec;
7949
+ if (!published.has(asset.path)) {
7950
+ try {
7951
+ const original = parseSpecDocument(Buffer.from(asset.content).toString("utf8")).document;
7952
+ published.set(asset.path, new Set(parseOpenApi(original).map((operation) => operationKey2(`${operation.method} ${operation.path}`))));
7953
+ } catch {
7954
+ published.set(asset.path, /* @__PURE__ */ new Set());
7955
+ }
7956
+ }
7957
+ return published.get(asset.path).has(key) ? { operation: ref.operation, source: asset.path } : void 0;
7958
+ };
7959
+ const reboundSources = /* @__PURE__ */ new Set();
7960
+ const withheld = /* @__PURE__ */ new Set();
7961
+ const kept = /* @__PURE__ */ new Set();
7962
+ const bind = (page, reference) => {
7963
+ const key = operationKey2(reference);
7964
+ represented.add(key);
7965
+ if (page.public === false || page.groups?.length) withheld.add(key);
7966
+ else kept.add(key);
7967
+ return { ...page, openapi: `${path} ${reference}` };
7968
+ };
7969
+ const pages = bundle.pages.map((page) => {
7970
+ if (bundle.sourceKind !== "url") return page;
7971
+ if (page.openapi) {
7972
+ const selected = splitOpenApiRef(page.openapi);
7973
+ if (selected && (selected.specRef === specUrl || selected.specRef === path && assets.some((asset) => asset.path === path)) && visible.has(operationKey2(selected.operation))) {
7974
+ return bind(page, selected.operation);
7975
+ }
7976
+ const bound = isExistingBinding(page.openapi);
7977
+ if (bound) {
7978
+ reboundSources.add(bound.source);
7979
+ return bind(page, bound.operation);
7980
+ }
7981
+ return page;
7982
+ }
7983
+ const operation = byRoute.get(page.navigationId.split("/").slice(-2).join("/").toLowerCase());
7984
+ if (!operation) return page;
7985
+ const reference = `${operation.method.toUpperCase()} ${operation.path}`;
7986
+ return bind(page, reference);
7987
+ });
7988
+ const remaining = operations.map((operation) => `${operation.method.toUpperCase()} ${operation.path}`).filter((reference) => !represented.has(operationKey2(reference)));
7989
+ const pruned = markExcluded(content, [...withheld].filter((key) => !kept.has(key)), [...kept]);
7990
+ const warning = pruned.excluded.length ? { code: "gated-page", source: path, message: sharedSpecMessage(path, pruned.excluded) } : void 0;
7991
+ const api = { source: path, ...represented.size ? remaining.length ? { groups: [{ group: "Endpoints", pages: remaining }] } : { navigation: false } : {} };
7992
+ const stillReferenced = new Set(pages.flatMap((page) => {
7993
+ const source = page.openapi ? splitOpenApiRef(page.openapi)?.specRef ?? defaultSource : void 0;
7994
+ if (!source) return [];
7995
+ const resolved = resolveSpecRef(source, assets, false);
7996
+ return resolved.kind === "match" ? [resolved.spec.path] : [];
7997
+ }));
7998
+ const tabs = bundle.docsConfig.tabs.filter((tab) => {
7999
+ if (!tab.hidden || tab.api?.navigation !== false || tab.pages?.length || tab.groups?.length || tab.href) return true;
8000
+ const source = resolveSpecRef(tab.api.source, assets, false);
8001
+ return source.kind !== "match" || !reboundSources.has(source.spec.path) || stillReferenced.has(source.spec.path);
8002
+ }).map((tab) => ({ ...tab }));
8003
+ const target2 = tabs.find((tab) => tab.api && [path, specUrl].includes(tab.api.source)) ?? tabs.find((tab) => !tab.api && tab.tab.toLowerCase().includes("api"));
8004
+ if (target2) target2.api = api;
8005
+ else {
8006
+ const taken = new Set(tabs.map((tab) => slug(tab.tab)));
8007
+ let label = "API Reference";
8008
+ let suffix = 2;
8009
+ while (taken.has(slug(label))) label = `API Reference ${suffix++}`;
8010
+ tabs.push({ tab: label, api });
8011
+ }
8012
+ return {
8013
+ ...bundle,
8014
+ pages,
8015
+ assets: [...bundle.assets.filter((asset) => asset.path !== path), { path, content: pruned.content, projectRelative: true }],
8016
+ docsConfig: { ...bundle.docsConfig, tabs },
8017
+ warnings: warning && !bundle.warnings.some((item) => item.code === warning.code && item.source === warning.source && item.message === warning.message) ? [...bundle.warnings, warning] : bundle.warnings
8018
+ };
8019
+ } catch (error) {
8020
+ return { ...bundle, warnings: [...bundle.warnings, { code: "fetch-failed", source: specUrl, message: `OpenAPI spec could not be imported: ${error instanceof Error ? error.message : String(error)}` }] };
8021
+ }
8022
+ }
8023
+
7636
8024
  // src/analytics.ts
7637
8025
  var GA4_ID = /^G-[A-Z0-9]{4,20}$/;
7638
8026
  var GTM_ID = /^GTM-[A-Z0-9]{4,12}$/;
@@ -7860,7 +8248,10 @@ var VIDEO_ATTRIBUTES = /* @__PURE__ */ new Set([
7860
8248
  "width",
7861
8249
  "height",
7862
8250
  "preload",
7863
- "aria-label"
8251
+ "aria-label",
8252
+ // Publisher asset provenance does not change playback and is never emitted.
8253
+ // Keep this explicit: arbitrary data attributes may drive custom controls.
8254
+ "data-path"
7864
8255
  ]);
7865
8256
  var YOUTUBE_ATTRIBUTES = /* @__PURE__ */ new Set([
7866
8257
  "src",
@@ -7925,7 +8316,7 @@ function projectMigrationMedia(html, base) {
7925
8316
  }
7926
8317
 
7927
8318
  // src/url-static-content.ts
7928
- import { createHash as createHash2 } from "crypto";
8319
+ import { createHash as createHash3 } from "crypto";
7929
8320
  import { load as load3 } from "cheerio";
7930
8321
 
7931
8322
  // src/svg-policy.ts
@@ -8247,7 +8638,7 @@ function projectMigrationSvg(html) {
8247
8638
  if (svg.attr("xmlns") === void 0) svg.attr("xmlns", "http://www.w3.org/2000/svg");
8248
8639
  const content = Buffer.from($.xml(svg));
8249
8640
  if (!isPassiveSvg(content)) return null;
8250
- const sha256 = createHash2("sha256").update(content).digest("hex");
8641
+ const sha256 = createHash3("sha256").update(content).digest("hex");
8251
8642
  const labelled = (svg.attr("aria-labelledby") ?? "").split(/\s+/).filter(Boolean).map((id) => svg.find("[id]").filter((_index, node) => $(node).attr("id") === id).text().trim()).filter(Boolean);
8252
8643
  const alternative = svg.attr("aria-label")?.trim() || labelled.join(" ") || svg.children("title").text().trim() || svg.children("desc").text().trim() || svg.find("text").text().trim();
8253
8644
  return { asset: { path: `migration-assets/${sha256}.svg`, content }, alternative };
@@ -8361,12 +8752,12 @@ function projectCapturedTabs($, root) {
8361
8752
  }
8362
8753
 
8363
8754
  // src/url-flight-content.ts
8364
- import { createHash as createHash3 } from "crypto";
8755
+ import { createHash as createHash4 } from "crypto";
8365
8756
  import remarkMdx3 from "remark-mdx";
8366
- import remarkParse3 from "remark-parse";
8757
+ import remarkParse4 from "remark-parse";
8367
8758
  import remarkGfm2 from "remark-gfm";
8368
- import { unified as unified3 } from "unified";
8369
- var parser2 = unified3().use(remarkParse3).use(remarkGfm2).use(remarkMdx3);
8759
+ import { unified as unified4 } from "unified";
8760
+ var parser2 = unified4().use(remarkParse4).use(remarkGfm2).use(remarkMdx3);
8370
8761
  var MAX_SOURCE_BYTES = 8e6;
8371
8762
  var sourceTrees = /* @__PURE__ */ new Map();
8372
8763
  var cachedSourceBytes = 0;
@@ -8538,7 +8929,7 @@ function sourceText(node) {
8538
8929
  return node.value ?? (node.children ?? []).map(sourceText).join("");
8539
8930
  }
8540
8931
  function sourceNodes(source) {
8541
- const key = createHash3("sha256").update(source).digest("hex");
8932
+ const key = createHash4("sha256").update(source).digest("hex");
8542
8933
  const cached = sourceTrees.get(key);
8543
8934
  if (cached) {
8544
8935
  sourceTrees.delete(key);
@@ -8887,7 +9278,7 @@ function recoverFlightDisclosures($, root, document, article, projectionBudget)
8887
9278
  if (!id || !target2 || title === null || labelText(title) !== labelText(button.text()) || panel.length !== 1 || panel.attr("role") !== "region" || panel.attr("aria-labelledby") !== id || root.find("[id]").filter((_index, node) => $(node).attr("id") === id).length !== 1 || panel.is('[aria-busy="true"]') || panel.find('[aria-busy="true"],[role="progressbar"]').length) continue;
8888
9279
  let html;
8889
9280
  if (!panel.text().trim() && !panel.children().length) {
8890
- const prefix = `thally-flight-${createHash3("sha256").update(target2).digest("hex").slice(0, 16)}`;
9281
+ const prefix = `thally-flight-${createHash4("sha256").update(target2).digest("hex").slice(0, 16)}`;
8891
9282
  const rendered = flightStaticHtml(document, source.props.children, prefix, projectionBudget);
8892
9283
  if (rendered === null || !rendered.trim()) continue;
8893
9284
  html = rendered;
@@ -9126,8 +9517,13 @@ function recoverFlightArticleCode($, root) {
9126
9517
 
9127
9518
  // src/redirects.ts
9128
9519
  var LISTED = 10;
9520
+ function trimTrailingSlashes2(route) {
9521
+ let end = route.length;
9522
+ while (end > 0 && route[end - 1] === "/") end--;
9523
+ return route.slice(0, end);
9524
+ }
9129
9525
  function exactRoute(route) {
9130
- return route.startsWith("/") && !/[?:*#()[\]{}]/.test(route) ? route.replace(/\/+$/, "") || "/" : null;
9526
+ return route.startsWith("/") && !/[?:*#()[\]{}]/.test(route) ? trimTrailingSlashes2(route) || "/" : null;
9131
9527
  }
9132
9528
  function literalRoute(route) {
9133
9529
  return exactRoute(route)?.toLowerCase() ?? null;
@@ -9390,8 +9786,8 @@ var MINTLIFY_ARTICLE_LABELS = [
9390
9786
  { baseUrl: "Select base URL", permalink: "Navigate to header" },
9391
9787
  { baseUrl: "\u9009\u62E9\u57FA\u7840URL", permalink: "\u5BFC\u822A\u5230\u6807\u9898" }
9392
9788
  ];
9393
- var remoteMarkdownParser = unified4().use(remarkParse4).use(remarkMdx4);
9394
- var embeddedOpenApiParser = unified4().use(remarkParse4);
9789
+ var remoteMarkdownParser = unified5().use(remarkParse5).use(remarkMdx4);
9790
+ var embeddedOpenApiParser = unified5().use(remarkParse5);
9395
9791
  function validateMigrationUrl(value) {
9396
9792
  let url;
9397
9793
  try {
@@ -10007,9 +10403,13 @@ function migratedHref(href, currentUrl, source, scopePath, importedIds, sourceHo
10007
10403
  return `/${target2.search}${target2.hash}`;
10008
10404
  }
10009
10405
  let path = target2.pathname;
10406
+ const isMarkdown = path.endsWith(".md");
10407
+ const machinePath = path.replace(/\/+$/, "");
10408
+ if (["/llms.txt", "/llms-full.txt", "/openapi.json", "/openapi.yaml", "/robots.txt", "/sitemap.xml"].includes(machinePath)) return `${machinePath}${target2.search}${target2.hash}`;
10010
10409
  if (scopePath !== "/" && (path === scopePath || path.startsWith(`${scopePath}/`))) {
10011
10410
  path = path.slice(scopePath.length);
10012
10411
  }
10412
+ if (["/llms.txt", "/llms-full.txt", "/openapi.json", "/openapi.yaml", "/robots.txt", "/sitemap.xml"].includes(path)) return `${path}${target2.search}${target2.hash}`;
10013
10413
  const id = pageIdFromReference(path.replace(/^\/+/, "") || "introduction");
10014
10414
  const localizedId = id ? canonicalLocalizedPageId(id) : id;
10015
10415
  const destinationId = localizedId === sourceHomePageId ? "introduction" : localizedId;
@@ -10019,7 +10419,7 @@ function migratedHref(href, currentUrl, source, scopePath, importedIds, sourceHo
10019
10419
  }
10020
10420
  if (importedDestinationId) {
10021
10421
  const destination = importedDestinationId === "introduction" ? "/" : `/${importedDestinationId}`;
10022
- return `${destination}${target2.search}${target2.hash}`;
10422
+ return `${destination === "/" && isMarkdown ? "/introduction" : destination}${isMarkdown ? ".md" : ""}${target2.search}${target2.hash}`;
10023
10423
  }
10024
10424
  return target2.toString();
10025
10425
  } catch {
@@ -11194,9 +11594,9 @@ async function migrateUrl(options) {
11194
11594
  ]
11195
11595
  };
11196
11596
  }
11197
- if (platform === "mintlify") docsConfig = addMintlifyDirectoryRedirects(docsConfig, pages);
11597
+ if (platform === "mintlify") docsConfig = { ...addMintlifyDirectoryRedirects(docsConfig, pages), markdown: { enabled: true } };
11198
11598
  docsConfig = dropConflictingRedirects(dropUnroutableRedirects(docsConfig, warnings), pages, warnings);
11199
- return {
11599
+ const bundle = {
11200
11600
  sourceUrl: source.toString(),
11201
11601
  sourceKind: "url",
11202
11602
  platform,
@@ -11216,6 +11616,20 @@ async function migrateUrl(options) {
11216
11616
  skipped: Math.max(0, visited.size - pages.length)
11217
11617
  }
11218
11618
  };
11619
+ const sourceHtml = scopedHtmlProbe ? load4(scopedHtmlProbe.body) : void 0;
11620
+ const linkedSpecs = sourceHtml && scopedHtmlProbe && /html/i.test(scopedHtmlProbe.contentType) ? [...new Set(sourceHtml('link[rel="service-desc"][href], a[href]').map((_index, element) => {
11621
+ const href = sourceHtml(element).attr("href");
11622
+ if (!href) return null;
11623
+ try {
11624
+ const url = new URL(href, scopedHtmlProbe.finalUrl);
11625
+ return url.protocol === "https:" && /\/(?:openapi|swagger)\.(?:json|ya?ml)$/i.test(url.pathname) ? url.toString() : null;
11626
+ } catch {
11627
+ return null;
11628
+ }
11629
+ }).get())] : [];
11630
+ const specUrl = options.openapiUrl ?? (!openApiAsset && linkedSpecs.length === 1 ? linkedSpecs[0] : void 0);
11631
+ if (!options.openapiUrl && linkedSpecs.length > 1) bundle.warnings.push({ code: "unsupported-config", message: "Multiple OpenAPI specifications were linked by the source. Select one explicitly with --openapi <https-url>." });
11632
+ return specUrl ? hydrateUrlOpenApi(bundle, specUrl, options.fetcher) : bundle;
11219
11633
  }
11220
11634
 
11221
11635
  // src/source-refs.ts
@@ -12367,12 +12781,12 @@ function classifyApiSpec(path) {
12367
12781
  return /asyncapi/i.test(topLevel) ? "asyncapi" : "openapi";
12368
12782
  }
12369
12783
  var SPEC_DIRECTORY = "openapi";
12370
- var MAX_SPEC_BYTES = 5e7;
12784
+ var MAX_SPEC_BYTES2 = 5e7;
12371
12785
  function readSpecFile(absolutePath, relativePath, warnings) {
12372
- if (lstatSync6(absolutePath).size > MAX_SPEC_BYTES) {
12786
+ if (lstatSync6(absolutePath).size > MAX_SPEC_BYTES2) {
12373
12787
  warnings.push({
12374
12788
  code: "skipped-file",
12375
- message: `The OpenAPI spec "${relativePath}" is larger than ${MAX_SPEC_BYTES / 1e6} MB and was not migrated; split it or reduce its size.`,
12789
+ message: `The OpenAPI spec "${relativePath}" is larger than ${MAX_SPEC_BYTES2 / 1e6} MB and was not migrated; split it or reduce its size.`,
12376
12790
  source: relativePath
12377
12791
  });
12378
12792
  return void 0;
@@ -12598,7 +13012,7 @@ function apiTabSpecIds(tabs) {
12598
13012
  return tabs.filter((tab) => !tab.hidden && tab.api).map((tab, index) => ({ tab, specId: index === 0 ? "default" : slugifyApiSpecId(tab.tab) }));
12599
13013
  }
12600
13014
  var flagged = (value) => value === true || value === "true";
12601
- var isWithheld = (node) => flagged(node["x-hidden"]) || flagged(node["x-excluded"]);
13015
+ var isWithheld2 = (node) => flagged(node["x-hidden"]) || flagged(node["x-excluded"]);
12602
13016
  var isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
12603
13017
  function resolveLocalSpecRef(document, ref) {
12604
13018
  if (ref !== "#" && !ref.startsWith("#/")) return void 0;
@@ -12625,7 +13039,7 @@ function viewSpecPathItem(document, entry) {
12625
13039
  }
12626
13040
  const item = Object.assign({}, ...[...chain].reverse());
12627
13041
  delete item.$ref;
12628
- return { item, withheld: chain.some(isWithheld) };
13042
+ return { item, withheld: chain.some(isWithheld2) };
12629
13043
  }
12630
13044
  function parseOpenApiOperationsForLinking(content) {
12631
13045
  const document = readSpecDocument(content);
@@ -12649,7 +13063,7 @@ function visibleOperations(document, entries) {
12649
13063
  const operation = item[method];
12650
13064
  if (!operation || typeof operation !== "object") continue;
12651
13065
  const op = operation;
12652
- if (isWithheld(op)) continue;
13066
+ if (isWithheld2(op)) continue;
12653
13067
  const tags = Array.isArray(op.tags) ? op.tags.filter((tag) => typeof tag === "string") : [];
12654
13068
  operations.push({
12655
13069
  method,
@@ -12663,6 +13077,28 @@ function visibleOperations(document, entries) {
12663
13077
  }
12664
13078
  return operations;
12665
13079
  }
13080
+ function migrationApiRoutes(bundle) {
13081
+ const routes = /* @__PURE__ */ new Set();
13082
+ for (const { tab, specId } of apiTabSpecIds(bundle.docsConfig.tabs)) {
13083
+ const source = tab.api.source.replace(/^\//, "");
13084
+ const asset = bundle.assets.find((entry) => entry.path.replace(/^\//, "") === source);
13085
+ if (!asset) continue;
13086
+ const document = readSpecDocument(Buffer.from(asset.content));
13087
+ if (!isRecord2(document)) continue;
13088
+ const operations = visibleOperations(document, document.paths);
13089
+ const webhooks = visibleOperations(document, document.webhooks);
13090
+ if (specId === "default" && (operations.length || webhooks.length)) routes.add("/api");
13091
+ for (const operation of operations) {
13092
+ routes.add(`/api/${specId}/${thallyOperationSlugSegments(operation.path, operation.method).join("/")}`);
13093
+ }
13094
+ for (const operation of webhooks) {
13095
+ const segments = thallyOperationSlugSegments(operation.path, operation.method);
13096
+ if (segments[0] !== "webhooks") segments.unshift("webhooks");
13097
+ routes.add(`/api/${specId}/${segments.join("/")}`);
13098
+ }
13099
+ }
13100
+ return [...routes];
13101
+ }
12666
13102
  function servedOperations(source, parsed) {
12667
13103
  let operations = parsed.get(source.filename);
12668
13104
  if (!operations) {
@@ -13376,10 +13812,13 @@ function rewriteMintlifyMountedLinks(body, pageIds) {
13376
13812
  if (!pathname.startsWith("/") || pathname.startsWith("//") || pathname.includes("\\")) return target2;
13377
13813
  const segments = pathname.slice(1).split("/");
13378
13814
  if (segments.some((segment) => !segment || segment === "." || segment === "..")) return target2;
13379
- if (pageIds.has(segments.join("/"))) return target2;
13815
+ const markdown = pathname.endsWith(".md");
13816
+ const sourceMdx = pathname.endsWith(".mdx");
13817
+ const routeSegments = markdown || sourceMdx ? [...segments.slice(0, -1), segments.at(-1).replace(/\.mdx?$/, "")] : segments;
13818
+ if (pageIds.has(routeSegments.join("/"))) return sourceMdx ? `/${routeSegments.join("/")}${suffix}` : target2;
13380
13819
  for (let count = 1; count < segments.length && count <= 3; count++) {
13381
- const candidate = segments.slice(count).join("/");
13382
- if (pageIds.has(candidate)) return `/${candidate}${suffix}`;
13820
+ const candidate = routeSegments.slice(count).join("/");
13821
+ if (pageIds.has(candidate)) return `/${candidate}${markdown ? ".md" : ""}${suffix}`;
13383
13822
  }
13384
13823
  return target2;
13385
13824
  };
@@ -13459,11 +13898,11 @@ function preserveLinkedAnchors(pages) {
13459
13898
  const headingSlugs = /* @__PURE__ */ new Map();
13460
13899
  const slugCounts = /* @__PURE__ */ new Map();
13461
13900
  for (const heading of headings) {
13462
- const slug2 = heading.text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
13463
- existing.add(slug2);
13464
- const seen = slugCounts.get(slug2) ?? 0;
13465
- slugCounts.set(slug2, seen + 1);
13466
- if (slug2) headingSlugs.set(seen === 0 ? slug2 : `${slug2}-${seen}`, heading.index);
13901
+ const slug3 = heading.text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
13902
+ existing.add(slug3);
13903
+ const seen = slugCounts.get(slug3) ?? 0;
13904
+ slugCounts.set(slug3, seen + 1);
13905
+ if (slug3) headingSlugs.set(seen === 0 ? slug3 : `${slug3}-${seen}`, heading.index);
13467
13906
  }
13468
13907
  const headingAliases = /* @__PURE__ */ new Map();
13469
13908
  const tableAliases = /* @__PURE__ */ new Map();
@@ -13528,9 +13967,9 @@ function addDatedHeadingAliases(body, date) {
13528
13967
  if (open) return [line];
13529
13968
  const heading = line.match(/^#{2,6}\s+(.+?)\s*#*\s*$/);
13530
13969
  if (!heading) return [line];
13531
- const slug2 = heading[1].replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_]/g, "").normalize("NFKD").toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/\s+/g, "-");
13532
- const id = `${date}-${slug2}`;
13533
- if (!slug2 || existingIds.has(id)) return [line];
13970
+ const slug3 = heading[1].replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_]/g, "").normalize("NFKD").toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/\s+/g, "-");
13971
+ const id = `${date}-${slug3}`;
13972
+ if (!slug3 || existingIds.has(id)) return [line];
13534
13973
  existingIds.add(id);
13535
13974
  return [`<a id="${id}" />`, "", line];
13536
13975
  }).join("\n");
@@ -14748,9 +15187,9 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
14748
15187
  // hierarchy (never just its own segment) and wins over a docs.yml slug.
14749
15188
  ...platform === "fern" ? {
14750
15189
  resolveIdentity: (frontmatter, fallback) => {
14751
- const slug2 = typeof frontmatter.slug === "string" ? frontmatter.slug.trim() : "";
14752
- if (!slug2) return fallback;
14753
- const overridden = pageIdFromReference(slug2, true);
15190
+ const slug3 = typeof frontmatter.slug === "string" ? frontmatter.slug.trim() : "";
15191
+ if (!slug3) return fallback;
15192
+ const overridden = pageIdFromReference(slug3, true);
14754
15193
  if (!overridden) return fallback;
14755
15194
  fernIdRenames.set(fallback.navigationId, overridden);
14756
15195
  return { ...fallback, id: overridden, navigationId: overridden };
@@ -15132,7 +15571,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
15132
15571
  message: `Skipped ${rootStyleProjection.omittedSelectors} stylesheet selector${rootStyleProjection.omittedSelectors === 1 ? "" : "s"} that target platform-owned or unreferenced markup. Recreate any intentional shell customization with Thally configuration.`
15133
15572
  });
15134
15573
  if (rootStyleProjection.css.trim()) {
15135
- const identity = createHash4("sha256").update(componentSourceIdentity(options.sourceUrl, repositoryDir, mintlifyProjectRoot)).digest("hex").slice(0, 12);
15574
+ const identity = createHash5("sha256").update(componentSourceIdentity(options.sourceUrl, repositoryDir, mintlifyProjectRoot)).digest("hex").slice(0, 12);
15136
15575
  const assetPath = `migrated/${identity}/style.css`;
15137
15576
  const content = Buffer.from(rootStyleProjection.css);
15138
15577
  assets.push({ path: assetPath, content });
@@ -16019,6 +16458,32 @@ function renderPage(bundle, page) {
16019
16458
  function isIntegrationsObject(value) {
16020
16459
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
16021
16460
  }
16461
+ function mergeRedirects(existing = [], incoming = []) {
16462
+ const redirects = [...existing];
16463
+ const key = (source) => literalRoute(source) ?? trimTrailingSlashes2(source).toLowerCase();
16464
+ const destinationKey = (destination) => literalRoute(destinationRoute(destination) ?? "");
16465
+ const sources = new Set(existing.map((redirect) => key(redirect.source)));
16466
+ for (const redirect of incoming) {
16467
+ const source = key(redirect.source);
16468
+ if (sources.has(source)) continue;
16469
+ const visited = /* @__PURE__ */ new Set([source]);
16470
+ let destination = destinationKey(redirect.destination);
16471
+ let cycle = false;
16472
+ while (destination) {
16473
+ if (visited.has(destination)) {
16474
+ cycle = true;
16475
+ break;
16476
+ }
16477
+ visited.add(destination);
16478
+ const next = redirects.find((candidate) => literalRoute(candidate.source) === destination);
16479
+ destination = next ? destinationKey(next.destination) : null;
16480
+ }
16481
+ if (cycle) continue;
16482
+ redirects.push(redirect);
16483
+ sources.add(source);
16484
+ }
16485
+ return redirects;
16486
+ }
16022
16487
  function mergeMigrationConfig(existing, incoming) {
16023
16488
  const tabs = existing.tabs.filter((tab) => tab.tab.toLowerCase() !== "changelog");
16024
16489
  const names = new Set(tabs.map((tab) => tab.tab.toLowerCase()));
@@ -16086,6 +16551,10 @@ function mergeMigrationConfig(existing, incoming) {
16086
16551
  ...incoming,
16087
16552
  ...existing,
16088
16553
  tabs,
16554
+ ...existing.redirects || incoming.redirects ? { redirects: mergeRedirects(existing.redirects, incoming.redirects) } : {},
16555
+ // A starter's primary action must not discard a newly imported logo.
16556
+ // Owner-authored fields, including an explicit null logo, remain authoritative.
16557
+ ...incoming.navbar || existing.navbar ? { navbar: { ...incoming.navbar, ...existing.navbar } } : {},
16089
16558
  // Collection presentation belongs to the imported information
16090
16559
  // architecture; starter defaults must not coerce source dropdowns to tabs.
16091
16560
  navigation: incoming.navigation ?? existing.navigation,
@@ -16117,11 +16586,12 @@ function renderMigrationFiles(bundle, options = {}) {
16117
16586
  }
16118
16587
 
16119
16588
  // src/url-run.ts
16120
- import { createHash as createHash6 } from "crypto";
16589
+ import { createHash as createHash7 } from "crypto";
16121
16590
  import { load as load8 } from "cheerio";
16122
- import remarkParse5 from "remark-parse";
16591
+ import { parse as parse2, postprocess, preprocess } from "micromark";
16592
+ import remarkParse6 from "remark-parse";
16123
16593
  import remarkMdx5 from "remark-mdx";
16124
- import { unified as unified5 } from "unified";
16594
+ import { unified as unified6 } from "unified";
16125
16595
 
16126
16596
  // src/url-asset-cache.ts
16127
16597
  function createMigrationAssetCache() {
@@ -16652,7 +17122,7 @@ function mergeSourceNavigation(existing, incoming) {
16652
17122
  }
16653
17123
 
16654
17124
  // src/url-identity.ts
16655
- import { createHash as createHash5 } from "crypto";
17125
+ import { createHash as createHash6 } from "crypto";
16656
17126
  import { load as load7 } from "cheerio";
16657
17127
  var directory = (path) => path.replace(/\/index\/?$/i, "").replace(/\/$/, "") || "/";
16658
17128
  function migrationSectionQueryCandidate(source, anchors) {
@@ -16713,7 +17183,7 @@ function observedMigrationArticleIdentity(document) {
16713
17183
  canonical2.pathname = canonical2.pathname.replace(/\/+$/, "") || "/";
16714
17184
  const removed = [...source.searchParams].filter(([key, value]) => !canonical2.searchParams.getAll(key).includes(value));
16715
17185
  const sectionAnchor = removed.length === 1 && anchors.includes(removed[0][1]) ? removed[0][1] : void 0;
16716
- return { canonicalUrl: canonical2.href, articleSha256: createHash5("sha256").update(payload).digest("hex"), ...sectionAnchor ? { sectionAnchor } : {} };
17186
+ return { canonicalUrl: canonical2.href, articleSha256: createHash6("sha256").update(payload).digest("hex"), ...sectionAnchor ? { sectionAnchor } : {} };
16717
17187
  }
16718
17188
 
16719
17189
  // src/url-api-schema-controls.ts
@@ -17040,8 +17510,8 @@ function apiOperationCodeControls(body, specification, operation) {
17040
17510
  const metadata = object2(resolveValue(value));
17041
17511
  const declaration = resolveValue(metadata?.openapi);
17042
17512
  if (typeof declaration !== "string") return false;
17043
- const tokens = declaration.trim().split(/\s+/);
17044
- return tokens.at(-2)?.toLowerCase() === operation.method.toLowerCase() && tokens.at(-1) === operation.path;
17513
+ const tokens2 = declaration.trim().split(/\s+/);
17514
+ return tokens2.at(-2)?.toLowerCase() === operation.method.toLowerCase() && tokens2.at(-1) === operation.path;
17045
17515
  };
17046
17516
  const active = /* @__PURE__ */ new Set();
17047
17517
  const visit = (input, provider, group, depth, owners = []) => {
@@ -17087,9 +17557,9 @@ function apiOperationCodeControls(body, specification, operation) {
17087
17557
  const hint = object2(resolveValue(scope?.pageMetadata)), metadata = object2(resolveValue(props.pageMetadata));
17088
17558
  const declaration = resolveValue(hint?.openapi);
17089
17559
  if (typeof declaration !== "string" || typeof hint?.href !== "string" || !hint.href || hint.href !== metadata?.href) return false;
17090
- const tokens = declaration.trim().split(/\s+/);
17560
+ const tokens2 = declaration.trim().split(/\s+/);
17091
17561
  const template = (path) => path.split("/").map((segment) => /^\{[^{}\/]+\}$/.test(segment) ? "{}" : segment).join("/");
17092
- return tokens.at(-2)?.toLowerCase() === operation.method.toLowerCase() && template(tokens.at(-1) ?? "") === template(operation.path);
17562
+ return tokens2.at(-2)?.toLowerCase() === operation.method.toLowerCase() && template(tokens2.at(-1) ?? "") === template(operation.path);
17093
17563
  })()) generatedControllers += 1;
17094
17564
  }
17095
17565
  let local = group;
@@ -17197,15 +17667,15 @@ function apiOperationCodeControls(body, specification, operation) {
17197
17667
  }
17198
17668
 
17199
17669
  // src/url-run.ts
17200
- var CONVERTER_VERSION = "url-evidence-v6";
17201
- var parser3 = unified5().use(remarkParse5).use(remarkMdx5);
17202
- var ACCEPT = "text/markdown,text/html,application/xhtml+xml;q=0.9";
17670
+ var CONVERTER_VERSION = "url-evidence-v7";
17671
+ var parser3 = unified6().use(remarkParse6).use(remarkMdx5);
17672
+ var ACCEPT = "text/html,application/xhtml+xml";
17203
17673
  var MAX_RESPONSE_BYTES = 8e6;
17204
17674
  var MAX_ASSET_BYTES2 = 1e7;
17205
17675
  var MAX_RECORDS = 1e5;
17206
17676
  var LOCALES = /* @__PURE__ */ new Set(["ar", "cs", "da", "de", "el", "en", "es", "fi", "fr", "he", "hi", "hu", "id", "it", "ja", "ko", "ms", "nl", "no", "pl", "pt", "pt-br", "ro", "ru", "sk", "sv", "th", "tr", "uk", "vi", "zh", "zh-cn", "zh-hans", "zh-hant"]);
17207
17677
  function hash2(value) {
17208
- return createHash6("sha256").update(value).digest("hex");
17678
+ return createHash7("sha256").update(value).digest("hex");
17209
17679
  }
17210
17680
  function canonicalJson(value) {
17211
17681
  const canonical2 = (value2) => Array.isArray(value2) ? value2.map(canonical2) : value2 !== null && typeof value2 === "object" ? Object.fromEntries(Object.entries(value2).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, field]) => [key, canonical2(field)])) : value2;
@@ -17229,6 +17699,11 @@ function canonical(value, base) {
17229
17699
  return null;
17230
17700
  }
17231
17701
  }
17702
+ function sameRouteMarkdownRepresentation(alternate, primary) {
17703
+ const target2 = new URL(alternate), source = new URL(primary);
17704
+ const directory2 = (path) => path.replace(/\.mdx?$/i, "").replace(/\/index\/?$/i, "").replace(/\/$/, "");
17705
+ return target2.origin === source.origin && target2.search === source.search && directory2(target2.pathname) === directory2(source.pathname) && /\.mdx?$/i.test(target2.pathname);
17706
+ }
17232
17707
  function classifyMigrationResponse(document) {
17233
17708
  if (document.status && document.status >= 400) return "unsupported";
17234
17709
  if (/\/(?:llms(?:-full)?|robots)\.txt$|\/sitemap[^/]*\.xml$|\/search(?:\.html)?$/i.test(document.finalUrl.pathname) || /\.(?:json|ya?ml)$/i.test(document.finalUrl.pathname) || /(?:xml|json)/i.test(document.contentType)) return "index";
@@ -17479,7 +17954,7 @@ function resolveMigrationRecord(records, record) {
17479
17954
  }
17480
17955
  return record;
17481
17956
  }
17482
- function navigationConfig(checkpoint, records) {
17957
+ function navigationConfig(checkpoint, records, occurrencePages = /* @__PURE__ */ new Map()) {
17483
17958
  const navigation = checkpoint.navigation;
17484
17959
  const shortcuts = [];
17485
17960
  const collectShortcuts = (nodes) => {
@@ -17504,7 +17979,7 @@ function navigationConfig(checkpoint, records) {
17504
17979
  const project2 = (nodes) => nodes.flatMap((node) => {
17505
17980
  const record = node.url ? records.get(canonical(node.url, node.url) ?? "") : void 0;
17506
17981
  const target2 = resolveMigrationRecord(records, record);
17507
- const page = target2?.page;
17982
+ const page = occurrencePages.get(node) ?? target2?.page;
17508
17983
  const children = project2(node.children ?? []);
17509
17984
  if (children.length) {
17510
17985
  const contains = (entries) => entries.some((entry) => typeof entry === "string" ? entry === (page && projection.navigationId(page)) : contains(entry.pages));
@@ -17804,8 +18279,19 @@ async function runUrlMigration(options) {
17804
18279
  if (!record.evidence) continue;
17805
18280
  const final = canonical(record.evidence.finalUrl, sourceUrl);
17806
18281
  if (!final) continue;
17807
- if (record.outcome === "imported") enqueueArticleLinks(record.evidence.links, final, record.url);
17808
- else if (record.outcome === "excluded") {
18282
+ if (record.outcome === "imported") {
18283
+ enqueueArticleLinks(record.evidence.links, final, record.url);
18284
+ for (const reference of record.evidence.markdownRepresentations ?? []) {
18285
+ const alternate = canonical(reference, final);
18286
+ if (!alternate || !sameRouteMarkdownRepresentation(alternate, final)) continue;
18287
+ enqueue(alternate, final, "declared-markdown-representation");
18288
+ const sibling = records.get(alternate);
18289
+ if (sibling && sibling.representationOf !== record.url) {
18290
+ sibling.representationOf = record.url;
18291
+ changed.push(sibling);
18292
+ }
18293
+ }
18294
+ } else if (record.outcome === "excluded") {
17809
18295
  if (classifyMigrationResponse({ finalUrl: new URL(final), contentType: record.evidence.contentType, body: "" }) === "index") {
17810
18296
  for (const link of record.evidence.links) enqueue(link, final, record.url);
17811
18297
  }
@@ -17888,6 +18374,7 @@ async function runUrlMigration(options) {
17888
18374
  let claimedId;
17889
18375
  let claimedFinal;
17890
18376
  let markdownRepresentation;
18377
+ const markdownRepresentations = [];
17891
18378
  let exactKey;
17892
18379
  try {
17893
18380
  delete record.reason;
@@ -17980,13 +18467,13 @@ async function runUrlMigration(options) {
17980
18467
  $('link[rel="alternate"][type="text/markdown"][href]').each((_index, node) => {
17981
18468
  const alternate = canonical($(node).attr("href"), final);
17982
18469
  if (!alternate) return;
17983
- const target2 = new URL(alternate), primary = new URL(final);
17984
- const directory2 = (path) => path.replace(/\.mdx?$/i, "").replace(/\/index\/?$/i, "").replace(/\/$/, "");
17985
- if (target2.origin !== primary.origin || target2.search !== primary.search || directory2(target2.pathname) !== directory2(primary.pathname) || !/\.mdx?$/i.test(target2.pathname)) return;
18470
+ if (!sameRouteMarkdownRepresentation(alternate, final)) return;
17986
18471
  enqueue(alternate, final, "declared-markdown-representation");
17987
18472
  const sibling = records.get(alternate);
18473
+ if (!sibling) return;
17988
18474
  sibling.representationOf = record.url;
17989
18475
  markdownRepresentation ??= alternate;
18476
+ markdownRepresentations.push(alternate);
17990
18477
  changed.push(sibling);
17991
18478
  });
17992
18479
  }
@@ -17999,7 +18486,8 @@ async function runUrlMigration(options) {
17999
18486
  }
18000
18487
  enqueueNavigation(observedNavigation, final);
18001
18488
  }
18002
- if (imported >= maxPages) {
18489
+ const replacesRepresentation = imported >= maxPages && markdownRepresentations.some((url) => records.get(url)?.page);
18490
+ if (imported >= maxPages && !replacesRepresentation) {
18003
18491
  record.outcome = "excluded";
18004
18492
  record.reason = "A discovery endpoint resolved to an article after the selected page limit.";
18005
18493
  return;
@@ -18018,14 +18506,17 @@ async function runUrlMigration(options) {
18018
18506
  const locale = language.locale;
18019
18507
  const navigationId = locale ? id.split("/").filter((segment) => segment.toLowerCase() !== locale.toLowerCase()).join("/") || "introduction" : id;
18020
18508
  const identityId = locale ? `${locale}/${navigationId}` : id;
18021
- if (usedIds.has(identityId) && usedIds.get(identityId) !== record.url) {
18509
+ const owner = records.get(usedIds.get(identityId) ?? "");
18510
+ const sharesDeclaredRepresentation = owner && (owner.representationOf === record.url || record.representationOf === owner.url);
18511
+ if (owner && owner !== record && !sharesDeclaredRepresentation) {
18022
18512
  record.outcome = "failed";
18023
18513
  record.reason = `Destination collision for ${identityId}.`;
18024
18514
  return;
18025
18515
  }
18026
- usedIds.set(identityId, record.url);
18516
+ if (!owner || owner === record || owner.representationOf === record.url) usedIds.set(identityId, record.url);
18027
18517
  claimedId = identityId;
18028
18518
  const evidence = evidenceOf(document);
18519
+ if (markdownRepresentations.length) evidence.markdownRepresentations = markdownRepresentations;
18029
18520
  const page = convert(document, id);
18030
18521
  if (!page) {
18031
18522
  record.outcome = "failed";
@@ -18236,6 +18727,7 @@ ${difference}`;
18236
18727
  if (!primary || primary === record) continue;
18237
18728
  const winner = primary.outcome === "imported" ? primary : record.outcome === "imported" && primary.outcome === "failed" ? record : void 0;
18238
18729
  if (!winner) continue;
18730
+ if (winner.page) usedIds.set(winner.page.id, winner.url);
18239
18731
  const alternate = winner === primary ? record : primary;
18240
18732
  if (alternate.outcome === "alias" && alternate.aliasOf === winner.url) continue;
18241
18733
  if (alternate.outcome === "imported") {
@@ -18257,7 +18749,17 @@ ${difference}`;
18257
18749
  settleArticleIdentities();
18258
18750
  while (cursor < pending.length && !options.signal?.aborted && !budgetExhausted && cursor < (options.maxCaptures ?? Infinity)) {
18259
18751
  if (imported >= maxPages) {
18260
- const index = pending.findIndex((record, position) => position >= cursor && isDiscoveryIndex(new URL(record.url), source));
18752
+ const index = pending.findIndex((record, position) => {
18753
+ if (position < cursor || record.outcome !== "pending") return false;
18754
+ const url = new URL(record.url);
18755
+ if (isDiscoveryIndex(url, source)) return true;
18756
+ if (/\.mdx?$/i.test(url.pathname)) return false;
18757
+ return [".md", ".mdx", "/index.md", "/index.mdx"].some((extension) => {
18758
+ const alternate = new URL(url);
18759
+ alternate.pathname = alternate.pathname.replace(/\/index$/i, "") + extension;
18760
+ return records.get(alternate.href)?.outcome === "imported";
18761
+ });
18762
+ });
18261
18763
  if (index < 0) break;
18262
18764
  pending.splice(cursor, 0, pending.splice(index, 1)[0]);
18263
18765
  }
@@ -18281,19 +18783,60 @@ ${difference}`;
18281
18783
  }
18282
18784
  const navigationLabels = /* @__PURE__ */ new Map();
18283
18785
  const navigationIssues = [];
18786
+ const navigationOccurrences = /* @__PURE__ */ new Map();
18284
18787
  const labelPages = (nodes) => {
18285
18788
  for (const node of nodes) {
18286
18789
  const entry = node.url ? records.get(canonical(node.url, sourceUrl) ?? "") : void 0;
18287
18790
  const target2 = resolveMigrationRecord(records, entry);
18288
- if (target2?.page && node.label && !node.children?.length && !entry?.aliasAnchor) {
18289
- const previous = navigationLabels.get(target2.url);
18290
- if (previous && previous !== node.label) navigationIssues.push({ code: "unsupported-component", source: target2.url, message: `This page has multiple source navigation labels (${previous}, ${node.label}); review the shared sidebar title.` });
18291
- else navigationLabels.set(target2.url, node.label);
18791
+ if (entry && target2?.page && node.label && !node.children?.length && !entry.aliasAnchor) {
18792
+ const occurrences = navigationOccurrences.get(target2) ?? [];
18793
+ occurrences.push({ node, entry });
18794
+ navigationOccurrences.set(target2, occurrences);
18292
18795
  }
18293
18796
  labelPages(node.children ?? []);
18294
18797
  }
18295
18798
  };
18296
18799
  labelPages(checkpoint.navigation);
18800
+ const occurrencePages = /* @__PURE__ */ new Map();
18801
+ const navigationAliases = /* @__PURE__ */ new Map();
18802
+ const occupiedPageIds = new Set(checkpoint.records.flatMap((record) => record.page ? [projection.pageId(record.page)] : []));
18803
+ const navigationRedirects = [];
18804
+ for (const [target2, occurrences] of navigationOccurrences) {
18805
+ const final = target2.evidence && canonical(target2.evidence.finalUrl, sourceUrl);
18806
+ const primary = occurrences.find(({ entry }) => entry.url === final) ?? occurrences.find(({ entry }) => entry === target2) ?? occurrences[0];
18807
+ navigationLabels.set(target2.url, primary.node.label);
18808
+ for (const { node, entry } of occurrences) {
18809
+ if (node.label === primary.node.label) continue;
18810
+ const existing = navigationAliases.get(entry.url);
18811
+ if (existing?.page.navTitle === node.label && existing.target === target2) {
18812
+ occurrencePages.set(node, existing.page);
18813
+ continue;
18814
+ }
18815
+ const sourceRoute = new URL(entry.url);
18816
+ const id = inScope(sourceRoute, source, scopePath) ? idForUrl(sourceRoute, scopePath) : null;
18817
+ const locale = projection.canonicalLocale(target2.page);
18818
+ let navigationId = id && (locale ? id.split("/").filter((segment) => segment.toLowerCase() !== locale.toLowerCase()).join("/") || "introduction" : id);
18819
+ const legacyPrefix = locale && `${locale.toLowerCase()}-`;
18820
+ if (navigationId && legacyPrefix && projection.navigationId(target2.page) !== target2.page.navigationId && navigationId.toLowerCase().startsWith(legacyPrefix)) navigationId = navigationId.slice(legacyPrefix.length);
18821
+ const page = navigationId ? {
18822
+ ...target2.page,
18823
+ id: locale ? `${locale}/${navigationId}` : navigationId,
18824
+ navigationId,
18825
+ ...locale ? { locale } : {},
18826
+ navTitle: node.label,
18827
+ source: entry.url,
18828
+ noindex: true
18829
+ } : void 0;
18830
+ if (!page || existing || occupiedPageIds.has(projection.pageId(page))) {
18831
+ navigationIssues.push({ code: "unsupported-component", source: target2.url, message: `This page has multiple source navigation labels (${primary.node.label}, ${node.label}); their source routes cannot retain distinct sidebar titles safely.` });
18832
+ continue;
18833
+ }
18834
+ occupiedPageIds.add(projection.pageId(page));
18835
+ navigationAliases.set(entry.url, { page, target: target2 });
18836
+ occurrencePages.set(node, page);
18837
+ navigationRedirects.push({ source: projection.route(page), destination: projection.route(target2.page), permanent: true });
18838
+ }
18839
+ }
18297
18840
  const pages = checkpoint.records.flatMap((record) => record.page ? [{ ...record.page, id: projection.pageId(record.page), navigationId: projection.navigationId(record.page), ...projection.canonicalLocale(record.page) ? { locale: projection.canonicalLocale(record.page) } : {}, ...navigationLabels.has(record.url) ? { navTitle: navigationLabels.get(record.url) } : {}, body: rewriteMarkdownDestinations(record.page.body, (value) => {
18298
18841
  let url;
18299
18842
  try {
@@ -18314,6 +18857,10 @@ ${difference}`;
18314
18857
  const hasBrokenSourceAnchor = Boolean(url.hash && destination?.outcome === "imported" && destination.evidence && !destination.evidence.anchors.includes(anchor));
18315
18858
  return `${route}${hasBrokenSourceAnchor ? "" : effectiveHash}`;
18316
18859
  }) }] : []);
18860
+ const projectedPages = new Map(pages.map((page) => [page.id, page]));
18861
+ for (const { page, target: target2 } of navigationAliases.values()) {
18862
+ pages.push({ ...page, id: projection.pageId(page), body: projectedPages.get(projection.pageId(target2.page)).body });
18863
+ }
18317
18864
  const validation = validateUrlMigration(checkpoint, Boolean(options.signal?.aborted));
18318
18865
  validation.issues.push(...navigationIssues);
18319
18866
  if (navigationIssues.length && validation.status === "passed") validation.status = "partial";
@@ -18334,12 +18881,12 @@ ${difference}`;
18334
18881
  platform: options.platform ?? "generic",
18335
18882
  pages,
18336
18883
  assets: [...assets.values()],
18337
- docsConfig: navigationConfig(checkpoint, records) ?? buildNavigationFromPages(pages),
18884
+ docsConfig: navigationConfig(checkpoint, records, occurrencePages) ?? buildNavigationFromPages(pages),
18338
18885
  warnings: [...validation.issues, ...validation.warnings ?? []].map((issue) => ({ code: issue.code === "missing-page" ? "missing-page" : "unsupported-config", message: issue.message, source: issue.source })),
18339
18886
  urlMigrationValidation: validation,
18340
18887
  stats: { discovered: records.size, imported: pages.length, skipped: records.size - pages.length }
18341
18888
  };
18342
- if (projection.redirects.length) bundle.docsConfig.redirects = projection.redirects;
18889
+ if (projection.redirects.length || navigationRedirects.length) bundle.docsConfig.redirects = [...projection.redirects, ...navigationRedirects];
18343
18890
  if (projection.locales.length) {
18344
18891
  const locales = [.../* @__PURE__ */ new Set([projection.defaultLocale, ...projection.locales])];
18345
18892
  bundle.docsConfig.i18n = { ...bundle.docsConfig.i18n, defaultLocale: projection.defaultLocale, locales: locales.map((code) => ({ code, label: code === "en" ? "English" : code })) };
@@ -18357,9 +18904,11 @@ function rewriteMarkdownDestinations(body, rewrite) {
18357
18904
  const original = body.slice(start, end);
18358
18905
  const replacement = rewrite(node.url);
18359
18906
  if (replacement === node.url) return null;
18360
- const index = original.lastIndexOf(node.url);
18361
- if (index < 0) return null;
18362
- return { start: start + index, end: start + index + node.url.length, replacement };
18907
+ const destinations = postprocess(parse2().document().write(preprocess()(original, void 0, true))).filter(([event, token]) => event === "enter" && ["resourceDestinationString", "definitionDestinationString", "autolinkProtocol", "autolinkEmail"].includes(token.type));
18908
+ const destination = destinations.at(-1)?.[1];
18909
+ if (!destination) return null;
18910
+ const escaped = replacement.replace(/[\\&()<>]/g, "\\$&").replace(/[\t\n\r ]/g, (character) => encodeURIComponent(character));
18911
+ return { start: start + destination.start.offset, end: start + destination.end.offset, replacement: escaped };
18363
18912
  }).filter((edit) => edit !== null).sort((a, b) => b.start - a.start);
18364
18913
  for (const node of markdownNodes(body)) for (const attribute of node.attributes ?? []) {
18365
18914
  if (!attribute.name || !["src", "poster", "href", "srcSet", "srcset", "img", "primaryHref", "secondaryHref"].includes(attribute.name) || !attribute.position) continue;
@@ -18391,133 +18940,8 @@ function rewriteMarkdownDestinations(body, rewrite) {
18391
18940
  }
18392
18941
 
18393
18942
  // src/remote-api.ts
18394
- import { createHash as createHash7 } from "crypto";
18395
- import { lookup } from "dns/promises";
18396
- import { request } from "https";
18397
- import { isIP } from "net";
18398
- var MAX_SPEC_BYTES2 = 25e6;
18399
- var MAX_REDIRECTS = 3;
18400
- function isPublicAddress(address, family) {
18401
- if (family === 4) {
18402
- const parts = address.split(".").map(Number);
18403
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
18404
- const [a, b] = parts;
18405
- return !(a === 0 || a === 10 || a === 127 || a >= 224 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && (b === 0 || b === 168) || a === 198 && (b === 18 || b === 19) || a === 203 && b === 0 || a === 198 && b === 51);
18406
- }
18407
- return family === 6 && /^[23][0-9a-f]{0,3}:/i.test(address) && !/^2001:db8:/i.test(address);
18408
- }
18409
- function validatedSpecUrl(value) {
18410
- const url = new URL(value);
18411
- if (url.protocol !== "https:" || url.username || url.password || isIP(url.hostname)) {
18412
- throw new Error("Remote OpenAPI references must use an HTTPS domain without credentials or an IP literal.");
18413
- }
18414
- url.hash = "";
18415
- return url;
18416
- }
18417
- async function getPinnedAddress(url) {
18418
- const addresses = await lookup(url.hostname, { all: true });
18419
- if (addresses.length === 0 || addresses.some((candidate) => !isPublicAddress(candidate.address, candidate.family))) {
18420
- throw new Error("The OpenAPI host did not resolve exclusively to public addresses.");
18421
- }
18422
- return addresses[0];
18423
- }
18424
- async function requestPinnedSpec(url) {
18425
- const pinned = await getPinnedAddress(url);
18426
- return new Promise((resolve5, reject) => {
18427
- const req = request(url, {
18428
- headers: { Accept: "application/json, application/yaml, text/yaml, */*", "User-Agent": "Thally-Migrate/1.0 (+https://thally.io)" },
18429
- signal: AbortSignal.timeout(3e4),
18430
- // Node's dual-stack connector asks lookup for `all: true`; that
18431
- // callback must receive address records, not a single address string.
18432
- lookup: (_hostname, options, callback) => {
18433
- if (typeof options === "object" && options.all) callback(null, [pinned]);
18434
- else callback(null, pinned.address, pinned.family);
18435
- }
18436
- }, (response) => {
18437
- const status = response.statusCode ?? 0;
18438
- if (status >= 300 && status < 400) {
18439
- response.resume();
18440
- resolve5({ status, location: response.headers.location, body: Buffer.alloc(0) });
18441
- return;
18442
- }
18443
- if (status < 200 || status >= 300) {
18444
- response.resume();
18445
- reject(new Error(`OpenAPI server returned ${status}.`));
18446
- return;
18447
- }
18448
- const statedBytes = Number(response.headers["content-length"] ?? 0);
18449
- if (statedBytes > MAX_SPEC_BYTES2) {
18450
- response.destroy();
18451
- reject(new Error("Remote OpenAPI spec exceeds the 25 MB import limit."));
18452
- return;
18453
- }
18454
- const chunks = [];
18455
- let bytes = 0;
18456
- response.on("data", (chunk) => {
18457
- bytes += chunk.length;
18458
- if (bytes > MAX_SPEC_BYTES2) {
18459
- response.destroy(new Error("Remote OpenAPI spec exceeds the 25 MB import limit."));
18460
- return;
18461
- }
18462
- chunks.push(chunk);
18463
- });
18464
- response.on("end", () => resolve5({ status, body: Buffer.concat(chunks) }));
18465
- response.on("error", reject);
18466
- });
18467
- req.on("error", reject);
18468
- req.end();
18469
- });
18470
- }
18471
- async function downloadSpec(value, fetcher) {
18472
- let url = validatedSpecUrl(value);
18473
- if (fetcher) {
18474
- const response = await fetcher(url, { accept: "application/json, application/yaml, text/yaml" });
18475
- validatedSpecUrl(response.finalUrl.toString());
18476
- const body = Buffer.from(response.body);
18477
- if (body.length > MAX_SPEC_BYTES2) throw new Error("Remote OpenAPI spec exceeds the 25 MB import limit.");
18478
- return body;
18479
- }
18480
- for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
18481
- const response = await requestPinnedSpec(url);
18482
- if (response.status >= 300 && response.status < 400) {
18483
- if (!response.location || hop === MAX_REDIRECTS) throw new Error("Remote OpenAPI spec redirected too many times.");
18484
- url = validatedSpecUrl(new URL(response.location, url).toString());
18485
- continue;
18486
- }
18487
- return response.body;
18488
- }
18489
- throw new Error("Remote OpenAPI spec redirected too many times.");
18490
- }
18491
- function parseOpenApi(parsed) {
18492
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("OpenAPI spec is not an object.");
18493
- const document = parsed;
18494
- if (typeof document.openapi !== "string" || !/^3\./.test(document.openapi)) {
18495
- throw new Error("Remote specification is not OpenAPI 3.x.");
18496
- }
18497
- if (!document.paths || typeof document.paths !== "object" || Array.isArray(document.paths)) {
18498
- throw new Error("Remote OpenAPI spec has no paths object.");
18499
- }
18500
- const paths = Object.entries(document.paths);
18501
- if (paths.length > 5e3) throw new Error("Remote OpenAPI spec has too many paths to import.");
18502
- const operations = [];
18503
- for (const [path, value] of paths) {
18504
- if (!value || typeof value !== "object" || Array.isArray(value) || isWithheld(value)) continue;
18505
- for (const [method, raw] of Object.entries(value)) {
18506
- if (!/^(get|post|put|patch|delete|head|options|trace)$/i.test(method) || !raw || typeof raw !== "object" || Array.isArray(raw)) continue;
18507
- const operation = raw;
18508
- if (isWithheld(operation)) continue;
18509
- const tags = Array.isArray(operation.tags) ? operation.tags : [];
18510
- operations.push({
18511
- method: method.toLowerCase(),
18512
- path,
18513
- tag: typeof tags[0] === "string" ? tags[0] : "API Reference",
18514
- summary: typeof operation.summary === "string" ? operation.summary : String(operation.operationId ?? "")
18515
- });
18516
- }
18517
- }
18518
- return operations;
18519
- }
18520
- function slug(value) {
18943
+ import { createHash as createHash8 } from "crypto";
18944
+ function slug2(value) {
18521
18945
  return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
18522
18946
  }
18523
18947
  function operationTarget(specId, operation) {
@@ -18566,7 +18990,7 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18566
18990
  }
18567
18991
  const { format: extension, document } = parseSpecDocument(body.toString("utf8"));
18568
18992
  const operations = parseOpenApi(document);
18569
- const filename = `openapi-${createHash7("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
18993
+ const filename = `openapi-${createHash8("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
18570
18994
  if (!assets.some((asset) => asset.path === `openapi/${filename}`)) assets.push({ path: `openapi/${filename}`, content: body, projectRelative: true });
18571
18995
  for (const page of pages) {
18572
18996
  const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;
@@ -18651,7 +19075,7 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18651
19075
  const listed = reference.directories?.length && reference.directories.every((directory2) => directory2.operations) ? new Set(reference.directories.flatMap((directory2) => directory2.operations ?? []).map((name) => name.trim().split(/\s+/)).map(([method, ...path]) => `${method.toLowerCase()} ${path.join(" ")}`)) : null;
18652
19076
  for (const operation of operations) {
18653
19077
  if (listed && !listed.has(`${operation.method} ${operation.path}`)) continue;
18654
- const key = `${slug(operation.tag)}/${slug(operation.summary)}`;
19078
+ const key = `${slug2(operation.tag)}/${slug2(operation.summary)}`;
18655
19079
  guesses.set(key, guesses.has(key) || !specId ? null : operationTarget(specId, operation));
18656
19080
  }
18657
19081
  }
@@ -18694,12 +19118,16 @@ export {
18694
19118
  detectRepositoryPlatform,
18695
19119
  gitmodulePaths,
18696
19120
  hydrateRemoteApiSpecs,
19121
+ hydrateUrlOpenApi,
18697
19122
  importSourceRef,
18698
19123
  isPassiveSvg,
19124
+ literalRoute,
18699
19125
  mergeMigrationConfig,
18700
19126
  migrateRepository,
18701
19127
  migrateUrl,
19128
+ migrationApiRoutes,
18702
19129
  normalizeMdx,
19130
+ pageEntryRoutes,
18703
19131
  parseGitHubRepositoryUrl,
18704
19132
  parseMarkdownPage,
18705
19133
  parseSourceRefFlags,