@thallylabs/migrate 0.2.21 → 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";
@@ -428,6 +429,9 @@ function bracketDelta(line) {
428
429
  return delta;
429
430
  }
430
431
  var ESM_OPEN = /^(?:import|export)\b/;
432
+ function latexElement(source, block = false) {
433
+ return `<Latex${block ? " block" : ""}>{${JSON.stringify(source)}}</Latex>`;
434
+ }
431
435
  function protectMathBlocks(raw) {
432
436
  const { front, body } = splitFrontmatterBlock(raw);
433
437
  const lines = body.split(/\r\n|\r|\n/);
@@ -442,7 +446,7 @@ function protectMathBlocks(raw) {
442
446
  const trimmed = line.trim();
443
447
  if (mathBlockDelimiter) {
444
448
  if (trimmed === mathBlockDelimiter) {
445
- output.push("```math", ...mathLines, "```");
449
+ output.push(latexElement(mathLines.join("\n"), true));
446
450
  mathBlockDelimiter = null;
447
451
  mathLines = [];
448
452
  converted = true;
@@ -484,11 +488,11 @@ function protectMathBlocks(raw) {
484
488
  (whole, block, inline) => {
485
489
  if (block !== void 0) {
486
490
  converted = true;
487
- return `\`$$${block}$$\``;
491
+ return latexElement(unmask(block));
488
492
  }
489
493
  if (inline !== void 0 && hasTexSignal(unmask(inline))) {
490
494
  converted = true;
491
- return `\`$${inline}$\``;
495
+ return latexElement(unmask(inline));
492
496
  }
493
497
  return whole;
494
498
  }
@@ -1222,6 +1226,32 @@ function injectReactHookImports(body) {
1222
1226
 
1223
1227
  ${body}` : body;
1224
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
+ }
1225
1255
  function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1226
1256
  const runFern = platform === "fern";
1227
1257
  const runMintlify = platform === "mintlify";
@@ -1233,6 +1263,13 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1233
1263
  /:([+\w-]+):/g,
1234
1264
  (shortcode, name) => nameToEmoji[name] ?? shortcode
1235
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
+ ));
1236
1273
  rewritten = normalizeDocusaurusTabBlocks(normalizeDocusaurusTabs(removeGlobalDocusaurusImports(rewritten)));
1237
1274
  rewritten = replaceOutsideCode(rewritten, (segment) => segment.replace(
1238
1275
  /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s+(['"])(@site\/[^'"]+)\2\s*;?/g,
@@ -1269,7 +1306,7 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1269
1306
  return normalizeMdx(body, platform, false);
1270
1307
  }
1271
1308
  }
1272
- return normalized;
1309
+ return normalizeFlowParagraphContainers(normalized);
1273
1310
  }
1274
1311
  function parseMarkdownPage(input) {
1275
1312
  const parsed = parseFrontmatter(input.raw);
@@ -2205,26 +2242,33 @@ function hash(value) {
2205
2242
  function portableSpecifier(path) {
2206
2243
  return path.replace(/\.tsx?$/, "");
2207
2244
  }
2208
- function wrapImpureCssModuleSelectors(css) {
2245
+ function sanitizeCssModuleSelectors(css) {
2209
2246
  const root = postcss.parse(css);
2247
+ let omitted = 0;
2210
2248
  root.walkRules((rule) => {
2211
- rule.selector = selectorParser((selectors) => {
2212
- selectors.each((selector) => {
2213
- let hasClassOrId = false;
2214
- selector.walk((node) => {
2215
- if (node.type === "class" || node.type === "id") hasClassOrId = true;
2216
- });
2217
- if (hasClassOrId) return;
2218
- const nodes = selector.nodes.splice(0, selector.nodes.length);
2219
- const container = selectorParser.selector({ value: "" });
2220
- for (const node of nodes) container.append(node);
2221
- const globalPseudo = selectorParser.pseudo({ value: ":global" });
2222
- globalPseudo.append(container);
2223
- 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;
2224
2262
  });
2225
- }).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();
2226
2270
  });
2227
- return root.toString();
2271
+ return { css: root.toString(), omitted };
2228
2272
  }
2229
2273
  function sourceFile(source, filename) {
2230
2274
  return ts2.createSourceFile(
@@ -2455,6 +2499,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2455
2499
  }
2456
2500
  function copyGraph(entry) {
2457
2501
  const staged = /* @__PURE__ */ new Map();
2502
+ const stagedWarnings = [];
2458
2503
  let stagedBytes = 0;
2459
2504
  function stageShim(filename, content) {
2460
2505
  const shimDestination = `${destinationRoot}/shims/${filename}`;
@@ -2475,13 +2520,14 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2475
2520
  if (!CODE_EXTENSIONS.has(extension)) {
2476
2521
  if (extension === ".css" && /@import\b|url\s*\(/i.test(content.toString("utf8"))) throw new Error("CSS resource dependencies require manual migration");
2477
2522
  if (extension === ".css" && path.toLowerCase().endsWith(".module.css")) {
2478
- let wrapped;
2523
+ let sanitized;
2479
2524
  try {
2480
- wrapped = wrapImpureCssModuleSelectors(content.toString("utf8"));
2525
+ sanitized = sanitizeCssModuleSelectors(content.toString("utf8"));
2481
2526
  } catch {
2482
2527
  throw new Error("CSS module could not be parsed to check for Turbopack-safe selectors");
2483
2528
  }
2484
- 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 });
2485
2531
  }
2486
2532
  return;
2487
2533
  }
@@ -2550,6 +2596,9 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2550
2596
  }
2551
2597
  visit(entry);
2552
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
+ }
2553
2602
  copiedBytes += stagedBytes;
2554
2603
  return outputPath(entry);
2555
2604
  }
@@ -2803,7 +2852,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2803
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);
2804
2853
  } else {
2805
2854
  edits.push({ start: importStart, end: importEnd, value: "" });
2806
- 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);
2807
2856
  }
2808
2857
  }
2809
2858
  }
@@ -3133,6 +3182,110 @@ import { customComponents as ${name} } from ${JSON.stringify(registryPath)}
3133
3182
  ];
3134
3183
  }
3135
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
+
3136
3289
  // src/docusaurus.ts
3137
3290
  import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3 } from "fs";
3138
3291
  import { extname as extname4, posix as posix2 } from "path";
@@ -4131,6 +4284,47 @@ function projectMintlifyNavigation(config, options = {}) {
4131
4284
  message: "Mintlify navigation could not be projected; generated navigation will be used."
4132
4285
  });
4133
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
+ }
4134
4328
  const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
4135
4329
  const redirect = objectValue2(value);
4136
4330
  if (!redirect || typeof redirect.source !== "string" || typeof redirect.destination !== "string") return [];
@@ -4155,7 +4349,11 @@ function projectMintlifyNavigation(config, options = {}) {
4155
4349
  return {
4156
4350
  docsConfig: {
4157
4351
  tabs,
4158
- ...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
+ } } : {},
4159
4357
  ...projectedCompatibleConfig(config, warnings),
4160
4358
  ...projectedApiMdx(config, warnings),
4161
4359
  ...i18n ? { i18n } : {},
@@ -4832,9 +5030,10 @@ function descriptorSort(left, right) {
4832
5030
  if (leftPosition !== rightPosition) return leftPosition - rightPosition;
4833
5031
  return left.sourcePath.localeCompare(right.sourcePath, void 0, { numeric: true });
4834
5032
  }
4835
- function registerDoc(docId, context) {
5033
+ function registerDoc(docId, context, sourceDirectory) {
4836
5034
  const key = normalizeDocId(docId);
4837
- 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));
4838
5037
  if (!descriptor) {
4839
5038
  context.warnings.push({
4840
5039
  code: "missing-page",
@@ -4846,10 +5045,10 @@ function registerDoc(docId, context) {
4846
5045
  context.referencedNavigationIds.add(descriptor.navigationId);
4847
5046
  return descriptor.navigationId;
4848
5047
  }
4849
- function generatedIndexPage(label, linkValue, context) {
5048
+ function generatedIndexPage(label, linkValue, context, sourceDirectory) {
4850
5049
  const link = objectValue3(linkValue);
4851
5050
  if (!link) return null;
4852
- 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);
4853
5052
  if (link.type !== "generated-index") return null;
4854
5053
  const fallback = `category/${slugifySegment(label)}`;
4855
5054
  const unprefixedNavigationId = typeof link.slug === "string" ? routeFromSlug(link.slug, fallback) ?? fallback : fallback;
@@ -5092,7 +5291,7 @@ function autogeneratedItems(dirName, context) {
5092
5291
  };
5093
5292
  }
5094
5293
  }
5095
- const landing = generatedIndexPage(metadata.label ?? titleCase2(segment), metadata.link, context);
5294
+ const landing = generatedIndexPage(metadata.label ?? titleCase2(segment), metadata.link, context, directory);
5096
5295
  if (landing && !pages2.includes(landing)) pages2.unshift(landing);
5097
5296
  return {
5098
5297
  position: metadata.position ?? Number.MAX_SAFE_INTEGER,
@@ -5926,6 +6125,7 @@ var FRONTMATTER_HEAD_BYTES = 65536;
5926
6125
  var MAX_WITHHELD_SCAN_BYTES = MAX_PAGE_BYTES * 8;
5927
6126
  var MAX_DROPPED_GATED_WARNINGS = 20;
5928
6127
  var MAX_ASSET_BYTES = 25e6;
6128
+ var MAX_REFERENCED_ASSET_BYTES = 1e8;
5929
6129
  var MAX_TOTAL_ASSET_BYTES = 5e8;
5930
6130
  var GIT_LFS_POINTER_PREFIX = "version https://git-lfs.github.com/spec/v1";
5931
6131
  function listAssetPaths(paths) {
@@ -7186,18 +7386,18 @@ function isolateFences(snippet, source, start, end) {
7186
7386
  return `${fence.test(lines[0]) && /\S/.test(before) ? "\n" : ""}${snippet}${fence.test(lines[lines.length - 1]) && /\S/.test(after) ? "\n" : ""}`;
7187
7387
  }
7188
7388
  function snippetComponentBody(source, componentName) {
7189
- 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);
7190
7390
  for (const statement of parsed.statements) {
7191
- if (!ts3.isVariableStatement(statement)) continue;
7391
+ if (!ts4.isVariableStatement(statement)) continue;
7192
7392
  for (const declaration of statement.declarationList.declarations) {
7193
- if (!ts3.isIdentifier(declaration.name) || declaration.name.text !== componentName) continue;
7393
+ if (!ts4.isIdentifier(declaration.name) || declaration.name.text !== componentName) continue;
7194
7394
  const initializer = declaration.initializer;
7195
- if (!initializer || !ts3.isArrowFunction(initializer)) continue;
7196
- 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;
7197
7397
  if (!returned) continue;
7198
7398
  let expression = returned;
7199
- while (ts3.isParenthesizedExpression(expression)) expression = expression.expression;
7200
- 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)) {
7201
7401
  return expression.getText(parsed);
7202
7402
  }
7203
7403
  }
@@ -7205,31 +7405,31 @@ function snippetComponentBody(source, componentName) {
7205
7405
  return source;
7206
7406
  }
7207
7407
  function statefulSnippetDeclaration(source, componentName) {
7208
- const parsed = ts3.createSourceFile("snippet.tsx", source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
7209
- 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);
7210
7410
  if (parsed.statements.length === 0 || !parsed.statements.every(isEsm)) return null;
7211
- 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);
7212
- 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;
7213
7413
  const declaration = statement.declarationList.declarations[0];
7214
- 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;
7215
7415
  const body = declaration.initializer.body.statements;
7216
- if (body.length < 2 || !body.some(ts3.isReturnStatement)) return null;
7416
+ if (body.length < 2 || !body.some(ts4.isReturnStatement)) return null;
7217
7417
  return source.trim();
7218
7418
  }
7219
7419
  function staticNamedSnippetValues(source) {
7220
7420
  const values = /* @__PURE__ */ new Map();
7221
- 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);
7222
7422
  for (const statement of parsed.statements) {
7223
- 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;
7224
7424
  for (const declaration of statement.declarationList.declarations) {
7225
- if (!ts3.isIdentifier(declaration.name) || !declaration.initializer) continue;
7425
+ if (!ts4.isIdentifier(declaration.name) || !declaration.initializer) continue;
7226
7426
  const expression = declaration.initializer;
7227
- if (ts3.isStringLiteral(expression) || ts3.isNoSubstitutionTemplateLiteral(expression)) {
7427
+ if (ts4.isStringLiteral(expression) || ts4.isNoSubstitutionTemplateLiteral(expression)) {
7228
7428
  values.set(declaration.name.text, JSON.stringify(expression.text));
7229
- } else if (ts3.isNumericLiteral(expression)) {
7429
+ } else if (ts4.isNumericLiteral(expression)) {
7230
7430
  values.set(declaration.name.text, expression.text);
7231
- } else if (expression.kind === ts3.SyntaxKind.TrueKeyword || expression.kind === ts3.SyntaxKind.FalseKeyword) {
7232
- 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");
7233
7433
  }
7234
7434
  }
7235
7435
  }
@@ -7556,6 +7756,19 @@ function rewriteRepositoryAssetLinks(body, currentFile, siteRoot, onReferenced)
7556
7756
  });
7557
7757
  }
7558
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
+ }
7559
7772
  function locallyDeclaredNames(raw) {
7560
7773
  const names = /* @__PURE__ */ new Set();
7561
7774
  let codeFence = null;
@@ -7601,7 +7814,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7601
7814
  });
7602
7815
  return `{/* ${verdict.kind === "oversized" ? "Oversized" : "Unreadable"} content not inlined: ${shown} */}`;
7603
7816
  };
7604
- let withoutImports = replaceOutsideCode(raw, (source2) => source2.replace(
7817
+ let withoutImports = replaceOutsideCodeAndMdxComments(raw, (source2) => source2.replace(
7605
7818
  SNIPPET_IMPORT_PATTERN,
7606
7819
  (_statement, namedComponent, defaultComponent, sourcePath) => {
7607
7820
  const componentName = namedComponent ?? defaultComponent;
@@ -7628,6 +7841,12 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7628
7841
  else snippets.set(componentName, snippetComponentBody(nested, componentName));
7629
7842
  return "";
7630
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 "";
7631
7850
  warnings.push({
7632
7851
  code: "missing-page",
7633
7852
  message: `Imported snippet ${sourcePath} could not be resolved and was left as a comment.`,
@@ -7638,7 +7857,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7638
7857
  }
7639
7858
  }
7640
7859
  ));
7641
- withoutImports = replaceOutsideCode(withoutImports, (source2) => source2.replace(
7860
+ withoutImports = replaceOutsideCodeAndMdxComments(withoutImports, (source2) => source2.replace(
7642
7861
  SNIPPET_VALUE_IMPORT_PATTERN,
7643
7862
  (statement, names, sourcePath) => {
7644
7863
  const bindings = names.split(",").map((name) => name.trim()).filter(Boolean).map((name) => {
@@ -7682,7 +7901,12 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7682
7901
  ));
7683
7902
  const localNames = locallyDeclaredNames(withoutImports);
7684
7903
  for (const [componentName, candidate] of globalAliases) {
7685
- 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;
7686
7910
  const blocked = blockInline(candidate, relative5(repositoryRoot, candidate).replace(/\\/g, "/"));
7687
7911
  if (blocked) {
7688
7912
  snippets.set(componentName, blocked);
@@ -7705,9 +7929,9 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7705
7929
  let result = replaceOutsideCodeAndComments(withoutImports, (segment) => {
7706
7930
  for (const [componentName, snippet] of snippets) {
7707
7931
  segment = segment.replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)\\s*/>`, "g"), (tag, attributes, offset, source2) => {
7708
- return isolateFences(interpolateSnippet(snippet, attributes), source2, offset, offset + tag.length);
7932
+ return isolateFences(closeOpenCodeFence(interpolateSnippet(snippet, attributes)), source2, offset, offset + tag.length);
7709
7933
  }).replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)>([\\s\\S]*?)<\\/${componentName}>`, "g"), (tag, attributes, children, offset, source2) => {
7710
- return isolateFences(interpolateSnippet(snippet, attributes, children), source2, offset, offset + tag.length);
7934
+ return isolateFences(closeOpenCodeFence(interpolateSnippet(snippet, attributes, children)), source2, offset, offset + tag.length);
7711
7935
  });
7712
7936
  }
7713
7937
  return segment.replace(SNIPPET_TAG_PATTERN, (tag, doubleQuoted, singleQuoted, offset, source2) => {
@@ -7726,7 +7950,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7726
7950
  if (!candidate) throw new Error("file not found");
7727
7951
  const blocked = blockInline(candidate, filePath, `Snippet file="${filePath}"`);
7728
7952
  if (blocked) return blocked;
7729
- return isolateFences(inlineMdxSnippets(
7953
+ return isolateFences(closeOpenCodeFence(inlineMdxSnippets(
7730
7954
  withoutFrontmatter(readFileSync5(candidate, "utf8")),
7731
7955
  candidate,
7732
7956
  repositoryRoot,
@@ -7735,7 +7959,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7735
7959
  siteRoot,
7736
7960
  globalAliases,
7737
7961
  gate
7738
- ), source2, offset, offset + tag.length);
7962
+ )), source2, offset, offset + tag.length);
7739
7963
  } catch {
7740
7964
  warnings.push({
7741
7965
  code: "missing-page",
@@ -7940,7 +8164,7 @@ function migrateRepository(options) {
7940
8164
  const sourceBudget = options.maxSourceFiles ?? MAX_SOURCE_FILES;
7941
8165
  const scannedFiles = scanFiles(contentRoot, repositoryDir, warnings, discoveryRank, sourceBudget);
7942
8166
  const mintignoreFilteredFiles = mintignoreMatcher ? scannedFiles.filter((file) => !mintignoreMatcher.ignores(file.relativePath)) : scannedFiles;
7943
- 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));
7944
8168
  const files = discoveryRank ? selectFilesWithinBudget(mintignoreFilteredFiles, discoveryRank, warnings, allVersionPrefixes, sourceBudget) : mintignoreFilteredFiles;
7945
8169
  if (platform === "fern" && fernProjectRoot) {
7946
8170
  const discovered2 = new Set(files.map((file) => file.relativePath));
@@ -8019,6 +8243,10 @@ function migrateRepository(options) {
8019
8243
  }
8020
8244
  for (const file of mintignoreFilteredFiles) {
8021
8245
  if (keptFiles.has(file) || !isDocFile(file)) continue;
8246
+ if (isUnreferencedSnippetModule(file)) {
8247
+ if (classifyPageGate(file).reason) withheldDocFiles.push(file);
8248
+ continue;
8249
+ }
8022
8250
  droppedPageCount++;
8023
8251
  if (!isGateCandidate(file)) {
8024
8252
  const snippetReason = classifyPageGate(file).reason;
@@ -8253,13 +8481,7 @@ function migrateRepository(options) {
8253
8481
  }
8254
8482
  if (platform === "fern" || platform === "mintlify" || platform === "docusaurus") {
8255
8483
  const protectedMath = protectMathBlocks(raw);
8256
- if (protectedMath.converted) {
8257
- warnings.push({
8258
- code: "unsupported-config",
8259
- message: "Math (KaTeX '$$...$$') has no renderer in Thally yet; it was kept as a fenced code block instead of being dropped.",
8260
- source: relative5(repositoryDir, file.absolutePath).replace(/\\/g, "/")
8261
- });
8262
- } else if (protectedMath.guardTriggered) {
8484
+ if (protectedMath.guardTriggered) {
8263
8485
  warnings.push({
8264
8486
  code: "unsupported-config",
8265
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.",
@@ -8351,7 +8573,7 @@ function migrateRepository(options) {
8351
8573
  page.body = replaceUnknownComponents(page.body, (name) => {
8352
8574
  warnings.push({
8353
8575
  code: "unsupported-config",
8354
- 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.`,
8355
8577
  source: file.relativePath
8356
8578
  });
8357
8579
  });
@@ -8478,9 +8700,31 @@ function migrateRepository(options) {
8478
8700
  const source = /^src\/mdx\/migrated\/[^/]+\/source\/(.+)$/.exec(file.path)?.[1];
8479
8701
  return source ? [resolvePath(repositoryDir, source)] : [];
8480
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
+ }
8481
8724
  const siteAssetPaths = /* @__PURE__ */ new Map();
8482
8725
  const fontAssetByRelative = /* @__PURE__ */ new Map();
8483
8726
  for (const file of [...files, ...repositoryAssets, ...fernReferencedAssets]) {
8727
+ if (rootStylesheetPath && file.absolutePath === rootStylesheetPath) continue;
8484
8728
  const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
8485
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;
8486
8730
  if (!siteKind && !ASSET_EXTENSIONS.has(extname6(file.relativePath).toLowerCase())) continue;
@@ -8526,7 +8770,8 @@ function migrateRepository(options) {
8526
8770
  const lfsPointerAssets = [];
8527
8771
  const ambiguouslyNamedAssets = [];
8528
8772
  for (const { file, assetPath, size } of orderedAssetCandidates) {
8529
- 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) {
8530
8775
  overBudgetAssets.push(file.relativePath);
8531
8776
  if (referencedAssetPaths.has(assetPath)) overBudgetReferenced++;
8532
8777
  continue;
@@ -8568,7 +8813,7 @@ function migrateRepository(options) {
8568
8813
  for (const entry of copiedSite.filter((item) => item.kind === "style")) {
8569
8814
  warnings.push({
8570
8815
  code: "unsupported-config",
8571
- 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.`,
8572
8817
  source: entry.path
8573
8818
  });
8574
8819
  }
@@ -8586,7 +8831,7 @@ function migrateRepository(options) {
8586
8831
  if (overBudgetAssets.length > 0) {
8587
8832
  warnings.push({
8588
8833
  code: "limit-reached",
8589
- 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.`
8590
8835
  });
8591
8836
  }
8592
8837
  if (lfsPointerAssets.length > 0) {
@@ -8595,7 +8840,31 @@ function migrateRepository(options) {
8595
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.`
8596
8841
  });
8597
8842
  }
8598
- 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) {
8599
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.` });
8600
8869
  }
8601
8870
  if (platform === "docusaurus") {
@@ -9268,6 +9537,7 @@ function mergeMigrationConfig(existing, incoming) {
9268
9537
  // Collection presentation belongs to the imported information
9269
9538
  // architecture; starter defaults must not coerce source dropdowns to tabs.
9270
9539
  navigation: incoming.navigation ?? existing.navigation,
9540
+ ...existing.stylesheets?.length || incoming.stylesheets?.length ? { stylesheets: [.../* @__PURE__ */ new Set([...existing.stylesheets ?? [], ...incoming.stylesheets ?? []])] } : {},
9271
9541
  ...i18n ? { i18n } : {},
9272
9542
  // Existing providers win per provider; imported ones fill the gaps.
9273
9543
  // A malformed (string/array) existing value must not spread into index keys.
@@ -9614,7 +9884,7 @@ function embeddedMintlifyConfig(body) {
9614
9884
  function plainDescription(value) {
9615
9885
  return value.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_>#-]/g, "").replace(/\s+/g, " ").trim().slice(0, 240);
9616
9886
  }
9617
- function withoutFencedCode(body) {
9887
+ function withoutFencedCode2(body) {
9618
9888
  let fenceCharacter = "";
9619
9889
  let fenceLength = 0;
9620
9890
  return body.split("\n").map((line) => {
@@ -9924,7 +10194,7 @@ function sanitizeRemoteMarkdown(body) {
9924
10194
  }).join("\n");
9925
10195
  const urlSafeMarkdown = sanitizeMarkdownUrls(sanitized);
9926
10196
  if (urlSafeMarkdown === null) return null;
9927
- 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, "");
9928
10198
  if (/^\s*(?:import|export)\s/m.test(executable)) return null;
9929
10199
  let hasUnsafeExpression = false;
9930
10200
  const withoutStaticProps = executable.replace(/=\{([^{}\n]*)\}/g, (original, value) => {
@@ -10874,7 +11144,7 @@ async function migrateUrl(options) {
10874
11144
  }
10875
11145
 
10876
11146
  // src/remote-api.ts
10877
- import { createHash as createHash2 } from "crypto";
11147
+ import { createHash as createHash3 } from "crypto";
10878
11148
  import { lookup } from "dns/promises";
10879
11149
  import { request } from "https";
10880
11150
  import { isIP } from "net";
@@ -11034,7 +11304,7 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
11034
11304
  const extension = /\.ya?ml$/i.test(url.pathname) ? "yaml" : "json";
11035
11305
  const body = await downloadSpec(reference.url, fetcher);
11036
11306
  const operations = parseOpenApi(body, extension);
11037
- 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}`;
11038
11308
  assets.push({ path: `openapi/${filename}`, content: body, projectRelative: true });
11039
11309
  for (const page of pages) {
11040
11310
  const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;