@thallylabs/migrate 0.2.12 → 0.2.17

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
@@ -14,7 +14,7 @@ import {
14
14
  statSync
15
15
  } from "fs";
16
16
  import { createRequire } from "module";
17
- import { basename as basename3, dirname as dirname4, extname as extname5, isAbsolute as isAbsolute2, relative as relative5, resolve as resolvePath, sep as sep2 } from "path";
17
+ 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
18
  import { parse as parseYaml4 } from "yaml";
19
19
  import * as ts3 from "typescript";
20
20
 
@@ -3104,13 +3104,96 @@ import { customComponents as ${name} } from ${JSON.stringify(registryPath)}
3104
3104
 
3105
3105
  // src/docusaurus.ts
3106
3106
  import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3 } from "fs";
3107
- import { extname as extname3, posix as posix2 } from "path";
3107
+ import { extname as extname4, posix as posix2 } from "path";
3108
3108
  import JSON5 from "json5";
3109
3109
  import { parse as parseYaml2 } from "yaml";
3110
3110
 
3111
3111
  // src/navigation.ts
3112
3112
  import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2, realpathSync } from "fs";
3113
- import { dirname as dirname2, extname as extname2, relative as relative3 } from "path";
3113
+ import { dirname as dirname2, extname as extname3, relative as relative3 } from "path";
3114
+
3115
+ // src/mintlify-extras.ts
3116
+ import { extname as extname2 } from "path";
3117
+ function objectValue(value) {
3118
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3119
+ }
3120
+ var PUBLIC_FALSE_STRINGS = /* @__PURE__ */ new Set(["false", "no", "off", "0"]);
3121
+ function isPublicFalse(value) {
3122
+ return value === false || value === 0 || typeof value === "string" && PUBLIC_FALSE_STRINGS.has(value.trim().toLowerCase());
3123
+ }
3124
+ function isPublicTrue(value) {
3125
+ return value === true || typeof value === "string" && value.trim().toLowerCase() === "true";
3126
+ }
3127
+ function frontmatterGateReason(data) {
3128
+ const groups = data.groups;
3129
+ const hasGroups = Array.isArray(groups) ? groups.length > 0 : typeof groups === "string" ? groups.trim() !== "" : groups !== void 0 && groups !== null && groups !== false;
3130
+ if (hasGroups) return "frontmatter `groups` restricts it to authenticated groups";
3131
+ if (isPublicFalse(data.public)) return "frontmatter `public: false`";
3132
+ return void 0;
3133
+ }
3134
+ function navigationGateReason(node) {
3135
+ const groups = node.groups;
3136
+ const isContainer = (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) && ["group", "pages", "tab", "groups", "page", "root", "menu", "href"].some((key) => key in entry);
3137
+ const restricted = typeof groups === "string" ? groups.trim() !== "" : Array.isArray(groups) && groups.length > 0 && !groups.every(isContainer);
3138
+ if (restricted) return "its navigation container sets `groups`";
3139
+ if (isPublicFalse(node.public)) return "its navigation container sets `public: false`";
3140
+ return void 0;
3141
+ }
3142
+ var CONFIG_BASENAME = /(?:^|[.-])(?:config|rc)\.[cm]?js$|^(?:tailwind|postcss|next|eslint|prettier|babel|webpack|vite|vitest|jest|rollup|gulpfile|gruntfile|stylelint|commitlint|lint-staged|svgo|metro)[.-]/i;
3143
+ var BUILD_OUTPUT_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "coverage"]);
3144
+ var FONT_EXTENSIONS = /* @__PURE__ */ new Set([".woff", ".woff2", ".ttf", ".otf"]);
3145
+ function isMintlifyServedScriptOrStyle(relativePath) {
3146
+ const extension = extname2(relativePath).toLowerCase();
3147
+ if (extension !== ".css" && extension !== ".js") return false;
3148
+ const segments = relativePath.split("/");
3149
+ if (segments.slice(0, -1).some((segment) => BUILD_OUTPUT_SEGMENTS.has(segment.toLowerCase()))) return false;
3150
+ return extension === ".css" || !CONFIG_BASENAME.test(segments[segments.length - 1]);
3151
+ }
3152
+ function mintlifyFontSources(config) {
3153
+ const fonts = objectValue(config?.fonts);
3154
+ if (!fonts) return [];
3155
+ const found = [];
3156
+ for (const entry of [fonts, objectValue(fonts.heading), objectValue(fonts.body)]) {
3157
+ if (!entry || typeof entry.source !== "string" || !entry.source.trim()) continue;
3158
+ const source = entry.source.trim();
3159
+ const family = typeof entry.family === "string" ? entry.family : "a font";
3160
+ const remote = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(source);
3161
+ const path = remote || /^[a-z]:[\\/]/i.test(source) ? null : normalizeAssetPath(source);
3162
+ found.push({ family, source, path: path && FONT_EXTENSIONS.has(extname2(path).toLowerCase()) ? path : null, remote });
3163
+ }
3164
+ return found;
3165
+ }
3166
+ function mintlifyAppearance(config, warnings) {
3167
+ const appearance = objectValue(config.appearance);
3168
+ const legacy = objectValue(config.modeToggle);
3169
+ const result = {};
3170
+ const isMode = (value) => value === "system" || value === "light" || value === "dark";
3171
+ const skip = (field, value) => {
3172
+ warnings.push({
3173
+ code: "unsupported-config",
3174
+ message: `Ignored invalid ${field} value ${JSON.stringify(value)?.slice(0, 40)}; the appearance default was left unchanged.`
3175
+ });
3176
+ };
3177
+ if (appearance?.default !== void 0) {
3178
+ if (isMode(appearance.default)) result.default = appearance.default;
3179
+ else skip("appearance.default", appearance.default);
3180
+ } else if (legacy?.default !== void 0) {
3181
+ if (isMode(legacy.default)) result.default = legacy.default;
3182
+ else skip("modeToggle.default", legacy.default);
3183
+ }
3184
+ if (appearance?.strict !== void 0) {
3185
+ if (typeof appearance.strict === "boolean") {
3186
+ if (appearance.strict) result.showToggle = false;
3187
+ } else skip("appearance.strict", appearance.strict);
3188
+ } else if (legacy?.isHidden !== void 0) {
3189
+ if (typeof legacy.isHidden === "boolean") {
3190
+ if (legacy.isHidden) result.showToggle = false;
3191
+ } else skip("modeToggle.isHidden", legacy.isHidden);
3192
+ }
3193
+ return result;
3194
+ }
3195
+
3196
+ // src/navigation.ts
3114
3197
  var MAX_MINTLIFY_CONFIG_BYTES = 2e7;
3115
3198
  function addMintlifyDirectoryRedirects(config, pages) {
3116
3199
  const pageIds = new Set(pages.map((page) => page.id));
@@ -3210,13 +3293,13 @@ var LANGUAGE_LABELS = {
3210
3293
  uk: "Ukrainian",
3211
3294
  zh: "Chinese"
3212
3295
  };
3213
- function objectValue(value) {
3296
+ function objectValue2(value) {
3214
3297
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3215
3298
  }
3216
3299
  function withFlattenedProductGroups(container) {
3217
3300
  if (!Array.isArray(container.productGroups)) return container;
3218
3301
  const flattened = container.productGroups.flatMap((entry) => {
3219
- const group = objectValue(entry);
3302
+ const group = objectValue2(entry);
3220
3303
  return group && Array.isArray(group.products) ? group.products : [];
3221
3304
  });
3222
3305
  if (flattened.length === 0) return container;
@@ -3235,7 +3318,7 @@ function projectedHref(value) {
3235
3318
  return href;
3236
3319
  }
3237
3320
  function labelFor(value, fallback) {
3238
- for (const key of ["tab", "group", "anchor", "product", "dropdown", "version", "menu", "label", "name", "title"]) {
3321
+ for (const key of ["tab", "group", "anchor", "product", "dropdown", "version", "menu", "item", "label", "name", "title"]) {
3239
3322
  if (typeof value[key] === "string" && value[key]) return String(value[key]);
3240
3323
  }
3241
3324
  return fallback;
@@ -3251,7 +3334,7 @@ function jsonPointer(root, pointer) {
3251
3334
  const index = Number(token);
3252
3335
  return Number.isSafeInteger(index) && index >= 0 ? current[index] : void 0;
3253
3336
  }
3254
- const object = objectValue(current);
3337
+ const object = objectValue2(current);
3255
3338
  if (!object) return void 0;
3256
3339
  return object[token.replace(/~1/g, "/").replace(/~0/g, "~")];
3257
3340
  }, root);
@@ -3272,7 +3355,7 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
3272
3355
  if (Array.isArray(value)) {
3273
3356
  return value.map((entry) => resolveJsonReferences(entry, currentFile, repositoryRoot, stack));
3274
3357
  }
3275
- const object = objectValue(value);
3358
+ const object = objectValue2(value);
3276
3359
  if (!object) return value;
3277
3360
  if (typeof object.$ref === "string") {
3278
3361
  const [filePart, pointer = ""] = object.$ref.split("#", 2);
@@ -3292,7 +3375,7 @@ function resolveJsonReferences(value, currentFile, repositoryRoot, stack) {
3292
3375
  resolveJsonReferences(entry, currentFile, repositoryRoot, stack)
3293
3376
  ])
3294
3377
  );
3295
- return objectValue(resolved) ? { ...resolved, ...siblings } : resolved;
3378
+ return objectValue2(resolved) ? { ...resolved, ...siblings } : resolved;
3296
3379
  }
3297
3380
  return Object.fromEntries(
3298
3381
  Object.entries(object).map(([key, entry]) => [
@@ -3307,34 +3390,45 @@ function readMintlifyConfig(repositoryRoot) {
3307
3390
  const raw = readMintlifyJson(configPath, repositoryRoot);
3308
3391
  return resolveJsonReferences(raw, configPath, repositoryRoot, /* @__PURE__ */ new Set());
3309
3392
  }
3393
+ function specSource(value) {
3394
+ const source = typeof value === "string" ? value : objectValue2(value)?.source;
3395
+ return typeof source === "string" && source.trim() ? source.trim() : void 0;
3396
+ }
3397
+ var hasNavigationChildren = (node) => ["groups", "pages"].some((key) => Array.isArray(node[key]) && node[key].length > 0);
3310
3398
  function mintlifyNavigationApiReferences(config) {
3311
- const navigation = objectValue(config.navigation) ?? config;
3399
+ const navigation = objectValue2(config.navigation) ?? config;
3312
3400
  const references = [];
3313
3401
  const containerKeys = ["tabs", "anchors", "products", "dropdowns", "versions", "menus", "languages"];
3314
3402
  const nestedKeys = ["groups", "pages"];
3315
- function visit(rawNode, tabLabel) {
3316
- const node = withFlattenedProductGroups(rawNode);
3403
+ function collect(node, tabLabel, sibling) {
3317
3404
  for (const kind of ["openapi", "asyncapi"]) {
3318
- const value = node[kind];
3319
- if (typeof value === "string" && value.trim()) {
3320
- references.push({ value: value.trim(), kind, tabLabel });
3321
- continue;
3322
- }
3323
- const object = objectValue(value);
3324
- if (object && typeof object.source === "string" && object.source.trim()) {
3405
+ const value = specSource(node[kind]);
3406
+ const directory = objectValue2(node[kind])?.directory;
3407
+ if (value) {
3325
3408
  references.push({
3326
- value: object.source.trim(),
3409
+ value,
3327
3410
  kind,
3328
3411
  tabLabel,
3329
- ...typeof object.directory === "string" && object.directory.trim() ? { directory: object.directory.trim() } : {}
3412
+ ...sibling,
3413
+ ...typeof directory === "string" && directory.trim() ? { directory: directory.trim() } : {}
3330
3414
  });
3331
3415
  }
3332
3416
  }
3417
+ }
3418
+ const siblingSources = /* @__PURE__ */ new Map();
3419
+ function uniqueSiblingLabel(label, source) {
3420
+ let candidate = label;
3421
+ for (let n = 2; siblingSources.has(candidate) && siblingSources.get(candidate) !== source; n++) candidate = `${label} (${n})`;
3422
+ siblingSources.set(candidate, source);
3423
+ return candidate;
3424
+ }
3425
+ function visitChildren(rawNode, tabLabel) {
3426
+ const node = withFlattenedProductGroups(rawNode);
3333
3427
  for (const key of containerKeys) {
3334
3428
  const entries = node[key];
3335
3429
  if (!Array.isArray(entries)) continue;
3336
3430
  for (const entry of entries) {
3337
- const object = objectValue(entry);
3431
+ const object = objectValue2(entry);
3338
3432
  if (!object) continue;
3339
3433
  visit(object, labelFor(object, tabLabel ?? "Documentation"));
3340
3434
  }
@@ -3344,14 +3438,49 @@ function mintlifyNavigationApiReferences(config) {
3344
3438
  if (!Array.isArray(entries)) continue;
3345
3439
  for (const entry of entries) {
3346
3440
  if (typeof entry === "string") continue;
3347
- const object = objectValue(entry);
3441
+ const object = objectValue2(entry);
3348
3442
  if (object) visit(object, tabLabel);
3349
3443
  }
3350
3444
  }
3445
+ if (Array.isArray(node.menu)) {
3446
+ const tab = tabLabel ?? "Documentation";
3447
+ const items = node.menu.flatMap((entry) => {
3448
+ const object = objectValue2(entry);
3449
+ return object ? [object] : [];
3450
+ });
3451
+ const apiItems = items.filter((item) => specSource(item.openapi) || specSource(item.asyncapi));
3452
+ const soleContent = apiItems.length === 1 && !hasNavigationChildren(node) && !items.some(hasNavigationChildren);
3453
+ items.forEach((item, index) => {
3454
+ if (soleContent && apiItems[0] === item) collect(item, tab);
3455
+ else if (apiItems.includes(item)) {
3456
+ const label = uniqueSiblingLabel(
3457
+ `${tab}: ${labelFor(item, `Menu item ${index + 1}`)}`,
3458
+ `${specSource(item.openapi) ?? ""}|${specSource(item.asyncapi) ?? ""}`
3459
+ );
3460
+ const icon = iconName(item.icon);
3461
+ collect(item, label, { parentTab: tab, ...icon ? { icon } : {}, ...item.hidden === true ? { hidden: true } : {} });
3462
+ }
3463
+ visitChildren(item, tabLabel);
3464
+ });
3465
+ }
3466
+ }
3467
+ function visit(node, tabLabel) {
3468
+ collect(node, tabLabel);
3469
+ visitChildren(node, tabLabel);
3351
3470
  }
3352
3471
  visit(navigation, void 0);
3353
3472
  return references;
3354
3473
  }
3474
+ function insertApiTab(tabs, tab, parentTab) {
3475
+ let index = -1;
3476
+ if (parentTab) {
3477
+ tabs.forEach((candidate, position) => {
3478
+ if (candidate.tab === parentTab || candidate.tab.startsWith(`${parentTab}: `)) index = position;
3479
+ });
3480
+ }
3481
+ tabs.splice(index === -1 ? tabs.length : index + 1, 0, tab);
3482
+ return tabs;
3483
+ }
3355
3484
  function normalizePageRef(value, pathPrefix = "") {
3356
3485
  if (/^(?:https?:)?\/\//i.test(value) || value.startsWith("#")) return null;
3357
3486
  let ref = localReference(value).split("?", 1)[0].replace(/^\/+/, "");
@@ -3362,6 +3491,23 @@ function normalizePageRef(value, pathPrefix = "") {
3362
3491
  ref = ref.replace(/\.(?:mdx?|rst|txt)$/i, "");
3363
3492
  return pageIdFromReference(ref, true);
3364
3493
  }
3494
+ function collectGatedReferences(node, gate, pathPrefix, out, depth = 0) {
3495
+ if (depth > 32) return;
3496
+ if (Array.isArray(node)) {
3497
+ for (const item of node) collectGatedReferences(item, gate, pathPrefix, out, depth + 1);
3498
+ return;
3499
+ }
3500
+ const object = objectValue2(node);
3501
+ if (!object) return;
3502
+ const reason = gate ?? navigationGateReason(object);
3503
+ if (reason) {
3504
+ const refs = [object.page, object.root, ...Array.isArray(object.pages) ? object.pages : []];
3505
+ for (const ref of refs) {
3506
+ if (typeof ref === "string" && normalizePageRef(ref, pathPrefix)) out.push({ ref, reason });
3507
+ }
3508
+ }
3509
+ for (const value of Object.values(object)) collectGatedReferences(value, reason, pathPrefix, out, depth + 1);
3510
+ }
3365
3511
  function warnOnce(context, key, message) {
3366
3512
  if (context.warningKeys.has(key)) return;
3367
3513
  context.warningKeys.add(key);
@@ -3369,7 +3515,7 @@ function warnOnce(context, key, message) {
3369
3515
  }
3370
3516
  function iconName(value) {
3371
3517
  if (typeof value === "string" && value) return value;
3372
- const icon = objectValue(value);
3518
+ const icon = objectValue2(value);
3373
3519
  return typeof icon?.name === "string" && icon.name ? icon.name : void 0;
3374
3520
  }
3375
3521
  function containerPresentation(value) {
@@ -3379,6 +3525,10 @@ function containerPresentation(value) {
3379
3525
  };
3380
3526
  }
3381
3527
  function registerReference(value, context) {
3528
+ if (context.gateReason) {
3529
+ if (normalizePageRef(value, context.pathPrefix)) context.gated.push({ ref: value, reason: context.gateReason });
3530
+ return null;
3531
+ }
3382
3532
  const localizedValue = context.locale ? mintlifyLocalizedReference(value, context.locale, context.defaultPageIds) : value;
3383
3533
  const navigationId = normalizePageRef(localizedValue, context.pathPrefix);
3384
3534
  if (!navigationId) return null;
@@ -3402,13 +3552,24 @@ function convertPage(value, context) {
3402
3552
  }
3403
3553
  return registerReference(value, context);
3404
3554
  }
3405
- const object = objectValue(value);
3555
+ const object = objectValue2(value);
3406
3556
  if (!object) return null;
3557
+ const outerGate = context.gateReason;
3558
+ const gate = navigationGateReason(object);
3559
+ if (gate && !outerGate) context.gateReason = gate;
3560
+ try {
3561
+ return convertPageObject(object, context);
3562
+ } finally {
3563
+ context.gateReason = outerGate;
3564
+ }
3565
+ }
3566
+ function convertPageObject(object, context) {
3407
3567
  if (typeof object.page === "string") return registerReference(object.page, context);
3408
3568
  const href = projectedHref(object.href);
3409
3569
  if (href && !object.pages && !object.groups) {
3410
3570
  const page = registerReference(href, context);
3411
3571
  if (page) return page;
3572
+ if (context.gateReason) return null;
3412
3573
  warnOnce(
3413
3574
  context,
3414
3575
  "external-page-link",
@@ -3437,6 +3598,65 @@ function convertPage(value, context) {
3437
3598
  }
3438
3599
  return null;
3439
3600
  }
3601
+ var KNOWN_NAVIGATION_KEYS = /* @__PURE__ */ new Set([
3602
+ "tabs",
3603
+ "anchors",
3604
+ "products",
3605
+ "dropdowns",
3606
+ "versions",
3607
+ "menus",
3608
+ "languages",
3609
+ "productGroups",
3610
+ "groups",
3611
+ "pages",
3612
+ "menu",
3613
+ "global"
3614
+ ]);
3615
+ function convertMenuItems(items, context) {
3616
+ return items.flatMap((value, index) => {
3617
+ const item = objectValue2(value);
3618
+ if (!item) return [];
3619
+ const label = labelFor(item, `Menu item ${index + 1}`);
3620
+ if (typeof item.description === "string" && item.description.trim()) {
3621
+ warnOnce(
3622
+ context,
3623
+ `menu-item-description:${label}`,
3624
+ `Mintlify menu item "${label}" has a description, which Thally groups cannot display; it was omitted.`
3625
+ );
3626
+ }
3627
+ const hasUnsourcedSpec = Boolean(item.openapi || item.asyncapi) && !specSource(item.openapi) && !specSource(item.asyncapi);
3628
+ if (hasUnsourcedSpec) {
3629
+ warnOnce(
3630
+ context,
3631
+ `menu-item-spec:${label}`,
3632
+ `Mintlify menu item "${label}" has an openapi/asyncapi value without a "source", so its API reference was not migrated.`
3633
+ );
3634
+ }
3635
+ const children = convertNavigationValues([
3636
+ ...Array.isArray(item.groups) ? item.groups : [],
3637
+ ...Array.isArray(item.pages) ? item.pages : []
3638
+ ], context);
3639
+ if (typeof item.href === "string" && item.href) {
3640
+ warnOnce(
3641
+ context,
3642
+ `menu-item-href:${label}`,
3643
+ `Mintlify menu item "${label}" links to ${item.href}, which cannot be represented as a Thally group; the link was omitted.`
3644
+ );
3645
+ }
3646
+ if (children.length === 0) {
3647
+ if (!specSource(item.openapi) && !specSource(item.asyncapi) && !hasUnsourcedSpec) {
3648
+ warnOnce(context, `menu-item-empty:${label}`, `Mintlify menu item "${label}" has no projectable pages and was omitted.`);
3649
+ }
3650
+ return [];
3651
+ }
3652
+ return [{
3653
+ group: label,
3654
+ ...iconName(item.icon) ? { icon: iconName(item.icon) } : {},
3655
+ ...item.hidden === true ? { hidden: true } : {},
3656
+ pages: children
3657
+ }];
3658
+ });
3659
+ }
3440
3660
  function convertNavigationValues(values, context) {
3441
3661
  return values.flatMap((value) => {
3442
3662
  const page = convertPage(value, context);
@@ -3444,22 +3664,29 @@ function convertNavigationValues(values, context) {
3444
3664
  });
3445
3665
  }
3446
3666
  function convertContainerToTabs(containerValue, context, fallbackTab, trace, depth = 0) {
3447
- const rawContainer = objectValue(containerValue);
3667
+ const rawContainer = objectValue2(containerValue);
3448
3668
  if (!rawContainer) return [];
3449
3669
  const container = withFlattenedProductGroups(rawContainer);
3450
3670
  const containerKeys = ["tabs", "anchors", "products", "dropdowns", "versions", "menus"];
3671
+ for (const key of Object.keys(container)) {
3672
+ if (KNOWN_NAVIGATION_KEYS.has(key)) continue;
3673
+ const entries = container[key];
3674
+ if (Array.isArray(entries) && entries.some((entry) => objectValue2(entry))) {
3675
+ warnOnce(context, `unknown-container:${key}`, `Mintlify navigation key "${key}" is not supported and its contents were not projected.`);
3676
+ }
3677
+ }
3451
3678
  for (const key of containerKeys) {
3452
3679
  if (!Array.isArray(container[key])) continue;
3453
3680
  const entries = [...container[key]];
3454
3681
  if (key === "versions") {
3455
- entries.sort((left, right) => Number(objectValue(right)?.default === true) - Number(objectValue(left)?.default === true));
3682
+ entries.sort((left, right) => Number(objectValue2(right)?.default === true) - Number(objectValue2(left)?.default === true));
3456
3683
  }
3457
3684
  const tabs = entries.flatMap((value, index) => {
3458
- const object = objectValue(value);
3685
+ const object = objectValue2(value);
3459
3686
  if (!object) return [];
3460
3687
  const tab = labelFor(object, `${fallbackTab} ${index + 1}`);
3461
3688
  const href = projectedHref(object.href);
3462
- const hasNestedContainers = containerKeys.some((containerKey) => Array.isArray(object[containerKey]));
3689
+ const hasNestedContainers = [...containerKeys, "menu"].some((containerKey) => Array.isArray(object[containerKey]));
3463
3690
  if (href && !object.pages && !object.groups && !hasNestedContainers) {
3464
3691
  return [{
3465
3692
  tab,
@@ -3468,7 +3695,15 @@ function convertContainerToTabs(containerValue, context, fallbackTab, trace, dep
3468
3695
  ...object.hidden === true ? { hidden: true } : {}
3469
3696
  }];
3470
3697
  }
3471
- const nested = convertContainerToTabs(object, context, tab, trace, depth + 1);
3698
+ const outerGate = context.gateReason;
3699
+ const gate = navigationGateReason(object);
3700
+ if (gate && !outerGate) context.gateReason = gate;
3701
+ let nested;
3702
+ try {
3703
+ nested = convertContainerToTabs(object, context, tab, trace, depth + 1);
3704
+ } finally {
3705
+ context.gateReason = outerGate;
3706
+ }
3472
3707
  if (nested.length > 0) {
3473
3708
  if (nested.length === 1) {
3474
3709
  return [{
@@ -3491,6 +3726,11 @@ function convertContainerToTabs(containerValue, context, fallbackTab, trace, dep
3491
3726
  return [];
3492
3727
  });
3493
3728
  if (tabs.length > 0) {
3729
+ for (const sibling of ["groups", "pages", "menu"]) {
3730
+ if (Array.isArray(container[sibling]) && container[sibling].length > 0) {
3731
+ warnOnce(context, `container-sibling:${key}:${sibling}`, `Mintlify "${sibling}" next to "${key}" was not projected; only the "${key}" containers were kept.`);
3732
+ }
3733
+ }
3494
3734
  if (depth === 0 && trace) trace.rootContainerKind = key;
3495
3735
  return tabs;
3496
3736
  }
@@ -3503,7 +3743,10 @@ function convertContainerToTabs(containerValue, context, fallbackTab, trace, dep
3503
3743
  ...rawGroups,
3504
3744
  ...rawPages
3505
3745
  ];
3506
- const children = convertNavigationValues(values, context);
3746
+ const children = [
3747
+ ...convertNavigationValues(values, context),
3748
+ ...Array.isArray(container.menu) ? convertMenuItems(container.menu, context) : []
3749
+ ];
3507
3750
  return children.length > 0 ? [{
3508
3751
  tab: fallbackTab,
3509
3752
  ...hasRootNodes ? { pages: children } : { groups: children },
@@ -3519,17 +3762,17 @@ function projectedTheme(value) {
3519
3762
  return typeof value === "string" ? "default" : void 0;
3520
3763
  }
3521
3764
  function projectedNavbar(value) {
3522
- const navbar = objectValue(value);
3765
+ const navbar = objectValue2(value);
3523
3766
  if (!navbar) return void 0;
3524
3767
  const links = Array.isArray(navbar.links) ? navbar.links.flatMap((entry) => {
3525
- const link = objectValue(entry);
3768
+ const link = objectValue2(entry);
3526
3769
  const href = projectedHref(link?.href);
3527
3770
  if (!link || !href) return [];
3528
3771
  const label = typeof link.label === "string" ? link.label : typeof link.name === "string" ? link.name : "";
3529
3772
  if (!label) return [];
3530
3773
  return [{ label, href, ...link.type === "github" ? { type: "github" } : {} }];
3531
3774
  }) : [];
3532
- const primaryValue = objectValue(navbar.primary);
3775
+ const primaryValue = objectValue2(navbar.primary);
3533
3776
  const primaryHref = projectedHref(primaryValue?.href);
3534
3777
  const primary = primaryValue && primaryHref ? {
3535
3778
  label: typeof primaryValue.label === "string" ? primaryValue.label : primaryValue.type === "github" ? "GitHub" : "Get started",
@@ -3537,8 +3780,22 @@ function projectedNavbar(value) {
3537
3780
  } : void 0;
3538
3781
  return links.length > 0 || primary ? { ...links.length > 0 ? { links } : {}, ...primary ? { primary } : {} } : void 0;
3539
3782
  }
3783
+ function legacyTopbarNavbar(config) {
3784
+ const links = Array.isArray(config.topbarLinks) ? config.topbarLinks.flatMap((entry) => {
3785
+ const link = objectValue2(entry);
3786
+ if (!link) return [];
3787
+ return [{
3788
+ label: link.name ?? link.label ?? (link.type === "github" ? "GitHub" : void 0),
3789
+ href: link.url ?? link.href,
3790
+ ...link.type === "github" ? { type: "github" } : {}
3791
+ }];
3792
+ }) : [];
3793
+ const cta = objectValue2(config.topbarCtaButton);
3794
+ const primary = cta ? { label: cta.name ?? cta.label, href: cta.url ?? cta.href, type: cta.type } : void 0;
3795
+ return links.length > 0 || primary ? { ...links.length > 0 ? { links } : {}, ...primary ? { primary } : {} } : void 0;
3796
+ }
3540
3797
  function projectedGlobalNavigationLinks(value) {
3541
- const global = objectValue(value);
3798
+ const global = objectValue2(value);
3542
3799
  if (!global) return [];
3543
3800
  const links = [];
3544
3801
  const visit = (entry) => {
@@ -3546,14 +3803,14 @@ function projectedGlobalNavigationLinks(value) {
3546
3803
  entry.forEach(visit);
3547
3804
  return;
3548
3805
  }
3549
- const item = objectValue(entry);
3806
+ const item = objectValue2(entry);
3550
3807
  if (!item) return;
3551
3808
  const href = projectedHref(item.href);
3552
3809
  if (href) {
3553
3810
  const label = labelFor(item, "");
3554
3811
  if (label) links.push({ label, href });
3555
3812
  }
3556
- for (const key of ["tabs", "anchors", "dropdowns", "products", "versions", "languages", "menus"]) {
3813
+ for (const key of ["tabs", "anchors", "dropdowns", "products", "versions", "languages", "menus", "menu"]) {
3557
3814
  if (Array.isArray(item[key])) visit(item[key]);
3558
3815
  }
3559
3816
  };
@@ -3561,19 +3818,19 @@ function projectedGlobalNavigationLinks(value) {
3561
3818
  return links;
3562
3819
  }
3563
3820
  function projectedFooter(value) {
3564
- const footer = objectValue(value);
3821
+ const footer = objectValue2(value);
3565
3822
  if (!footer) return void 0;
3566
- const socials = objectValue(footer.socials);
3823
+ const socials = objectValue2(footer.socials);
3567
3824
  const socialLinks = socials ? Object.fromEntries(Object.entries(socials).flatMap(([key, value2]) => {
3568
3825
  const href = projectedHref(value2);
3569
3826
  return href ? [[key, href]] : [];
3570
3827
  })) : void 0;
3571
3828
  const links = Array.isArray(footer.links) ? footer.links.flatMap((entry) => {
3572
- const column = objectValue(entry);
3829
+ const column = objectValue2(entry);
3573
3830
  if (!column || !Array.isArray(column.items)) return [];
3574
3831
  const heading = typeof column.heading === "string" ? column.heading : typeof column.header === "string" ? column.header : "";
3575
3832
  const items = column.items.flatMap((item) => {
3576
- const link = objectValue(item);
3833
+ const link = objectValue2(item);
3577
3834
  const href = projectedHref(link?.href);
3578
3835
  return link && typeof link.label === "string" && href ? [{ label: link.label, href }] : [];
3579
3836
  });
@@ -3583,26 +3840,45 @@ function projectedFooter(value) {
3583
3840
  }
3584
3841
  function projectedFont(value) {
3585
3842
  if (typeof value === "string" && value) return { family: value };
3586
- const font = objectValue(value);
3843
+ const font = objectValue2(value);
3587
3844
  if (!font) return void 0;
3588
3845
  const family = typeof font.family === "string" ? font.family : typeof font.name === "string" ? font.name : void 0;
3589
3846
  if (!family) return void 0;
3590
3847
  const weight = Array.isArray(font.weight) ? font.weight.map(String) : Array.isArray(font.weights) ? font.weights.map(String) : void 0;
3591
3848
  return { family, ...weight && weight.length > 0 ? { weight } : {} };
3592
3849
  }
3593
- function projectedCompatibleConfig(config) {
3594
- const banner = objectValue(config.banner);
3595
- const bannerContent = banner && (typeof banner.content === "string" || objectValue(banner.content)) ? banner.content : void 0;
3850
+ function projectedCompatibleConfig(config, warnings) {
3851
+ const banner = objectValue2(config.banner);
3852
+ const bannerContent = banner && (typeof banner.content === "string" || objectValue2(banner.content)) ? banner.content : void 0;
3596
3853
  const bannerType = banner && ["info", "warning", "critical"].includes(String(banner.type ?? banner.variant)) ? String(banner.type ?? banner.variant) : void 0;
3597
- const bannerColor = objectValue(banner?.color);
3598
- const bodyFont = projectedFont(objectValue(config.fonts)?.body ?? config.fonts);
3599
- const headingFont = projectedFont(objectValue(config.fonts)?.heading);
3600
- const feedback = objectValue(config.feedback);
3601
- const seo = objectValue(config.seo);
3602
- const iconLibrary = objectValue(config.icons)?.library;
3854
+ const bannerColor = objectValue2(banner?.color);
3855
+ const bodyFont = projectedFont(objectValue2(config.fonts)?.body ?? config.fonts);
3856
+ const headingFont = projectedFont(objectValue2(config.fonts)?.heading);
3857
+ const feedback = objectValue2(config.feedback);
3858
+ const seo = objectValue2(config.seo);
3859
+ const iconLibrary = objectValue2(config.icons)?.library;
3603
3860
  const projectedIconLibrary = ["lucide", "fontawesome", "tabler"].includes(String(iconLibrary)) ? String(iconLibrary) : void 0;
3604
- const navbar = projectedNavbar(config.navbar);
3605
- const globalLinks = projectedGlobalNavigationLinks(objectValue(config.navigation)?.global);
3861
+ const docsNavbar = projectedNavbar(config.navbar);
3862
+ const legacyRaw = legacyTopbarNavbar(config);
3863
+ const legacyNavbar = projectedNavbar(legacyRaw);
3864
+ const legacyLinkCount = Array.isArray(legacyRaw?.links) ? legacyRaw.links.length : 0;
3865
+ if (legacyNavbar?.links && legacyNavbar.links.length < legacyLinkCount) {
3866
+ warnings.push({ code: "unsupported-config", message: `${legacyLinkCount - legacyNavbar.links.length} mint.json topbarLinks entr${legacyLinkCount - legacyNavbar.links.length === 1 ? "y" : "ies"} without a valid name and url were skipped.` });
3867
+ }
3868
+ if (docsNavbar?.links && legacyNavbar?.links) {
3869
+ warnings.push({ code: "unsupported-config", message: "Both docs.json navbar.links and mint.json topbarLinks are set; the docs.json navbar.links were used and topbarLinks ignored." });
3870
+ }
3871
+ if (docsNavbar?.primary && legacyNavbar?.primary) {
3872
+ warnings.push({ code: "unsupported-config", message: "Both docs.json navbar.primary and mint.json topbarCtaButton are set; navbar.primary was used and topbarCtaButton ignored." });
3873
+ }
3874
+ if (legacyRaw?.primary && !legacyNavbar?.primary) {
3875
+ warnings.push({ code: "unsupported-config", message: "mint.json topbarCtaButton has no valid url and was skipped." });
3876
+ }
3877
+ const navbar = docsNavbar || legacyNavbar ? {
3878
+ ...docsNavbar?.links ?? legacyNavbar?.links ? { links: docsNavbar?.links ?? legacyNavbar?.links } : {},
3879
+ ...docsNavbar?.primary ?? legacyNavbar?.primary ? { primary: docsNavbar?.primary ?? legacyNavbar?.primary } : {}
3880
+ } : void 0;
3881
+ const globalLinks = projectedGlobalNavigationLinks(objectValue2(config.navigation)?.global);
3606
3882
  const navbarLinks = [...new Map(
3607
3883
  [...navbar?.links ?? [], ...globalLinks].map((link) => [`${link.label}:${link.href}`, link])
3608
3884
  ).values()];
@@ -3669,11 +3945,11 @@ function isRedirectPathSafe(rawSource, rawDestination) {
3669
3945
  return true;
3670
3946
  }
3671
3947
  function mintlifyDefaultVersionPrefixes(config) {
3672
- const navigation = objectValue(config.navigation) ?? config;
3948
+ const navigation = objectValue2(config.navigation) ?? config;
3673
3949
  const prefixes = /* @__PURE__ */ new Set();
3674
3950
  function collect(container) {
3675
3951
  if (Array.isArray(container.versions)) {
3676
- const entries = container.versions.map((value) => objectValue(value)).filter((value) => value !== null);
3952
+ const entries = container.versions.map((value) => objectValue2(value)).filter((value) => value !== null);
3677
3953
  const defaultEntry = entries.find((entry) => entry.default === true) ?? entries[0];
3678
3954
  if (defaultEntry && typeof defaultEntry.version === "string" && defaultEntry.version.trim()) {
3679
3955
  prefixes.add(defaultEntry.version.trim());
@@ -3681,7 +3957,7 @@ function mintlifyDefaultVersionPrefixes(config) {
3681
3957
  }
3682
3958
  if (Array.isArray(container.languages)) {
3683
3959
  for (const value of container.languages) {
3684
- const language = objectValue(value);
3960
+ const language = objectValue2(value);
3685
3961
  if (language) collect(language);
3686
3962
  }
3687
3963
  }
@@ -3690,18 +3966,18 @@ function mintlifyDefaultVersionPrefixes(config) {
3690
3966
  return prefixes;
3691
3967
  }
3692
3968
  function mintlifyAllVersionPrefixes(config) {
3693
- const navigation = objectValue(config.navigation) ?? config;
3969
+ const navigation = objectValue2(config.navigation) ?? config;
3694
3970
  const prefixes = /* @__PURE__ */ new Set();
3695
3971
  function collect(container) {
3696
3972
  if (Array.isArray(container.versions)) {
3697
3973
  for (const value of container.versions) {
3698
- const entry = objectValue(value);
3974
+ const entry = objectValue2(value);
3699
3975
  if (entry && typeof entry.version === "string" && entry.version.trim()) prefixes.add(entry.version.trim());
3700
3976
  }
3701
3977
  }
3702
3978
  if (Array.isArray(container.languages)) {
3703
3979
  for (const value of container.languages) {
3704
- const language = objectValue(value);
3980
+ const language = objectValue2(value);
3705
3981
  if (language) collect(language);
3706
3982
  }
3707
3983
  }
@@ -3714,9 +3990,10 @@ function projectMintlifyNavigation(config, options = {}) {
3714
3990
  const references = [];
3715
3991
  const seenReferences = /* @__PURE__ */ new Set();
3716
3992
  const warningKeys = /* @__PURE__ */ new Set();
3717
- const navigation = objectValue(config.navigation) ?? config;
3993
+ const gated = [];
3994
+ const navigation = objectValue2(config.navigation) ?? config;
3718
3995
  const languages = Array.isArray(navigation.languages) ? navigation.languages.flatMap((value) => {
3719
- const language = objectValue(value);
3996
+ const language = objectValue2(value);
3720
3997
  if (!language) return [];
3721
3998
  try {
3722
3999
  const code = Intl.getCanonicalLocales(String(language.language ?? language.locale ?? "en"))[0];
@@ -3753,7 +4030,8 @@ function projectMintlifyNavigation(config, options = {}) {
3753
4030
  references,
3754
4031
  seenReferences,
3755
4032
  warnings,
3756
- warningKeys
4033
+ warningKeys,
4034
+ gated
3757
4035
  };
3758
4036
  const languageTabs = convertContainerToTabs(
3759
4037
  language,
@@ -3770,7 +4048,7 @@ function projectMintlifyNavigation(config, options = {}) {
3770
4048
  }
3771
4049
  if (Object.keys(localizedNavigation).length > 0) i18n.navigation = localizedNavigation;
3772
4050
  } else {
3773
- const context = { references, seenReferences, warnings, warningKeys, pathPrefix: options.pathPrefix };
4051
+ const context = { references, seenReferences, warnings, warningKeys, gated, pathPrefix: options.pathPrefix };
3774
4052
  tabs = convertContainerToTabs(navigation, context, "Documentation", projectionTrace);
3775
4053
  if (tabs.length === 0 && Array.isArray(config.navigation)) {
3776
4054
  const children = convertNavigationValues(config.navigation, context);
@@ -3790,7 +4068,7 @@ function projectMintlifyNavigation(config, options = {}) {
3790
4068
  });
3791
4069
  }
3792
4070
  const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
3793
- const redirect = objectValue(value);
4071
+ const redirect = objectValue2(value);
3794
4072
  if (!redirect || typeof redirect.source !== "string" || typeof redirect.destination !== "string") return [];
3795
4073
  const rawSource = redirect.source.trim();
3796
4074
  const rawDestination = redirect.destination.trim();
@@ -3809,15 +4087,17 @@ function projectMintlifyNavigation(config, options = {}) {
3809
4087
  ...typeof redirect.permanent === "boolean" ? { permanent: redirect.permanent } : {}
3810
4088
  }];
3811
4089
  }) : [];
4090
+ collectGatedReferences(navigation, void 0, options.pathPrefix, gated);
3812
4091
  return {
3813
4092
  docsConfig: {
3814
4093
  tabs,
3815
4094
  ...projectionTrace.rootContainerKind === "dropdowns" ? { navigation: { display: "dropdown" } } : {},
3816
- ...projectedCompatibleConfig(config),
4095
+ ...projectedCompatibleConfig(config, warnings),
3817
4096
  ...i18n ? { i18n } : {},
3818
4097
  ...redirects.length > 0 ? { redirects } : {}
3819
4098
  },
3820
4099
  pageReferences: references,
4100
+ gatedReferences: gated,
3821
4101
  warnings
3822
4102
  };
3823
4103
  }
@@ -3923,7 +4203,7 @@ function buildNavigationFromPages(pages, options = {}) {
3923
4203
  return { tabs: [{ tab: "Documentation", groups }] };
3924
4204
  }
3925
4205
  function isDocumentationExtension(filename) {
3926
- return [".md", ".mdx", ".rst", ".txt"].includes(extname2(filename).toLowerCase());
4206
+ return [".md", ".mdx", ".rst", ".txt"].includes(extname3(filename).toLowerCase());
3927
4207
  }
3928
4208
  function pruneMissingNavigationPages(config, availableIds) {
3929
4209
  const pruneNodes = (nodes) => {
@@ -3963,7 +4243,7 @@ var SIDEBAR_FILENAMES = [
3963
4243
  "sidebars.mjs",
3964
4244
  "sidebars.ts"
3965
4245
  ];
3966
- function objectValue2(value) {
4246
+ function objectValue3(value) {
3967
4247
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3968
4248
  }
3969
4249
  function stripNumberPrefix(value) {
@@ -4285,7 +4565,7 @@ function parseStaticSidebarModule(source) {
4285
4565
  literal
4286
4566
  );
4287
4567
  const parsed = JSON5.parse(substituted);
4288
- const object = objectValue2(parsed);
4568
+ const object = objectValue3(parsed);
4289
4569
  if (!object) continue;
4290
4570
  const bindingName = candidate[0].match(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)/)?.[1];
4291
4571
  if (bindingName) {
@@ -4390,7 +4670,7 @@ function readDocusaurusSidebars(repositoryRoot, versionedSidebarPath) {
4390
4670
  if (!sourcePath) return null;
4391
4671
  const absolutePath = resolveWithin(repositoryRoot, sourcePath);
4392
4672
  const source = readBoundedText(absolutePath);
4393
- const parsed = extname3(sourcePath).toLowerCase() === ".json" ? objectValue2(JSON5.parse(source)) : parseStaticSidebarModule(source);
4673
+ const parsed = extname4(sourcePath).toLowerCase() === ".json" ? objectValue3(JSON5.parse(source)) : parseStaticSidebarModule(source);
4394
4674
  if (!parsed) throw new Error("Docusaurus sidebar config must export an object.");
4395
4675
  return { config: parsed, sourcePath };
4396
4676
  }
@@ -4416,7 +4696,7 @@ function redirectEntriesFromObjectLiteralText(objectLiteral) {
4416
4696
  if (!Array.isArray(parsed)) return [];
4417
4697
  const entries = [];
4418
4698
  for (const raw of parsed) {
4419
- const entry = objectValue2(raw);
4699
+ const entry = objectValue3(raw);
4420
4700
  if (!entry || typeof entry.to !== "string") continue;
4421
4701
  const froms = Array.isArray(entry.from) ? entry.from : [entry.from];
4422
4702
  for (const from of froms) {
@@ -4468,7 +4748,7 @@ function readCategoryMetadata(contentRoot, directory) {
4468
4748
  try {
4469
4749
  const raw = readBoundedText(path);
4470
4750
  const parsed = filename.endsWith(".json") ? JSON5.parse(raw) : parseYaml2(raw);
4471
- const object = objectValue2(parsed);
4751
+ const object = objectValue3(parsed);
4472
4752
  if (!object) return {};
4473
4753
  return {
4474
4754
  ...typeof object.label === "string" ? { label: object.label } : {},
@@ -4502,7 +4782,7 @@ function registerDoc(docId, context) {
4502
4782
  return descriptor.navigationId;
4503
4783
  }
4504
4784
  function generatedIndexPage(label, linkValue, context) {
4505
- const link = objectValue2(linkValue);
4785
+ const link = objectValue3(linkValue);
4506
4786
  if (!link) return null;
4507
4787
  if (link.type === "doc" && typeof link.id === "string") return registerDoc(link.id, context);
4508
4788
  if (link.type !== "generated-index") return null;
@@ -4769,7 +5049,7 @@ function convertItems(value, context) {
4769
5049
  return page ? [page] : [];
4770
5050
  }
4771
5051
  if (Array.isArray(value)) return value.flatMap((entry) => convertItems(entry, context));
4772
- const object = objectValue2(value);
5052
+ const object = objectValue3(value);
4773
5053
  if (!object) return [];
4774
5054
  if (object.type === "doc" || object.type === "ref") {
4775
5055
  const page = typeof object.id === "string" ? registerDoc(object.id, context) : null;
@@ -4835,7 +5115,7 @@ function projectDocusaurusNavigation(input) {
4835
5115
 
4836
5116
  // src/fern.ts
4837
5117
  import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as readFileSync4, readdirSync } from "fs";
4838
- import { basename as basename2, dirname as dirname3, extname as extname4, relative as relative4 } from "path";
5118
+ import { basename as basename2, dirname as dirname3, extname as extname5, relative as relative4 } from "path";
4839
5119
  import { load } from "cheerio";
4840
5120
  import { parse as parseYaml3 } from "yaml";
4841
5121
  var MAX_CONFIG_BYTES2 = 2e7;
@@ -4865,7 +5145,7 @@ function announcementMarkdown(message) {
4865
5145
  for (const [marker, link] of links) text = text.replaceAll(marker, link);
4866
5146
  return text.trim();
4867
5147
  }
4868
- function objectValue3(value) {
5148
+ function objectValue4(value) {
4869
5149
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
4870
5150
  }
4871
5151
  function titleCase3(value) {
@@ -4881,7 +5161,7 @@ function readFernConfig(fernRoot) {
4881
5161
  const sourcePath = "docs.yml";
4882
5162
  const path = resolveWithin(fernRoot, sourcePath);
4883
5163
  if (!existsSync4(path) || !lstatSync4(path).isFile()) return null;
4884
- const config = objectValue3(readBoundedYaml(path));
5164
+ const config = objectValue4(readBoundedYaml(path));
4885
5165
  if (!config) throw new Error("fern/docs.yml must contain an object.");
4886
5166
  return { config, sourcePath };
4887
5167
  }
@@ -4988,8 +5268,8 @@ function registerFolder(object, parentSegments, context) {
4988
5268
  if (entry.isDirectory()) {
4989
5269
  const nested = visit(path, [...routeSegments, fernBasicSlug(entry.name)], depth + 1);
4990
5270
  if (nested.length) pages2.push({ group: titleCase3(entry.name), pages: nested });
4991
- } else if (entry.isFile() && [".md", ".mdx"].includes(extname4(entry.name).toLowerCase())) {
4992
- const stem = basename2(entry.name, extname4(entry.name));
5271
+ } else if (entry.isFile() && [".md", ".mdx"].includes(extname5(entry.name).toLowerCase())) {
5272
+ const stem = basename2(entry.name, extname5(entry.name));
4993
5273
  const pageSegment = stem.toLowerCase() === "index" ? null : fernBasicSlug(stem);
4994
5274
  const route = [...routeSegments, ...pageSegment ? [pageSegment] : []].join("/");
4995
5275
  const source = lstatSync4(path).size <= MAX_CONFIG_BYTES2 ? readFileSync4(path, "utf8") : "";
@@ -5014,7 +5294,7 @@ function registerPage(object, parentSegments, context) {
5014
5294
  return registerPageAt(object.path, base, context, label, object.hidden === true) || null;
5015
5295
  }
5016
5296
  function convertNode(node, parentSegments, context) {
5017
- const object = objectValue3(node);
5297
+ const object = objectValue4(node);
5018
5298
  if (!object) return null;
5019
5299
  if (typeof object.page === "string") {
5020
5300
  const page = registerPage(object, parentSegments, context);
@@ -5102,7 +5382,7 @@ function resolveVersionedNavigation(config, configDir, fernRoot, context) {
5102
5382
  }
5103
5383
  if (!Array.isArray(config.versions)) return config;
5104
5384
  const versions = config.versions.flatMap((entry) => {
5105
- const version = objectValue3(entry);
5385
+ const version = objectValue4(entry);
5106
5386
  return version ? [version] : [];
5107
5387
  });
5108
5388
  const chosen = versions.find((version) => version.default === true) ?? versions[0];
@@ -5121,7 +5401,7 @@ function resolveVersionedNavigation(config, configDir, fernRoot, context) {
5121
5401
  warnOnce2(context, "fern-version-not-file", `Fern version file "${chosen.path}" is not a regular file and was skipped.`);
5122
5402
  return config;
5123
5403
  }
5124
- const versionConfig = objectValue3(readBoundedYaml(versionPath));
5404
+ const versionConfig = objectValue4(readBoundedYaml(versionPath));
5125
5405
  if (!versionConfig) return config;
5126
5406
  context.pathPrefix = relative4(fernRoot, dirname3(versionPath)).replace(/\\/g, "/");
5127
5407
  return { ...config, navigation: versionConfig.navigation, tabs: versionConfig.tabs ?? config.tabs };
@@ -5136,13 +5416,13 @@ function resolveVersionedNavigation(config, configDir, fernRoot, context) {
5136
5416
  }
5137
5417
  function buildTabsFromConfig(rawConfig, configDir, fernRoot, routePrefix, fallbackTabLabel, context) {
5138
5418
  const config = resolveVersionedNavigation(rawConfig, configDir, fernRoot, context);
5139
- const tabsMeta = objectValue3(config.tabs) ?? {};
5419
+ const tabsMeta = objectValue4(config.tabs) ?? {};
5140
5420
  const navigation = config.navigation;
5141
- const isTabbedNavigation = Array.isArray(navigation) && navigation.length > 0 && navigation.every((entry) => typeof objectValue3(entry)?.tab === "string");
5421
+ const isTabbedNavigation = Array.isArray(navigation) && navigation.length > 0 && navigation.every((entry) => typeof objectValue4(entry)?.tab === "string");
5142
5422
  if (isTabbedNavigation) {
5143
5423
  return navigation.flatMap((entry) => {
5144
5424
  const id = String(entry.tab);
5145
- const meta = objectValue3(tabsMeta[id]) ?? {};
5425
+ const meta = objectValue4(tabsMeta[id]) ?? {};
5146
5426
  const label = typeof meta["display-name"] === "string" ? meta["display-name"] : titleCase3(id);
5147
5427
  const tabSegment = segmentFor(meta, label, context);
5148
5428
  const segments = [...routePrefix, ...tabSegment ? [tabSegment] : []];
@@ -5164,7 +5444,7 @@ function buildTabsFromConfig(rawConfig, configDir, fernRoot, routePrefix, fallba
5164
5444
  const index = visiblePages.find((page) => {
5165
5445
  const descriptor = descriptors.get(page);
5166
5446
  if (!descriptor) return false;
5167
- const stem = basename2(descriptor.sourcePath, extname4(descriptor.sourcePath));
5447
+ const stem = basename2(descriptor.sourcePath, extname5(descriptor.sourcePath));
5168
5448
  return page === indexRoute || stem === "overview" || stem === "index";
5169
5449
  });
5170
5450
  const entries = visiblePages.filter((page) => page !== index).sort((left, right) => {
@@ -5213,7 +5493,7 @@ function buildTabsFromConfig(rawConfig, configDir, fernRoot, routePrefix, fallba
5213
5493
  }
5214
5494
  function projectFernProducts(rawProducts, fernRoot, context) {
5215
5495
  return rawProducts.flatMap((entry) => {
5216
- const product = objectValue3(entry);
5496
+ const product = objectValue4(entry);
5217
5497
  const rawPath = typeof product?.path === "string" ? product.path.trim() : "";
5218
5498
  if (!product || !rawPath) return [];
5219
5499
  const label = typeof product["display-name"] === "string" && product["display-name"].trim() ? product["display-name"].trim() : "Product";
@@ -5230,7 +5510,7 @@ function projectFernProducts(rawProducts, fernRoot, context) {
5230
5510
  }
5231
5511
  let productConfig;
5232
5512
  try {
5233
- productConfig = objectValue3(readBoundedYaml(productPath));
5513
+ productConfig = objectValue4(readBoundedYaml(productPath));
5234
5514
  } catch (error) {
5235
5515
  warnOnce2(
5236
5516
  context,
@@ -5284,7 +5564,7 @@ function projectFernNavigation(input) {
5284
5564
  });
5285
5565
  }
5286
5566
  const navbarLinks = Array.isArray(config["navbar-links"]) ? config["navbar-links"].flatMap((entry) => {
5287
- const link = objectValue3(entry);
5567
+ const link = objectValue4(entry);
5288
5568
  const href = typeof link?.url === "string" ? link.url : typeof link?.href === "string" ? link.href : typeof link?.value === "string" ? link.value : void 0;
5289
5569
  const label = typeof link?.text === "string" ? link.text : typeof link?.label === "string" ? link.label : link?.type === "github" ? "GitHub" : void 0;
5290
5570
  return href && label ? [{ label, href, ...link?.type === "github" ? { type: "github" } : {} }] : [];
@@ -5294,11 +5574,11 @@ function projectFernNavigation(input) {
5294
5574
  navbarLinks.push(link);
5295
5575
  }
5296
5576
  }
5297
- const announcement = objectValue3(config.announcement);
5577
+ const announcement = objectValue4(config.announcement);
5298
5578
  const announcementMessage = typeof announcement?.message === "string" ? announcement.message.trim() : "";
5299
5579
  const bannerContent = announcementMarkdown(announcementMessage);
5300
5580
  const redirects = Array.isArray(config.redirects) ? config.redirects.flatMap((value) => {
5301
- const redirect = objectValue3(value);
5581
+ const redirect = objectValue4(value);
5302
5582
  if (!redirect || typeof redirect.source !== "string" || typeof redirect.destination !== "string") return [];
5303
5583
  const rawSource = redirect.source.trim();
5304
5584
  const rawDestination = redirect.destination.trim();
@@ -5384,9 +5664,16 @@ function projectFernNavigation(input) {
5384
5664
  // src/repository.ts
5385
5665
  var MAX_SOURCE_FILES = 5e3;
5386
5666
  var MAX_PAGE_BYTES = 2e6;
5667
+ var FRONTMATTER_HEAD_BYTES = 65536;
5668
+ var MAX_WITHHELD_SCAN_BYTES = MAX_PAGE_BYTES * 8;
5669
+ var MAX_DROPPED_GATED_WARNINGS = 20;
5387
5670
  var MAX_ASSET_BYTES = 25e6;
5388
5671
  var MAX_TOTAL_ASSET_BYTES = 5e8;
5389
5672
  var GIT_LFS_POINTER_PREFIX = "version https://git-lfs.github.com/spec/v1";
5673
+ function listAssetPaths(paths) {
5674
+ const shown = paths.slice(0, 5).join(", ");
5675
+ return paths.length > 5 ? `${shown}, and ${paths.length - 5} more` : shown;
5676
+ }
5390
5677
  function isGitLfsPointer(content) {
5391
5678
  return content.length < 1024 && content.toString("utf8", 0, GIT_LFS_POINTER_PREFIX.length) === GIT_LFS_POINTER_PREFIX;
5392
5679
  }
@@ -5405,6 +5692,7 @@ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
5405
5692
  function isIgnoredContentDirectory(name) {
5406
5693
  return name.startsWith(".") || name === "node_modules";
5407
5694
  }
5695
+ var QUARANTINE_DIRECTORY = "migration-quarantine";
5408
5696
  var ASSET_DIRECTORIES = /* @__PURE__ */ new Set(["assets", "images", "img", "media", "public", "static"]);
5409
5697
  var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
5410
5698
  ".avif",
@@ -5810,17 +6098,23 @@ function parseGitHubRepositoryUrl(rawUrl) {
5810
6098
  }
5811
6099
  var CLONE_RETRY_ATTEMPTS = 3;
5812
6100
  var CLONE_RETRY_DELAY_MS = 1e3;
5813
- var DEFAULT_CLONE_TIMEOUT_MS = 10 * 6e4;
6101
+ var DEFAULT_CLONE_IDLE_TIMEOUT_MS = 2 * 6e4;
6102
+ var MAX_GIT_PROCESS_MS = 60 * 6e4;
6103
+ var HARD_TIMEOUT_REASON = `still running after ${MAX_GIT_PROCESS_MS / 6e4} minutes`;
6104
+ var GIT_EXIT_DRAIN_MS = 2e3;
5814
6105
  var RETRYABLE_CLONE_ERROR = /RPC failed|Recv failure|early EOF|curl \d+|Could not resolve host|Connection (?:reset|refused|timed out)|The remote end hung up|SSL[_ ]?(?:read|connect|write) error|timed out|network is unreachable/i;
5815
6106
  function delay(ms) {
5816
6107
  return new Promise((resolve3) => {
5817
6108
  setTimeout(resolve3, ms);
5818
6109
  });
5819
6110
  }
5820
- function gitProcessTimeoutMs() {
6111
+ function gitIdleTimeoutMs() {
5821
6112
  const raw = process.env.THALLY_MIGRATE_CLONE_TIMEOUT_MS;
5822
6113
  const parsed = raw ? Number(raw) : NaN;
5823
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
6114
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_IDLE_TIMEOUT_MS;
6115
+ }
6116
+ function gitErrorTail(stderr) {
6117
+ return stderr.split(/[\r\n]+/).map((line) => line.trim()).filter((line) => line && !/^(?:remote: )?[A-Za-z][A-Za-z ]*:\s+\d+%/.test(line)).slice(-5).join("\n");
5824
6118
  }
5825
6119
  function runGit(args, options) {
5826
6120
  return new Promise((resolve3, reject) => {
@@ -5830,24 +6124,66 @@ function runGit(args, options) {
5830
6124
  env: { ...process.env, ...options.env }
5831
6125
  });
5832
6126
  let stderr = "";
5833
- let timedOut = false;
5834
- const timer = setTimeout(() => {
5835
- timedOut = true;
6127
+ let timeoutReason = "";
6128
+ let settled = false;
6129
+ let exitCode;
6130
+ let drainTimer;
6131
+ const idleMs = gitIdleTimeoutMs();
6132
+ const idleMessage = `no progress for ${idleMs < 1e3 ? `${idleMs}ms` : `${Math.round(idleMs / 1e3)}s`}`;
6133
+ const clearTimers = () => {
6134
+ clearTimeout(idleTimer);
6135
+ clearTimeout(hardTimer);
6136
+ clearTimeout(drainTimer);
6137
+ };
6138
+ const finish = (code) => {
6139
+ if (settled) return;
6140
+ settled = true;
6141
+ clearTimers();
6142
+ child.stderr.destroy();
6143
+ if (code === 0) resolve3();
6144
+ else if (timeoutReason) {
6145
+ const hint = timeoutReason === HARD_TIMEOUT_REASON ? "" : "; on a very slow link, raise THALLY_MIGRATE_CLONE_TIMEOUT_MS (milliseconds of allowed silence)";
6146
+ reject(new Error(`${options.label}: timed out (${timeoutReason}) and was killed. Check your network connection and try again${hint}.`));
6147
+ } else reject(new Error(`${options.label}: ${gitErrorTail(stderr) || `git exited ${code}`}`));
6148
+ };
6149
+ const kill = (reason) => {
6150
+ if (exitCode !== void 0) {
6151
+ finish(exitCode);
6152
+ return;
6153
+ }
6154
+ timeoutReason = reason;
5836
6155
  child.kill("SIGKILL");
5837
- }, gitProcessTimeoutMs());
6156
+ };
6157
+ let idleTimer = setTimeout(() => {
6158
+ kill(idleMessage);
6159
+ }, idleMs);
6160
+ const hardTimer = setTimeout(() => {
6161
+ kill(HARD_TIMEOUT_REASON);
6162
+ }, MAX_GIT_PROCESS_MS);
5838
6163
  child.stderr.setEncoding("utf8");
5839
6164
  child.stderr.on("data", (chunk) => {
5840
- if (stderr.length < 16e3) stderr += chunk;
6165
+ stderr = (stderr + chunk).slice(-16e3);
6166
+ if (exitCode !== void 0 || settled) return;
6167
+ clearTimeout(idleTimer);
6168
+ idleTimer = setTimeout(() => {
6169
+ kill(idleMessage);
6170
+ }, idleMs);
5841
6171
  });
5842
6172
  child.on("error", (error) => {
5843
- clearTimeout(timer);
6173
+ if (settled) return;
6174
+ settled = true;
6175
+ clearTimers();
5844
6176
  reject(error);
5845
6177
  });
5846
- child.on("close", (code) => {
5847
- clearTimeout(timer);
5848
- if (code === 0) resolve3();
5849
- else if (timedOut) reject(new Error(`${options.label} timed out after ${gitProcessTimeoutMs()}ms and was killed.`));
5850
- else reject(new Error(`${options.label}: ${stderr.trim() || `git exited ${code}`}`));
6178
+ child.on("close", finish);
6179
+ child.on("exit", (code) => {
6180
+ exitCode = code;
6181
+ clearTimeout(idleTimer);
6182
+ clearTimeout(hardTimer);
6183
+ if (timeoutReason) finish(code);
6184
+ else drainTimer = setTimeout(() => {
6185
+ finish(code);
6186
+ }, GIT_EXIT_DRAIN_MS);
5851
6187
  });
5852
6188
  });
5853
6189
  }
@@ -5863,7 +6199,7 @@ var LFS_FILTER_OVERRIDE_ENV = {
5863
6199
  GIT_CONFIG_VALUE_3: "false"
5864
6200
  };
5865
6201
  function cloneOnce(source, targetDir) {
5866
- const args = ["clone", "--depth", "1", "--single-branch"];
6202
+ const args = ["clone", "--progress", "--depth", "1", "--single-branch"];
5867
6203
  if (source.branch !== "HEAD") args.push("--branch", source.branch);
5868
6204
  args.push("--", source.cloneUrl, targetDir);
5869
6205
  return runGit(args, {
@@ -5912,6 +6248,7 @@ async function initSubmodules(targetDir, warnings) {
5912
6248
  "submodule",
5913
6249
  "update",
5914
6250
  "--init",
6251
+ "--progress",
5915
6252
  "--depth",
5916
6253
  "1",
5917
6254
  "--",
@@ -5971,7 +6308,7 @@ function containsMarkdown(directory, depth = 0) {
5971
6308
  for (const entry of readdirSync2(directory, { withFileTypes: true })) {
5972
6309
  if (isIgnoredDirectory(entry.name) || entry.isSymbolicLink()) continue;
5973
6310
  const path = resolveWithin(directory, entry.name);
5974
- if (entry.isFile() && [".md", ".mdx"].includes(extname5(entry.name).toLowerCase())) return true;
6311
+ if (entry.isFile() && [".md", ".mdx"].includes(extname6(entry.name).toLowerCase())) return true;
5975
6312
  if (entry.isDirectory() && containsMarkdown(path, depth + 1)) return true;
5976
6313
  }
5977
6314
  return false;
@@ -5983,10 +6320,10 @@ function detectRepositoryDocsDir(repositoryDir) {
5983
6320
  }
5984
6321
  return "";
5985
6322
  }
5986
- var MAX_RANKED_WALK_FILES = MAX_SOURCE_FILES * 20;
5987
- function scanFiles(root, confinementRoot = root, warnings, rank) {
6323
+ var RANKED_WALK_FACTOR = 20;
6324
+ function scanFiles(root, confinementRoot = root, warnings, rank, budget = MAX_SOURCE_FILES) {
5988
6325
  const files = [];
5989
- const walkCap = rank ? MAX_RANKED_WALK_FILES : MAX_SOURCE_FILES;
6326
+ const walkCap = rank ? budget * RANKED_WALK_FACTOR : budget;
5990
6327
  let confinementReal;
5991
6328
  try {
5992
6329
  confinementReal = realpathSync2(confinementRoot);
@@ -6056,10 +6393,10 @@ function scanFiles(root, confinementRoot = root, warnings, rank) {
6056
6393
  }
6057
6394
  return files;
6058
6395
  }
6059
- function selectGroupWithinBudget(scanned, rank, warnings, allVersionPrefixes, label) {
6060
- if (scanned.length <= MAX_SOURCE_FILES) return scanned;
6396
+ function selectGroupWithinBudget(scanned, rank, warnings, allVersionPrefixes, label, budget = MAX_SOURCE_FILES) {
6397
+ if (scanned.length <= budget) return scanned;
6061
6398
  const ranked = scanned.map((file) => ({ file, priority: rank(file.relativePath) })).sort((left, right) => left.priority - right.priority || (left.file.relativePath < right.file.relativePath ? -1 : left.file.relativePath > right.file.relativePath ? 1 : 0));
6062
- const dropped = ranked.slice(MAX_SOURCE_FILES).map(({ file }) => file);
6399
+ const dropped = ranked.slice(budget).map(({ file }) => file);
6063
6400
  if (warnings) {
6064
6401
  const droppedPages = label === "file" ? dropped.filter((file) => isDocumentationExtension(file.relativePath)) : dropped;
6065
6402
  const noun = label === "file" ? "page" : "image or media file";
@@ -6072,18 +6409,18 @@ function selectGroupWithinBudget(scanned, rank, warnings, allVersionPrefixes, la
6072
6409
  const rest = droppedPages.length - examples.length;
6073
6410
  warnings.push({
6074
6411
  code: "limit-reached",
6075
- message: `This repository has more than ${MAX_SOURCE_FILES} ${label === "file" ? "files" : "assets"}, so only the first ${MAX_SOURCE_FILES} (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.")
6412
+ 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.")
6076
6413
  });
6077
6414
  }
6078
- return ranked.slice(0, MAX_SOURCE_FILES).map(({ file }) => file);
6415
+ return ranked.slice(0, budget).map(({ file }) => file);
6079
6416
  }
6080
- function selectFilesWithinBudget(scanned, rank, warnings, allVersionPrefixes) {
6081
- const isAsset = (file) => ASSET_EXTENSIONS.has(extname5(file.relativePath).toLowerCase());
6417
+ function selectFilesWithinBudget(scanned, rank, warnings, allVersionPrefixes, budget = MAX_SOURCE_FILES) {
6418
+ const isAsset = (file) => ASSET_EXTENSIONS.has(extname6(file.relativePath).toLowerCase());
6082
6419
  const assetFiles = scanned.filter(isAsset);
6083
6420
  const otherFiles = scanned.filter((file) => !isAsset(file));
6084
6421
  return [
6085
- ...selectGroupWithinBudget(otherFiles, rank, warnings, allVersionPrefixes, "file"),
6086
- ...selectGroupWithinBudget(assetFiles, rank, warnings, allVersionPrefixes, "asset")
6422
+ ...selectGroupWithinBudget(otherFiles, rank, warnings, allVersionPrefixes, "file", budget),
6423
+ ...selectGroupWithinBudget(assetFiles, rank, warnings, allVersionPrefixes, "asset", budget)
6087
6424
  ];
6088
6425
  }
6089
6426
  function readMintignoreMatcher(mintlifyRoot) {
@@ -6128,6 +6465,45 @@ function normalizedReferenceKey(value) {
6128
6465
  function exactReferenceKey(value) {
6129
6466
  return value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/").replace(/\.(?:mdx?|rst|txt)$/i, "");
6130
6467
  }
6468
+ function specRefMatches(ref, specPath, specFilename, isDefault) {
6469
+ const prefix = /^(?:(\S+)\s+)?(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|WEBHOOK)\s/i.exec(`${ref} `)?.[1];
6470
+ if (!prefix) return isDefault;
6471
+ const wanted = prefix.replace(/^\/+/, "").toLowerCase();
6472
+ const lowerPath = specPath.toLowerCase();
6473
+ return lowerPath === wanted || lowerPath.endsWith(`/${wanted}`) || specFilename.toLowerCase() === wanted;
6474
+ }
6475
+ var TOKEN_SEPARATORS = /[\s"'()<>[\]{}=,;:|\\/`*!?#&]+/;
6476
+ var PATH_SEPARATORS = /[\s"'()<>[\]{}=,;:|\\`*!?#&]+/;
6477
+ function publicAssetKey(repoRelative) {
6478
+ return repoRelative.split("/", 1)[0].toLowerCase() === "public" ? repoRelative.slice("public/".length) : repoRelative;
6479
+ }
6480
+ function addPathReferences(text, baseDir, exact, loose) {
6481
+ for (const word of text.split(PATH_SEPARATORS)) {
6482
+ let token = word.replace(/\.+$/, "");
6483
+ if (!/\.[a-z0-9]{2,5}$/i.test(token) || token.startsWith("//")) continue;
6484
+ if (token.includes("%")) {
6485
+ try {
6486
+ token = decodeURIComponent(token);
6487
+ } catch {
6488
+ }
6489
+ }
6490
+ if (!token.includes("/")) {
6491
+ loose.add(token.toLowerCase());
6492
+ continue;
6493
+ }
6494
+ if (!token.startsWith("/") && baseDir === void 0) continue;
6495
+ const resolved = normalizeAssetPath(posix3.normalize(token.startsWith("/") ? token.slice(1) : posix3.join(baseDir ?? "", token)));
6496
+ if (!resolved) continue;
6497
+ const key = publicAssetKey(resolved);
6498
+ exact.add(key);
6499
+ loose.add(key.toLowerCase());
6500
+ }
6501
+ }
6502
+ function looselyNamed(assetPath, loose) {
6503
+ const lower = assetPath.toLowerCase();
6504
+ const name = lower.split("/").pop() ?? lower;
6505
+ return loose.has(lower) || loose.has(name) || loose.has(name.split(TOKEN_SEPARATORS).filter(Boolean).pop() ?? name);
6506
+ }
6131
6507
  function findOpenApi(files) {
6132
6508
  return files.find((file) => OPENAPI_FILENAMES.has(basename3(file.relativePath).toLowerCase()) && classifyApiSpec(file.absolutePath) === "openapi") ?? null;
6133
6509
  }
@@ -6149,6 +6525,13 @@ function classifyApiSpec(path) {
6149
6525
  if (!topLevel) return "unknown";
6150
6526
  return /asyncapi/i.test(topLevel) ? "asyncapi" : "openapi";
6151
6527
  }
6528
+ var SPEC_DIRECTORY = "openapi";
6529
+ function specAssetPath(filename) {
6530
+ return `${SPEC_DIRECTORY}/${filename}`;
6531
+ }
6532
+ function specAsset(filename, content) {
6533
+ return { path: specAssetPath(filename), content, projectRelative: true };
6534
+ }
6152
6535
  function mintlifyTopLevelApiReferences(config) {
6153
6536
  const api = config?.api && typeof config.api === "object" && !Array.isArray(config.api) ? config.api : null;
6154
6537
  if (!api) return [];
@@ -6169,7 +6552,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
6169
6552
  const seen = /* @__PURE__ */ new Set();
6170
6553
  const specs = [];
6171
6554
  for (const reference of references) {
6172
- const dedupeKey = `${reference.kind}:${reference.value}`;
6555
+ const dedupeKey = `${reference.kind}:${reference.value}:${reference.tabLabel ?? ""}`;
6173
6556
  if (seen.has(dedupeKey)) continue;
6174
6557
  seen.add(dedupeKey);
6175
6558
  const tabSuffix = reference.tabLabel ? ` (tab "${reference.tabLabel}")` : "";
@@ -6181,7 +6564,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
6181
6564
  continue;
6182
6565
  }
6183
6566
  if (/^https:\/\//i.test(reference.value)) {
6184
- remoteSpecs.push({ url: reference.value, ...reference.tabLabel ? { tabLabel: reference.tabLabel } : {} });
6567
+ remoteSpecs.push({ url: reference.value, ...reference.tabLabel ? { tabLabel: reference.tabLabel } : {}, ...reference.parentTab ? { parentTab: reference.parentTab } : {}, ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {} });
6185
6568
  warnings.push({
6186
6569
  code: "unsupported-config",
6187
6570
  message: `The remote OpenAPI spec "${reference.value}"${tabSuffix} requires a network download before this import is complete.`,
@@ -6212,15 +6595,19 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
6212
6595
  filename: basename3(match.relativePath),
6213
6596
  content: readFileSync5(match.absolutePath),
6214
6597
  tabLabel: reference.tabLabel,
6598
+ parentTab: reference.parentTab,
6599
+ icon: reference.icon,
6600
+ hidden: reference.hidden,
6215
6601
  sourcePath: match.relativePath,
6216
6602
  directory: reference.directory
6217
6603
  });
6218
6604
  }
6219
6605
  const filenameOwners = /* @__PURE__ */ new Map();
6606
+ const filenameSources = /* @__PURE__ */ new Map();
6220
6607
  for (const spec of specs) {
6221
6608
  const groupKey = spec.tabLabel ?? "\0default";
6222
6609
  const owner = filenameOwners.get(spec.filename);
6223
- if (owner !== void 0 && owner !== groupKey) {
6610
+ if (owner !== void 0 && owner !== groupKey && filenameSources.get(spec.filename) !== spec.sourcePath) {
6224
6611
  let disambiguated = spec.sourcePath.replace(/\//g, "-");
6225
6612
  while (filenameOwners.has(disambiguated) && filenameOwners.get(disambiguated) !== groupKey) {
6226
6613
  disambiguated = `${groupKey.replace(/\W+/g, "-")}-${disambiguated}`;
@@ -6228,6 +6615,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
6228
6615
  spec.filename = disambiguated;
6229
6616
  }
6230
6617
  filenameOwners.set(spec.filename, groupKey);
6618
+ filenameSources.set(spec.filename, spec.sourcePath);
6231
6619
  }
6232
6620
  return specs;
6233
6621
  }
@@ -6282,7 +6670,7 @@ function apiOperationLinkMap(sources, docsConfig) {
6282
6670
  const prefixLandings = /* @__PURE__ */ new Map();
6283
6671
  const apiTabs = docsConfig.tabs.filter((tab) => !tab.hidden && tab.api);
6284
6672
  apiTabs.forEach((tab, index) => {
6285
- const source = sources.find((entry) => tab.api?.source === `/${entry.filename}`);
6673
+ const source = sources.find((entry) => tab.api?.source === specAssetPath(entry.filename));
6286
6674
  if (!source || !source.prefix) return;
6287
6675
  const specId = index === 0 ? "default" : slugifyApiSpecId(tab.tab);
6288
6676
  const prefix = source.prefix.replace(/^\/+|\/+$/g, "").toLowerCase();
@@ -6450,6 +6838,31 @@ function fernDefinitionExists(fernRoot, apiName) {
6450
6838
  }
6451
6839
  return candidateDirs.some((dir) => existsSync5(dir) && lstatSync5(dir).isDirectory());
6452
6840
  }
6841
+ function readFrontmatterHead(path) {
6842
+ const fd = openSync(path, "r");
6843
+ try {
6844
+ const buffer = Buffer.alloc(FRONTMATTER_HEAD_BYTES);
6845
+ const head = buffer.toString("utf8", 0, readSync(fd, buffer, 0, buffer.length, 0));
6846
+ const opening = /^\uFEFF?---([^\r\n]*)\r?\n/.exec(head);
6847
+ if (!opening || opening[1].startsWith("-")) return head;
6848
+ return /^---[ \t]*\r?\n/m.test(head.slice(opening[0].length)) ? head : void 0;
6849
+ } finally {
6850
+ closeSync(fd);
6851
+ }
6852
+ }
6853
+ function canonicalPath(path) {
6854
+ try {
6855
+ return realpathSync2.native(path);
6856
+ } catch {
6857
+ return path;
6858
+ }
6859
+ }
6860
+ function pageHeadGateReason(raw) {
6861
+ const parsed = parseFrontmatter(raw);
6862
+ const unreadable = parsed.error && /^(groups|public)\s*:/m.test(raw.slice(0, raw.length - parsed.content.length)) ? "frontmatter could not be parsed and declares `groups` or `public`" : void 0;
6863
+ const openapi = typeof parsed.data.openapi === "string" ? parsed.data.openapi.trim() : void 0;
6864
+ return { reason: frontmatterGateReason(parsed.data) ?? unreadable, publicTrue: isPublicTrue(parsed.data.public), openapi };
6865
+ }
6453
6866
  function withoutFrontmatter(value) {
6454
6867
  return value.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
6455
6868
  }
@@ -6584,7 +6997,7 @@ function fernSourceSiteUrl(config) {
6584
6997
  function externalizeMissingFernLinks(body, pageIds, sourceSiteUrl, externalized) {
6585
6998
  const rewrite = (target) => {
6586
6999
  const path = target.split(/[?#]/, 1)[0].replace(/^\//, "").replace(/\/$/, "");
6587
- if (!path || pageIds.has(path) || ASSET_EXTENSIONS.has(extname5(path).toLowerCase())) return target;
7000
+ if (!path || pageIds.has(path) || ASSET_EXTENSIONS.has(extname6(path).toLowerCase())) return target;
6588
7001
  try {
6589
7002
  const base = new URL(`${sourceSiteUrl}/`);
6590
7003
  const sourceLinkUrl = new URL(target.slice(1), base);
@@ -6682,7 +7095,15 @@ function preserveLinkedAnchors(pages) {
6682
7095
  const key = comparable(cell.text);
6683
7096
  candidateIndexes.set(key, [...candidateIndexes.get(key) ?? [], { index: cell.index, kind: "table" }]);
6684
7097
  }
6685
- for (const heading of headings) existing.add(heading.text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""));
7098
+ const headingSlugs = /* @__PURE__ */ new Map();
7099
+ const slugCounts = /* @__PURE__ */ new Map();
7100
+ for (const heading of headings) {
7101
+ const slug2 = heading.text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
7102
+ existing.add(slug2);
7103
+ const seen = slugCounts.get(slug2) ?? 0;
7104
+ slugCounts.set(slug2, seen + 1);
7105
+ if (slug2) headingSlugs.set(seen === 0 ? slug2 : `${slug2}-${seen}`, heading.index);
7106
+ }
6686
7107
  const headingAliases = /* @__PURE__ */ new Map();
6687
7108
  const tableAliases = /* @__PURE__ */ new Map();
6688
7109
  for (const fragment of fragments) {
@@ -6696,6 +7117,14 @@ function preserveLinkedAnchors(pages) {
6696
7117
  const selections = linkIndexes.map((index2) => matches.filter((candidate) => candidate.index < index2).at(-1));
6697
7118
  if (selections.length && selections.every((candidate) => candidate?.index === selections[0]?.index)) match = selections[0];
6698
7119
  }
7120
+ if (!match && matches.length > 1) {
7121
+ const headingMatches = matches.filter((candidate) => candidate.kind === "heading");
7122
+ if (headingMatches.length === 1) match = headingMatches[0];
7123
+ }
7124
+ if (!match) {
7125
+ const index2 = headingSlugs.get(fragment.toLowerCase());
7126
+ if (index2 !== void 0) match = { index: index2, kind: "heading" };
7127
+ }
6699
7128
  if (!match) continue;
6700
7129
  const { index, kind } = match;
6701
7130
  const aliases = kind === "heading" ? headingAliases : tableAliases;
@@ -6762,8 +7191,13 @@ function globalSnippetAliases(files, repositoryRoot, siteRoot) {
6762
7191
  if (conventionalAlias) aliases.set(conventionalAlias, file.absolutePath);
6763
7192
  }
6764
7193
  for (const file of files) {
6765
- if (![".md", ".mdx"].includes(extname5(file.relativePath).toLowerCase())) continue;
6766
- const raw = readFileSync5(file.absolutePath, "utf8");
7194
+ if (![".md", ".mdx"].includes(extname6(file.relativePath).toLowerCase())) continue;
7195
+ let raw;
7196
+ try {
7197
+ raw = readFileSync5(file.absolutePath, "utf8");
7198
+ } catch {
7199
+ continue;
7200
+ }
6767
7201
  for (const match of raw.matchAll(SNIPPET_IMPORT_PATTERN)) {
6768
7202
  try {
6769
7203
  const componentName = match[1] ?? match[2];
@@ -6790,7 +7224,7 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
6790
7224
  } catch {
6791
7225
  return null;
6792
7226
  }
6793
- if (!ASSET_EXTENSIONS.has(extname5(decodedPath).toLowerCase())) return null;
7227
+ if (!ASSET_EXTENSIONS.has(extname6(decodedPath).toLowerCase())) return null;
6794
7228
  try {
6795
7229
  const candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithinRoot(dirname4(currentFile), decodedPath, siteRoot);
6796
7230
  const realRoot = realpathSync2(siteRoot);
@@ -6802,7 +7236,9 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
6802
7236
  if (!existsSync5(candidate) || !lstatSync5(candidate).isFile()) return null;
6803
7237
  const normalized = normalizeAssetPath(siteRelative);
6804
7238
  if (!normalized) return null;
6805
- onReferenced?.(normalized);
7239
+ const onDiskRelative = relative5(realRoot, realpathSync2.native(candidate)).replace(/\\/g, "/");
7240
+ const onDisk = onDiskRelative.toLowerCase() === siteRelative.toLowerCase() ? normalizeAssetPath(onDiskRelative) : null;
7241
+ onReferenced?.(normalized, onDisk ?? void 0);
6806
7242
  const rewritten = `/${normalized}${suffix}`;
6807
7243
  return isBracketed ? `<${rewritten}>` : rewritten;
6808
7244
  } catch {
@@ -6847,18 +7283,47 @@ function locallyDeclaredNames(raw) {
6847
7283
  }
6848
7284
  return names;
6849
7285
  }
6850
- function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0, siteRoot = repositoryRoot, globalAliases = /* @__PURE__ */ new Map()) {
7286
+ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0, siteRoot = repositoryRoot, globalAliases = /* @__PURE__ */ new Map(), gate) {
6851
7287
  if (depth >= 8) return raw;
6852
7288
  raw = normalizeIndentedFences(raw);
6853
7289
  const snippets = /* @__PURE__ */ new Map();
6854
7290
  const preservedDeclarations = /* @__PURE__ */ new Map();
6855
- let withoutImports = replaceOutsideCode(raw, (source) => source.replace(
7291
+ const source = relative5(repositoryRoot, currentFile).replace(/\\/g, "/");
7292
+ const blockInline = (candidate, shownPath, label = shownPath) => {
7293
+ const verdict = gate?.(candidate);
7294
+ if (!verdict) return void 0;
7295
+ const shown = shownPath.replace(/\*\//g, "* /");
7296
+ if (verdict.kind === "gated") {
7297
+ warnings.push({ code: "gated-page", message: `${label} is access-restricted and was NOT inlined; it was left as a comment.`, source });
7298
+ return `{/* Access-restricted content withheld: ${shown} */}`;
7299
+ }
7300
+ if (verdict.kind === "snippet-gated") {
7301
+ warnings.push({
7302
+ code: "gated-page",
7303
+ message: `${label} declares access rules (${verdict.reason}). Mintlify does not enforce groups on snippets, but it was NOT inlined to be safe; it was left as a comment.`,
7304
+ source
7305
+ });
7306
+ return `{/* Access-restricted content withheld: ${shown} */}`;
7307
+ }
7308
+ warnings.push({
7309
+ code: "skipped-file",
7310
+ message: `${label} ${verdict.kind === "oversized" ? "is too large to inline (over 2 MB)" : "has frontmatter that could not be read"} and was NOT inlined; it was left as a comment.`,
7311
+ source
7312
+ });
7313
+ return `{/* ${verdict.kind === "oversized" ? "Oversized" : "Unreadable"} content not inlined: ${shown} */}`;
7314
+ };
7315
+ let withoutImports = replaceOutsideCode(raw, (source2) => source2.replace(
6856
7316
  SNIPPET_IMPORT_PATTERN,
6857
7317
  (_statement, namedComponent, defaultComponent, sourcePath) => {
6858
7318
  const componentName = namedComponent ?? defaultComponent;
6859
7319
  try {
6860
7320
  const candidate = resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot);
6861
7321
  if (!existsSync5(candidate) || !lstatSync5(candidate).isFile()) throw new Error("file not found");
7322
+ const blocked = blockInline(candidate, sourcePath, `Imported ${sourcePath}`);
7323
+ if (blocked) {
7324
+ snippets.set(componentName, blocked);
7325
+ return "";
7326
+ }
6862
7327
  const nested = inlineMdxSnippets(
6863
7328
  withoutFrontmatter(readFileSync5(candidate, "utf8")),
6864
7329
  candidate,
@@ -6866,7 +7331,8 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
6866
7331
  warnings,
6867
7332
  depth + 1,
6868
7333
  siteRoot,
6869
- globalAliases
7334
+ globalAliases,
7335
+ gate
6870
7336
  );
6871
7337
  const declaration = statefulSnippetDeclaration(nested, componentName);
6872
7338
  if (declaration) preservedDeclarations.set(componentName, declaration);
@@ -6883,7 +7349,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
6883
7349
  }
6884
7350
  }
6885
7351
  ));
6886
- withoutImports = replaceOutsideCode(withoutImports, (source) => source.replace(
7352
+ withoutImports = replaceOutsideCode(withoutImports, (source2) => source2.replace(
6887
7353
  SNIPPET_VALUE_IMPORT_PATTERN,
6888
7354
  (statement, names, sourcePath) => {
6889
7355
  const bindings = names.split(",").map((name) => name.trim()).filter(Boolean).map((name) => {
@@ -6893,7 +7359,16 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
6893
7359
  if (bindings.length === 0 || bindings.some((binding) => !binding)) return statement;
6894
7360
  try {
6895
7361
  const candidate = resolveSnippetPath(sourcePath, currentFile, repositoryRoot, siteRoot);
6896
- if (!existsSync5(candidate) || !lstatSync5(candidate).isFile()) return statement;
7362
+ if (!existsSync5(candidate) || !lstatSync5(candidate).isFile() || gate?.(candidate)?.kind === "gated") return statement;
7363
+ const blocked = blockInline(candidate, sourcePath);
7364
+ if (blocked) {
7365
+ const declarations2 = [];
7366
+ for (const binding of bindings) {
7367
+ if (/^[A-Z]/.test(binding.exported)) snippets.set(binding.local, blocked);
7368
+ else declarations2.push(`export const ${binding.local} = undefined;`);
7369
+ }
7370
+ return declarations2.join("\n");
7371
+ }
6897
7372
  const snippetSource = readFileSync5(candidate, "utf8");
6898
7373
  const values = staticNamedSnippetValues(snippetSource);
6899
7374
  const declarations = [];
@@ -6918,7 +7393,12 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
6918
7393
  ));
6919
7394
  const localNames = locallyDeclaredNames(withoutImports);
6920
7395
  for (const [componentName, candidate] of globalAliases) {
6921
- if (snippets.has(componentName) || localNames.has(componentName) || !new RegExp(`<${componentName}(?:\\s|/?>)`).test(withoutImports)) continue;
7396
+ if (snippets.has(componentName) || localNames.has(componentName) || gate?.(candidate)?.kind === "gated" || !new RegExp(`<${componentName}(?:\\s|/?>)`).test(withoutImports)) continue;
7397
+ const blocked = blockInline(candidate, relative5(repositoryRoot, candidate).replace(/\\/g, "/"));
7398
+ if (blocked) {
7399
+ snippets.set(componentName, blocked);
7400
+ continue;
7401
+ }
6922
7402
  const nested = inlineMdxSnippets(
6923
7403
  withoutFrontmatter(readFileSync5(candidate, "utf8")),
6924
7404
  candidate,
@@ -6926,7 +7406,8 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
6926
7406
  warnings,
6927
7407
  depth + 1,
6928
7408
  siteRoot,
6929
- globalAliases
7409
+ globalAliases,
7410
+ gate
6930
7411
  );
6931
7412
  const declaration = statefulSnippetDeclaration(nested, componentName);
6932
7413
  if (declaration) preservedDeclarations.set(componentName, declaration);
@@ -6954,6 +7435,8 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
6954
7435
  }
6955
7436
  }).find((path) => path !== void 0 && existsSync5(path) && lstatSync5(path).isFile());
6956
7437
  if (!candidate) throw new Error("file not found");
7438
+ const blocked = blockInline(candidate, filePath, `Snippet file="${filePath}"`);
7439
+ if (blocked) return blocked;
6957
7440
  return inlineMdxSnippets(
6958
7441
  withoutFrontmatter(readFileSync5(candidate, "utf8")),
6959
7442
  candidate,
@@ -6961,7 +7444,8 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
6961
7444
  warnings,
6962
7445
  depth + 1,
6963
7446
  siteRoot,
6964
- globalAliases
7447
+ globalAliases,
7448
+ gate
6965
7449
  );
6966
7450
  } catch {
6967
7451
  warnings.push({
@@ -6982,25 +7466,30 @@ ${result.slice(prefix.length)}`;
6982
7466
  return depth === 0 ? hoistMdxImports(result) : result;
6983
7467
  }
6984
7468
  function injectOpenApiSpecs(config, specs, warnings) {
6985
- let tabs = config.tabs.map((tab) => ({ ...tab }));
7469
+ const tabs = config.tabs.map((tab) => ({ ...tab }));
6986
7470
  for (const spec of specs) {
6987
7471
  const apiTab = spec.tabLabel ? tabs.find((tab) => tab.tab === spec.tabLabel) : tabs.find((tab) => tab.tab.toLowerCase().includes("api"));
6988
7472
  if (apiTab) {
6989
7473
  if (apiTab.api && warnings) {
6990
7474
  warnings.push({
6991
7475
  code: "unsupported-config",
6992
- message: `Tab "${apiTab.tab}" already uses another OpenAPI spec, and Thally supports one API spec per tab, so "/${spec.filename}" was not added. Put it in its own tab to include it.`
7476
+ message: `Tab "${apiTab.tab}" already uses another OpenAPI spec, and Thally supports one API spec per tab, so "${specAssetPath(spec.filename)}" was not added. Put it in its own tab to include it.`
6993
7477
  });
6994
7478
  continue;
6995
7479
  }
6996
7480
  apiTab.api = {
6997
- source: `/${spec.filename}`,
7481
+ source: specAssetPath(spec.filename),
6998
7482
  // An API-only tab needs generated endpoint navigation; an authored
6999
7483
  // page tab keeps its own groups alongside the bound spec.
7000
7484
  ...apiTab.groups?.length || apiTab.pages?.length ? { navigation: false } : {}
7001
7485
  };
7002
7486
  } else {
7003
- tabs = [...tabs, { tab: spec.tabLabel ?? "API Reference", api: { source: `/${spec.filename}` } }];
7487
+ insertApiTab(tabs, {
7488
+ tab: spec.tabLabel ?? "API Reference",
7489
+ ...spec.icon ? { icon: spec.icon } : {},
7490
+ ...spec.hidden ? { hidden: true } : {},
7491
+ api: { source: specAssetPath(spec.filename) }
7492
+ }, spec.parentTab);
7004
7493
  }
7005
7494
  }
7006
7495
  return { ...config, tabs };
@@ -7048,6 +7537,7 @@ function migrateRepository(options) {
7048
7537
  const referenceOrder = /* @__PURE__ */ new Map();
7049
7538
  let docusaurusSidebars = null;
7050
7539
  let mintlifyConfig = null;
7540
+ const mintlifyGatedRefs = /* @__PURE__ */ new Map();
7051
7541
  let fernRawConfig = null;
7052
7542
  let fernBasePath = "";
7053
7543
  let fernVersionPath = "";
@@ -7068,6 +7558,10 @@ function migrateRepository(options) {
7068
7558
  const projected = projectMintlifyNavigation(config);
7069
7559
  docsConfig = projected.docsConfig;
7070
7560
  warnings.push(...projected.warnings);
7561
+ for (const gated of projected.gatedReferences) {
7562
+ const gatedKey = normalizedReferenceKey(gated.ref).toLowerCase();
7563
+ if (!mintlifyGatedRefs.has(gatedKey)) mintlifyGatedRefs.set(gatedKey, gated.reason);
7564
+ }
7071
7565
  for (const [index, reference] of projected.pageReferences.entries()) {
7072
7566
  const key = normalizedReferenceKey(reference.ref);
7073
7567
  if (!referenceMap.has(key)) referenceMap.set(key, reference);
@@ -7153,10 +7647,11 @@ function migrateRepository(options) {
7153
7647
  const versionCeiling = lastReferencedIndexByVersionLocale.get(blockKey);
7154
7648
  return versionCeiling !== void 0 ? versionCeiling + 1 : Number.MAX_SAFE_INTEGER;
7155
7649
  } : void 0;
7156
- const scannedFiles = scanFiles(contentRoot, repositoryDir, warnings, discoveryRank);
7650
+ const sourceBudget = options.maxSourceFiles ?? MAX_SOURCE_FILES;
7651
+ const scannedFiles = scanFiles(contentRoot, repositoryDir, warnings, discoveryRank, sourceBudget);
7157
7652
  const mintignoreFilteredFiles = mintignoreMatcher ? scannedFiles.filter((file) => !mintignoreMatcher.ignores(file.relativePath)) : scannedFiles;
7158
- const discoveryBudgetApplied = discoveryRank !== void 0 && mintignoreFilteredFiles.length > MAX_SOURCE_FILES;
7159
- const files = discoveryRank ? selectFilesWithinBudget(mintignoreFilteredFiles, discoveryRank, warnings, allVersionPrefixes) : mintignoreFilteredFiles;
7653
+ const discoveryBudgetApplied = discoveryRank !== void 0 && mintignoreFilteredFiles.length > sourceBudget;
7654
+ const files = discoveryRank ? selectFilesWithinBudget(mintignoreFilteredFiles, discoveryRank, warnings, allVersionPrefixes, sourceBudget) : mintignoreFilteredFiles;
7160
7655
  if (platform === "fern" && fernProjectRoot) {
7161
7656
  const discovered2 = new Set(files.map((file) => file.relativePath));
7162
7657
  for (const sourcePath of fernExternalSourcePaths) {
@@ -7186,6 +7681,108 @@ function migrateRepository(options) {
7186
7681
  const docusaurusDescriptors = [];
7187
7682
  const docusaurusDocCardRoutes = /* @__PURE__ */ new Set();
7188
7683
  const seenPageIds = /* @__PURE__ */ new Set();
7684
+ const quarantinedFiles = [];
7685
+ const withheldAssetPaths = /* @__PURE__ */ new Set();
7686
+ const publishedExact = /* @__PURE__ */ new Set();
7687
+ const publishedLoose = /* @__PURE__ */ new Set();
7688
+ const withholdAsset = (assetPath, onDiskSpelling) => {
7689
+ withheldAssetPaths.add(publicAssetKey(onDiskSpelling ?? assetPath));
7690
+ };
7691
+ let sawPublicTrue = false;
7692
+ const gateByPath = /* @__PURE__ */ new Map();
7693
+ const withheldSpecRefs = [];
7694
+ const publishedSpecRefs = [];
7695
+ const withheldPaths = /* @__PURE__ */ new Set();
7696
+ const isDocFile = (file) => [".md", ".mdx"].includes(extname6(file.relativePath).toLowerCase());
7697
+ const isGateCandidate = (file) => isDocFile(file) && !file.relativePath.split("/").some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase()));
7698
+ const classifyPageGate = (file) => {
7699
+ let head = { publicTrue: false };
7700
+ let unreadableGate;
7701
+ try {
7702
+ const size = lstatSync5(file.absolutePath).size;
7703
+ const raw = readFrontmatterHead(file.absolutePath) ?? (size > MAX_PAGE_BYTES ? void 0 : readFileSync5(file.absolutePath, "utf8"));
7704
+ if (raw === void 0) throw new Error("frontmatter is not terminated within the bounded read");
7705
+ head = pageHeadGateReason(raw);
7706
+ } catch {
7707
+ unreadableGate = "frontmatter could not be read";
7708
+ }
7709
+ const reason = head.reason ?? unreadableGate ?? mintlifyGatedRefs.get(normalizedReferenceKey(file.relativePath).toLowerCase());
7710
+ return { reason, publicTrue: head.publicTrue, openapi: head.openapi };
7711
+ };
7712
+ const keptFiles = new Set(files);
7713
+ const droppedGatedFiles = [];
7714
+ const withheldDocFiles = [];
7715
+ let droppedPageCount = 0;
7716
+ const scanTruncated = scannedFiles.length >= (discoveryRank ? sourceBudget * RANKED_WALK_FACTOR : sourceBudget);
7717
+ if (platform === "mintlify") {
7718
+ for (const file of files) {
7719
+ if (isDocFile(file) && !isGateCandidate(file)) {
7720
+ const snippetReason = classifyPageGate(file).reason;
7721
+ if (snippetReason) withheldDocFiles.push(file);
7722
+ continue;
7723
+ }
7724
+ if (!isGateCandidate(file)) continue;
7725
+ const verdict = classifyPageGate(file);
7726
+ gateByPath.set(file.absolutePath, { reason: verdict.reason, publicTrue: verdict.publicTrue });
7727
+ if (verdict.openapi) (verdict.reason ? withheldSpecRefs : publishedSpecRefs).push(verdict.openapi);
7728
+ if (verdict.reason) withheldPaths.add(file.absolutePath);
7729
+ }
7730
+ for (const file of mintignoreFilteredFiles) {
7731
+ if (keptFiles.has(file) || !isDocFile(file)) continue;
7732
+ droppedPageCount++;
7733
+ if (!isGateCandidate(file)) {
7734
+ const snippetReason = classifyPageGate(file).reason;
7735
+ if (snippetReason) {
7736
+ withheldDocFiles.push(file);
7737
+ warnings.push({ code: "gated-page", message: `Access-restricted snippet (${snippetReason}) was dropped by the file limit; its assets were kept out of public/.`, source: file.relativePath });
7738
+ }
7739
+ continue;
7740
+ }
7741
+ const verdict = classifyPageGate(file);
7742
+ if (!verdict.reason) continue;
7743
+ if (verdict.openapi) withheldSpecRefs.push(verdict.openapi);
7744
+ withheldPaths.add(file.absolutePath);
7745
+ droppedGatedFiles.push({ file, reason: verdict.reason });
7746
+ }
7747
+ for (const file of scannedFiles) {
7748
+ if (!mintignoreMatcher?.ignores(file.relativePath) || !isDocFile(file)) continue;
7749
+ const verdict = classifyPageGate(file);
7750
+ if (!verdict.reason) continue;
7751
+ if (verdict.openapi) withheldSpecRefs.push(verdict.openapi);
7752
+ withheldDocFiles.push(file);
7753
+ }
7754
+ }
7755
+ const pagesNotClassified = droppedPageCount > 0 || platform === "mintlify" && scanTruncated;
7756
+ const hasWithheldContent = withheldPaths.size > 0 || withheldDocFiles.length > 0;
7757
+ const trackPublishedRefs = platform === "mintlify" && (hasWithheldContent || pagesNotClassified);
7758
+ const withheldCanonical = new Set([...withheldPaths].map(canonicalPath));
7759
+ const verdictCache = /* @__PURE__ */ new Map();
7760
+ const classifyInlineCandidate = (candidate, key) => {
7761
+ if (withheldCanonical.has(key)) return { kind: "gated", reason: "access-restricted" };
7762
+ const shown = relative5(contentRoot, candidate).replace(/\\/g, "/");
7763
+ const inSnippetDirectory = shown.split("/").some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase()));
7764
+ let head;
7765
+ try {
7766
+ head = readFrontmatterHead(candidate);
7767
+ } catch {
7768
+ return { kind: "unreadable", reason: "could not be read" };
7769
+ }
7770
+ const gated = head === void 0 ? void 0 : pageHeadGateReason(head).reason;
7771
+ if (gated) return { kind: inSnippetDirectory ? "snippet-gated" : "gated", reason: gated };
7772
+ const navigationGated = inSnippetDirectory ? void 0 : mintlifyGatedRefs.get(normalizedReferenceKey(shown).toLowerCase());
7773
+ if (navigationGated) return { kind: "gated", reason: navigationGated };
7774
+ if (lstatSync5(candidate).size > MAX_PAGE_BYTES) return { kind: "oversized", reason: "over 2 MB" };
7775
+ return head === void 0 ? { kind: "unreadable", reason: "frontmatter not terminated in the bounded read" } : void 0;
7776
+ };
7777
+ const inlineGate = platform === "mintlify" ? (candidate) => {
7778
+ const key = canonicalPath(candidate);
7779
+ let verdict = verdictCache.get(key);
7780
+ if (verdict === void 0) {
7781
+ verdict = classifyInlineCandidate(candidate, key) ?? null;
7782
+ verdictCache.set(key, verdict);
7783
+ }
7784
+ return verdict ?? void 0;
7785
+ } : void 0;
7189
7786
  const fernIdRenames = /* @__PURE__ */ new Map();
7190
7787
  let skipped = 0;
7191
7788
  let discovered = files.length;
@@ -7198,6 +7795,47 @@ function migrateRepository(options) {
7198
7795
  return leftOrder - rightOrder || left.relativePath.localeCompare(right.relativePath);
7199
7796
  });
7200
7797
  const snippetAliases = platform === "mintlify" && mintlifyProjectRoot ? globalSnippetAliases(files, repositoryDir, mintlifyProjectRoot) : /* @__PURE__ */ new Map();
7798
+ droppedGatedFiles.forEach(({ file, reason }, index) => {
7799
+ if (index < MAX_DROPPED_GATED_WARNINGS) {
7800
+ warnings.push({
7801
+ code: "gated-page",
7802
+ message: `Access-restricted page (${reason}) was dropped by the file limit, so it was NOT published and was not saved under ${QUARANTINE_DIRECTORY}/; recover it from the source repository.`,
7803
+ source: file.relativePath
7804
+ });
7805
+ }
7806
+ if (!mintlifyProjectRoot) return;
7807
+ try {
7808
+ if (lstatSync5(file.absolutePath).size > MAX_WITHHELD_SCAN_BYTES) throw new Error("too large to scan");
7809
+ rewriteRepositoryAssetLinks(
7810
+ inlineMdxSnippets(readFileSync5(file.absolutePath, "utf8"), file.absolutePath, repositoryDir, [], 0, mintlifyProjectRoot, snippetAliases, inlineGate),
7811
+ file.absolutePath,
7812
+ mintlifyProjectRoot,
7813
+ withholdAsset
7814
+ );
7815
+ } catch {
7816
+ }
7817
+ });
7818
+ for (const file of withheldDocFiles) {
7819
+ try {
7820
+ if (!mintlifyProjectRoot) continue;
7821
+ if (lstatSync5(file.absolutePath).size > MAX_WITHHELD_SCAN_BYTES) {
7822
+ warnings.push({
7823
+ code: "gated-page",
7824
+ message: "Access-restricted file is too large to scan, so the assets it uses could not be checked and may have been copied to public/. Review them before publishing.",
7825
+ source: file.relativePath
7826
+ });
7827
+ continue;
7828
+ }
7829
+ rewriteRepositoryAssetLinks(readFileSync5(file.absolutePath, "utf8"), file.absolutePath, mintlifyProjectRoot, withholdAsset);
7830
+ } catch {
7831
+ }
7832
+ }
7833
+ if (droppedGatedFiles.length > MAX_DROPPED_GATED_WARNINGS) {
7834
+ warnings.push({
7835
+ code: "gated-page",
7836
+ message: `${droppedGatedFiles.length - MAX_DROPPED_GATED_WARNINGS} more access-restricted page(s) were dropped by the file limit and NOT published; recover them from the source repository.`
7837
+ });
7838
+ }
7201
7839
  for (const file of pageFiles) {
7202
7840
  if (!isDocumentationExtension(file.relativePath)) continue;
7203
7841
  if (platform === "fern" && !exactReferenceMap.has(exactReferenceKey(file.relativePath)) && !referenceMap.has(normalizedReferenceKey(file.relativePath))) {
@@ -7214,7 +7852,7 @@ function migrateRepository(options) {
7214
7852
  skipped++;
7215
7853
  continue;
7216
7854
  }
7217
- if (![".md", ".mdx"].includes(extname5(file.relativePath).toLowerCase())) {
7855
+ if (![".md", ".mdx"].includes(extname6(file.relativePath).toLowerCase())) {
7218
7856
  skipped++;
7219
7857
  warnings.push({ code: "skipped-file", message: "Only Markdown and MDX are imported without an explicit converter.", source: file.relativePath });
7220
7858
  continue;
@@ -7223,9 +7861,64 @@ function migrateRepository(options) {
7223
7861
  if (size > MAX_PAGE_BYTES) {
7224
7862
  skipped++;
7225
7863
  warnings.push({ code: "skipped-file", message: "Page exceeded the 2 MB repository import limit.", source: file.relativePath });
7864
+ const oversizedGate = gateByPath.get(file.absolutePath)?.reason;
7865
+ if (oversizedGate) {
7866
+ warnings.push({
7867
+ code: "gated-page",
7868
+ message: `Access-restricted page (${oversizedGate}) is too large to migrate (over 2 MB), so it was NOT published and was not saved under ${QUARANTINE_DIRECTORY}/; recover it from the source repository.`,
7869
+ source: file.relativePath
7870
+ });
7871
+ }
7872
+ if (oversizedGate && mintlifyProjectRoot) {
7873
+ if (size > MAX_WITHHELD_SCAN_BYTES) {
7874
+ warnings.push({
7875
+ code: "gated-page",
7876
+ message: "Access-restricted page is too large to scan, so the assets it uses could not be checked and may have been copied to public/. Review them before publishing.",
7877
+ source: file.relativePath
7878
+ });
7879
+ } else {
7880
+ rewriteRepositoryAssetLinks(readFileSync5(file.absolutePath, "utf8"), file.absolutePath, mintlifyProjectRoot, withholdAsset);
7881
+ }
7882
+ }
7226
7883
  continue;
7227
7884
  }
7228
7885
  const key = normalizedReferenceKey(file.relativePath);
7886
+ if (platform === "mintlify") {
7887
+ const gate = gateByPath.get(file.absolutePath);
7888
+ if (gate?.publicTrue) sawPublicTrue = true;
7889
+ const gateReason = gate?.reason;
7890
+ if (gateReason) {
7891
+ skipped++;
7892
+ if (file.relativePath.split("/").some((segment) => segment === "..")) {
7893
+ warnings.push({ code: "gated-page", message: `Access-restricted page (${gateReason}) was withheld from the site, but its path could not be preserved safely; recover it from the source repository.`, source: file.relativePath });
7894
+ continue;
7895
+ }
7896
+ try {
7897
+ if (mintlifyProjectRoot) {
7898
+ rewriteRepositoryAssetLinks(
7899
+ inlineMdxSnippets(readFileSync5(file.absolutePath, "utf8"), file.absolutePath, repositoryDir, [], 0, mintlifyProjectRoot, snippetAliases, inlineGate),
7900
+ file.absolutePath,
7901
+ mintlifyProjectRoot,
7902
+ withholdAsset
7903
+ );
7904
+ }
7905
+ quarantinedFiles.push({ path: `${QUARANTINE_DIRECTORY}/${file.relativePath}`, content: readFileSync5(file.absolutePath) });
7906
+ } catch {
7907
+ warnings.push({
7908
+ code: "gated-page",
7909
+ message: `Access-restricted page (${gateReason}) was NOT published, but it could not be read, so it was not copied to ${QUARANTINE_DIRECTORY}/; recover it from the source repository.`,
7910
+ source: file.relativePath
7911
+ });
7912
+ continue;
7913
+ }
7914
+ warnings.push({
7915
+ code: "gated-page",
7916
+ message: `Access-restricted on the source site (${gateReason}), so it was NOT published. The original is saved at ${QUARANTINE_DIRECTORY}/${file.relativePath}; links from other pages to it will break.`,
7917
+ source: file.relativePath
7918
+ });
7919
+ continue;
7920
+ }
7921
+ }
7229
7922
  const referenced = exactReferenceMap.get(exactReferenceKey(file.relativePath)) ?? referenceMap.get(key);
7230
7923
  let locale = referenced?.locale;
7231
7924
  let navigationId = referenced?.navigationId ?? pageIdFromReference(file.relativePath, platform === "mintlify");
@@ -7252,8 +7945,11 @@ function migrateRepository(options) {
7252
7945
  warnings,
7253
7946
  0,
7254
7947
  mintlifyProjectRoot ?? docusaurusProjectRoot ?? fernProjectRoot ?? repositoryDir,
7255
- snippetAliases
7948
+ snippetAliases,
7949
+ inlineGate
7256
7950
  );
7951
+ const publishedText = trackPublishedRefs ? raw : void 0;
7952
+ const pageAssetReferences = [];
7257
7953
  if (platform === "fern" || platform === "mintlify" || platform === "docusaurus") {
7258
7954
  raw = normalizeExplicitHeadingIds(raw);
7259
7955
  }
@@ -7345,8 +8041,8 @@ function migrateRepository(options) {
7345
8041
  }
7346
8042
  if (platform === "fern" && fernHiddenIds.has(navigationId)) page.hidden = true;
7347
8043
  if (platform === "mintlify" && mintlifyProjectRoot) {
7348
- page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, mintlifyProjectRoot, (assetPath) => {
7349
- addAssetReference(assetPath, file.relativePath);
8044
+ page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, mintlifyProjectRoot, (assetPath, onDiskSpelling) => {
8045
+ pageAssetReferences.push(publicAssetKey(onDiskSpelling ?? assetPath));
7350
8046
  });
7351
8047
  }
7352
8048
  if (platform === "fern" && fernProjectRoot) {
@@ -7410,6 +8106,8 @@ function migrateRepository(options) {
7410
8106
  }
7411
8107
  seenPageIds.add(page.id);
7412
8108
  pages.push(page);
8109
+ for (const assetPath of pageAssetReferences) addAssetReference(assetPath, file.relativePath);
8110
+ if (publishedText !== void 0) addPathReferences(publishedText, posix3.dirname(file.relativePath).replace(/^\.$/, ""), publishedExact, publishedLoose);
7413
8111
  if (hasDocusaurusDocCardList) {
7414
8112
  docusaurusDocCardRoutes.add(page.navigationId);
7415
8113
  }
@@ -7452,7 +8150,7 @@ function migrateRepository(options) {
7452
8150
  const withinFern = relative5(fernProjectRoot, absolutePath);
7453
8151
  const assetRelative = withinFern !== ".." && !withinFern.startsWith(`..${sep2}`) && !isAbsolute2(withinFern) ? withinFern : relative5(repositoryDir, absolutePath);
7454
8152
  const assetPath = normalizeAssetPath(assetRelative.replace(/\\/g, "/"));
7455
- if (!assetPath || !ASSET_EXTENSIONS.has(extname5(assetPath).toLowerCase())) return;
8153
+ if (!assetPath || !ASSET_EXTENSIONS.has(extname6(assetPath).toLowerCase())) return;
7456
8154
  if (source) {
7457
8155
  fernBrandAssetPaths.set(source, assetPath);
7458
8156
  addAssetReference(assetPath, "fern/docs.yml");
@@ -7483,45 +8181,131 @@ function migrateRepository(options) {
7483
8181
  }));
7484
8182
  }) : [];
7485
8183
  const assetCandidates = [];
8184
+ const mintlifyFonts = platform === "mintlify" ? mintlifyFontSources(mintlifyConfig) : [];
8185
+ const fontRelativePaths = new Set(mintlifyFonts.flatMap((font) => font.path ? [font.path] : []));
8186
+ const importedComponentSources = new Set((componentMigrator?.files() ?? []).flatMap((file) => {
8187
+ const source = /^src\/mdx\/migrated\/[^/]+\/source\/(.+)$/.exec(file.path)?.[1];
8188
+ return source ? [resolvePath(repositoryDir, source)] : [];
8189
+ }));
8190
+ const siteAssetPaths = /* @__PURE__ */ new Map();
8191
+ const fontAssetByRelative = /* @__PURE__ */ new Map();
7486
8192
  for (const file of [...files, ...repositoryAssets, ...fernReferencedAssets]) {
7487
8193
  const firstSegment = file.relativePath.split("/", 1)[0].toLowerCase();
7488
- if (!ASSET_EXTENSIONS.has(extname5(file.relativePath).toLowerCase())) continue;
8194
+ 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;
8195
+ if (!siteKind && !ASSET_EXTENSIONS.has(extname6(file.relativePath).toLowerCase())) continue;
7489
8196
  if (platform !== "mintlify" && !ASSET_DIRECTORIES.has(firstSegment) && !(platform === "fern" && referencedAssetPaths.has(file.relativePath))) continue;
7490
8197
  const isDocusaurusStatic = platform === "docusaurus" && firstSegment === "static";
7491
8198
  const assetPath = normalizeAssetPath(firstSegment === "public" ? file.relativePath.slice("public/".length) : isDocusaurusStatic ? file.relativePath.slice("static/".length) : file.relativePath);
7492
8199
  if (!assetPath) continue;
7493
- assetCandidates.push({ file, assetPath, size: lstatSync5(file.absolutePath).size });
8200
+ if (siteKind) {
8201
+ siteAssetPaths.set(assetPath, siteKind);
8202
+ if (siteKind === "font") fontAssetByRelative.set(file.relativePath, assetPath);
8203
+ }
8204
+ const candidateSize = lstatSync5(file.absolutePath).size;
8205
+ if (siteKind === "style" || siteKind === "script") {
8206
+ if (trackPublishedRefs && candidateSize <= MAX_PAGE_BYTES) addPathReferences(readFileSync5(file.absolutePath, "utf8"), posix3.dirname(file.relativePath).replace(/^\.$/, ""), publishedExact, publishedLoose);
8207
+ }
8208
+ assetCandidates.push({ file, assetPath, size: candidateSize });
7494
8209
  }
7495
8210
  const isReferenced = (candidate) => referencedAssetPaths.has(candidate.assetPath);
7496
8211
  const orderedAssetCandidates = [
7497
8212
  ...assetCandidates.filter((candidate) => isReferenced(candidate)),
7498
8213
  ...assetCandidates.filter((candidate) => !isReferenced(candidate))
7499
8214
  ];
8215
+ const quarantinedPageCount = quarantinedFiles.length;
8216
+ const destinationCounts = /* @__PURE__ */ new Map();
8217
+ for (const { assetPath } of assetCandidates) destinationCounts.set(assetPath, (destinationCounts.get(assetPath) ?? 0) + 1);
8218
+ if (trackPublishedRefs) {
8219
+ const publishedConfig = JSON.stringify(mintlifyConfig ?? {}, (_key, value) => value && typeof value === "object" && !Array.isArray(value) && navigationGateReason(value) ? void 0 : value);
8220
+ addPathReferences(publishedConfig, "", publishedExact, publishedLoose);
8221
+ }
8222
+ for (const file of componentMigrator?.files() ?? []) {
8223
+ if (!trackPublishedRefs || typeof file.content !== "string") continue;
8224
+ const original = /^src\/mdx\/migrated\/[^/]+\/source\/(.+)$/.exec(file.path)?.[1];
8225
+ addPathReferences(file.content, original === void 0 ? "" : posix3.dirname(original).replace(/^\.$/, ""), publishedExact, publishedLoose);
8226
+ }
8227
+ const failClosedAssets = trackPublishedRefs;
8228
+ const isPublicReachable = (assetPath) => referencedAssetPaths.has(assetPath) || publishedExact.has(assetPath);
8229
+ const isQuarantinedAsset = (assetPath) => (withheldAssetPaths.has(assetPath) || failClosedAssets && !siteAssetPaths.has(assetPath)) && platform === "mintlify" && !(isPublicReachable(assetPath) && !(failClosedAssets && (destinationCounts.get(assetPath) ?? 0) > 1));
8230
+ let withheldAssetCount = 0;
8231
+ let unreferencedAssetCount = 0;
7500
8232
  let totalAssetBytes = 0;
8233
+ const overBudgetAssets = [];
8234
+ let overBudgetReferenced = 0;
8235
+ const lfsPointerAssets = [];
8236
+ const ambiguouslyNamedAssets = [];
7501
8237
  for (const { file, assetPath, size } of orderedAssetCandidates) {
7502
8238
  if (size > MAX_ASSET_BYTES || totalAssetBytes + size > MAX_TOTAL_ASSET_BYTES) {
7503
- const referencingPages = referencedAssetPaths.get(assetPath);
7504
- warnings.push({
7505
- code: "limit-reached",
7506
- message: "This file was not copied because it is larger than 25 MB or the copied files would exceed 500 MB in total. Add it to public/ manually" + (referencingPages ? `; it is used by ${[...referencingPages].slice(0, 3).join(", ")}${referencingPages.size > 3 ? `, and ${referencingPages.size - 3} more` : ""}.` : "."),
7507
- source: file.relativePath
7508
- });
8239
+ overBudgetAssets.push(file.relativePath);
8240
+ if (referencedAssetPaths.has(assetPath)) overBudgetReferenced++;
7509
8241
  continue;
7510
8242
  }
7511
8243
  const content = readFileSync5(file.absolutePath);
7512
8244
  if (isGitLfsPointer(content)) {
7513
- warnings.push({
7514
- code: "unsupported-config",
7515
- message: "This asset is a Git LFS pointer, not its real content (Git LFS was skipped during clone because the host has no git-lfs binary). Install git-lfs and re-run the migration, or add the real file to public/ manually.",
7516
- source: file.relativePath
7517
- });
8245
+ lfsPointerAssets.push(file.relativePath);
8246
+ continue;
8247
+ }
8248
+ if (isQuarantinedAsset(assetPath)) {
8249
+ quarantinedFiles.push({ path: `${QUARANTINE_DIRECTORY}/assets/${assetPath}`, content });
8250
+ if (looselyNamed(assetPath, publishedLoose) || (destinationCounts.get(assetPath) ?? 0) > 1) ambiguouslyNamedAssets.push(assetPath);
8251
+ if (withheldAssetPaths.has(assetPath)) withheldAssetCount++;
8252
+ else unreferencedAssetCount++;
7518
8253
  continue;
7519
8254
  }
7520
8255
  assets.push({ path: assetPath, content });
7521
8256
  totalAssetBytes += size;
7522
8257
  }
7523
- if (!discoveryBudgetApplied && files.length >= MAX_SOURCE_FILES) {
7524
- warnings.push({ code: "limit-reached", message: `Stopped scanning after ${MAX_SOURCE_FILES} files, so the rest of the repository was not looked at. Run the migration on a smaller part of the repository with --docs-dir.` });
8258
+ if (ambiguouslyNamedAssets.length > 0) {
8259
+ warnings.push({
8260
+ code: "gated-page",
8261
+ message: `${ambiguouslyNamedAssets.length} asset(s) were kept out of public/ because a published page names a file like ${ambiguouslyNamedAssets.length === 1 ? "it" : "them"} in a way that does not give its folder (or differs in letter case), so it cannot be matched to one file (or two files share one public path): ${listAssetPaths(ambiguouslyNamedAssets)}. Copy any that published pages need from ${QUARANTINE_DIRECTORY}/assets/ into public/ by hand.`
8262
+ });
8263
+ }
8264
+ if (platform === "mintlify") {
8265
+ const copiedSite = assets.flatMap((asset) => siteAssetPaths.has(asset.path) ? [{ path: asset.path, kind: siteAssetPaths.get(asset.path) }] : []);
8266
+ const scripts = copiedSite.filter((entry) => entry.kind === "script");
8267
+ if (scripts.length > 0) {
8268
+ docsConfig = {
8269
+ ...docsConfig,
8270
+ customScripts: [...docsConfig.customScripts ?? [], ...scripts.map((entry) => ({ src: `/${entry.path.split("/").map(encodeURIComponent).join("/")}`, strategy: "afterInteractive" }))]
8271
+ };
8272
+ warnings.push({
8273
+ code: "unsupported-config",
8274
+ message: `${scripts.length} script(s) from the content directory (${scripts.map((entry) => entry.path).join(", ")}) now load on every page via docs.json customScripts, as they did on Mintlify. They may target Mintlify's page structure; review them.`
8275
+ });
8276
+ }
8277
+ for (const entry of copiedSite.filter((item) => item.kind === "style")) {
8278
+ warnings.push({
8279
+ code: "unsupported-config",
8280
+ 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.`,
8281
+ source: entry.path
8282
+ });
8283
+ }
8284
+ for (const font of mintlifyFonts) {
8285
+ const copiedPath = font.path ? fontAssetByRelative.get(font.path) : void 0;
8286
+ const copied = copiedPath !== void 0 && assets.some((asset) => asset.path === copiedPath);
8287
+ const detail = font.remote ? `its source ${font.source} is a remote URL and was not downloaded` : !font.path ? `its source ${font.source} is not a safe local .woff/.woff2/.ttf/.otf path inside the docs directory and was not imported` : !copiedPath ? `its source file ${font.source} was not found in the docs directory` : copied ? `its file was copied to public/${copiedPath}` : null;
8288
+ if (detail === null) continue;
8289
+ warnings.push({
8290
+ code: "unsupported-config",
8291
+ message: `Self-hosted font "${font.family}" is not applied: Thally's fonts setting loads Google Fonts by family only; ${detail}. The family name was still mapped, which loads a Google font of that name if one exists.`
8292
+ });
8293
+ }
8294
+ }
8295
+ if (overBudgetAssets.length > 0) {
8296
+ warnings.push({
8297
+ code: "limit-reached",
8298
+ 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.`
8299
+ });
8300
+ }
8301
+ if (lfsPointerAssets.length > 0) {
8302
+ warnings.push({
8303
+ code: "unsupported-config",
8304
+ 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.`
8305
+ });
8306
+ }
8307
+ if (!discoveryBudgetApplied && files.length >= sourceBudget) {
8308
+ 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.` });
7525
8309
  }
7526
8310
  if (platform === "docusaurus") {
7527
8311
  const projected = projectDocusaurusNavigation({
@@ -7608,8 +8392,8 @@ function migrateRepository(options) {
7608
8392
  }
7609
8393
  specFilenameSources.set(filename, spec.absolutePath);
7610
8394
  const specContent = readFileSync5(spec.absolutePath);
7611
- if (!assets.some((asset) => asset.path === filename)) {
7612
- assets.push({ path: filename, content: specContent });
8395
+ if (!assets.some((asset) => asset.path === specAssetPath(filename))) {
8396
+ assets.push(specAsset(filename, specContent));
7613
8397
  }
7614
8398
  resolvedSpecs.push({ filename, tabLabel: section.tabLabel, content: specContent, routeSegments: section.routeSegments ?? [] });
7615
8399
  continue;
@@ -7648,11 +8432,19 @@ function migrateRepository(options) {
7648
8432
  rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings);
7649
8433
  }
7650
8434
  } else if (platform === "mintlify") {
8435
+ const warnSharedSpec = (path) => {
8436
+ warnings.push({
8437
+ code: "gated-page",
8438
+ message: `OpenAPI spec ${path} is shared with access-restricted pages and is published; it may describe restricted endpoints. Review it before publishing.`,
8439
+ source: path
8440
+ });
8441
+ };
7651
8442
  const resolvedSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs);
7652
8443
  for (const spec of resolvedSpecs) {
7653
- if (!assets.some((asset) => asset.path === spec.filename)) {
7654
- assets.push({ path: spec.filename, content: spec.content });
8444
+ if (!assets.some((asset) => asset.path === specAssetPath(spec.filename))) {
8445
+ assets.push(specAsset(spec.filename, spec.content));
7655
8446
  }
8447
+ if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath);
7656
8448
  }
7657
8449
  if (resolvedSpecs.length > 0) {
7658
8450
  docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs, warnings);
@@ -7665,10 +8457,17 @@ function migrateRepository(options) {
7665
8457
  const fallback = findOpenApi(files);
7666
8458
  if (fallback) {
7667
8459
  const filename = basename3(fallback.relativePath);
7668
- if (!assets.some((asset) => asset.path === filename)) {
7669
- assets.push({ path: filename, content: readFileSync5(fallback.absolutePath) });
8460
+ const matches = (ref) => specRefMatches(ref, fallback.relativePath, filename, true);
8461
+ if (withheldSpecRefs.some(matches) && !publishedSpecRefs.some(matches)) {
8462
+ quarantinedFiles.push({ path: `${QUARANTINE_DIRECTORY}/assets/${specAssetPath(filename)}`, content: readFileSync5(fallback.absolutePath) });
8463
+ withheldAssetCount++;
8464
+ } else {
8465
+ if (!assets.some((asset) => asset.path === specAssetPath(filename))) {
8466
+ assets.push(specAsset(filename, readFileSync5(fallback.absolutePath)));
8467
+ }
8468
+ docsConfig = injectOpenApiSpecs(docsConfig, [{ filename }]);
8469
+ if (withheldSpecRefs.some(matches)) warnSharedSpec(fallback.relativePath);
7670
8470
  }
7671
- docsConfig = injectOpenApiSpecs(docsConfig, [{ filename }]);
7672
8471
  }
7673
8472
  }
7674
8473
  }
@@ -7979,10 +8778,9 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
7979
8778
  docsConfig = { ...docsConfig, navbar: { ...docsConfig.navbar, logo: null } };
7980
8779
  }
7981
8780
  if (mintlifyConfig) {
7982
- const appearance = mintlifyConfig.appearance && typeof mintlifyConfig.appearance === "object" ? mintlifyConfig.appearance : null;
7983
- const defaultMode = appearance?.default;
7984
- if (defaultMode === "light" || defaultMode === "dark" || defaultMode === "system") {
7985
- docsConfig = { ...docsConfig, appearance: { ...docsConfig.appearance, default: defaultMode } };
8781
+ const appearance = mintlifyAppearance(mintlifyConfig, warnings);
8782
+ if (Object.keys(appearance).length > 0) {
8783
+ docsConfig = { ...docsConfig, appearance: { ...docsConfig.appearance, ...appearance } };
7986
8784
  }
7987
8785
  const background = mintlifyConfig.background && typeof mintlifyConfig.background === "object" ? mintlifyConfig.background : null;
7988
8786
  if (background) {
@@ -8015,6 +8813,25 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
8015
8813
  code: "unsupported-config",
8016
8814
  message: `Brand asset(s) were referenced but not imported: ${[...new Set(missingBrand)].join(", ")}. Add these files under public/ or update docs.json.`
8017
8815
  });
8816
+ if (platform === "mintlify") {
8817
+ docsConfig = { ...docsConfig, markdown: { enabled: true } };
8818
+ if (quarantinedPageCount > 0 || withheldAssetCount > 0 || unreferencedAssetCount > 0) {
8819
+ const assetNote = withheldAssetCount > 0 ? `${withheldAssetCount} file(s) used only by access-restricted pages were kept out of public/ and saved under ${QUARANTINE_DIRECTORY}/assets/. ` : "";
8820
+ const unreferencedReason = [
8821
+ ...hasWithheldContent ? ["this site has access-restricted content"] : [],
8822
+ ...pagesNotClassified ? ["pages were dropped by the file limit (or the file scan stopped at its cap) and could not all be checked for access restrictions"] : []
8823
+ ].join(" and ");
8824
+ const unreferencedNote = unreferencedAssetCount > 0 ? `${unreferencedAssetCount} unreferenced asset(s) were kept out of public/ because ${unreferencedReason}; review ${QUARANTINE_DIRECTORY}/assets/ and copy any that published pages need. ` : "";
8825
+ warnings.push({
8826
+ code: "gated-page",
8827
+ message: (quarantinedPageCount > 0 ? `${quarantinedPageCount} access-restricted page(s) were withheld from the published site and saved under ${QUARANTINE_DIRECTORY}/ (local only: git-ignored, never served or deployed). ` : "") + assetNote + unreferencedNote + "Assets that published pages also use are still copied to public/. Review them before deciding how to publish or protect that content."
8828
+ });
8829
+ }
8830
+ warnings.push({
8831
+ code: "gated-page",
8832
+ message: (sawPublicTrue ? "Some pages set `public: true`, which means the source site used Mintlify authentication and every page WITHOUT it was private. " : "") + "Access control set in the Mintlify dashboard is not visible in the repository. Check the source site's dashboard access settings before publishing: Thally will publish ALL imported pages publicly. Confirm nothing here was meant to stay private before deploying."
8833
+ });
8834
+ }
8018
8835
  if (fernRawConfig?.logo && !logoLight && !logo.light) warnings.push({
8019
8836
  code: "unsupported-config",
8020
8837
  message: "Fern supplied a logo through its global theme without a local asset; add a logo path to docs.json after import."
@@ -8027,6 +8844,8 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
8027
8844
  assets,
8028
8845
  ...remoteApiSpecs.length > 0 ? { remoteApiSpecs } : {},
8029
8846
  ...componentMigrator ? { componentFiles: componentMigrator.files() } : {},
8847
+ ...quarantinedFiles.length > 0 ? { quarantinedFiles } : {},
8848
+ ...droppedGatedFiles.length > 0 ? { droppedGatedPages: droppedGatedFiles.length } : {},
8030
8849
  docsConfig,
8031
8850
  ...mintlifyConfig ? {
8032
8851
  site: {
@@ -8157,8 +8976,9 @@ function renderMigrationFiles(bundle, options = {}) {
8157
8976
  path: `src/content/${page.id}.mdx`,
8158
8977
  content: renderPage(bundle, page)
8159
8978
  })),
8160
- ...bundle.assets.map((asset) => ({ path: `public/${asset.path}`, content: asset.content })),
8979
+ ...bundle.assets.map((asset) => ({ path: asset.projectRelative ? asset.path : `public/${asset.path}`, content: asset.content })),
8161
8980
  ...(bundle.componentFiles ?? []).flatMap((file) => file.path === "src/mdx/custom-components.tsx" && options.existingComponentRegistry !== void 0 && typeof file.content === "string" ? mergeComponentRegistry(options.existingComponentRegistry, file.content) : [file]),
8981
+ ...bundle.quarantinedFiles ?? [],
8162
8982
  { path: "docs.json", content: `${JSON.stringify(config, null, 2)}
8163
8983
  ` }
8164
8984
  ];
@@ -9899,10 +10719,10 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
9899
10719
  const body = await downloadSpec(reference.url, fetcher);
9900
10720
  const operations = parseOpenApi(body, extension);
9901
10721
  const filename = `openapi-${createHash2("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
9902
- assets.push({ path: filename, content: body });
10722
+ assets.push({ path: `openapi/${filename}`, content: body, projectRelative: true });
9903
10723
  const tab = reference.tabLabel ? tabs.find((candidate) => candidate.tab === reference.tabLabel) : tabs.find((candidate) => candidate.tab.toLowerCase().includes("api"));
9904
- if (tab) tab.api = { source: `/${filename}` };
9905
- else tabs.push({ tab: reference.tabLabel ?? "API Reference", api: { source: `/${filename}` } });
10724
+ if (tab) tab.api = { source: `openapi/${filename}` };
10725
+ else insertApiTab(tabs, { tab: reference.tabLabel ?? "API Reference", ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, api: { source: `openapi/${filename}` } }, reference.parentTab);
9906
10726
  const routes = /* @__PURE__ */ new Map();
9907
10727
  for (const operation of operations) {
9908
10728
  const key = `${slug(operation.tag)}/${slug(operation.summary)}`;