@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.
@@ -1,9 +1,10 @@
1
1
  // src/url-run.ts
2
- import { createHash as createHash4 } from "crypto";
2
+ import { createHash as createHash5 } from "crypto";
3
3
  import { load as load7 } from "cheerio";
4
- import remarkParse4 from "remark-parse";
4
+ import { parse as parse2, postprocess, preprocess } from "micromark";
5
+ import remarkParse5 from "remark-parse";
5
6
  import remarkMdx4 from "remark-mdx";
6
- import { unified as unified4 } from "unified";
7
+ import { unified as unified5 } from "unified";
7
8
 
8
9
  // src/navigation.ts
9
10
  import { existsSync, lstatSync, readFileSync, realpathSync as realpathSync2 } from "fs";
@@ -496,15 +497,15 @@ function createMigrationAssetCache() {
496
497
  load(url, acquire) {
497
498
  const existing = get(url);
498
499
  if (existing) return existing;
499
- const request = Promise.resolve().then(acquire).then((asset) => {
500
+ const request2 = Promise.resolve().then(acquire).then((asset) => {
500
501
  pending.delete(url);
501
502
  return remember(url, asset);
502
503
  }, (error) => {
503
504
  pending.delete(url);
504
505
  throw error;
505
506
  });
506
- pending.set(url, request);
507
- return request;
507
+ pending.set(url, request2);
508
+ return request2;
508
509
  },
509
510
  delete(url) {
510
511
  settled.delete(url);
@@ -1815,7 +1816,10 @@ var VIDEO_ATTRIBUTES = /* @__PURE__ */ new Set([
1815
1816
  "width",
1816
1817
  "height",
1817
1818
  "preload",
1818
- "aria-label"
1819
+ "aria-label",
1820
+ // Publisher asset provenance does not change playback and is never emitted.
1821
+ // Keep this explicit: arbitrary data attributes may drive custom controls.
1822
+ "data-path"
1819
1823
  ]);
1820
1824
  var YOUTUBE_ATTRIBUTES = /* @__PURE__ */ new Set([
1821
1825
  "src",
@@ -1880,16 +1884,27 @@ function projectMigrationMedia(html, base) {
1880
1884
  }
1881
1885
 
1882
1886
  // src/url-identity.ts
1883
- import { createHash as createHash3 } from "crypto";
1887
+ import { createHash as createHash4 } from "crypto";
1884
1888
  import { load as load6 } from "cheerio";
1885
1889
 
1886
1890
  // src/url.ts
1887
1891
  import { load as load5 } from "cheerio";
1888
1892
  import remarkMdx3 from "remark-mdx";
1889
- import remarkParse3 from "remark-parse";
1893
+ import remarkParse4 from "remark-parse";
1890
1894
  import TurndownService from "turndown";
1891
- import { unified as unified3 } from "unified";
1892
- import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
1895
+ import { unified as unified4 } from "unified";
1896
+ import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
1897
+
1898
+ // src/url-openapi.ts
1899
+ import { createHash as createHash2 } from "crypto";
1900
+
1901
+ // src/remote-spec.ts
1902
+ import { lookup } from "dns/promises";
1903
+ import { request } from "https";
1904
+ import { isIP } from "net";
1905
+
1906
+ // src/spec-exclude.ts
1907
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
1893
1908
 
1894
1909
  // src/analytics.ts
1895
1910
  var HOSTNAME_LABEL = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?";
@@ -1901,8 +1916,8 @@ import { compileSync } from "@mdx-js/mdx";
1901
1916
  import { nameToEmoji } from "gemoji";
1902
1917
  import remarkGfm2 from "remark-gfm";
1903
1918
  import remarkMdx2 from "remark-mdx";
1904
- import remarkParse2 from "remark-parse";
1905
- import { unified as unified2 } from "unified";
1919
+ import remarkParse3 from "remark-parse";
1920
+ import { unified as unified3 } from "unified";
1906
1921
 
1907
1922
  // src/builtin-components.ts
1908
1923
  var THALLY_BUILTIN_COMPONENTS = /* @__PURE__ */ new Set([
@@ -2339,13 +2354,13 @@ function unresolvedExpressionNames(expression, pageNames) {
2339
2354
  }
2340
2355
 
2341
2356
  // src/frontmatter.ts
2342
- import { parse as parseYaml } from "yaml";
2357
+ import { parse as parseYaml2 } from "yaml";
2343
2358
  function salvageFrontmatterLines(matter) {
2344
2359
  const data = {};
2345
2360
  for (const line of matter.split(/\r?\n/)) {
2346
2361
  if (!/^[A-Za-z_][\w-]*:\s?/.test(line)) continue;
2347
2362
  try {
2348
- const parsedLine = parseYaml(line);
2363
+ const parsedLine = parseYaml2(line);
2349
2364
  if (parsedLine && typeof parsedLine === "object" && !Array.isArray(parsedLine)) {
2350
2365
  Object.assign(data, parsedLine);
2351
2366
  }
@@ -2369,7 +2384,7 @@ function parseFrontmatter(raw) {
2369
2384
  return { content, data: {} };
2370
2385
  }
2371
2386
  try {
2372
- const parsed = parseYaml(matter);
2387
+ const parsed = parseYaml2(matter);
2373
2388
  return {
2374
2389
  content,
2375
2390
  data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
@@ -2383,13 +2398,103 @@ function parseFrontmatter(raw) {
2383
2398
  }
2384
2399
  }
2385
2400
 
2401
+ // src/fence-meta.ts
2402
+ import remarkParse2 from "remark-parse";
2403
+ import { unified as unified2 } from "unified";
2404
+ function delimitedEnd(meta, start) {
2405
+ const opening = meta[start];
2406
+ if (opening === '"' || opening === "'") {
2407
+ let close = start + 1;
2408
+ while (close < meta.length && meta[close] !== opening) close += meta[close] === "\\" && meta[close + 1] === opening ? 2 : 1;
2409
+ return close < meta.length ? close + 1 : close;
2410
+ }
2411
+ if (opening !== "{") return void 0;
2412
+ let depth = 0;
2413
+ let quote = "";
2414
+ for (let index = start; index < meta.length; index++) {
2415
+ const char = meta[index];
2416
+ if (quote) {
2417
+ if (char === quote) quote = "";
2418
+ } else if (char === '"' || char === "'") quote = char;
2419
+ else if (char === "{") depth++;
2420
+ else if (char === "}" && --depth === 0) return index + 1;
2421
+ }
2422
+ return meta.length;
2423
+ }
2424
+ function tokens(meta) {
2425
+ const result = [];
2426
+ let index = 0;
2427
+ while (index < meta.length) {
2428
+ if (/\s/.test(meta[index])) {
2429
+ index++;
2430
+ continue;
2431
+ }
2432
+ const start = index;
2433
+ const delimited = delimitedEnd(meta, index);
2434
+ if (delimited !== void 0) {
2435
+ result.push({ start, end: delimited, value: meta.slice(start, delimited), quoted: meta[start] !== "{", group: meta[start] === "{" });
2436
+ index = delimited;
2437
+ continue;
2438
+ }
2439
+ const key = /^([A-Za-z][\w-]*)=/.exec(meta.slice(index));
2440
+ if (key) {
2441
+ index += key[0].length;
2442
+ const groupEnd = delimitedEnd(meta, index);
2443
+ if (groupEnd !== void 0) index = groupEnd;
2444
+ else while (index < meta.length && !/\s/.test(meta[index])) index++;
2445
+ result.push({ start, end: index, key: key[1], value: meta.slice(start, index) });
2446
+ continue;
2447
+ }
2448
+ while (index < meta.length && !/\s/.test(meta[index])) {
2449
+ if (index > start && meta[index] === "{" && /^\{[\d,\s-]*\}/.test(meta.slice(index))) break;
2450
+ index++;
2451
+ }
2452
+ result.push({ start, end: index, value: meta.slice(start, index) });
2453
+ }
2454
+ return result;
2455
+ }
2456
+ function normalizeMintlifyCodeFences(body, warn) {
2457
+ const tree = unified2().use(remarkParse2).parse(body);
2458
+ const edits = [];
2459
+ const removed = /* @__PURE__ */ new Set();
2460
+ const unknown = /* @__PURE__ */ new Set();
2461
+ const supported = /* @__PURE__ */ new Set(["title", "filename", "framework", "tag", "icon", "highlight", "focus", "wrap", "lines", "expandable", "nocopy"]);
2462
+ function visit(node) {
2463
+ if (node.type === "code" && node.position?.start.offset !== void 0) {
2464
+ const start = node.position.start.offset;
2465
+ const lineEnd = body.indexOf("\n", start);
2466
+ const end = lineEnd < 0 ? body.length : lineEnd;
2467
+ const line = body.slice(start, end);
2468
+ const opening = /^([`~]{3,}[^\s]*)([ \t]+)(.*?)(\r?)$/.exec(line);
2469
+ if (opening) {
2470
+ const meta = opening[3];
2471
+ const kept = tokens(meta).filter((token) => {
2472
+ if (token.key === "theme" || !token.key && !token.quoted && !token.group && token.value === "twoslash") {
2473
+ removed.add(token.key ?? token.value);
2474
+ return false;
2475
+ }
2476
+ if (token.key && !supported.has(token.key)) unknown.add(token.key);
2477
+ return true;
2478
+ });
2479
+ const normalized = kept.map((token) => meta.slice(token.start, token.end)).join(" ");
2480
+ if (normalized !== meta && kept.length !== tokens(meta).length) edits.push({ start, end, value: `${opening[1]}${normalized ? ` ${normalized}` : ""}${opening[4]}` });
2481
+ }
2482
+ }
2483
+ for (const child of node.children ?? []) visit(child);
2484
+ }
2485
+ visit(tree);
2486
+ if (removed.size) warn?.(`Removed unsupported Mintlify code-fence options: ${[...removed].join(", ")}. Code examples and supported display options were preserved.`);
2487
+ if (unknown.size) warn?.(`Unsupported Mintlify code-fence options are ignored: ${[...unknown].join(", ")}. Review these renderer-specific options.`);
2488
+ return edits.sort((a, b) => b.start - a.start).reduce((text2, edit) => text2.slice(0, edit.start) + edit.value + text2.slice(edit.end), body);
2489
+ }
2490
+
2386
2491
  // src/mdx.ts
2387
2492
  function titleFromId(id) {
2388
2493
  const value = id.split("/").at(-1) ?? id;
2389
2494
  if (value === "introduction") return "Introduction";
2390
2495
  return value.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
2391
2496
  }
2392
- var descriptionParser = unified2().use(remarkParse2).use(remarkGfm2).use(remarkMdx2);
2497
+ var descriptionParser = unified3().use(remarkParse3).use(remarkGfm2).use(remarkMdx2);
2393
2498
  function plainText(node) {
2394
2499
  if (node.type === "text" || node.type === "inlineCode") return node.value ?? "";
2395
2500
  if (node.type === "image") return node.alt ?? "";
@@ -3404,13 +3509,41 @@ function externalPageUrl(value) {
3404
3509
  }
3405
3510
  function parseMarkdownPage(input) {
3406
3511
  const parsed = parseFrontmatter(input.raw);
3512
+ if (input.platform === "mintlify") {
3513
+ const supported = /* @__PURE__ */ new Set([
3514
+ "title",
3515
+ "description",
3516
+ "sidebarTitle",
3517
+ "navTitle",
3518
+ "icon",
3519
+ "iconType",
3520
+ "tag",
3521
+ "badge",
3522
+ "keywords",
3523
+ "mode",
3524
+ "hidden",
3525
+ "noindex",
3526
+ "url",
3527
+ "openapi",
3528
+ "api",
3529
+ "authMethod",
3530
+ "playground",
3531
+ // Access settings are enforced by the repository classifier, never lost here.
3532
+ "groups",
3533
+ "public",
3534
+ ...PAGE_META_FIELDS.map(([field]) => field)
3535
+ ]);
3536
+ const dropped = Object.keys(parsed.data).filter((key) => !supported.has(key));
3537
+ if (dropped.length) input.warn?.(`Unsupported Mintlify frontmatter fields were omitted: ${dropped.join(", ")}. Review their behavior before publishing.`);
3538
+ }
3407
3539
  const fallbackIdentity = {
3408
3540
  id: input.id,
3409
3541
  navigationId: input.navigationId ?? input.id,
3410
3542
  ...input.locale ? { locale: input.locale } : {}
3411
3543
  };
3412
3544
  const identity = input.resolveIdentity?.(parsed.data, fallbackIdentity) ?? fallbackIdentity;
3413
- const body = removeUndefinedExpressions(injectReactHookImports(normalizeMdx(parsed.content, input.platform)), input.warn).trim();
3545
+ const normalized = normalizeMdx(parsed.content, input.platform);
3546
+ const body = removeUndefinedExpressions(injectReactHookImports(input.platform === "mintlify" ? normalizeMintlifyCodeFences(normalized, input.warn) : normalized), input.warn).trim();
3414
3547
  const keywords = Array.isArray(parsed.data.keywords) ? parsed.data.keywords.filter((value) => typeof value === "string") : [];
3415
3548
  const heading = leadingPageHeading(body);
3416
3549
  const title = typeof parsed.data.title === "string" && parsed.data.title.trim() ? parsed.data.title.trim() : heading?.title ?? titleFromId(identity.navigationId);
@@ -3483,7 +3616,7 @@ function apiFrontmatter(data, warn) {
3483
3616
  }
3484
3617
 
3485
3618
  // src/url-static-content.ts
3486
- import { createHash as createHash2 } from "crypto";
3619
+ import { createHash as createHash3 } from "crypto";
3487
3620
  import { load as load4 } from "cheerio";
3488
3621
  function projectMigrationSvg(html) {
3489
3622
  const $ = load4(html, { xmlMode: true }, false);
@@ -3492,7 +3625,7 @@ function projectMigrationSvg(html) {
3492
3625
  if (svg.attr("xmlns") === void 0) svg.attr("xmlns", "http://www.w3.org/2000/svg");
3493
3626
  const content = Buffer.from($.xml(svg));
3494
3627
  if (!isPassiveSvg(content)) return null;
3495
- const sha256 = createHash2("sha256").update(content).digest("hex");
3628
+ const sha256 = createHash3("sha256").update(content).digest("hex");
3496
3629
  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);
3497
3630
  const alternative = svg.attr("aria-label")?.trim() || labelled.join(" ") || svg.children("title").text().trim() || svg.children("desc").text().trim() || svg.find("text").text().trim();
3498
3631
  return { asset: { path: `migration-assets/${sha256}.svg`, content }, alternative };
@@ -3605,6 +3738,32 @@ function projectCapturedTabs($, root) {
3605
3738
  }
3606
3739
  }
3607
3740
 
3741
+ // src/redirects.ts
3742
+ function trimTrailingSlashes(route) {
3743
+ let end = route.length;
3744
+ while (end > 0 && route[end - 1] === "/") end--;
3745
+ return route.slice(0, end);
3746
+ }
3747
+ function exactRoute(route) {
3748
+ return route.startsWith("/") && !/[?:*#()[\]{}]/.test(route) ? trimTrailingSlashes(route) || "/" : null;
3749
+ }
3750
+ function literalRoute(route) {
3751
+ return exactRoute(route)?.toLowerCase() ?? null;
3752
+ }
3753
+ function resolveDotSegments(route) {
3754
+ const segments = [];
3755
+ for (const segment of route.split("/").slice(1)) {
3756
+ const dots = segment.replace(/%2e/gi, ".");
3757
+ if (dots === "..") segments.pop();
3758
+ else if (dots !== ".") segments.push(segment);
3759
+ }
3760
+ return `/${segments.join("/")}`;
3761
+ }
3762
+ function destinationRoute(destination) {
3763
+ const route = destination.startsWith("//") ? null : exactRoute(destination.split(/[?#]/, 1)[0]);
3764
+ return route === null ? null : resolveDotSegments(route);
3765
+ }
3766
+
3608
3767
  // src/url.ts
3609
3768
  var MAX_LOCAL_RESPONSE_BYTES = 8e6;
3610
3769
  var MAX_EMBEDDED_SCHEMA_BYTES = 2e6;
@@ -3684,8 +3843,8 @@ var MINTLIFY_ARTICLE_LABELS = [
3684
3843
  { baseUrl: "Select base URL", permalink: "Navigate to header" },
3685
3844
  { baseUrl: "\u9009\u62E9\u57FA\u7840URL", permalink: "\u5BFC\u822A\u5230\u6807\u9898" }
3686
3845
  ];
3687
- var remoteMarkdownParser = unified3().use(remarkParse3).use(remarkMdx3);
3688
- var embeddedOpenApiParser = unified3().use(remarkParse3);
3846
+ var remoteMarkdownParser = unified4().use(remarkParse4).use(remarkMdx3);
3847
+ var embeddedOpenApiParser = unified4().use(remarkParse4);
3689
3848
  function validateMigrationUrl(value) {
3690
3849
  let url;
3691
3850
  try {
@@ -3725,10 +3884,10 @@ async function readResponseBody(response) {
3725
3884
  }
3726
3885
  return new TextDecoder().decode(body);
3727
3886
  }
3728
- var defaultMigrationFetcher = async (url, request) => {
3887
+ var defaultMigrationFetcher = async (url, request2) => {
3729
3888
  const response = await fetch(url, {
3730
3889
  headers: {
3731
- Accept: request.accept,
3890
+ Accept: request2.accept,
3732
3891
  "User-Agent": "Thally-Migrate/1.0 (+https://thally.io)"
3733
3892
  },
3734
3893
  redirect: "follow",
@@ -4202,7 +4361,7 @@ function extractEmbeddedOpenApi(content) {
4202
4361
  const opening = lines[0].match(/^ {0,3}(`{3,}|~{3,})/);
4203
4362
  const closing = lines.at(-1)?.match(/^ {0,3}(`{3,}|~{3,})[\t ]*$/);
4204
4363
  if (!opening || !closing || opening[1][0] !== closing[1][0] || closing[1].length < opening[1].length) return { content };
4205
- const parsed = parseYaml2(node.value ?? "", { maxAliasCount: 50 });
4364
+ const parsed = parseYaml3(node.value ?? "", { maxAliasCount: 50 });
4206
4365
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { content };
4207
4366
  const document = parsed;
4208
4367
  if (typeof document.openapi !== "string" && typeof document.swagger !== "string") return { content };
@@ -4732,7 +4891,7 @@ function observedMigrationArticleIdentity(document) {
4732
4891
  canonical2.pathname = canonical2.pathname.replace(/\/+$/, "") || "/";
4733
4892
  const removed = [...source.searchParams].filter(([key, value]) => !canonical2.searchParams.getAll(key).includes(value));
4734
4893
  const sectionAnchor = removed.length === 1 && anchors.includes(removed[0][1]) ? removed[0][1] : void 0;
4735
- return { canonicalUrl: canonical2.href, articleSha256: createHash3("sha256").update(payload).digest("hex"), ...sectionAnchor ? { sectionAnchor } : {} };
4894
+ return { canonicalUrl: canonical2.href, articleSha256: createHash4("sha256").update(payload).digest("hex"), ...sectionAnchor ? { sectionAnchor } : {} };
4736
4895
  }
4737
4896
 
4738
4897
  // src/url-api-schema-controls.ts
@@ -4825,8 +4984,8 @@ function apiSchemaTabGroups(body, document, operation) {
4825
4984
  let schema;
4826
4985
  let scope;
4827
4986
  if (title === "body") {
4828
- const request = resolve2(document, selected.requestBody);
4829
- schema = object2(object2(request?.content)?.[media[0]])?.schema;
4987
+ const request2 = resolve2(document, selected.requestBody);
4988
+ schema = object2(object2(request2?.content)?.[media[0]])?.schema;
4830
4989
  scope = `request:${media[0]}`;
4831
4990
  } else if (title === "response") {
4832
4991
  const statuses = heading.find('button[aria-haspopup="menu"]').toArray().map((button) => $(button).text().trim()).filter((text2) => /^(?:[1-5]\d{2}|default)$/.test(text2));
@@ -4925,8 +5084,8 @@ function apiSchemaDisclosureGroups(body, document, operation) {
4925
5084
  schemas = [object2(object2(response?.content)?.[media[0]])?.schema];
4926
5085
  prefix = "response";
4927
5086
  } else if (media.length === 1 && !statuses.length) {
4928
- const request = resolve2(document, selected.requestBody);
4929
- schemas = [object2(object2(request?.content)?.[media[0]])?.schema];
5087
+ const request2 = resolve2(document, selected.requestBody);
5088
+ schemas = [object2(object2(request2?.content)?.[media[0]])?.schema];
4930
5089
  prefix = "body";
4931
5090
  } else if (!media.length && !statuses.length) {
4932
5091
  const head = $(chain[0]).find(".param-head[id]").filter((_index, element) => $(element).closest(fieldSelector)[0] === chain[0]).first().attr("id") ?? "";
@@ -5059,8 +5218,8 @@ function apiOperationCodeControls(body, specification, operation) {
5059
5218
  const metadata = object2(resolveValue(value));
5060
5219
  const declaration = resolveValue(metadata?.openapi);
5061
5220
  if (typeof declaration !== "string") return false;
5062
- const tokens = declaration.trim().split(/\s+/);
5063
- return tokens.at(-2)?.toLowerCase() === operation.method.toLowerCase() && tokens.at(-1) === operation.path;
5221
+ const tokens2 = declaration.trim().split(/\s+/);
5222
+ return tokens2.at(-2)?.toLowerCase() === operation.method.toLowerCase() && tokens2.at(-1) === operation.path;
5064
5223
  };
5065
5224
  const active = /* @__PURE__ */ new Set();
5066
5225
  const visit = (input, provider, group, depth, owners = []) => {
@@ -5106,9 +5265,9 @@ function apiOperationCodeControls(body, specification, operation) {
5106
5265
  const hint = object2(resolveValue(scope?.pageMetadata)), metadata = object2(resolveValue(props.pageMetadata));
5107
5266
  const declaration = resolveValue(hint?.openapi);
5108
5267
  if (typeof declaration !== "string" || typeof hint?.href !== "string" || !hint.href || hint.href !== metadata?.href) return false;
5109
- const tokens = declaration.trim().split(/\s+/);
5268
+ const tokens2 = declaration.trim().split(/\s+/);
5110
5269
  const template = (path) => path.split("/").map((segment) => /^\{[^{}\/]+\}$/.test(segment) ? "{}" : segment).join("/");
5111
- return tokens.at(-2)?.toLowerCase() === operation.method.toLowerCase() && template(tokens.at(-1) ?? "") === template(operation.path);
5270
+ return tokens2.at(-2)?.toLowerCase() === operation.method.toLowerCase() && template(tokens2.at(-1) ?? "") === template(operation.path);
5112
5271
  })()) generatedControllers += 1;
5113
5272
  }
5114
5273
  let local = group;
@@ -5216,15 +5375,15 @@ function apiOperationCodeControls(body, specification, operation) {
5216
5375
  }
5217
5376
 
5218
5377
  // src/url-run.ts
5219
- var CONVERTER_VERSION = "url-evidence-v6";
5220
- var parser2 = unified4().use(remarkParse4).use(remarkMdx4);
5221
- var ACCEPT = "text/markdown,text/html,application/xhtml+xml;q=0.9";
5378
+ var CONVERTER_VERSION = "url-evidence-v7";
5379
+ var parser2 = unified5().use(remarkParse5).use(remarkMdx4);
5380
+ var ACCEPT = "text/html,application/xhtml+xml";
5222
5381
  var MAX_RESPONSE_BYTES = 8e6;
5223
5382
  var MAX_ASSET_BYTES = 1e7;
5224
5383
  var MAX_RECORDS = 1e5;
5225
5384
  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"]);
5226
5385
  function hash(value) {
5227
- return createHash4("sha256").update(value).digest("hex");
5386
+ return createHash5("sha256").update(value).digest("hex");
5228
5387
  }
5229
5388
  function canonicalJson(value) {
5230
5389
  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;
@@ -5248,6 +5407,11 @@ function canonical(value, base) {
5248
5407
  return null;
5249
5408
  }
5250
5409
  }
5410
+ function sameRouteMarkdownRepresentation(alternate, primary) {
5411
+ const target2 = new URL(alternate), source = new URL(primary);
5412
+ const directory2 = (path) => path.replace(/\.mdx?$/i, "").replace(/\/index\/?$/i, "").replace(/\/$/, "");
5413
+ return target2.origin === source.origin && target2.search === source.search && directory2(target2.pathname) === directory2(source.pathname) && /\.mdx?$/i.test(target2.pathname);
5414
+ }
5251
5415
  function classifyMigrationResponse(document) {
5252
5416
  if (document.status && document.status >= 400) return "unsupported";
5253
5417
  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";
@@ -5498,7 +5662,7 @@ function resolveMigrationRecord(records, record) {
5498
5662
  }
5499
5663
  return record;
5500
5664
  }
5501
- function navigationConfig(checkpoint, records) {
5665
+ function navigationConfig(checkpoint, records, occurrencePages = /* @__PURE__ */ new Map()) {
5502
5666
  const navigation = checkpoint.navigation;
5503
5667
  const shortcuts = [];
5504
5668
  const collectShortcuts = (nodes) => {
@@ -5523,7 +5687,7 @@ function navigationConfig(checkpoint, records) {
5523
5687
  const project = (nodes) => nodes.flatMap((node) => {
5524
5688
  const record = node.url ? records.get(canonical(node.url, node.url) ?? "") : void 0;
5525
5689
  const target2 = resolveMigrationRecord(records, record);
5526
- const page = target2?.page;
5690
+ const page = occurrencePages.get(node) ?? target2?.page;
5527
5691
  const children = project(node.children ?? []);
5528
5692
  if (children.length) {
5529
5693
  const contains = (entries) => entries.some((entry) => typeof entry === "string" ? entry === (page && projection.navigationId(page)) : contains(entry.pages));
@@ -5740,7 +5904,7 @@ async function runUrlMigration(options) {
5740
5904
  } catch {
5741
5905
  }
5742
5906
  }
5743
- const capture = (request) => initial && canonical(request.url.href, sourceUrl) === sourceUrl ? initial : acquire(request);
5907
+ const capture = (request2) => initial && canonical(request2.url.href, sourceUrl) === sourceUrl ? initial : acquire(request2);
5744
5908
  const requestedScope = options.scopePath ?? options.checkpoint?.scopePath ?? inferredScope ?? (source.pathname.endsWith("/") ? source.pathname : source.pathname.slice(0, source.pathname.lastIndexOf("/")) || "/");
5745
5909
  let scopeEnd = requestedScope.length;
5746
5910
  while (scopeEnd > 0 && requestedScope.charCodeAt(scopeEnd - 1) === 47) scopeEnd--;
@@ -5823,8 +5987,19 @@ async function runUrlMigration(options) {
5823
5987
  if (!record.evidence) continue;
5824
5988
  const final = canonical(record.evidence.finalUrl, sourceUrl);
5825
5989
  if (!final) continue;
5826
- if (record.outcome === "imported") enqueueArticleLinks(record.evidence.links, final, record.url);
5827
- else if (record.outcome === "excluded") {
5990
+ if (record.outcome === "imported") {
5991
+ enqueueArticleLinks(record.evidence.links, final, record.url);
5992
+ for (const reference of record.evidence.markdownRepresentations ?? []) {
5993
+ const alternate = canonical(reference, final);
5994
+ if (!alternate || !sameRouteMarkdownRepresentation(alternate, final)) continue;
5995
+ enqueue(alternate, final, "declared-markdown-representation");
5996
+ const sibling = records.get(alternate);
5997
+ if (sibling && sibling.representationOf !== record.url) {
5998
+ sibling.representationOf = record.url;
5999
+ changed.push(sibling);
6000
+ }
6001
+ }
6002
+ } else if (record.outcome === "excluded") {
5828
6003
  if (classifyMigrationResponse({ finalUrl: new URL(final), contentType: record.evidence.contentType, body: "" }) === "index") {
5829
6004
  for (const link of record.evidence.links) enqueue(link, final, record.url);
5830
6005
  }
@@ -5907,6 +6082,7 @@ async function runUrlMigration(options) {
5907
6082
  let claimedId;
5908
6083
  let claimedFinal;
5909
6084
  let markdownRepresentation;
6085
+ const markdownRepresentations = [];
5910
6086
  let exactKey;
5911
6087
  try {
5912
6088
  delete record.reason;
@@ -5999,13 +6175,13 @@ async function runUrlMigration(options) {
5999
6175
  $('link[rel="alternate"][type="text/markdown"][href]').each((_index, node) => {
6000
6176
  const alternate = canonical($(node).attr("href"), final);
6001
6177
  if (!alternate) return;
6002
- const target2 = new URL(alternate), primary = new URL(final);
6003
- const directory2 = (path) => path.replace(/\.mdx?$/i, "").replace(/\/index\/?$/i, "").replace(/\/$/, "");
6004
- if (target2.origin !== primary.origin || target2.search !== primary.search || directory2(target2.pathname) !== directory2(primary.pathname) || !/\.mdx?$/i.test(target2.pathname)) return;
6178
+ if (!sameRouteMarkdownRepresentation(alternate, final)) return;
6005
6179
  enqueue(alternate, final, "declared-markdown-representation");
6006
6180
  const sibling = records.get(alternate);
6181
+ if (!sibling) return;
6007
6182
  sibling.representationOf = record.url;
6008
6183
  markdownRepresentation ??= alternate;
6184
+ markdownRepresentations.push(alternate);
6009
6185
  changed.push(sibling);
6010
6186
  });
6011
6187
  }
@@ -6018,7 +6194,8 @@ async function runUrlMigration(options) {
6018
6194
  }
6019
6195
  enqueueNavigation(observedNavigation, final);
6020
6196
  }
6021
- if (imported >= maxPages) {
6197
+ const replacesRepresentation = imported >= maxPages && markdownRepresentations.some((url) => records.get(url)?.page);
6198
+ if (imported >= maxPages && !replacesRepresentation) {
6022
6199
  record.outcome = "excluded";
6023
6200
  record.reason = "A discovery endpoint resolved to an article after the selected page limit.";
6024
6201
  return;
@@ -6037,14 +6214,17 @@ async function runUrlMigration(options) {
6037
6214
  const locale = language.locale;
6038
6215
  const navigationId = locale ? id.split("/").filter((segment) => segment.toLowerCase() !== locale.toLowerCase()).join("/") || "introduction" : id;
6039
6216
  const identityId = locale ? `${locale}/${navigationId}` : id;
6040
- if (usedIds.has(identityId) && usedIds.get(identityId) !== record.url) {
6217
+ const owner = records.get(usedIds.get(identityId) ?? "");
6218
+ const sharesDeclaredRepresentation = owner && (owner.representationOf === record.url || record.representationOf === owner.url);
6219
+ if (owner && owner !== record && !sharesDeclaredRepresentation) {
6041
6220
  record.outcome = "failed";
6042
6221
  record.reason = `Destination collision for ${identityId}.`;
6043
6222
  return;
6044
6223
  }
6045
- usedIds.set(identityId, record.url);
6224
+ if (!owner || owner === record || owner.representationOf === record.url) usedIds.set(identityId, record.url);
6046
6225
  claimedId = identityId;
6047
6226
  const evidence = evidenceOf(document);
6227
+ if (markdownRepresentations.length) evidence.markdownRepresentations = markdownRepresentations;
6048
6228
  const page = convert(document, id);
6049
6229
  if (!page) {
6050
6230
  record.outcome = "failed";
@@ -6255,6 +6435,7 @@ ${difference}`;
6255
6435
  if (!primary || primary === record) continue;
6256
6436
  const winner = primary.outcome === "imported" ? primary : record.outcome === "imported" && primary.outcome === "failed" ? record : void 0;
6257
6437
  if (!winner) continue;
6438
+ if (winner.page) usedIds.set(winner.page.id, winner.url);
6258
6439
  const alternate = winner === primary ? record : primary;
6259
6440
  if (alternate.outcome === "alias" && alternate.aliasOf === winner.url) continue;
6260
6441
  if (alternate.outcome === "imported") {
@@ -6276,7 +6457,17 @@ ${difference}`;
6276
6457
  settleArticleIdentities();
6277
6458
  while (cursor < pending.length && !options.signal?.aborted && !budgetExhausted && cursor < (options.maxCaptures ?? Infinity)) {
6278
6459
  if (imported >= maxPages) {
6279
- const index = pending.findIndex((record, position) => position >= cursor && isDiscoveryIndex(new URL(record.url), source));
6460
+ const index = pending.findIndex((record, position) => {
6461
+ if (position < cursor || record.outcome !== "pending") return false;
6462
+ const url = new URL(record.url);
6463
+ if (isDiscoveryIndex(url, source)) return true;
6464
+ if (/\.mdx?$/i.test(url.pathname)) return false;
6465
+ return [".md", ".mdx", "/index.md", "/index.mdx"].some((extension) => {
6466
+ const alternate = new URL(url);
6467
+ alternate.pathname = alternate.pathname.replace(/\/index$/i, "") + extension;
6468
+ return records.get(alternate.href)?.outcome === "imported";
6469
+ });
6470
+ });
6280
6471
  if (index < 0) break;
6281
6472
  pending.splice(cursor, 0, pending.splice(index, 1)[0]);
6282
6473
  }
@@ -6300,19 +6491,60 @@ ${difference}`;
6300
6491
  }
6301
6492
  const navigationLabels = /* @__PURE__ */ new Map();
6302
6493
  const navigationIssues = [];
6494
+ const navigationOccurrences = /* @__PURE__ */ new Map();
6303
6495
  const labelPages = (nodes) => {
6304
6496
  for (const node of nodes) {
6305
6497
  const entry = node.url ? records.get(canonical(node.url, sourceUrl) ?? "") : void 0;
6306
6498
  const target2 = resolveMigrationRecord(records, entry);
6307
- if (target2?.page && node.label && !node.children?.length && !entry?.aliasAnchor) {
6308
- const previous = navigationLabels.get(target2.url);
6309
- 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.` });
6310
- else navigationLabels.set(target2.url, node.label);
6499
+ if (entry && target2?.page && node.label && !node.children?.length && !entry.aliasAnchor) {
6500
+ const occurrences = navigationOccurrences.get(target2) ?? [];
6501
+ occurrences.push({ node, entry });
6502
+ navigationOccurrences.set(target2, occurrences);
6311
6503
  }
6312
6504
  labelPages(node.children ?? []);
6313
6505
  }
6314
6506
  };
6315
6507
  labelPages(checkpoint.navigation);
6508
+ const occurrencePages = /* @__PURE__ */ new Map();
6509
+ const navigationAliases = /* @__PURE__ */ new Map();
6510
+ const occupiedPageIds = new Set(checkpoint.records.flatMap((record) => record.page ? [projection.pageId(record.page)] : []));
6511
+ const navigationRedirects = [];
6512
+ for (const [target2, occurrences] of navigationOccurrences) {
6513
+ const final = target2.evidence && canonical(target2.evidence.finalUrl, sourceUrl);
6514
+ const primary = occurrences.find(({ entry }) => entry.url === final) ?? occurrences.find(({ entry }) => entry === target2) ?? occurrences[0];
6515
+ navigationLabels.set(target2.url, primary.node.label);
6516
+ for (const { node, entry } of occurrences) {
6517
+ if (node.label === primary.node.label) continue;
6518
+ const existing = navigationAliases.get(entry.url);
6519
+ if (existing?.page.navTitle === node.label && existing.target === target2) {
6520
+ occurrencePages.set(node, existing.page);
6521
+ continue;
6522
+ }
6523
+ const sourceRoute = new URL(entry.url);
6524
+ const id = inScope(sourceRoute, source, scopePath) ? idForUrl(sourceRoute, scopePath) : null;
6525
+ const locale = projection.canonicalLocale(target2.page);
6526
+ let navigationId = id && (locale ? id.split("/").filter((segment) => segment.toLowerCase() !== locale.toLowerCase()).join("/") || "introduction" : id);
6527
+ const legacyPrefix = locale && `${locale.toLowerCase()}-`;
6528
+ if (navigationId && legacyPrefix && projection.navigationId(target2.page) !== target2.page.navigationId && navigationId.toLowerCase().startsWith(legacyPrefix)) navigationId = navigationId.slice(legacyPrefix.length);
6529
+ const page = navigationId ? {
6530
+ ...target2.page,
6531
+ id: locale ? `${locale}/${navigationId}` : navigationId,
6532
+ navigationId,
6533
+ ...locale ? { locale } : {},
6534
+ navTitle: node.label,
6535
+ source: entry.url,
6536
+ noindex: true
6537
+ } : void 0;
6538
+ if (!page || existing || occupiedPageIds.has(projection.pageId(page))) {
6539
+ 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.` });
6540
+ continue;
6541
+ }
6542
+ occupiedPageIds.add(projection.pageId(page));
6543
+ navigationAliases.set(entry.url, { page, target: target2 });
6544
+ occurrencePages.set(node, page);
6545
+ navigationRedirects.push({ source: projection.route(page), destination: projection.route(target2.page), permanent: true });
6546
+ }
6547
+ }
6316
6548
  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) => {
6317
6549
  let url;
6318
6550
  try {
@@ -6333,6 +6565,10 @@ ${difference}`;
6333
6565
  const hasBrokenSourceAnchor = Boolean(url.hash && destination?.outcome === "imported" && destination.evidence && !destination.evidence.anchors.includes(anchor));
6334
6566
  return `${route}${hasBrokenSourceAnchor ? "" : effectiveHash}`;
6335
6567
  }) }] : []);
6568
+ const projectedPages = new Map(pages.map((page) => [page.id, page]));
6569
+ for (const { page, target: target2 } of navigationAliases.values()) {
6570
+ pages.push({ ...page, id: projection.pageId(page), body: projectedPages.get(projection.pageId(target2.page)).body });
6571
+ }
6336
6572
  const validation = validateUrlMigration(checkpoint, Boolean(options.signal?.aborted));
6337
6573
  validation.issues.push(...navigationIssues);
6338
6574
  if (navigationIssues.length && validation.status === "passed") validation.status = "partial";
@@ -6353,12 +6589,12 @@ ${difference}`;
6353
6589
  platform: options.platform ?? "generic",
6354
6590
  pages,
6355
6591
  assets: [...assets.values()],
6356
- docsConfig: navigationConfig(checkpoint, records) ?? buildNavigationFromPages(pages),
6592
+ docsConfig: navigationConfig(checkpoint, records, occurrencePages) ?? buildNavigationFromPages(pages),
6357
6593
  warnings: [...validation.issues, ...validation.warnings ?? []].map((issue) => ({ code: issue.code === "missing-page" ? "missing-page" : "unsupported-config", message: issue.message, source: issue.source })),
6358
6594
  urlMigrationValidation: validation,
6359
6595
  stats: { discovered: records.size, imported: pages.length, skipped: records.size - pages.length }
6360
6596
  };
6361
- if (projection.redirects.length) bundle.docsConfig.redirects = projection.redirects;
6597
+ if (projection.redirects.length || navigationRedirects.length) bundle.docsConfig.redirects = [...projection.redirects, ...navigationRedirects];
6362
6598
  if (projection.locales.length) {
6363
6599
  const locales = [.../* @__PURE__ */ new Set([projection.defaultLocale, ...projection.locales])];
6364
6600
  bundle.docsConfig.i18n = { ...bundle.docsConfig.i18n, defaultLocale: projection.defaultLocale, locales: locales.map((code) => ({ code, label: code === "en" ? "English" : code })) };
@@ -6376,9 +6612,11 @@ function rewriteMarkdownDestinations(body, rewrite) {
6376
6612
  const original = body.slice(start, end);
6377
6613
  const replacement = rewrite(node.url);
6378
6614
  if (replacement === node.url) return null;
6379
- const index = original.lastIndexOf(node.url);
6380
- if (index < 0) return null;
6381
- return { start: start + index, end: start + index + node.url.length, replacement };
6615
+ const destinations = postprocess(parse2().document().write(preprocess()(original, void 0, true))).filter(([event, token]) => event === "enter" && ["resourceDestinationString", "definitionDestinationString", "autolinkProtocol", "autolinkEmail"].includes(token.type));
6616
+ const destination = destinations.at(-1)?.[1];
6617
+ if (!destination) return null;
6618
+ const escaped = replacement.replace(/[\\&()<>]/g, "\\$&").replace(/[\t\n\r ]/g, (character) => encodeURIComponent(character));
6619
+ return { start: start + destination.start.offset, end: start + destination.end.offset, replacement: escaped };
6382
6620
  }).filter((edit) => edit !== null).sort((a, b) => b.start - a.start);
6383
6621
  for (const node of markdownNodes(body)) for (const attribute of node.attributes ?? []) {
6384
6622
  if (!attribute.name || !["src", "poster", "href", "srcSet", "srcset", "img", "primaryHref", "secondaryHref"].includes(attribute.name) || !attribute.position) continue;
@@ -6410,16 +6648,16 @@ function rewriteMarkdownDestinations(body, rewrite) {
6410
6648
  }
6411
6649
 
6412
6650
  // src/components.ts
6413
- import { createHash as createHash5 } from "crypto";
6651
+ import { createHash as createHash6 } from "crypto";
6414
6652
  import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2 } from "fs";
6415
6653
  import { basename, dirname as dirname2, extname as extname3, posix as posix2, relative as relative3, resolve as resolve3 } from "path";
6416
6654
  import postcss from "postcss";
6417
6655
  import selectorParser from "postcss-selector-parser";
6418
6656
  import remarkMdx5 from "remark-mdx";
6419
- import remarkParse5 from "remark-parse";
6657
+ import remarkParse6 from "remark-parse";
6420
6658
  import ts2 from "typescript";
6421
- import { unified as unified5 } from "unified";
6422
- var parser3 = unified5().use(remarkParse5).use(remarkMdx5);
6659
+ import { unified as unified6 } from "unified";
6660
+ var parser3 = unified6().use(remarkParse6).use(remarkMdx5);
6423
6661
  var DOCUSAURUS_THEME_SHIMS = {
6424
6662
  "@theme/CodeBlock": {
6425
6663
  filename: "docusaurus-code-block.tsx",
@@ -6459,7 +6697,7 @@ function applyReplacements(source, replacements) {
6459
6697
  );
6460
6698
  }
6461
6699
  function hash2(value) {
6462
- return createHash5("sha256").update(value).digest("hex").slice(0, 12);
6700
+ return createHash6("sha256").update(value).digest("hex").slice(0, 12);
6463
6701
  }
6464
6702
  function sourceFile(source, filename) {
6465
6703
  return ts2.createSourceFile(filename, source, ts2.ScriptTarget.Latest, true, scriptKindFor(filename));
@@ -6536,6 +6774,32 @@ function renderPage(bundle, page) {
6536
6774
  function isIntegrationsObject(value) {
6537
6775
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6538
6776
  }
6777
+ function mergeRedirects(existing = [], incoming = []) {
6778
+ const redirects = [...existing];
6779
+ const key = (source) => literalRoute(source) ?? trimTrailingSlashes(source).toLowerCase();
6780
+ const destinationKey = (destination) => literalRoute(destinationRoute(destination) ?? "");
6781
+ const sources = new Set(existing.map((redirect) => key(redirect.source)));
6782
+ for (const redirect of incoming) {
6783
+ const source = key(redirect.source);
6784
+ if (sources.has(source)) continue;
6785
+ const visited = /* @__PURE__ */ new Set([source]);
6786
+ let destination = destinationKey(redirect.destination);
6787
+ let cycle = false;
6788
+ while (destination) {
6789
+ if (visited.has(destination)) {
6790
+ cycle = true;
6791
+ break;
6792
+ }
6793
+ visited.add(destination);
6794
+ const next = redirects.find((candidate) => literalRoute(candidate.source) === destination);
6795
+ destination = next ? destinationKey(next.destination) : null;
6796
+ }
6797
+ if (cycle) continue;
6798
+ redirects.push(redirect);
6799
+ sources.add(source);
6800
+ }
6801
+ return redirects;
6802
+ }
6539
6803
  function mergeMigrationConfig(existing, incoming) {
6540
6804
  const tabs = existing.tabs.filter((tab) => tab.tab.toLowerCase() !== "changelog");
6541
6805
  const names = new Set(tabs.map((tab) => tab.tab.toLowerCase()));
@@ -6603,6 +6867,10 @@ function mergeMigrationConfig(existing, incoming) {
6603
6867
  ...incoming,
6604
6868
  ...existing,
6605
6869
  tabs,
6870
+ ...existing.redirects || incoming.redirects ? { redirects: mergeRedirects(existing.redirects, incoming.redirects) } : {},
6871
+ // A starter's primary action must not discard a newly imported logo.
6872
+ // Owner-authored fields, including an explicit null logo, remain authoritative.
6873
+ ...incoming.navbar || existing.navbar ? { navbar: { ...incoming.navbar, ...existing.navbar } } : {},
6606
6874
  // Collection presentation belongs to the imported information
6607
6875
  // architecture; starter defaults must not coerce source dropdowns to tabs.
6608
6876
  navigation: incoming.navigation ?? existing.navigation,