@thallylabs/migrate 0.2.42 → 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}$/;
@@ -7928,7 +8316,7 @@ function projectMigrationMedia(html, base) {
7928
8316
  }
7929
8317
 
7930
8318
  // src/url-static-content.ts
7931
- import { createHash as createHash2 } from "crypto";
8319
+ import { createHash as createHash3 } from "crypto";
7932
8320
  import { load as load3 } from "cheerio";
7933
8321
 
7934
8322
  // src/svg-policy.ts
@@ -8250,7 +8638,7 @@ function projectMigrationSvg(html) {
8250
8638
  if (svg.attr("xmlns") === void 0) svg.attr("xmlns", "http://www.w3.org/2000/svg");
8251
8639
  const content = Buffer.from($.xml(svg));
8252
8640
  if (!isPassiveSvg(content)) return null;
8253
- const sha256 = createHash2("sha256").update(content).digest("hex");
8641
+ const sha256 = createHash3("sha256").update(content).digest("hex");
8254
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);
8255
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();
8256
8644
  return { asset: { path: `migration-assets/${sha256}.svg`, content }, alternative };
@@ -8364,12 +8752,12 @@ function projectCapturedTabs($, root) {
8364
8752
  }
8365
8753
 
8366
8754
  // src/url-flight-content.ts
8367
- import { createHash as createHash3 } from "crypto";
8755
+ import { createHash as createHash4 } from "crypto";
8368
8756
  import remarkMdx3 from "remark-mdx";
8369
- import remarkParse3 from "remark-parse";
8757
+ import remarkParse4 from "remark-parse";
8370
8758
  import remarkGfm2 from "remark-gfm";
8371
- import { unified as unified3 } from "unified";
8372
- 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);
8373
8761
  var MAX_SOURCE_BYTES = 8e6;
8374
8762
  var sourceTrees = /* @__PURE__ */ new Map();
8375
8763
  var cachedSourceBytes = 0;
@@ -8541,7 +8929,7 @@ function sourceText(node) {
8541
8929
  return node.value ?? (node.children ?? []).map(sourceText).join("");
8542
8930
  }
8543
8931
  function sourceNodes(source) {
8544
- const key = createHash3("sha256").update(source).digest("hex");
8932
+ const key = createHash4("sha256").update(source).digest("hex");
8545
8933
  const cached = sourceTrees.get(key);
8546
8934
  if (cached) {
8547
8935
  sourceTrees.delete(key);
@@ -8890,7 +9278,7 @@ function recoverFlightDisclosures($, root, document, article, projectionBudget)
8890
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;
8891
9279
  let html;
8892
9280
  if (!panel.text().trim() && !panel.children().length) {
8893
- 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)}`;
8894
9282
  const rendered = flightStaticHtml(document, source.props.children, prefix, projectionBudget);
8895
9283
  if (rendered === null || !rendered.trim()) continue;
8896
9284
  html = rendered;
@@ -9129,8 +9517,13 @@ function recoverFlightArticleCode($, root) {
9129
9517
 
9130
9518
  // src/redirects.ts
9131
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
+ }
9132
9525
  function exactRoute(route) {
9133
- return route.startsWith("/") && !/[?:*#()[\]{}]/.test(route) ? route.replace(/\/+$/, "") || "/" : null;
9526
+ return route.startsWith("/") && !/[?:*#()[\]{}]/.test(route) ? trimTrailingSlashes2(route) || "/" : null;
9134
9527
  }
9135
9528
  function literalRoute(route) {
9136
9529
  return exactRoute(route)?.toLowerCase() ?? null;
@@ -9393,8 +9786,8 @@ var MINTLIFY_ARTICLE_LABELS = [
9393
9786
  { baseUrl: "Select base URL", permalink: "Navigate to header" },
9394
9787
  { baseUrl: "\u9009\u62E9\u57FA\u7840URL", permalink: "\u5BFC\u822A\u5230\u6807\u9898" }
9395
9788
  ];
9396
- var remoteMarkdownParser = unified4().use(remarkParse4).use(remarkMdx4);
9397
- var embeddedOpenApiParser = unified4().use(remarkParse4);
9789
+ var remoteMarkdownParser = unified5().use(remarkParse5).use(remarkMdx4);
9790
+ var embeddedOpenApiParser = unified5().use(remarkParse5);
9398
9791
  function validateMigrationUrl(value) {
9399
9792
  let url;
9400
9793
  try {
@@ -10010,9 +10403,13 @@ function migratedHref(href, currentUrl, source, scopePath, importedIds, sourceHo
10010
10403
  return `/${target2.search}${target2.hash}`;
10011
10404
  }
10012
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}`;
10013
10409
  if (scopePath !== "/" && (path === scopePath || path.startsWith(`${scopePath}/`))) {
10014
10410
  path = path.slice(scopePath.length);
10015
10411
  }
10412
+ if (["/llms.txt", "/llms-full.txt", "/openapi.json", "/openapi.yaml", "/robots.txt", "/sitemap.xml"].includes(path)) return `${path}${target2.search}${target2.hash}`;
10016
10413
  const id = pageIdFromReference(path.replace(/^\/+/, "") || "introduction");
10017
10414
  const localizedId = id ? canonicalLocalizedPageId(id) : id;
10018
10415
  const destinationId = localizedId === sourceHomePageId ? "introduction" : localizedId;
@@ -10022,7 +10419,7 @@ function migratedHref(href, currentUrl, source, scopePath, importedIds, sourceHo
10022
10419
  }
10023
10420
  if (importedDestinationId) {
10024
10421
  const destination = importedDestinationId === "introduction" ? "/" : `/${importedDestinationId}`;
10025
- return `${destination}${target2.search}${target2.hash}`;
10422
+ return `${destination === "/" && isMarkdown ? "/introduction" : destination}${isMarkdown ? ".md" : ""}${target2.search}${target2.hash}`;
10026
10423
  }
10027
10424
  return target2.toString();
10028
10425
  } catch {
@@ -11197,9 +11594,9 @@ async function migrateUrl(options) {
11197
11594
  ]
11198
11595
  };
11199
11596
  }
11200
- if (platform === "mintlify") docsConfig = addMintlifyDirectoryRedirects(docsConfig, pages);
11597
+ if (platform === "mintlify") docsConfig = { ...addMintlifyDirectoryRedirects(docsConfig, pages), markdown: { enabled: true } };
11201
11598
  docsConfig = dropConflictingRedirects(dropUnroutableRedirects(docsConfig, warnings), pages, warnings);
11202
- return {
11599
+ const bundle = {
11203
11600
  sourceUrl: source.toString(),
11204
11601
  sourceKind: "url",
11205
11602
  platform,
@@ -11219,6 +11616,20 @@ async function migrateUrl(options) {
11219
11616
  skipped: Math.max(0, visited.size - pages.length)
11220
11617
  }
11221
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;
11222
11633
  }
11223
11634
 
11224
11635
  // src/source-refs.ts
@@ -12370,12 +12781,12 @@ function classifyApiSpec(path) {
12370
12781
  return /asyncapi/i.test(topLevel) ? "asyncapi" : "openapi";
12371
12782
  }
12372
12783
  var SPEC_DIRECTORY = "openapi";
12373
- var MAX_SPEC_BYTES = 5e7;
12784
+ var MAX_SPEC_BYTES2 = 5e7;
12374
12785
  function readSpecFile(absolutePath, relativePath, warnings) {
12375
- if (lstatSync6(absolutePath).size > MAX_SPEC_BYTES) {
12786
+ if (lstatSync6(absolutePath).size > MAX_SPEC_BYTES2) {
12376
12787
  warnings.push({
12377
12788
  code: "skipped-file",
12378
- 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.`,
12379
12790
  source: relativePath
12380
12791
  });
12381
12792
  return void 0;
@@ -12601,7 +13012,7 @@ function apiTabSpecIds(tabs) {
12601
13012
  return tabs.filter((tab) => !tab.hidden && tab.api).map((tab, index) => ({ tab, specId: index === 0 ? "default" : slugifyApiSpecId(tab.tab) }));
12602
13013
  }
12603
13014
  var flagged = (value) => value === true || value === "true";
12604
- var isWithheld = (node) => flagged(node["x-hidden"]) || flagged(node["x-excluded"]);
13015
+ var isWithheld2 = (node) => flagged(node["x-hidden"]) || flagged(node["x-excluded"]);
12605
13016
  var isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
12606
13017
  function resolveLocalSpecRef(document, ref) {
12607
13018
  if (ref !== "#" && !ref.startsWith("#/")) return void 0;
@@ -12628,7 +13039,7 @@ function viewSpecPathItem(document, entry) {
12628
13039
  }
12629
13040
  const item = Object.assign({}, ...[...chain].reverse());
12630
13041
  delete item.$ref;
12631
- return { item, withheld: chain.some(isWithheld) };
13042
+ return { item, withheld: chain.some(isWithheld2) };
12632
13043
  }
12633
13044
  function parseOpenApiOperationsForLinking(content) {
12634
13045
  const document = readSpecDocument(content);
@@ -12652,7 +13063,7 @@ function visibleOperations(document, entries) {
12652
13063
  const operation = item[method];
12653
13064
  if (!operation || typeof operation !== "object") continue;
12654
13065
  const op = operation;
12655
- if (isWithheld(op)) continue;
13066
+ if (isWithheld2(op)) continue;
12656
13067
  const tags = Array.isArray(op.tags) ? op.tags.filter((tag) => typeof tag === "string") : [];
12657
13068
  operations.push({
12658
13069
  method,
@@ -12666,6 +13077,28 @@ function visibleOperations(document, entries) {
12666
13077
  }
12667
13078
  return operations;
12668
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
+ }
12669
13102
  function servedOperations(source, parsed) {
12670
13103
  let operations = parsed.get(source.filename);
12671
13104
  if (!operations) {
@@ -13379,10 +13812,13 @@ function rewriteMintlifyMountedLinks(body, pageIds) {
13379
13812
  if (!pathname.startsWith("/") || pathname.startsWith("//") || pathname.includes("\\")) return target2;
13380
13813
  const segments = pathname.slice(1).split("/");
13381
13814
  if (segments.some((segment) => !segment || segment === "." || segment === "..")) return target2;
13382
- 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;
13383
13819
  for (let count = 1; count < segments.length && count <= 3; count++) {
13384
- const candidate = segments.slice(count).join("/");
13385
- if (pageIds.has(candidate)) return `/${candidate}${suffix}`;
13820
+ const candidate = routeSegments.slice(count).join("/");
13821
+ if (pageIds.has(candidate)) return `/${candidate}${markdown ? ".md" : ""}${suffix}`;
13386
13822
  }
13387
13823
  return target2;
13388
13824
  };
@@ -13462,11 +13898,11 @@ function preserveLinkedAnchors(pages) {
13462
13898
  const headingSlugs = /* @__PURE__ */ new Map();
13463
13899
  const slugCounts = /* @__PURE__ */ new Map();
13464
13900
  for (const heading of headings) {
13465
- const slug2 = heading.text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
13466
- existing.add(slug2);
13467
- const seen = slugCounts.get(slug2) ?? 0;
13468
- slugCounts.set(slug2, seen + 1);
13469
- 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);
13470
13906
  }
13471
13907
  const headingAliases = /* @__PURE__ */ new Map();
13472
13908
  const tableAliases = /* @__PURE__ */ new Map();
@@ -13531,9 +13967,9 @@ function addDatedHeadingAliases(body, date) {
13531
13967
  if (open) return [line];
13532
13968
  const heading = line.match(/^#{2,6}\s+(.+?)\s*#*\s*$/);
13533
13969
  if (!heading) return [line];
13534
- const slug2 = heading[1].replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_]/g, "").normalize("NFKD").toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/\s+/g, "-");
13535
- const id = `${date}-${slug2}`;
13536
- 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];
13537
13973
  existingIds.add(id);
13538
13974
  return [`<a id="${id}" />`, "", line];
13539
13975
  }).join("\n");
@@ -14751,9 +15187,9 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
14751
15187
  // hierarchy (never just its own segment) and wins over a docs.yml slug.
14752
15188
  ...platform === "fern" ? {
14753
15189
  resolveIdentity: (frontmatter, fallback) => {
14754
- const slug2 = typeof frontmatter.slug === "string" ? frontmatter.slug.trim() : "";
14755
- if (!slug2) return fallback;
14756
- 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);
14757
15193
  if (!overridden) return fallback;
14758
15194
  fernIdRenames.set(fallback.navigationId, overridden);
14759
15195
  return { ...fallback, id: overridden, navigationId: overridden };
@@ -15135,7 +15571,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
15135
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.`
15136
15572
  });
15137
15573
  if (rootStyleProjection.css.trim()) {
15138
- 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);
15139
15575
  const assetPath = `migrated/${identity}/style.css`;
15140
15576
  const content = Buffer.from(rootStyleProjection.css);
15141
15577
  assets.push({ path: assetPath, content });
@@ -16022,6 +16458,32 @@ function renderPage(bundle, page) {
16022
16458
  function isIntegrationsObject(value) {
16023
16459
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
16024
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
+ }
16025
16487
  function mergeMigrationConfig(existing, incoming) {
16026
16488
  const tabs = existing.tabs.filter((tab) => tab.tab.toLowerCase() !== "changelog");
16027
16489
  const names = new Set(tabs.map((tab) => tab.tab.toLowerCase()));
@@ -16089,6 +16551,10 @@ function mergeMigrationConfig(existing, incoming) {
16089
16551
  ...incoming,
16090
16552
  ...existing,
16091
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 } } : {},
16092
16558
  // Collection presentation belongs to the imported information
16093
16559
  // architecture; starter defaults must not coerce source dropdowns to tabs.
16094
16560
  navigation: incoming.navigation ?? existing.navigation,
@@ -16120,12 +16586,12 @@ function renderMigrationFiles(bundle, options = {}) {
16120
16586
  }
16121
16587
 
16122
16588
  // src/url-run.ts
16123
- import { createHash as createHash6 } from "crypto";
16589
+ import { createHash as createHash7 } from "crypto";
16124
16590
  import { load as load8 } from "cheerio";
16125
16591
  import { parse as parse2, postprocess, preprocess } from "micromark";
16126
- import remarkParse5 from "remark-parse";
16592
+ import remarkParse6 from "remark-parse";
16127
16593
  import remarkMdx5 from "remark-mdx";
16128
- import { unified as unified5 } from "unified";
16594
+ import { unified as unified6 } from "unified";
16129
16595
 
16130
16596
  // src/url-asset-cache.ts
16131
16597
  function createMigrationAssetCache() {
@@ -16656,7 +17122,7 @@ function mergeSourceNavigation(existing, incoming) {
16656
17122
  }
16657
17123
 
16658
17124
  // src/url-identity.ts
16659
- import { createHash as createHash5 } from "crypto";
17125
+ import { createHash as createHash6 } from "crypto";
16660
17126
  import { load as load7 } from "cheerio";
16661
17127
  var directory = (path) => path.replace(/\/index\/?$/i, "").replace(/\/$/, "") || "/";
16662
17128
  function migrationSectionQueryCandidate(source, anchors) {
@@ -16717,7 +17183,7 @@ function observedMigrationArticleIdentity(document) {
16717
17183
  canonical2.pathname = canonical2.pathname.replace(/\/+$/, "") || "/";
16718
17184
  const removed = [...source.searchParams].filter(([key, value]) => !canonical2.searchParams.getAll(key).includes(value));
16719
17185
  const sectionAnchor = removed.length === 1 && anchors.includes(removed[0][1]) ? removed[0][1] : void 0;
16720
- 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 } : {} };
16721
17187
  }
16722
17188
 
16723
17189
  // src/url-api-schema-controls.ts
@@ -17044,8 +17510,8 @@ function apiOperationCodeControls(body, specification, operation) {
17044
17510
  const metadata = object2(resolveValue(value));
17045
17511
  const declaration = resolveValue(metadata?.openapi);
17046
17512
  if (typeof declaration !== "string") return false;
17047
- const tokens = declaration.trim().split(/\s+/);
17048
- 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;
17049
17515
  };
17050
17516
  const active = /* @__PURE__ */ new Set();
17051
17517
  const visit = (input, provider, group, depth, owners = []) => {
@@ -17091,9 +17557,9 @@ function apiOperationCodeControls(body, specification, operation) {
17091
17557
  const hint = object2(resolveValue(scope?.pageMetadata)), metadata = object2(resolveValue(props.pageMetadata));
17092
17558
  const declaration = resolveValue(hint?.openapi);
17093
17559
  if (typeof declaration !== "string" || typeof hint?.href !== "string" || !hint.href || hint.href !== metadata?.href) return false;
17094
- const tokens = declaration.trim().split(/\s+/);
17560
+ const tokens2 = declaration.trim().split(/\s+/);
17095
17561
  const template = (path) => path.split("/").map((segment) => /^\{[^{}\/]+\}$/.test(segment) ? "{}" : segment).join("/");
17096
- 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);
17097
17563
  })()) generatedControllers += 1;
17098
17564
  }
17099
17565
  let local = group;
@@ -17202,14 +17668,14 @@ function apiOperationCodeControls(body, specification, operation) {
17202
17668
 
17203
17669
  // src/url-run.ts
17204
17670
  var CONVERTER_VERSION = "url-evidence-v7";
17205
- var parser3 = unified5().use(remarkParse5).use(remarkMdx5);
17671
+ var parser3 = unified6().use(remarkParse6).use(remarkMdx5);
17206
17672
  var ACCEPT = "text/html,application/xhtml+xml";
17207
17673
  var MAX_RESPONSE_BYTES = 8e6;
17208
17674
  var MAX_ASSET_BYTES2 = 1e7;
17209
17675
  var MAX_RECORDS = 1e5;
17210
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"]);
17211
17677
  function hash2(value) {
17212
- return createHash6("sha256").update(value).digest("hex");
17678
+ return createHash7("sha256").update(value).digest("hex");
17213
17679
  }
17214
17680
  function canonicalJson(value) {
17215
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;
@@ -18474,133 +18940,8 @@ function rewriteMarkdownDestinations(body, rewrite) {
18474
18940
  }
18475
18941
 
18476
18942
  // src/remote-api.ts
18477
- import { createHash as createHash7 } from "crypto";
18478
- import { lookup } from "dns/promises";
18479
- import { request } from "https";
18480
- import { isIP } from "net";
18481
- var MAX_SPEC_BYTES2 = 25e6;
18482
- var MAX_REDIRECTS = 3;
18483
- function isPublicAddress(address, family) {
18484
- if (family === 4) {
18485
- const parts = address.split(".").map(Number);
18486
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
18487
- const [a, b] = parts;
18488
- 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);
18489
- }
18490
- return family === 6 && /^[23][0-9a-f]{0,3}:/i.test(address) && !/^2001:db8:/i.test(address);
18491
- }
18492
- function validatedSpecUrl(value) {
18493
- const url = new URL(value);
18494
- if (url.protocol !== "https:" || url.username || url.password || isIP(url.hostname)) {
18495
- throw new Error("Remote OpenAPI references must use an HTTPS domain without credentials or an IP literal.");
18496
- }
18497
- url.hash = "";
18498
- return url;
18499
- }
18500
- async function getPinnedAddress(url) {
18501
- const addresses = await lookup(url.hostname, { all: true });
18502
- if (addresses.length === 0 || addresses.some((candidate) => !isPublicAddress(candidate.address, candidate.family))) {
18503
- throw new Error("The OpenAPI host did not resolve exclusively to public addresses.");
18504
- }
18505
- return addresses[0];
18506
- }
18507
- async function requestPinnedSpec(url) {
18508
- const pinned = await getPinnedAddress(url);
18509
- return new Promise((resolve5, reject) => {
18510
- const req = request(url, {
18511
- headers: { Accept: "application/json, application/yaml, text/yaml, */*", "User-Agent": "Thally-Migrate/1.0 (+https://thally.io)" },
18512
- signal: AbortSignal.timeout(3e4),
18513
- // Node's dual-stack connector asks lookup for `all: true`; that
18514
- // callback must receive address records, not a single address string.
18515
- lookup: (_hostname, options, callback) => {
18516
- if (typeof options === "object" && options.all) callback(null, [pinned]);
18517
- else callback(null, pinned.address, pinned.family);
18518
- }
18519
- }, (response) => {
18520
- const status = response.statusCode ?? 0;
18521
- if (status >= 300 && status < 400) {
18522
- response.resume();
18523
- resolve5({ status, location: response.headers.location, body: Buffer.alloc(0) });
18524
- return;
18525
- }
18526
- if (status < 200 || status >= 300) {
18527
- response.resume();
18528
- reject(new Error(`OpenAPI server returned ${status}.`));
18529
- return;
18530
- }
18531
- const statedBytes = Number(response.headers["content-length"] ?? 0);
18532
- if (statedBytes > MAX_SPEC_BYTES2) {
18533
- response.destroy();
18534
- reject(new Error("Remote OpenAPI spec exceeds the 25 MB import limit."));
18535
- return;
18536
- }
18537
- const chunks = [];
18538
- let bytes = 0;
18539
- response.on("data", (chunk) => {
18540
- bytes += chunk.length;
18541
- if (bytes > MAX_SPEC_BYTES2) {
18542
- response.destroy(new Error("Remote OpenAPI spec exceeds the 25 MB import limit."));
18543
- return;
18544
- }
18545
- chunks.push(chunk);
18546
- });
18547
- response.on("end", () => resolve5({ status, body: Buffer.concat(chunks) }));
18548
- response.on("error", reject);
18549
- });
18550
- req.on("error", reject);
18551
- req.end();
18552
- });
18553
- }
18554
- async function downloadSpec(value, fetcher) {
18555
- let url = validatedSpecUrl(value);
18556
- if (fetcher) {
18557
- const response = await fetcher(url, { accept: "application/json, application/yaml, text/yaml" });
18558
- validatedSpecUrl(response.finalUrl.toString());
18559
- const body = Buffer.from(response.body);
18560
- if (body.length > MAX_SPEC_BYTES2) throw new Error("Remote OpenAPI spec exceeds the 25 MB import limit.");
18561
- return body;
18562
- }
18563
- for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
18564
- const response = await requestPinnedSpec(url);
18565
- if (response.status >= 300 && response.status < 400) {
18566
- if (!response.location || hop === MAX_REDIRECTS) throw new Error("Remote OpenAPI spec redirected too many times.");
18567
- url = validatedSpecUrl(new URL(response.location, url).toString());
18568
- continue;
18569
- }
18570
- return response.body;
18571
- }
18572
- throw new Error("Remote OpenAPI spec redirected too many times.");
18573
- }
18574
- function parseOpenApi(parsed) {
18575
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("OpenAPI spec is not an object.");
18576
- const document = parsed;
18577
- if (typeof document.openapi !== "string" || !/^3\./.test(document.openapi)) {
18578
- throw new Error("Remote specification is not OpenAPI 3.x.");
18579
- }
18580
- if (!document.paths || typeof document.paths !== "object" || Array.isArray(document.paths)) {
18581
- throw new Error("Remote OpenAPI spec has no paths object.");
18582
- }
18583
- const paths = Object.entries(document.paths);
18584
- if (paths.length > 5e3) throw new Error("Remote OpenAPI spec has too many paths to import.");
18585
- const operations = [];
18586
- for (const [path, value] of paths) {
18587
- if (!value || typeof value !== "object" || Array.isArray(value) || isWithheld(value)) continue;
18588
- for (const [method, raw] of Object.entries(value)) {
18589
- if (!/^(get|post|put|patch|delete|head|options|trace)$/i.test(method) || !raw || typeof raw !== "object" || Array.isArray(raw)) continue;
18590
- const operation = raw;
18591
- if (isWithheld(operation)) continue;
18592
- const tags = Array.isArray(operation.tags) ? operation.tags : [];
18593
- operations.push({
18594
- method: method.toLowerCase(),
18595
- path,
18596
- tag: typeof tags[0] === "string" ? tags[0] : "API Reference",
18597
- summary: typeof operation.summary === "string" ? operation.summary : String(operation.operationId ?? "")
18598
- });
18599
- }
18600
- }
18601
- return operations;
18602
- }
18603
- function slug(value) {
18943
+ import { createHash as createHash8 } from "crypto";
18944
+ function slug2(value) {
18604
18945
  return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
18605
18946
  }
18606
18947
  function operationTarget(specId, operation) {
@@ -18649,7 +18990,7 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18649
18990
  }
18650
18991
  const { format: extension, document } = parseSpecDocument(body.toString("utf8"));
18651
18992
  const operations = parseOpenApi(document);
18652
- 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}`;
18653
18994
  if (!assets.some((asset) => asset.path === `openapi/${filename}`)) assets.push({ path: `openapi/${filename}`, content: body, projectRelative: true });
18654
18995
  for (const page of pages) {
18655
18996
  const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;
@@ -18734,7 +19075,7 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
18734
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;
18735
19076
  for (const operation of operations) {
18736
19077
  if (listed && !listed.has(`${operation.method} ${operation.path}`)) continue;
18737
- const key = `${slug(operation.tag)}/${slug(operation.summary)}`;
19078
+ const key = `${slug2(operation.tag)}/${slug2(operation.summary)}`;
18738
19079
  guesses.set(key, guesses.has(key) || !specId ? null : operationTarget(specId, operation));
18739
19080
  }
18740
19081
  }
@@ -18777,12 +19118,16 @@ export {
18777
19118
  detectRepositoryPlatform,
18778
19119
  gitmodulePaths,
18779
19120
  hydrateRemoteApiSpecs,
19121
+ hydrateUrlOpenApi,
18780
19122
  importSourceRef,
18781
19123
  isPassiveSvg,
19124
+ literalRoute,
18782
19125
  mergeMigrationConfig,
18783
19126
  migrateRepository,
18784
19127
  migrateUrl,
19128
+ migrationApiRoutes,
18785
19129
  normalizeMdx,
19130
+ pageEntryRoutes,
18786
19131
  parseGitHubRepositoryUrl,
18787
19132
  parseMarkdownPage,
18788
19133
  parseSourceRefFlags,