@thallylabs/migrate 0.2.19 → 0.2.22

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,5 +1,6 @@
1
1
  // src/repository.ts
2
2
  import { compileSync as compileSync2 } from "@mdx-js/mdx";
3
+ import { createHash as createHash2 } from "crypto";
3
4
  import { spawn } from "child_process";
4
5
  import {
5
6
  closeSync,
@@ -16,7 +17,7 @@ import {
16
17
  import { createRequire } from "module";
17
18
  import { basename as basename3, dirname as dirname4, extname as extname6, isAbsolute as isAbsolute2, posix as posix3, relative as relative5, resolve as resolvePath, sep as sep2 } from "path";
18
19
  import { parse as parseYaml4 } from "yaml";
19
- import * as ts3 from "typescript";
20
+ import * as ts4 from "typescript";
20
21
 
21
22
  // src/components.ts
22
23
  import { createHash } from "crypto";
@@ -263,7 +264,8 @@ function unwrapDocusaurusMdxCodeBlocks(body) {
263
264
  }
264
265
  return isMdxCodeBlock ? body : output.join("\n");
265
266
  }
266
- function normalizeExplicitHeadingIds(raw) {
267
+ var HTML_ID = /^[^\s"'<>`{}&]+$/u;
268
+ function normalizeExplicitHeadingIds(raw, warn) {
267
269
  const { front, body } = splitFrontmatterBlock(raw);
268
270
  return front + replaceOutsideCode(body, (segment) => segment.split("\n").map((line) => {
269
271
  if (!/^[ \t]{0,3}#{1,6}[ \t]+/.test(line)) return line;
@@ -272,10 +274,12 @@ function normalizeExplicitHeadingIds(raw) {
272
274
  const marker = trimmed.lastIndexOf(" {#");
273
275
  if (marker >= 0) {
274
276
  const id = trimmed.slice(marker + 3, -1);
275
- if (/^[A-Za-z][A-Za-z0-9_-]*$/.test(id)) {
276
- return `<a id="${id}"></a>
277
- ${trimmed.slice(0, marker).trimEnd()}`;
278
- }
277
+ const heading = trimmed.slice(0, marker).trimEnd();
278
+ if (/[{}]/.test(id)) return line;
279
+ if (HTML_ID.test(id)) return `<a id="${id}"></a>
280
+ ${heading}`;
281
+ warn?.(`Heading anchor {#${id}} is not a valid HTML id and was removed from "${heading.replace(/^\s*#+\s*/, "")}".`);
282
+ return heading;
279
283
  }
280
284
  }
281
285
  if (trimmed.endsWith("*/}")) {
@@ -283,7 +287,7 @@ ${trimmed.slice(0, marker).trimEnd()}`;
283
287
  if (marker >= 0) {
284
288
  const comment = trimmed.slice(marker + 4, -3).trim();
285
289
  const id = comment.startsWith("#") ? comment.slice(1) : "";
286
- if (/^[A-Za-z][A-Za-z0-9_.:-]*$/.test(id)) {
290
+ if (HTML_ID.test(id)) {
287
291
  return `<a id="${id}"></a>
288
292
  ${trimmed.slice(0, marker).trimEnd()}`;
289
293
  }
@@ -425,6 +429,9 @@ function bracketDelta(line) {
425
429
  return delta;
426
430
  }
427
431
  var ESM_OPEN = /^(?:import|export)\b/;
432
+ function latexElement(source, block = false) {
433
+ return `<Latex${block ? " block" : ""}>{${JSON.stringify(source)}}</Latex>`;
434
+ }
428
435
  function protectMathBlocks(raw) {
429
436
  const { front, body } = splitFrontmatterBlock(raw);
430
437
  const lines = body.split(/\r\n|\r|\n/);
@@ -439,7 +446,7 @@ function protectMathBlocks(raw) {
439
446
  const trimmed = line.trim();
440
447
  if (mathBlockDelimiter) {
441
448
  if (trimmed === mathBlockDelimiter) {
442
- output.push("```math", ...mathLines, "```");
449
+ output.push(latexElement(mathLines.join("\n"), true));
443
450
  mathBlockDelimiter = null;
444
451
  mathLines = [];
445
452
  converted = true;
@@ -481,11 +488,11 @@ function protectMathBlocks(raw) {
481
488
  (whole, block, inline) => {
482
489
  if (block !== void 0) {
483
490
  converted = true;
484
- return `\`$$${block}$$\``;
491
+ return latexElement(unmask(block));
485
492
  }
486
493
  if (inline !== void 0 && hasTexSignal(unmask(inline))) {
487
494
  converted = true;
488
- return `\`$${inline}$\``;
495
+ return latexElement(unmask(inline));
489
496
  }
490
497
  return whole;
491
498
  }
@@ -671,13 +678,25 @@ function maskCode(body) {
671
678
  if (fenceBuffer.length) lines.push(stash(fenceBuffer.join("\n")));
672
679
  return {
673
680
  masked: lines.join("\n"),
674
- unmask: (text2) => text2.replace(/\u0000(\d+)\u0000/g, (_match, index) => blocks[Number(index)])
681
+ unmask: (text2) => text2.replace(/\u0000(\d+)\u0000/g, (_match, index) => blocks[Number(index)]),
682
+ openFence: codeFence
675
683
  };
676
684
  }
685
+ function closeOpenFence(body) {
686
+ const { openFence } = maskCode(body);
687
+ return openFence ? `${body}${body.endsWith("\n") ? "" : "\n"}${openFence}
688
+ ` : body;
689
+ }
677
690
  function replaceOutsideCode(body, transform) {
678
691
  const { masked, unmask } = maskCode(body);
679
692
  return unmask(transform(masked));
680
693
  }
694
+ function mdxComment(text2) {
695
+ return `{/*${text2.replace(/\*\//g, "* /")}*/}`;
696
+ }
697
+ function replaceOutsideCodeAndComments(body, transform) {
698
+ return replaceOutsideCode(body, (masked) => masked.split(/(\{\s*\/\*[\s\S]*?\*\/\s*\})/).map((part, index) => index % 2 ? part : transform(part)).join(""));
699
+ }
681
700
  function rewriteFernRelativePageLinks(body, currentSourcePath, routesBySourcePath) {
682
701
  const rewrite = (target) => {
683
702
  if (!target || target.startsWith("/") || target.startsWith("#") || target.startsWith("?") || target.includes("\\") || /[\u0000-\u001f\u007f]/.test(target) || /^[A-Za-z][A-Za-z\d+.-]*:/.test(target)) return target;
@@ -739,7 +758,7 @@ function normalizeFernFileTrees(body) {
739
758
  ));
740
759
  }
741
760
  function normalizeHtmlComments(body) {
742
- return replaceOutsideCode(body, (whole) => whole.replace(/<!--([\s\S]*?)-->/g, (_match, comment) => `{/*${comment}*/}`));
761
+ return replaceOutsideCode(body, (whole) => whole.replace(/<!--([\s\S]*?)-->/g, (_match, comment) => mdxComment(comment)));
743
762
  }
744
763
  function normalizeFernCallouts(body) {
745
764
  return replaceOutsideCode(body, (whole) => {
@@ -1207,6 +1226,32 @@ function injectReactHookImports(body) {
1207
1226
 
1208
1227
  ${body}` : body;
1209
1228
  }
1229
+ function normalizeFlowParagraphContainers(body) {
1230
+ if (!body.includes("<p")) return body;
1231
+ let tree;
1232
+ try {
1233
+ tree = descriptionParser.parse(body);
1234
+ } catch {
1235
+ return body;
1236
+ }
1237
+ const edits = [];
1238
+ function visit(node) {
1239
+ if (node.type === "mdxJsxFlowElement" && node.name === "p" && node.children?.some((child) => child.type === "paragraph" || child.type === "list" || child.type === "mdxJsxFlowElement")) {
1240
+ const start = node.position?.start.offset;
1241
+ const end = node.position?.end.offset;
1242
+ if (start !== void 0 && end !== void 0 && body.slice(start, start + 2) === "<p") {
1243
+ const closing = body.slice(start, end).match(/<\/p\s*>\s*$/i);
1244
+ if (closing?.index !== void 0) {
1245
+ edits.push({ offset: start + 1, value: "div" });
1246
+ edits.push({ offset: start + closing.index + 2, value: "div" });
1247
+ }
1248
+ }
1249
+ }
1250
+ for (const child of node.children ?? []) visit(child);
1251
+ }
1252
+ visit(tree);
1253
+ return edits.sort((a, b) => b.offset - a.offset).reduce((text2, edit) => text2.slice(0, edit.offset) + edit.value + text2.slice(edit.offset + 1), body);
1254
+ }
1210
1255
  function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1211
1256
  const runFern = platform === "fern";
1212
1257
  const runMintlify = platform === "mintlify";
@@ -1218,6 +1263,13 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1218
1263
  /:([+\w-]+):/g,
1219
1264
  (shortcode, name) => nameToEmoji[name] ?? shortcode
1220
1265
  ));
1266
+ rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1267
+ /\b(href|src)=\{\s*require\(\s*(['"])@docusaurus\/useBaseUrl\2\s*\)\.default\(\s*(['"])([^'"]+)\3\s*\)\s*\}/g,
1268
+ (original, attribute, _moduleQuote, _pathQuote, path) => {
1269
+ if (!/^[\w./~%-]+$/.test(path) || path.split("/").includes("..")) return original;
1270
+ return `${attribute}="/${path.replace(/^\.\//, "").replace(/^\/+/, "")}"`;
1271
+ }
1272
+ ));
1221
1273
  rewritten = normalizeDocusaurusTabBlocks(normalizeDocusaurusTabs(removeGlobalDocusaurusImports(rewritten)));
1222
1274
  rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1223
1275
  /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s+(['"])(@site\/[^'"]+)\2\s*;?/g,
@@ -1230,7 +1282,7 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1230
1282
  }
1231
1283
  if (runMintlify) rewritten = normalizeMintlifyParameterAnchors(rewritten);
1232
1284
  rewritten = replaceOutsideCode(rewritten, (segment) => {
1233
- let result = convertHtmlStyleAttributes(segment).replace(/<!--([\s\S]*?)-->/g, (_match, content) => `{/*${content}*/}`).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Warn(\s[^>]*)?>/g, "<Warning$1>").replace(/<\/Warn>/g, "</Warning>").replace(/<Check(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Check>/g, "</Note>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
1285
+ let result = convertHtmlStyleAttributes(segment).replace(/<!--([\s\S]*?)-->/g, (_match, content) => mdxComment(content)).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Warn(\s[^>]*)?>/g, "<Warning$1>").replace(/<\/Warn>/g, "</Warning>").replace(/<Check(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Check>/g, "</Note>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
1234
1286
  if (runDocusaurus) {
1235
1287
  result = result.replace(/<TabItem\b([^>]*)>/g, (_match, attributes) => {
1236
1288
  const title = attributes.match(/\blabel=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? attributes.match(/\bvalue=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? "Tab";
@@ -1254,7 +1306,7 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1254
1306
  return normalizeMdx(body, platform, false);
1255
1307
  }
1256
1308
  }
1257
- return normalized;
1309
+ return normalizeFlowParagraphContainers(normalized);
1258
1310
  }
1259
1311
  function parseMarkdownPage(input) {
1260
1312
  const parsed = parseFrontmatter(input.raw);
@@ -2190,26 +2242,33 @@ function hash(value) {
2190
2242
  function portableSpecifier(path) {
2191
2243
  return path.replace(/\.tsx?$/, "");
2192
2244
  }
2193
- function wrapImpureCssModuleSelectors(css) {
2245
+ function sanitizeCssModuleSelectors(css) {
2194
2246
  const root = postcss.parse(css);
2247
+ let omitted = 0;
2195
2248
  root.walkRules((rule) => {
2196
- rule.selector = selectorParser((selectors) => {
2197
- selectors.each((selector) => {
2198
- let hasClassOrId = false;
2199
- selector.walk((node) => {
2200
- if (node.type === "class" || node.type === "id") hasClassOrId = true;
2201
- });
2202
- if (hasClassOrId) return;
2203
- const nodes = selector.nodes.splice(0, selector.nodes.length);
2204
- const container = selectorParser.selector({ value: "" });
2205
- for (const node of nodes) container.append(node);
2206
- const globalPseudo = selectorParser.pseudo({ value: ":global" });
2207
- globalPseudo.append(container);
2208
- selector.append(globalPseudo);
2249
+ if (rule.parent?.type === "atrule" && /keyframes$/i.test(rule.parent.name)) return;
2250
+ if (/^:export$|^:import\(/.test(rule.selector.trim())) return;
2251
+ const selectors = selectorParser().astSync(rule.selector);
2252
+ selectors.each((selector) => {
2253
+ let hasLocalClassOrId = false;
2254
+ selector.walk((node) => {
2255
+ if (node.type !== "class" && node.type !== "id") return;
2256
+ let parent = node.parent;
2257
+ while (parent) {
2258
+ if (parent.type === "pseudo" && parent.value === ":global") return;
2259
+ parent = parent.parent;
2260
+ }
2261
+ hasLocalClassOrId = true;
2209
2262
  });
2210
- }).processSync(rule.selector);
2263
+ if (!hasLocalClassOrId) {
2264
+ selector.remove();
2265
+ omitted++;
2266
+ }
2267
+ });
2268
+ if (selectors.nodes.length === 0) rule.remove();
2269
+ else rule.selector = selectors.toString();
2211
2270
  });
2212
- return root.toString();
2271
+ return { css: root.toString(), omitted };
2213
2272
  }
2214
2273
  function sourceFile(source, filename) {
2215
2274
  return ts2.createSourceFile(
@@ -2380,7 +2439,8 @@ function neutralizeUnresolvableImportsWithoutAst(content, currentFile, warn) {
2380
2439
  });
2381
2440
  for (const { name, specifier } of dropped) {
2382
2441
  warn(`Component import ${JSON.stringify(specifier)} could not be resolved because this page has invalid MDX (see the previous warning), so the import and its usage were removed to keep the site building.`, currentFile);
2383
- result = result.replace(new RegExp(`<${name}(?:\\s[^>]*)?/>`, "g"), `{/* Removed <${name}>: unresolved during fallback migration */}`).replace(new RegExp(`<${name}(?:\\s[^>]*)?>[\\s\\S]*?<\\/${name}>`, "g"), `{/* Removed <${name}>: unresolved during fallback migration */}`);
2442
+ const removed = mdxComment(` Removed <${name}>: unresolved during fallback migration `);
2443
+ result = replaceOutsideCodeAndComments(result, (segment) => segment.replace(new RegExp(`<${name}(?:\\s[^>]*)?/>`, "g"), removed).replace(new RegExp(`<${name}(?:\\s[^>]*)?>[\\s\\S]*?<\\/${name}>`, "g"), removed));
2384
2444
  }
2385
2445
  return result;
2386
2446
  }
@@ -2439,6 +2499,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2439
2499
  }
2440
2500
  function copyGraph(entry) {
2441
2501
  const staged = /* @__PURE__ */ new Map();
2502
+ const stagedWarnings = [];
2442
2503
  let stagedBytes = 0;
2443
2504
  function stageShim(filename, content) {
2444
2505
  const shimDestination = `${destinationRoot}/shims/${filename}`;
@@ -2459,13 +2520,14 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2459
2520
  if (!CODE_EXTENSIONS.has(extension)) {
2460
2521
  if (extension === ".css" && /@import\b|url\s*\(/i.test(content.toString("utf8"))) throw new Error("CSS resource dependencies require manual migration");
2461
2522
  if (extension === ".css" && path.toLowerCase().endsWith(".module.css")) {
2462
- let wrapped;
2523
+ let sanitized;
2463
2524
  try {
2464
- wrapped = wrapImpureCssModuleSelectors(content.toString("utf8"));
2525
+ sanitized = sanitizeCssModuleSelectors(content.toString("utf8"));
2465
2526
  } catch {
2466
2527
  throw new Error("CSS module could not be parsed to check for Turbopack-safe selectors");
2467
2528
  }
2468
- staged.set(destination, { path: destination, content: wrapped });
2529
+ if (sanitized.omitted > 0) stagedWarnings.push({ path, omitted: sanitized.omitted });
2530
+ staged.set(destination, { path: destination, content: sanitized.css });
2469
2531
  }
2470
2532
  return;
2471
2533
  }
@@ -2534,6 +2596,9 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2534
2596
  }
2535
2597
  visit(entry);
2536
2598
  for (const [path, file] of staged) copied.set(path, file);
2599
+ for (const { path, omitted } of stagedWarnings) {
2600
+ warn(`Site-authored CSS module ${relative2(root, path).replace(/\\/g, "/")} has ${omitted} global-only selector branch(es) that cannot be copied into Thally CSS Modules; those styles were omitted.`, path);
2601
+ }
2537
2602
  copiedBytes += stagedBytes;
2538
2603
  return outputPath(entry);
2539
2604
  }
@@ -2787,7 +2852,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2787
2852
  warn(`Asset import ${JSON.stringify(rawSpecifier)} could not be copied as a component; it was copied to ${rescuedAssetHref} and bound to that URL instead.`, currentFile);
2788
2853
  } else {
2789
2854
  edits.push({ start: importStart, end: importEnd, value: "" });
2790
- warn(`Component import ${JSON.stringify(rawSpecifier)} could not be copied and was removed: ${error instanceof Error ? error.message : "unsupported dependency"}. Its usage on this page was neutralized.`, currentFile);
2855
+ warn(`Site-authored component import ${JSON.stringify(rawSpecifier)} could not be copied and was removed: ${error instanceof Error ? error.message : "unsupported dependency"}. Its usage on this page was neutralized; review this custom implementation manually.`, currentFile);
2791
2856
  }
2792
2857
  }
2793
2858
  }
@@ -3043,7 +3108,7 @@ ${inlineSource}` });
3043
3108
  const start = node.position.start.offset;
3044
3109
  const end = node.position.end.offset;
3045
3110
  const videoId = youtubeEmbedVideoId(node, specifier);
3046
- const value = videoId ? `<iframe width="560" height="315" src="https://www.youtube.com/embed/${videoId}" title="Embedded video" allowFullScreen />` : `{/* Removed <${node.name}>: unsupported import '${specifier}' */}`;
3111
+ const value = videoId ? `<iframe width="560" height="315" src="https://www.youtube.com/embed/${videoId}" title="Embedded video" allowFullScreen />` : mdxComment(` Removed <${node.name}>: unsupported import '${specifier}' `);
3047
3112
  edits.push({ start, end, value });
3048
3113
  });
3049
3114
  const rendered = applyReplacements(content, edits);
@@ -3117,6 +3182,110 @@ import { customComponents as ${name} } from ${JSON.stringify(registryPath)}
3117
3182
  ];
3118
3183
  }
3119
3184
 
3185
+ // src/source-styles.ts
3186
+ import postcss2 from "postcss";
3187
+ import selectorParser2 from "postcss-selector-parser";
3188
+ import ts3 from "typescript";
3189
+ var PLATFORM_CLASSES = /* @__PURE__ */ new Set(["navbar-link"]);
3190
+ var PLATFORM_IDS = /* @__PURE__ */ new Set(["footer", "navbar", "sidebar"]);
3191
+ var PLATFORM_CLASS_PREFIX = /^(?:mintlify[-_]|fern[-_]|docusaurus[-_]|theme-doc-|theme-code-block|DocSearch|pagination-nav|menu__)/i;
3192
+ var SHARED_THEME_CLASSES = /* @__PURE__ */ new Set(["dark", "light"]);
3193
+ var UTILITY_CLASS = /^(?:[mp][trblxy]?|w|h|min-w|max-w|min-h|max-h|text|bg|border|rounded|flex|grid|gap|space-[xy]|items|justify|overflow|truncate|hidden|block|inline|relative|absolute|sticky|z|opacity)-/;
3194
+ function withoutFencedCode(content) {
3195
+ let fence;
3196
+ return content.split("\n").map((line) => {
3197
+ const marker = line.match(/^ {0,3}(`{3,}|~{3,})/)?.[1];
3198
+ if (fence) {
3199
+ if (marker?.[0] === fence.marker && marker.length >= fence.length) fence = void 0;
3200
+ return "";
3201
+ }
3202
+ if (marker) {
3203
+ fence = { marker: marker[0], length: marker.length };
3204
+ return "";
3205
+ }
3206
+ return line;
3207
+ }).join("\n");
3208
+ }
3209
+ function authoredStyleNames(contents) {
3210
+ const classes = /* @__PURE__ */ new Set();
3211
+ const ids = /* @__PURE__ */ new Set();
3212
+ for (const original of contents) {
3213
+ const content = withoutFencedCode(original);
3214
+ for (const match of content.matchAll(/\b(?:className|class)\s*=\s*["']([^"']+)["']/g)) {
3215
+ for (const name of match[1].split(/\s+/)) if (name) classes.add(name);
3216
+ }
3217
+ for (const match of content.matchAll(/\bid\s*=\s*["']([^"']+)["']/g)) ids.add(match[1]);
3218
+ if (!content.includes("className={")) continue;
3219
+ const source = ts3.createSourceFile("component.tsx", content, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
3220
+ const visit = (node) => {
3221
+ if (ts3.isJsxAttribute(node) && ts3.isIdentifier(node.name) && node.name.text === "className" && node.initializer) {
3222
+ const collect = (value) => {
3223
+ if (ts3.isStringLiteral(value) || ts3.isNoSubstitutionTemplateLiteral(value)) {
3224
+ for (const name of value.text.split(/\s+/)) if (name) classes.add(name);
3225
+ } else ts3.forEachChild(value, collect);
3226
+ };
3227
+ collect(node.initializer);
3228
+ }
3229
+ ts3.forEachChild(node, visit);
3230
+ };
3231
+ visit(source);
3232
+ }
3233
+ return { classes, ids };
3234
+ }
3235
+ function projectAuthoredStyles(css, contents) {
3236
+ const names = authoredStyleNames(contents);
3237
+ const root = postcss2.parse(css);
3238
+ let omittedSelectors = 0;
3239
+ let rewrittenSelectors = 0;
3240
+ root.walkRules((rule) => {
3241
+ if (rule.parent?.type === "atrule" && /keyframes$/i.test(rule.parent.name)) return;
3242
+ const retained = [];
3243
+ try {
3244
+ selectorParser2((selectors) => {
3245
+ selectors.each((selector) => {
3246
+ let hasAuthoredAnchor = false;
3247
+ let hasPlatformAnchor = false;
3248
+ selector.walk((node) => {
3249
+ let ancestor = node.parent;
3250
+ let isNestedInPseudo = false;
3251
+ while (ancestor && ancestor !== selector) {
3252
+ if (ancestor.type === "pseudo") isNestedInPseudo = true;
3253
+ ancestor = ancestor.parent;
3254
+ }
3255
+ if (node.type === "class") {
3256
+ if (SHARED_THEME_CLASSES.has(node.value)) return;
3257
+ if (PLATFORM_CLASSES.has(node.value) || PLATFORM_CLASS_PREFIX.test(node.value) || UTILITY_CLASS.test(node.value) || !names.classes.has(node.value)) hasPlatformAnchor = true;
3258
+ else if (!isNestedInPseudo) hasAuthoredAnchor = true;
3259
+ } else if (node.type === "id") {
3260
+ if (PLATFORM_IDS.has(node.value) || !names.ids.has(node.value)) hasPlatformAnchor = true;
3261
+ else if (!isNestedInPseudo) hasAuthoredAnchor = true;
3262
+ } else if (node.type === "attribute" && node.attribute === "data-as") {
3263
+ node.remove();
3264
+ rewrittenSelectors += 1;
3265
+ }
3266
+ });
3267
+ if (hasAuthoredAnchor && !hasPlatformAnchor) retained.push(selector.toString());
3268
+ else omittedSelectors += 1;
3269
+ });
3270
+ }).processSync(rule.selector);
3271
+ } catch {
3272
+ omittedSelectors += 1;
3273
+ }
3274
+ if (retained.length) rule.selector = retained.join(", ");
3275
+ else rule.remove();
3276
+ });
3277
+ const hasStyleRule = (nodes) => Boolean(nodes?.some((node) => node.type === "rule" || node.type === "atrule" && hasStyleRule(node.nodes)));
3278
+ root.walkAtRules((rule) => {
3279
+ if (rule.name === "font-face" || rule.name === "property") return;
3280
+ if (!hasStyleRule(rule.nodes)) rule.remove();
3281
+ });
3282
+ if (omittedSelectors === 0 && rewrittenSelectors === 0) return { css, omittedSelectors };
3283
+ root.walkComments((comment) => {
3284
+ comment.remove();
3285
+ });
3286
+ return { css: root.toString(), omittedSelectors };
3287
+ }
3288
+
3120
3289
  // src/docusaurus.ts
3121
3290
  import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3 } from "fs";
3122
3291
  import { extname as extname4, posix as posix2 } from "path";
@@ -4115,6 +4284,47 @@ function projectMintlifyNavigation(config, options = {}) {
4115
4284
  message: "Mintlify navigation could not be projected; generated navigation will be used."
4116
4285
  });
4117
4286
  }
4287
+ const versionOwner = languages.length > 0 ? languages.find((entry) => entry.default === true) ?? languages[0] : navigation;
4288
+ const globalNavigation = objectValue2(versionOwner.global) ?? objectValue2(navigation.global);
4289
+ const shortcuts = Array.isArray(globalNavigation?.anchors) ? globalNavigation.anchors.flatMap((value) => {
4290
+ const anchor = objectValue2(value);
4291
+ const label = typeof anchor?.anchor === "string" ? anchor.anchor.trim() : "";
4292
+ const href = projectedHref(anchor?.href);
4293
+ if (!label || !href) return [];
4294
+ return [{ label, href, ...iconName(anchor?.icon) ? { icon: iconName(anchor?.icon) } : {} }];
4295
+ }) : [];
4296
+ const rawVersions = Array.isArray(versionOwner.versions) ? versionOwner.versions.map(objectValue2).filter((value) => value !== null).sort((left, right) => Number(right.default === true) - Number(left.default === true)) : [];
4297
+ const versionNames = rawVersions.map((entry) => typeof entry.version === "string" ? entry.version.trim() : "").filter(Boolean);
4298
+ const annotateVersions = (items) => items.map((item) => {
4299
+ const version = versionNames.find((name) => item.tab.startsWith(`${name}: `));
4300
+ return version ? { ...item, version, displayLabel: item.tab.slice(version.length + 2) } : item;
4301
+ });
4302
+ const firstPage = (item) => {
4303
+ const visit = (entries) => {
4304
+ for (const entry of entries) {
4305
+ if (typeof entry === "string") return entry;
4306
+ const nested = visit(entry.pages);
4307
+ if (nested) return nested;
4308
+ }
4309
+ return void 0;
4310
+ };
4311
+ return visit([...item.pages ?? [], ...item.groups ?? []]);
4312
+ };
4313
+ const defaultVersion = rawVersions.find((entry) => entry.default === true) ?? rawVersions[0];
4314
+ const versions = rawVersions.length > 1 ? rawVersions.flatMap((entry) => {
4315
+ const label = typeof entry.version === "string" ? entry.version.trim() : "";
4316
+ if (!label) return [];
4317
+ const candidates = tabs.filter((item) => item.tab.startsWith(`${label}: `)).map(firstPage).filter((page) => Boolean(page));
4318
+ const landing = candidates.find((page) => page.startsWith(`${label}/`)) ?? candidates[0];
4319
+ if (!landing) return [];
4320
+ const firstSegment = landing.split("/")[0];
4321
+ const prefix = firstSegment === label ? label : "";
4322
+ return [{ label, prefix, href: `/${landing}`, ...entry === defaultVersion ? { default: true } : {} }];
4323
+ }) : [];
4324
+ if (versions.length > 1) {
4325
+ tabs = annotateVersions(tabs);
4326
+ for (const [locale, items] of Object.entries(localizedNavigation)) localizedNavigation[locale] = annotateVersions(items);
4327
+ }
4118
4328
  const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
4119
4329
  const redirect = objectValue2(value);
4120
4330
  if (!redirect || typeof redirect.source !== "string" || typeof redirect.destination !== "string") return [];
@@ -4139,7 +4349,11 @@ function projectMintlifyNavigation(config, options = {}) {
4139
4349
  return {
4140
4350
  docsConfig: {
4141
4351
  tabs,
4142
- ...projectionTrace.rootContainerKind === "dropdowns" ? { navigation: { display: "dropdown" } } : {},
4352
+ ...projectionTrace.rootContainerKind === "dropdowns" || versions.length > 1 || shortcuts.length > 0 ? { navigation: {
4353
+ ...projectionTrace.rootContainerKind === "dropdowns" ? { display: "dropdown" } : {},
4354
+ ...versions.length > 1 ? { versions } : {},
4355
+ ...shortcuts.length > 0 ? { shortcuts } : {}
4356
+ } } : {},
4143
4357
  ...projectedCompatibleConfig(config, warnings),
4144
4358
  ...projectedApiMdx(config, warnings),
4145
4359
  ...i18n ? { i18n } : {},
@@ -4816,9 +5030,10 @@ function descriptorSort(left, right) {
4816
5030
  if (leftPosition !== rightPosition) return leftPosition - rightPosition;
4817
5031
  return left.sourcePath.localeCompare(right.sourcePath, void 0, { numeric: true });
4818
5032
  }
4819
- function registerDoc(docId, context) {
5033
+ function registerDoc(docId, context, sourceDirectory) {
4820
5034
  const key = normalizeDocId(docId);
4821
- const descriptor = context.descriptorByDocId.get(key) ?? context.descriptors.find((page) => page.navigationId === docusaurusRouteId(key));
5035
+ const scopedKey = sourceDirectory && !docId.startsWith("/") ? normalizeDocId(posix2.join(sourceDirectory, key)) : void 0;
5036
+ const descriptor = (scopedKey ? context.descriptorByDocId.get(scopedKey) : void 0) ?? context.descriptorByDocId.get(key) ?? context.descriptors.find((page) => page.navigationId === docusaurusRouteId(key));
4822
5037
  if (!descriptor) {
4823
5038
  context.warnings.push({
4824
5039
  code: "missing-page",
@@ -4830,10 +5045,10 @@ function registerDoc(docId, context) {
4830
5045
  context.referencedNavigationIds.add(descriptor.navigationId);
4831
5046
  return descriptor.navigationId;
4832
5047
  }
4833
- function generatedIndexPage(label, linkValue, context) {
5048
+ function generatedIndexPage(label, linkValue, context, sourceDirectory) {
4834
5049
  const link = objectValue3(linkValue);
4835
5050
  if (!link) return null;
4836
- if (link.type === "doc" && typeof link.id === "string") return registerDoc(link.id, context);
5051
+ if (link.type === "doc" && typeof link.id === "string") return registerDoc(link.id, context, sourceDirectory);
4837
5052
  if (link.type !== "generated-index") return null;
4838
5053
  const fallback = `category/${slugifySegment(label)}`;
4839
5054
  const unprefixedNavigationId = typeof link.slug === "string" ? routeFromSlug(link.slug, fallback) ?? fallback : fallback;
@@ -5076,7 +5291,7 @@ function autogeneratedItems(dirName, context) {
5076
5291
  };
5077
5292
  }
5078
5293
  }
5079
- const landing = generatedIndexPage(metadata.label ?? titleCase2(segment), metadata.link, context);
5294
+ const landing = generatedIndexPage(metadata.label ?? titleCase2(segment), metadata.link, context, directory);
5080
5295
  if (landing && !pages2.includes(landing)) pages2.unshift(landing);
5081
5296
  return {
5082
5297
  position: metadata.position ?? Number.MAX_SAFE_INTEGER,
@@ -5910,6 +6125,7 @@ var FRONTMATTER_HEAD_BYTES = 65536;
5910
6125
  var MAX_WITHHELD_SCAN_BYTES = MAX_PAGE_BYTES * 8;
5911
6126
  var MAX_DROPPED_GATED_WARNINGS = 20;
5912
6127
  var MAX_ASSET_BYTES = 25e6;
6128
+ var MAX_REFERENCED_ASSET_BYTES = 1e8;
5913
6129
  var MAX_TOTAL_ASSET_BYTES = 5e8;
5914
6130
  var GIT_LFS_POINTER_PREFIX = "version https://git-lfs.github.com/spec/v1";
5915
6131
  function listAssetPaths(paths) {
@@ -6641,7 +6857,7 @@ function selectGroupWithinBudget(scanned, rank, warnings, allVersionPrefixes, la
6641
6857
  const dropped = ranked.slice(budget).map(({ file }) => file);
6642
6858
  if (warnings) {
6643
6859
  const droppedPages = label === "file" ? dropped.filter((file) => isDocumentationExtension(file.relativePath)) : dropped;
6644
- const noun = label === "file" ? "page" : "image or media file";
6860
+ const noun = label === "file" ? "page" : label === "snippet" ? "snippet file" : "image or media file";
6645
6861
  const droppedVersions = /* @__PURE__ */ new Set();
6646
6862
  for (const file of dropped) {
6647
6863
  const firstSegment = file.relativePath.split("/", 1)[0];
@@ -6651,16 +6867,19 @@ function selectGroupWithinBudget(scanned, rank, warnings, allVersionPrefixes, la
6651
6867
  const rest = droppedPages.length - examples.length;
6652
6868
  warnings.push({
6653
6869
  code: "limit-reached",
6654
- message: `This repository has more than ${budget} ${label === "file" ? "files" : "assets"}, so only the first ${budget} (in navigation order, default version first) were migrated. ${droppedPages.length} ${noun}(s) were left out` + (examples.length > 0 ? `: ${examples.join(", ")}${rest > 0 ? `, and ${rest} more` : ""}` : "") + (droppedVersions.size > 0 ? ` (versions: ${[...droppedVersions].slice(0, 5).join(", ")}${droppedVersions.size > 5 ? `, and ${droppedVersions.size - 5} more` : ""})` : "") + (label === "file" ? ". To include them, run the migration on a smaller part of the repository with --docs-dir." : ". Copy them into public/ manually if your pages use them.")
6870
+ message: `This repository has more than ${budget} ${label === "file" ? "files" : label === "snippet" ? "snippets" : "assets"}, so only the first ${budget} (in navigation order, default version first) were migrated. ${droppedPages.length} ${noun}(s) were left out` + (examples.length > 0 ? `: ${examples.join(", ")}${rest > 0 ? `, and ${rest} more` : ""}` : "") + (droppedVersions.size > 0 ? ` (versions: ${[...droppedVersions].slice(0, 5).join(", ")}${droppedVersions.size > 5 ? `, and ${droppedVersions.size - 5} more` : ""})` : "") + (label === "asset" ? ". Copy them into public/ manually if your pages use them." : ". To include them, run the migration on a smaller part of the repository with --docs-dir.")
6655
6871
  });
6656
6872
  }
6657
6873
  return ranked.slice(0, budget).map(({ file }) => file);
6658
6874
  }
6659
6875
  function selectFilesWithinBudget(scanned, rank, warnings, allVersionPrefixes, budget = MAX_SOURCE_FILES) {
6660
6876
  const isAsset = (file) => ASSET_EXTENSIONS.has(extname6(file.relativePath).toLowerCase());
6877
+ const isSnippet = (file) => file.relativePath.split("/").some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase()));
6878
+ const snippetFiles = scanned.filter((file) => !isAsset(file) && isSnippet(file));
6661
6879
  const assetFiles = scanned.filter(isAsset);
6662
- const otherFiles = scanned.filter((file) => !isAsset(file));
6880
+ const otherFiles = scanned.filter((file) => !isAsset(file) && !isSnippet(file));
6663
6881
  return [
6882
+ ...selectGroupWithinBudget(snippetFiles, rank, warnings, allVersionPrefixes, "snippet", budget),
6664
6883
  ...selectGroupWithinBudget(otherFiles, rank, warnings, allVersionPrefixes, "file", budget),
6665
6884
  ...selectGroupWithinBudget(assetFiles, rank, warnings, allVersionPrefixes, "asset", budget)
6666
6885
  ];
@@ -7159,19 +7378,26 @@ function interpolateSnippet(snippet, attributes, children) {
7159
7378
  });
7160
7379
  }).join("\n");
7161
7380
  }
7381
+ function isolateFences(snippet, source, start, end) {
7382
+ const fence = /^\s*(?:`{3,}|~{3,})/;
7383
+ const lines = snippet.split("\n");
7384
+ const before = source.slice(source.lastIndexOf("\n", start - 1) + 1, start);
7385
+ const after = source.slice(end, source.indexOf("\n", end) < 0 ? void 0 : source.indexOf("\n", end));
7386
+ return `${fence.test(lines[0]) && /\S/.test(before) ? "\n" : ""}${snippet}${fence.test(lines[lines.length - 1]) && /\S/.test(after) ? "\n" : ""}`;
7387
+ }
7162
7388
  function snippetComponentBody(source, componentName) {
7163
- const parsed = ts3.createSourceFile("snippet.tsx", source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
7389
+ const parsed = ts4.createSourceFile("snippet.tsx", source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
7164
7390
  for (const statement of parsed.statements) {
7165
- if (!ts3.isVariableStatement(statement)) continue;
7391
+ if (!ts4.isVariableStatement(statement)) continue;
7166
7392
  for (const declaration of statement.declarationList.declarations) {
7167
- if (!ts3.isIdentifier(declaration.name) || declaration.name.text !== componentName) continue;
7393
+ if (!ts4.isIdentifier(declaration.name) || declaration.name.text !== componentName) continue;
7168
7394
  const initializer = declaration.initializer;
7169
- if (!initializer || !ts3.isArrowFunction(initializer)) continue;
7170
- const returned = ts3.isBlock(initializer.body) ? initializer.body.statements.find(ts3.isReturnStatement)?.expression : initializer.body;
7395
+ if (!initializer || !ts4.isArrowFunction(initializer)) continue;
7396
+ const returned = ts4.isBlock(initializer.body) ? initializer.body.statements.find(ts4.isReturnStatement)?.expression : initializer.body;
7171
7397
  if (!returned) continue;
7172
7398
  let expression = returned;
7173
- while (ts3.isParenthesizedExpression(expression)) expression = expression.expression;
7174
- if (ts3.isJsxElement(expression) || ts3.isJsxSelfClosingElement(expression) || ts3.isJsxFragment(expression)) {
7399
+ while (ts4.isParenthesizedExpression(expression)) expression = expression.expression;
7400
+ if (ts4.isJsxElement(expression) || ts4.isJsxSelfClosingElement(expression) || ts4.isJsxFragment(expression)) {
7175
7401
  return expression.getText(parsed);
7176
7402
  }
7177
7403
  }
@@ -7179,31 +7405,31 @@ function snippetComponentBody(source, componentName) {
7179
7405
  return source;
7180
7406
  }
7181
7407
  function statefulSnippetDeclaration(source, componentName) {
7182
- const parsed = ts3.createSourceFile("snippet.tsx", source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
7183
- const isEsm = (statement2) => ts3.isImportDeclaration(statement2) || (ts3.isVariableStatement(statement2) || ts3.isFunctionDeclaration(statement2) || ts3.isClassDeclaration(statement2)) && !!ts3.getModifiers(statement2)?.some((modifier) => modifier.kind === ts3.SyntaxKind.ExportKeyword);
7408
+ const parsed = ts4.createSourceFile("snippet.tsx", source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
7409
+ const isEsm = (statement2) => ts4.isImportDeclaration(statement2) || (ts4.isVariableStatement(statement2) || ts4.isFunctionDeclaration(statement2) || ts4.isClassDeclaration(statement2)) && !!ts4.getModifiers(statement2)?.some((modifier) => modifier.kind === ts4.SyntaxKind.ExportKeyword);
7184
7410
  if (parsed.statements.length === 0 || !parsed.statements.every(isEsm)) return null;
7185
- const statement = parsed.statements.find((candidate) => ts3.isVariableStatement(candidate) && candidate.declarationList.declarations.length === 1 && ts3.isIdentifier(candidate.declarationList.declarations[0].name) && candidate.declarationList.declarations[0].name.text === componentName);
7186
- if (!statement || !ts3.isVariableStatement(statement)) return null;
7411
+ const statement = parsed.statements.find((candidate) => ts4.isVariableStatement(candidate) && candidate.declarationList.declarations.length === 1 && ts4.isIdentifier(candidate.declarationList.declarations[0].name) && candidate.declarationList.declarations[0].name.text === componentName);
7412
+ if (!statement || !ts4.isVariableStatement(statement)) return null;
7187
7413
  const declaration = statement.declarationList.declarations[0];
7188
- if (!declaration.initializer || !ts3.isArrowFunction(declaration.initializer) || !ts3.isBlock(declaration.initializer.body)) return null;
7414
+ if (!declaration.initializer || !ts4.isArrowFunction(declaration.initializer) || !ts4.isBlock(declaration.initializer.body)) return null;
7189
7415
  const body = declaration.initializer.body.statements;
7190
- if (body.length < 2 || !body.some(ts3.isReturnStatement)) return null;
7416
+ if (body.length < 2 || !body.some(ts4.isReturnStatement)) return null;
7191
7417
  return source.trim();
7192
7418
  }
7193
7419
  function staticNamedSnippetValues(source) {
7194
7420
  const values = /* @__PURE__ */ new Map();
7195
- const parsed = ts3.createSourceFile("snippet.tsx", source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
7421
+ const parsed = ts4.createSourceFile("snippet.tsx", source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
7196
7422
  for (const statement of parsed.statements) {
7197
- if (!ts3.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts3.SyntaxKind.ExportKeyword)) continue;
7423
+ if (!ts4.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts4.SyntaxKind.ExportKeyword)) continue;
7198
7424
  for (const declaration of statement.declarationList.declarations) {
7199
- if (!ts3.isIdentifier(declaration.name) || !declaration.initializer) continue;
7425
+ if (!ts4.isIdentifier(declaration.name) || !declaration.initializer) continue;
7200
7426
  const expression = declaration.initializer;
7201
- if (ts3.isStringLiteral(expression) || ts3.isNoSubstitutionTemplateLiteral(expression)) {
7427
+ if (ts4.isStringLiteral(expression) || ts4.isNoSubstitutionTemplateLiteral(expression)) {
7202
7428
  values.set(declaration.name.text, JSON.stringify(expression.text));
7203
- } else if (ts3.isNumericLiteral(expression)) {
7429
+ } else if (ts4.isNumericLiteral(expression)) {
7204
7430
  values.set(declaration.name.text, expression.text);
7205
- } else if (expression.kind === ts3.SyntaxKind.TrueKeyword || expression.kind === ts3.SyntaxKind.FalseKeyword) {
7206
- values.set(declaration.name.text, expression.kind === ts3.SyntaxKind.TrueKeyword ? "true" : "false");
7431
+ } else if (expression.kind === ts4.SyntaxKind.TrueKeyword || expression.kind === ts4.SyntaxKind.FalseKeyword) {
7432
+ values.set(declaration.name.text, expression.kind === ts4.SyntaxKind.TrueKeyword ? "true" : "false");
7207
7433
  }
7208
7434
  }
7209
7435
  }
@@ -7530,6 +7756,19 @@ function rewriteRepositoryAssetLinks(body, currentFile, siteRoot, onReferenced)
7530
7756
  });
7531
7757
  }
7532
7758
  var LOCAL_EXPORT_DECLARATION = /^\s*export\s+(?:const|let|var|function|class|async\s+function)\s+([A-Za-z_$][\w$]*)/;
7759
+ function replaceOutsideMdxComments(body, transform) {
7760
+ const comment = /\{\/\*[\s\S]*?\*\/\}/g;
7761
+ let cursor = 0;
7762
+ let output = "";
7763
+ for (const match of body.matchAll(comment)) {
7764
+ output += transform(body.slice(cursor, match.index)) + match[0];
7765
+ cursor = match.index + match[0].length;
7766
+ }
7767
+ return output + transform(body.slice(cursor));
7768
+ }
7769
+ function replaceOutsideCodeAndMdxComments(body, transform) {
7770
+ return replaceOutsideCode(body, (segment) => replaceOutsideMdxComments(segment, transform));
7771
+ }
7533
7772
  function locallyDeclaredNames(raw) {
7534
7773
  const names = /* @__PURE__ */ new Set();
7535
7774
  let codeFence = null;
@@ -7575,7 +7814,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7575
7814
  });
7576
7815
  return `{/* ${verdict.kind === "oversized" ? "Oversized" : "Unreadable"} content not inlined: ${shown} */}`;
7577
7816
  };
7578
- let withoutImports = replaceOutsideCode(raw, (source2) => source2.replace(
7817
+ let withoutImports = replaceOutsideCodeAndMdxComments(raw, (source2) => source2.replace(
7579
7818
  SNIPPET_IMPORT_PATTERN,
7580
7819
  (_statement, namedComponent, defaultComponent, sourcePath) => {
7581
7820
  const componentName = namedComponent ?? defaultComponent;
@@ -7602,17 +7841,23 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7602
7841
  else snippets.set(componentName, snippetComponentBody(nested, componentName));
7603
7842
  return "";
7604
7843
  } catch {
7844
+ let hasLiveUsage = false;
7845
+ replaceOutsideCodeAndMdxComments(raw, (segment) => {
7846
+ if (new RegExp(`<${componentName}(?:\\s|/?>)`).test(segment)) hasLiveUsage = true;
7847
+ return segment;
7848
+ });
7849
+ if (!hasLiveUsage) return "";
7605
7850
  warnings.push({
7606
7851
  code: "missing-page",
7607
7852
  message: `Imported snippet ${sourcePath} could not be resolved and was left as a comment.`,
7608
7853
  source: relative5(repositoryRoot, currentFile).replace(/\\/g, "/")
7609
7854
  });
7610
- snippets.set(componentName, `{/* Missing imported snippet: ${sourcePath} */}`);
7855
+ snippets.set(componentName, mdxComment(` Missing imported snippet: ${sourcePath} `));
7611
7856
  return "";
7612
7857
  }
7613
7858
  }
7614
7859
  ));
7615
- withoutImports = replaceOutsideCode(withoutImports, (source2) => source2.replace(
7860
+ withoutImports = replaceOutsideCodeAndMdxComments(withoutImports, (source2) => source2.replace(
7616
7861
  SNIPPET_VALUE_IMPORT_PATTERN,
7617
7862
  (statement, names, sourcePath) => {
7618
7863
  const bindings = names.split(",").map((name) => name.trim()).filter(Boolean).map((name) => {
@@ -7656,7 +7901,12 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7656
7901
  ));
7657
7902
  const localNames = locallyDeclaredNames(withoutImports);
7658
7903
  for (const [componentName, candidate] of globalAliases) {
7659
- if (snippets.has(componentName) || localNames.has(componentName) || gate?.(candidate)?.kind === "gated" || !new RegExp(`<${componentName}(?:\\s|/?>)`).test(withoutImports)) continue;
7904
+ let hasLiveUsage = false;
7905
+ replaceOutsideCodeAndMdxComments(withoutImports, (source2) => {
7906
+ if (new RegExp(`<${componentName}(?:\\s|/?>)`).test(source2)) hasLiveUsage = true;
7907
+ return source2;
7908
+ });
7909
+ if (snippets.has(componentName) || localNames.has(componentName) || gate?.(candidate)?.kind === "gated" || !hasLiveUsage) continue;
7660
7910
  const blocked = blockInline(candidate, relative5(repositoryRoot, candidate).replace(/\\/g, "/"));
7661
7911
  if (blocked) {
7662
7912
  snippets.set(componentName, blocked);
@@ -7676,48 +7926,49 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7676
7926
  if (declaration) preservedDeclarations.set(componentName, declaration);
7677
7927
  else snippets.set(componentName, snippetComponentBody(nested, componentName));
7678
7928
  }
7679
- let result = withoutImports;
7680
- for (const [componentName, snippet] of snippets) {
7681
- result = result.replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)\\s*/>`, "g"), (_tag, attributes) => {
7682
- return interpolateSnippet(snippet, attributes);
7683
- }).replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)>([\\s\\S]*?)<\\/${componentName}>`, "g"), (_tag, attributes, children) => {
7684
- return interpolateSnippet(snippet, attributes, children);
7685
- });
7686
- }
7687
- result = result.replace(SNIPPET_TAG_PATTERN, (_tag, doubleQuoted, singleQuoted) => {
7688
- const filePath = doubleQuoted ?? singleQuoted;
7689
- try {
7690
- const candidate = [
7691
- () => resolveWithin(siteRoot, `snippets/${filePath}`),
7692
- () => resolveSnippetPath(filePath, currentFile, repositoryRoot, siteRoot)
7693
- ].map((resolveCandidate) => {
7694
- try {
7695
- return resolveCandidate();
7696
- } catch {
7697
- return void 0;
7698
- }
7699
- }).find((path) => path !== void 0 && existsSync5(path) && lstatSync5(path).isFile());
7700
- if (!candidate) throw new Error("file not found");
7701
- const blocked = blockInline(candidate, filePath, `Snippet file="${filePath}"`);
7702
- if (blocked) return blocked;
7703
- return inlineMdxSnippets(
7704
- withoutFrontmatter(readFileSync5(candidate, "utf8")),
7705
- candidate,
7706
- repositoryRoot,
7707
- warnings,
7708
- depth + 1,
7709
- siteRoot,
7710
- globalAliases,
7711
- gate
7712
- );
7713
- } catch {
7714
- warnings.push({
7715
- code: "missing-page",
7716
- message: `Snippet file="${filePath}" could not be resolved and was left as a comment.`,
7717
- source: relative5(repositoryRoot, currentFile).replace(/\\/g, "/")
7929
+ let result = replaceOutsideCodeAndComments(withoutImports, (segment) => {
7930
+ for (const [componentName, snippet] of snippets) {
7931
+ segment = segment.replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)\\s*/>`, "g"), (tag, attributes, offset, source2) => {
7932
+ return isolateFences(closeOpenCodeFence(interpolateSnippet(snippet, attributes)), source2, offset, offset + tag.length);
7933
+ }).replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)>([\\s\\S]*?)<\\/${componentName}>`, "g"), (tag, attributes, children, offset, source2) => {
7934
+ return isolateFences(closeOpenCodeFence(interpolateSnippet(snippet, attributes, children)), source2, offset, offset + tag.length);
7718
7935
  });
7719
- return `{/* Missing snippet: ${filePath} */}`;
7720
7936
  }
7937
+ return segment.replace(SNIPPET_TAG_PATTERN, (tag, doubleQuoted, singleQuoted, offset, source2) => {
7938
+ const filePath = doubleQuoted ?? singleQuoted;
7939
+ try {
7940
+ const candidate = [
7941
+ () => resolveWithin(siteRoot, `snippets/${filePath}`),
7942
+ () => resolveSnippetPath(filePath, currentFile, repositoryRoot, siteRoot)
7943
+ ].map((resolveCandidate) => {
7944
+ try {
7945
+ return resolveCandidate();
7946
+ } catch {
7947
+ return void 0;
7948
+ }
7949
+ }).find((path) => path !== void 0 && existsSync5(path) && lstatSync5(path).isFile());
7950
+ if (!candidate) throw new Error("file not found");
7951
+ const blocked = blockInline(candidate, filePath, `Snippet file="${filePath}"`);
7952
+ if (blocked) return blocked;
7953
+ return isolateFences(closeOpenCodeFence(inlineMdxSnippets(
7954
+ withoutFrontmatter(readFileSync5(candidate, "utf8")),
7955
+ candidate,
7956
+ repositoryRoot,
7957
+ warnings,
7958
+ depth + 1,
7959
+ siteRoot,
7960
+ globalAliases,
7961
+ gate
7962
+ )), source2, offset, offset + tag.length);
7963
+ } catch {
7964
+ warnings.push({
7965
+ code: "missing-page",
7966
+ message: `Snippet file="${filePath}" could not be resolved and was left as a comment.`,
7967
+ source: relative5(repositoryRoot, currentFile).replace(/\\/g, "/")
7968
+ });
7969
+ return mdxComment(` Missing snippet: ${filePath} `);
7970
+ }
7971
+ });
7721
7972
  });
7722
7973
  if (preservedDeclarations.size > 0) {
7723
7974
  const frontmatter = result.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
@@ -7726,7 +7977,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7726
7977
 
7727
7978
  ${result.slice(prefix.length)}`;
7728
7979
  }
7729
- return depth === 0 ? hoistMdxImports(result) : result;
7980
+ return depth === 0 ? hoistMdxImports(result) : closeOpenFence(result);
7730
7981
  }
7731
7982
  function injectOpenApiSpecs(config, specs, warnings) {
7732
7983
  const tabs = config.tabs.map((tab) => ({ ...tab }));
@@ -7913,7 +8164,7 @@ function migrateRepository(options) {
7913
8164
  const sourceBudget = options.maxSourceFiles ?? MAX_SOURCE_FILES;
7914
8165
  const scannedFiles = scanFiles(contentRoot, repositoryDir, warnings, discoveryRank, sourceBudget);
7915
8166
  const mintignoreFilteredFiles = mintignoreMatcher ? scannedFiles.filter((file) => !mintignoreMatcher.ignores(file.relativePath)) : scannedFiles;
7916
- const discoveryBudgetApplied = discoveryRank !== void 0 && mintignoreFilteredFiles.length > sourceBudget;
8167
+ const isUnreferencedSnippetModule = (file) => file.relativePath.split("/").some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase())) && isDocumentationExtension(file.relativePath) && !referenceOrder.has(normalizedReferenceKey(file.relativePath));
7917
8168
  const files = discoveryRank ? selectFilesWithinBudget(mintignoreFilteredFiles, discoveryRank, warnings, allVersionPrefixes, sourceBudget) : mintignoreFilteredFiles;
7918
8169
  if (platform === "fern" && fernProjectRoot) {
7919
8170
  const discovered2 = new Set(files.map((file) => file.relativePath));
@@ -7992,6 +8243,10 @@ function migrateRepository(options) {
7992
8243
  }
7993
8244
  for (const file of mintignoreFilteredFiles) {
7994
8245
  if (keptFiles.has(file) || !isDocFile(file)) continue;
8246
+ if (isUnreferencedSnippetModule(file)) {
8247
+ if (classifyPageGate(file).reason) withheldDocFiles.push(file);
8248
+ continue;
8249
+ }
7995
8250
  droppedPageCount++;
7996
8251
  if (!isGateCandidate(file)) {
7997
8252
  const snippetReason = classifyPageGate(file).reason;
@@ -8214,7 +8469,7 @@ function migrateRepository(options) {
8214
8469
  const publishedText = trackPublishedRefs ? raw : void 0;
8215
8470
  const pageAssetReferences = [];
8216
8471
  if (platform === "fern" || platform === "mintlify" || platform === "docusaurus") {
8217
- raw = normalizeExplicitHeadingIds(raw);
8472
+ raw = normalizeExplicitHeadingIds(raw, (message) => warnings.push({ code: "invalid-page", message, source: file.relativePath }));
8218
8473
  }
8219
8474
  if (componentMigrator) {
8220
8475
  const warningsBeforeTransform = warnings.length;
@@ -8226,13 +8481,7 @@ function migrateRepository(options) {
8226
8481
  }
8227
8482
  if (platform === "fern" || platform === "mintlify" || platform === "docusaurus") {
8228
8483
  const protectedMath = protectMathBlocks(raw);
8229
- if (protectedMath.converted) {
8230
- warnings.push({
8231
- code: "unsupported-config",
8232
- message: "Math (KaTeX '$$...$$') has no renderer in Thally yet; it was kept as a fenced code block instead of being dropped.",
8233
- source: relative5(repositoryDir, file.absolutePath).replace(/\\/g, "/")
8234
- });
8235
- } else if (protectedMath.guardTriggered) {
8484
+ if (protectedMath.guardTriggered) {
8236
8485
  warnings.push({
8237
8486
  code: "unsupported-config",
8238
8487
  message: "A suspected math span looked like it needed converting, but doing so broke the page; the page was kept as originally authored instead.",
@@ -8324,7 +8573,7 @@ function migrateRepository(options) {
8324
8573
  page.body = replaceUnknownComponents(page.body, (name) => {
8325
8574
  warnings.push({
8326
8575
  code: "unsupported-config",
8327
- message: `Component <${name}> has no equivalent in Thally and wasn't found on this page; it was replaced with a plain <div> (or removed, if self-closing) so the page still builds. Add a matching component or edit the page.`,
8576
+ message: `Unresolved MDX component <${name}> was replaced with a plain <div> (or removed, if self-closing) so the page still builds. It may be a site-authored or third-party implementation; review its source before adding a Thally equivalent.`,
8328
8577
  source: file.relativePath
8329
8578
  });
8330
8579
  });
@@ -8451,9 +8700,31 @@ function migrateRepository(options) {
8451
8700
  const source = /^src\/mdx\/migrated\/[^/]+\/source\/(.+)$/.exec(file.path)?.[1];
8452
8701
  return source ? [resolvePath(repositoryDir, source)] : [];
8453
8702
  }));
8703
+ const rootStylesheetPath = platform === "mintlify" && mintlifyProjectRoot ? resolveWithin(mintlifyProjectRoot, "style.css") : void 0;
8704
+ const hasRootStylesheet = Boolean(rootStylesheetPath && existsSync5(rootStylesheetPath) && lstatSync5(rootStylesheetPath).isFile() && !lstatSync5(rootStylesheetPath).isSymbolicLink());
8705
+ let rootStyleProjection;
8706
+ let rootStyleProblem;
8707
+ if (hasRootStylesheet && rootStylesheetPath && lstatSync5(rootStylesheetPath).size <= MAX_ASSET_BYTES) {
8708
+ const css = readFileSync5(rootStylesheetPath, "utf8");
8709
+ if (/@import\b|url\s*\(\s*['"]?\s*(?:javascript:|file:)/i.test(css)) rootStyleProblem = "unsafe";
8710
+ else {
8711
+ try {
8712
+ rootStyleProjection = projectAuthoredStyles(css, [
8713
+ ...pages.map((page) => page.body),
8714
+ ...(componentMigrator?.files() ?? []).map((file) => typeof file.content === "string" ? file.content : "")
8715
+ ]);
8716
+ } catch {
8717
+ rootStyleProblem = "parse";
8718
+ }
8719
+ }
8720
+ }
8721
+ if (trackPublishedRefs && rootStyleProjection?.css) {
8722
+ addPathReferences(rootStyleProjection.css, "", publishedExact, publishedLoose);
8723
+ }
8454
8724
  const siteAssetPaths = /* @__PURE__ */ new Map();
8455
8725
  const fontAssetByRelative = /* @__PURE__ */ new Map();
8456
8726
  for (const file of [...files, ...repositoryAssets, ...fernReferencedAssets]) {
8727
+ if (rootStylesheetPath && file.absolutePath === rootStylesheetPath) continue;
8457
8728
  const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
8458
8729
  const siteKind = platform !== "mintlify" ? void 0 : fontRelativePaths.has(file.relativePath) ? "font" : isMintlifyServedScriptOrStyle(file.relativePath) && !importedComponentSources.has(file.absolutePath) ? extname6(file.relativePath).toLowerCase() === ".css" ? "style" : "script" : void 0;
8459
8730
  if (!siteKind && !ASSET_EXTENSIONS.has(extname6(file.relativePath).toLowerCase())) continue;
@@ -8499,7 +8770,8 @@ function migrateRepository(options) {
8499
8770
  const lfsPointerAssets = [];
8500
8771
  const ambiguouslyNamedAssets = [];
8501
8772
  for (const { file, assetPath, size } of orderedAssetCandidates) {
8502
- if (size > MAX_ASSET_BYTES || totalAssetBytes + size > MAX_TOTAL_ASSET_BYTES) {
8773
+ const individualLimit = referencedAssetPaths.has(assetPath) ? MAX_REFERENCED_ASSET_BYTES : MAX_ASSET_BYTES;
8774
+ if (size > individualLimit || totalAssetBytes + size > MAX_TOTAL_ASSET_BYTES) {
8503
8775
  overBudgetAssets.push(file.relativePath);
8504
8776
  if (referencedAssetPaths.has(assetPath)) overBudgetReferenced++;
8505
8777
  continue;
@@ -8541,7 +8813,7 @@ function migrateRepository(options) {
8541
8813
  for (const entry of copiedSite.filter((item) => item.kind === "style")) {
8542
8814
  warnings.push({
8543
8815
  code: "unsupported-config",
8544
- message: `Migration gap: custom stylesheet ${entry.path} is NOT applied. It was copied to public/${entry.path} but no page loads it, Thally cannot load a stylesheet from docs.json, and Mintlify-specific selectors will not match Thally's markup. Port the styles you need by hand into src/app/globals.css.`,
8816
+ message: `Migration gap: custom stylesheet ${entry.path} is NOT applied. It was copied to public/${entry.path} but was not declared in docs.json stylesheets; its selectors may target Mintlify's markup. Review and port its authored rules into a Thally stylesheet.`,
8545
8817
  source: entry.path
8546
8818
  });
8547
8819
  }
@@ -8559,7 +8831,7 @@ function migrateRepository(options) {
8559
8831
  if (overBudgetAssets.length > 0) {
8560
8832
  warnings.push({
8561
8833
  code: "limit-reached",
8562
- message: `${overBudgetAssets.length} asset file${overBudgetAssets.length === 1 ? " was" : "s were"} not copied because files over ${MAX_ASSET_BYTES / 1e6} MB, or beyond ${MAX_TOTAL_ASSET_BYTES / 1e6} MB in total, are skipped: ${listAssetPaths(overBudgetAssets)}. ` + (overBudgetReferenced > 0 ? `${overBudgetReferenced} of them ${overBudgetReferenced === 1 ? "is" : "are"} used by pages, so those images will be broken until you copy ${overBudgetAssets.length === 1 ? "it" : "them"}. ` : "") + `Copy ${overBudgetAssets.length === 1 ? "it" : "them"} into public/ manually.`
8834
+ message: `${overBudgetAssets.length} asset file${overBudgetAssets.length === 1 ? " was" : "s were"} not copied because unreferenced files over ${MAX_ASSET_BYTES / 1e6} MB, referenced files over ${MAX_REFERENCED_ASSET_BYTES / 1e6} MB, or files beyond ${MAX_TOTAL_ASSET_BYTES / 1e6} MB in total, are skipped: ${listAssetPaths(overBudgetAssets)}. ` + (overBudgetReferenced > 0 ? `${overBudgetReferenced} of them ${overBudgetReferenced === 1 ? "is" : "are"} used by pages, so those images will be broken until you copy ${overBudgetAssets.length === 1 ? "it" : "them"}. ` : "") + `Copy ${overBudgetAssets.length === 1 ? "it" : "them"} into public/ manually.`
8563
8835
  });
8564
8836
  }
8565
8837
  if (lfsPointerAssets.length > 0) {
@@ -8568,7 +8840,31 @@ function migrateRepository(options) {
8568
8840
  message: `${lfsPointerAssets.length} asset file${lfsPointerAssets.length === 1 ? " is a Git LFS pointer" : "s are Git LFS pointers"}, not real content, and ${lfsPointerAssets.length === 1 ? "was" : "were"} not copied (Git LFS was skipped during clone because the host has no git-lfs binary): ${listAssetPaths(lfsPointerAssets)}. Install git-lfs and re-run the migration, or copy the real files into public/ manually.`
8569
8841
  });
8570
8842
  }
8571
- if (!discoveryBudgetApplied && files.length >= sourceBudget) {
8843
+ if (hasRootStylesheet && rootStylesheetPath && mintlifyProjectRoot) {
8844
+ const size = lstatSync5(rootStylesheetPath).size;
8845
+ if (size > MAX_ASSET_BYTES || totalAssetBytes + size > MAX_TOTAL_ASSET_BYTES) {
8846
+ warnings.push({ code: "limit-reached", message: "Mintlify root style.css exceeded the asset budget and was not imported.", source: "style.css" });
8847
+ } else if (rootStyleProblem === "unsafe") {
8848
+ warnings.push({ code: "unsupported-config", message: "Mintlify root style.css contains a CSS import or unsafe URL; review and import it manually.", source: "style.css" });
8849
+ } else if (rootStyleProblem === "parse" || !rootStyleProjection) {
8850
+ warnings.push({ code: "unsupported-config", message: "Mintlify root style.css could not be parsed and was not imported.", source: "style.css" });
8851
+ } else {
8852
+ if (rootStyleProjection.omittedSelectors > 0) warnings.push({
8853
+ code: "unsupported-config",
8854
+ source: "style.css",
8855
+ 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.`
8856
+ });
8857
+ if (rootStyleProjection.css.trim()) {
8858
+ const identity = createHash2("sha256").update(componentSourceIdentity(options.sourceUrl, repositoryDir, mintlifyProjectRoot)).digest("hex").slice(0, 12);
8859
+ const assetPath = `migrated/${identity}/style.css`;
8860
+ const content = Buffer.from(rootStyleProjection.css);
8861
+ assets.push({ path: assetPath, content });
8862
+ docsConfig = { ...docsConfig, stylesheets: [...docsConfig.stylesheets ?? [], `/${assetPath}`] };
8863
+ totalAssetBytes += content.length;
8864
+ }
8865
+ }
8866
+ }
8867
+ if (!discoveryRank && files.length >= sourceBudget) {
8572
8868
  warnings.push({ code: "limit-reached", message: `Stopped scanning after ${sourceBudget} files, so the rest of the repository was not looked at. Run the migration on a smaller part of the repository with --docs-dir.` });
8573
8869
  }
8574
8870
  if (platform === "docusaurus") {
@@ -9241,6 +9537,7 @@ function mergeMigrationConfig(existing, incoming) {
9241
9537
  // Collection presentation belongs to the imported information
9242
9538
  // architecture; starter defaults must not coerce source dropdowns to tabs.
9243
9539
  navigation: incoming.navigation ?? existing.navigation,
9540
+ ...existing.stylesheets?.length || incoming.stylesheets?.length ? { stylesheets: [.../* @__PURE__ */ new Set([...existing.stylesheets ?? [], ...incoming.stylesheets ?? []])] } : {},
9244
9541
  ...i18n ? { i18n } : {},
9245
9542
  // Existing providers win per provider; imported ones fill the gaps.
9246
9543
  // A malformed (string/array) existing value must not spread into index keys.
@@ -9587,7 +9884,7 @@ function embeddedMintlifyConfig(body) {
9587
9884
  function plainDescription(value) {
9588
9885
  return value.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_>#-]/g, "").replace(/\s+/g, " ").trim().slice(0, 240);
9589
9886
  }
9590
- function withoutFencedCode(body) {
9887
+ function withoutFencedCode2(body) {
9591
9888
  let fenceCharacter = "";
9592
9889
  let fenceLength = 0;
9593
9890
  return body.split("\n").map((line) => {
@@ -9897,7 +10194,7 @@ function sanitizeRemoteMarkdown(body) {
9897
10194
  }).join("\n");
9898
10195
  const urlSafeMarkdown = sanitizeMarkdownUrls(sanitized);
9899
10196
  if (urlSafeMarkdown === null) return null;
9900
- const executable = withoutFencedCode(urlSafeMarkdown).replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<(?:https?:\/\/|mailto:)[^>]+>/gi, "").replace(/(`+)[\s\S]*?\1/g, "");
10197
+ const executable = withoutFencedCode2(urlSafeMarkdown).replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<(?:https?:\/\/|mailto:)[^>]+>/gi, "").replace(/(`+)[\s\S]*?\1/g, "");
9901
10198
  if (/^\s*(?:import|export)\s/m.test(executable)) return null;
9902
10199
  let hasUnsafeExpression = false;
9903
10200
  const withoutStaticProps = executable.replace(/=\{([^{}\n]*)\}/g, (original, value) => {
@@ -10847,7 +11144,7 @@ async function migrateUrl(options) {
10847
11144
  }
10848
11145
 
10849
11146
  // src/remote-api.ts
10850
- import { createHash as createHash2 } from "crypto";
11147
+ import { createHash as createHash3 } from "crypto";
10851
11148
  import { lookup } from "dns/promises";
10852
11149
  import { request } from "https";
10853
11150
  import { isIP } from "net";
@@ -11007,7 +11304,7 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
11007
11304
  const extension = /\.ya?ml$/i.test(url.pathname) ? "yaml" : "json";
11008
11305
  const body = await downloadSpec(reference.url, fetcher);
11009
11306
  const operations = parseOpenApi(body, extension);
11010
- const filename = `openapi-${createHash2("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
11307
+ const filename = `openapi-${createHash3("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
11011
11308
  assets.push({ path: `openapi/${filename}`, content: body, projectRelative: true });
11012
11309
  for (const page of pages) {
11013
11310
  const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;