@thallylabs/migrate 0.2.33 → 0.2.35

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
@@ -230,8 +230,11 @@ var JS_GLOBALS = /* @__PURE__ */ new Set([
230
230
  "BroadcastChannel"
231
231
  ]);
232
232
  var IMPLICIT_MODULE_NAMES = /* @__PURE__ */ new Set(["React", "MintlifyComponents"]);
233
- function parse(source) {
234
- return ts.createSourceFile("inline.tsx", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
233
+ function scriptKindFor(fileName) {
234
+ return /\.[cm]?ts$/.test(fileName) ? ts.ScriptKind.TS : ts.ScriptKind.TSX;
235
+ }
236
+ function parse(source, fileName = "inline.tsx") {
237
+ return ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, scriptKindFor(fileName));
235
238
  }
236
239
  function bindingNames(name, into) {
237
240
  if (ts.isIdentifier(name)) into.add(name.text);
@@ -240,20 +243,44 @@ function bindingNames(name, into) {
240
243
  function isFunctionLike(node) {
241
244
  return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node);
242
245
  }
246
+ function hoistedVarNames(root, into) {
247
+ const collect = (node) => {
248
+ if (isFunctionLike(node) || ts.isClassLike(node) || ts.isModuleDeclaration(node)) return;
249
+ if (ts.isVariableDeclaration(node) && !ts.isCatchClause(node.parent) && !(ts.getCombinedNodeFlags(node) & ts.NodeFlags.BlockScoped)) bindingNames(node.name, into);
250
+ ts.forEachChild(node, collect);
251
+ };
252
+ collect(root);
253
+ }
243
254
  function functionScope(fn) {
244
255
  const scope = /* @__PURE__ */ new Set();
245
256
  if ((ts.isFunctionExpression(fn) || ts.isFunctionDeclaration(fn)) && fn.name) scope.add(fn.name.text);
246
257
  for (const parameter of fn.parameters) bindingNames(parameter.name, scope);
247
- function collect(node) {
248
- if (ts.isVariableDeclaration(node)) bindingNames(node.name, scope);
249
- if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) && node.name) scope.add(node.name.text);
250
- if (isFunctionLike(node)) return;
251
- ts.forEachChild(node, collect);
252
- }
253
- if (fn.body) ts.forEachChild(fn.body, collect);
254
- if (fn.body && !ts.isBlock(fn.body)) collect(fn.body);
255
258
  return scope;
256
259
  }
260
+ function lexicalNames(statements) {
261
+ const names = /* @__PURE__ */ new Set();
262
+ for (const statement of statements) {
263
+ if (ts.isVariableStatement(statement)) {
264
+ if (statement.declarationList.flags & ts.NodeFlags.BlockScoped) for (const declaration of statement.declarationList.declarations) bindingNames(declaration.name, names);
265
+ } else if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name) names.add(statement.name.text);
266
+ }
267
+ return names;
268
+ }
269
+ function ownScope(node) {
270
+ if (ts.isBlock(node)) return lexicalNames(node.statements);
271
+ if (ts.isCaseBlock(node)) return lexicalNames(node.clauses.flatMap((clause) => [...clause.statements]));
272
+ if ((ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node)) && node.initializer && ts.isVariableDeclarationList(node.initializer)) {
273
+ const names = /* @__PURE__ */ new Set();
274
+ if (node.initializer.flags & ts.NodeFlags.BlockScoped) for (const declaration of node.initializer.declarations) bindingNames(declaration.name, names);
275
+ return names;
276
+ }
277
+ if (ts.isCatchClause(node) && node.variableDeclaration) {
278
+ const names = /* @__PURE__ */ new Set();
279
+ bindingNames(node.variableDeclaration.name, names);
280
+ return names;
281
+ }
282
+ return void 0;
283
+ }
257
284
  function collectRefs(root) {
258
285
  const refs = [];
259
286
  const assigned = [];
@@ -268,13 +295,22 @@ function collectRefs(root) {
268
295
  function walkNode(node, scopes, inAsync) {
269
296
  if (ts.isTypeNode(node)) return;
270
297
  if (isFunctionLike(node)) {
271
- const nextScopes = [...scopes, functionScope(node)];
298
+ const parameterScopes = [...scopes, functionScope(node)];
272
299
  const async = !!node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword);
273
300
  for (const parameter of node.parameters) {
274
- if (!ts.isIdentifier(parameter.name)) walkNode(parameter.name, nextScopes, async);
275
- if (parameter.initializer) walkNode(parameter.initializer, nextScopes, async);
301
+ if (!ts.isIdentifier(parameter.name)) walkNode(parameter.name, parameterScopes, async);
302
+ if (parameter.initializer) walkNode(parameter.initializer, parameterScopes, async);
303
+ }
304
+ if (node.body) {
305
+ const hoisted = /* @__PURE__ */ new Set();
306
+ hoistedVarNames(node.body, hoisted);
307
+ walkNode(node.body, [...parameterScopes, hoisted], async);
276
308
  }
277
- if (node.body) walkNode(node.body, nextScopes, async);
309
+ return;
310
+ }
311
+ const own2 = ownScope(node);
312
+ if (own2) {
313
+ ts.forEachChild(node, (child) => walkNode(child, [...scopes, own2], inAsync));
278
314
  return;
279
315
  }
280
316
  if (ts.isAwaitExpression(node) && !inAsync) awaitOutsideAsync = true;
@@ -651,6 +687,23 @@ function unboundTags(moduleSource, boundNames) {
651
687
  }
652
688
  return [...tags].sort();
653
689
  }
690
+ function unboundBuiltinTags(moduleSource, boundNames, fileName) {
691
+ const tags = /* @__PURE__ */ new Set();
692
+ const values = /* @__PURE__ */ new Set();
693
+ for (const statement of parse(moduleSource, fileName).statements) {
694
+ if (ts.isImportDeclaration(statement)) continue;
695
+ for (const { name, kind } of collectRefs(statement).refs) {
696
+ if (boundNames.has(name)) continue;
697
+ if (kind === "tag" || kind === "member") tags.add(name);
698
+ else values.add(name);
699
+ }
700
+ }
701
+ const isGlobal = (name) => JS_GLOBALS.has(name) || BROWSER_IDENTIFIERS.has(name) || SERVER_ONLY_IDENTIFIERS.has(name);
702
+ const builtinTags = [...tags].filter(isThallyBuiltinComponent);
703
+ const shadowed = builtinTags.filter((name) => JS_GLOBALS.has(name) && values.has(name)).sort();
704
+ const builtinValues = [...values].filter((name) => isThallyBuiltinComponent(name) && !isGlobal(name));
705
+ return { bind: [.../* @__PURE__ */ new Set([...builtinTags.filter((name) => !shadowed.includes(name)), ...builtinValues])].sort(), shadowed };
706
+ }
654
707
  var INLINE_SAFE_GLOBALS = /* @__PURE__ */ new Set([
655
708
  "undefined",
656
709
  "NaN",
@@ -719,12 +772,14 @@ function isSelfContainedFunction(node) {
719
772
  function pageScopeNames(esmSources) {
720
773
  const names = /* @__PURE__ */ new Set();
721
774
  for (const source of esmSources) {
722
- for (const statement of parse(source).statements) {
775
+ const sf = parse(source);
776
+ for (const statement of sf.statements) {
723
777
  for (const local of importLocals(statement.getText())) names.add(local);
724
778
  if (ts.isImportDeclaration(statement)) continue;
725
779
  const target2 = ts.isExportDeclaration(statement) || ts.isExportAssignment(statement) ? void 0 : statement;
726
780
  if (target2) for (const name of declaredNames(target2).names) names.add(name);
727
781
  }
782
+ hoistedVarNames(sf, names);
728
783
  }
729
784
  return names;
730
785
  }
@@ -2934,13 +2989,7 @@ function sanitizeCssModuleSelectors(css) {
2934
2989
  return { css: root.toString(), omitted };
2935
2990
  }
2936
2991
  function sourceFile(source, filename) {
2937
- return ts2.createSourceFile(
2938
- filename,
2939
- source,
2940
- ts2.ScriptTarget.Latest,
2941
- true,
2942
- /\.[cm]?ts$/.test(filename) ? ts2.ScriptKind.TS : ts2.ScriptKind.TSX
2943
- );
2992
+ return ts2.createSourceFile(filename, source, ts2.ScriptTarget.Latest, true, scriptKindFor(filename));
2944
2993
  }
2945
2994
  function walk(node, visitor) {
2946
2995
  visitor(node);
@@ -2997,7 +3046,7 @@ function needsClientBoundary(source) {
2997
3046
  visitNode(source);
2998
3047
  return needed;
2999
3048
  }
3000
- function implicitReactImports(source) {
3049
+ function implicitReactImports(source, onShadowed) {
3001
3050
  const bindings = /* @__PURE__ */ new Set();
3002
3051
  const references = /* @__PURE__ */ new Set();
3003
3052
  function bind(name) {
@@ -3015,7 +3064,9 @@ function implicitReactImports(source) {
3015
3064
  }
3016
3065
  if (ts2.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) bind(declaration.name);
3017
3066
  if ((ts2.isFunctionDeclaration(statement) || ts2.isClassDeclaration(statement)) && statement.name) bindings.add(statement.name.text);
3067
+ if ((ts2.isEnumDeclaration(statement) || ts2.isModuleDeclaration(statement) || ts2.isImportEqualsDeclaration(statement)) && ts2.isIdentifier(statement.name)) bindings.add(statement.name.text);
3018
3068
  }
3069
+ hoistedVarNames(source, bindings);
3019
3070
  function visit(node) {
3020
3071
  if (ts2.isIdentifier(node) && !(ts2.isPropertyAccessExpression(node.parent) && node.parent.name === node) && !(ts2.isPropertyAssignment(node.parent) && node.parent.name === node)) references.add(node.text);
3021
3072
  ts2.forEachChild(node, visit);
@@ -3023,26 +3074,50 @@ function implicitReactImports(source) {
3023
3074
  visit(source);
3024
3075
  const hooks = [...REACT_GLOBALS].filter((name) => references.has(name) && !bindings.has(name)).sort();
3025
3076
  const usesMintlifyComponents = references.has("MintlifyComponents") && !bindings.has("MintlifyComponents");
3077
+ const leafBound = /* @__PURE__ */ new Set();
3026
3078
  const implicitComponents = MINTLIFY_COMPONENT_MODULES.map(({ module, names }) => {
3027
- const needed = names.filter((name) => references.has(name) && !bindings.has(name));
3079
+ const needed = names.filter((name) => name !== "Note" && references.has(name) && !bindings.has(name));
3080
+ for (const name of needed) leafBound.add(name);
3028
3081
  return needed.length ? `import { ${needed.join(", ")} } from '${module}';` : "";
3029
3082
  }).filter(Boolean);
3083
+ const { bind: builtinTags, shadowed } = unboundBuiltinTags(source.text, bindings, source.fileName);
3084
+ const registryNames = builtinTags.filter((name) => !leafBound.has(name));
3085
+ if (shadowed.length) onShadowed?.(shadowed);
3030
3086
  return [
3031
3087
  references.has("React") && !bindings.has("React") ? "import * as React from 'react';" : "",
3032
3088
  hooks.length ? `import { ${hooks.join(", ")} } from 'react';` : "",
3033
3089
  ...implicitComponents,
3090
+ registryNames.length ? builtinRegistryBindings(registryNames, identifierNames(source)) : "",
3034
3091
  usesMintlifyComponents ? mintlifyComponentsShim() : ""
3035
3092
  ].filter(Boolean).join("\n");
3036
3093
  }
3037
3094
  function resolveInlineBuiltinReferences(source, moduleNames) {
3038
3095
  const unresolved = unboundTags(source, moduleNames);
3039
3096
  if (unresolved.length === 0) return source;
3097
+ return [builtinRegistryBindings(unresolved, identifierNames(sourceFile(source, "inline.jsx"))), source].join("\n");
3098
+ }
3099
+ function identifierNames(root) {
3100
+ const names = /* @__PURE__ */ new Set();
3101
+ const visit = (node) => {
3102
+ if (ts2.isIdentifier(node)) names.add(node.text);
3103
+ ts2.forEachChild(node, visit);
3104
+ };
3105
+ visit(root);
3106
+ return names;
3107
+ }
3108
+ function builtinRegistryBindings(names, used) {
3109
+ const registry = "builtinMdxComponents";
3110
+ let alias = registry;
3111
+ for (let suffix = 2; used.has(alias); suffix++) alias = `${registry}${suffix}`;
3040
3112
  return [
3041
- "import { builtinMdxComponents } from '@/components/mdx/builtin-components';",
3042
- `const { ${unresolved.join(", ")} } = builtinMdxComponents;`,
3043
- source
3113
+ `import { ${alias === registry ? registry : `${registry} as ${alias}`} } from '@/components/mdx/builtin-components';`,
3114
+ `const { ${names.join(", ")} } = ${alias};`
3044
3115
  ].join("\n");
3045
3116
  }
3117
+ function shadowedBuiltinMessage(file, names) {
3118
+ const tags = names.map((name) => `<${name}>`).join(", ");
3119
+ return `${file} renders ${tags} as a Thally built-in component but also uses the JavaScript global of the same name, so the tag was left unbound. Rename the component or the global use in the source.`;
3120
+ }
3046
3121
  function imports(statement) {
3047
3122
  if (!ts2.isStringLiteral(statement.moduleSpecifier) || !statement.importClause) return [];
3048
3123
  const clause = statement.importClause;
@@ -3269,6 +3344,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
3269
3344
  function copyGraph(entry) {
3270
3345
  const staged = /* @__PURE__ */ new Map();
3271
3346
  const stagedWarnings = [];
3347
+ const stagedShadowed = [];
3272
3348
  const modules = /* @__PURE__ */ new Map();
3273
3349
  let stagedBytes = 0;
3274
3350
  function stageShim(filename, content) {
@@ -3373,7 +3449,8 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
3373
3449
  const prologue = `// @ts-nocheck
3374
3450
  ${client ? "'use client';\n" : ""}
3375
3451
  `;
3376
- return shebangMatch ? shebangMatch[0] + prologue + implicitReactImports(ast) + "\n" + replaced.slice(shebangMatch[0].length) : prologue + implicitReactImports(ast) + "\n" + replaced;
3452
+ const implicit = implicitReactImports(ast, (names) => stagedShadowed.push({ path, names }));
3453
+ return shebangMatch ? shebangMatch[0] + prologue + implicit + "\n" + replaced.slice(shebangMatch[0].length) : prologue + implicit + "\n" + replaced;
3377
3454
  }
3378
3455
  });
3379
3456
  }
@@ -3391,6 +3468,7 @@ ${client ? "'use client';\n" : ""}
3391
3468
  }
3392
3469
  for (const [path, module] of modules) staged.set(path, { path, content: module.render(clientModules.has(path)) });
3393
3470
  for (const [path, file] of staged) copied.set(path, file);
3471
+ for (const { path, names } of stagedShadowed) warn(shadowedBuiltinMessage(relative2(root, path).replace(/\\/g, "/"), names), path);
3394
3472
  for (const { path, omitted } of stagedWarnings) {
3395
3473
  warn(`Site-authored CSS module ${relative2(root, path).replace(/\\/g, "/")} has ${omitted} global-only selector branch(es) that cannot be copied into Thally CSS Modules; those styles were omitted.`, path);
3396
3474
  }
@@ -3845,8 +3923,12 @@ ${inlineSource}` });
3845
3923
  }`),
3846
3924
  ""
3847
3925
  ].join("\n");
3926
+ const implicit = implicitReactImports(
3927
+ sourceFile(inlineSource, "inline.jsx"),
3928
+ (names) => warn(shadowedBuiltinMessage(relative2(root, currentFile).replace(/\\/g, "/"), names), currentFile)
3929
+ );
3848
3930
  copied.set(path, { path, content: `'use client';
3849
- ${implicitReactImports(sourceFile(inlineSource, "inline.jsx"))}
3931
+ ${implicit}
3850
3932
  ${inlineSource}` });
3851
3933
  copiedBytes += Buffer.byteLength(inlineSource);
3852
3934
  for (const { node, name } of extracted) {
@@ -4442,39 +4524,106 @@ function specSource(value) {
4442
4524
  }
4443
4525
  function listedOperations(container) {
4444
4526
  const operations = [];
4445
- const visit = (entry, root) => {
4527
+ const visit = (entry, depth) => {
4446
4528
  if (typeof entry === "string") {
4447
4529
  if (OPENAPI_OPERATION_REF.test(entry)) operations.push(entry.trim());
4448
4530
  return;
4449
4531
  }
4450
4532
  const object2 = objectValue2(entry);
4451
- if (!object2 || !root && (specSource(object2.openapi) || specSource(object2.asyncapi))) return;
4533
+ if (!object2 || depth > SWEEP_DEPTH_LIMIT || depth > 0 && (specSource(object2.openapi) || specSource(object2.asyncapi))) return;
4452
4534
  for (const key of ["groups", "pages"]) {
4453
- if (Array.isArray(object2[key])) object2[key].forEach((child) => visit(child, false));
4535
+ if (Array.isArray(object2[key])) object2[key].forEach((child) => visit(child, depth + 1));
4454
4536
  }
4455
4537
  };
4456
- visit(container, true);
4538
+ visit(container, 0);
4457
4539
  return operations;
4458
4540
  }
4541
+ var bindsSpec = (node) => Boolean(specSource(node.openapi) || specSource(node.asyncapi));
4542
+ var hasGroupShape = (node) => "group" in node || Array.isArray(node.pages);
4543
+ var API_GROUPS_MAX_DEPTH = 32;
4544
+ var operationKey = (entry) => entry.trim().replace(/^(\S+)\s+/, "$1 ");
4545
+ function operationGroups(container, maxLevel = API_GROUPS_MAX_DEPTH) {
4546
+ const entries = (node, level) => ["groups", "pages"].flatMap((key) => (Array.isArray(node[key]) ? node[key] : []).flatMap((entry) => {
4547
+ if (typeof entry === "string") return OPENAPI_OPERATION_REF.test(entry) ? [operationKey(entry)] : [];
4548
+ const object2 = objectValue2(entry);
4549
+ const group = object2 && !bindsSpec(object2) ? groupOf(object2, level + 1) : void 0;
4550
+ return group ? [group] : [];
4551
+ }));
4552
+ const groupOf = (node, level) => {
4553
+ if (level > maxLevel || node.hidden === true || navigationGateReason(node)) return void 0;
4554
+ if (!hasGroupShape(node)) return void 0;
4555
+ const pages = entries(node, level);
4556
+ const icon = iconName(node.icon);
4557
+ return pages.length > 0 ? { group: labelFor(node, "Documentation"), ...icon ? { icon } : {}, pages } : void 0;
4558
+ };
4559
+ if (typeof container.group === "string") {
4560
+ const group = groupOf(container, 1);
4561
+ return group ? [group] : [];
4562
+ }
4563
+ const label = labelFor(container, "API reference");
4564
+ const groups = [];
4565
+ let run;
4566
+ for (const entry of entries(container, 0)) {
4567
+ if (typeof entry !== "string") {
4568
+ groups.push(entry);
4569
+ run = void 0;
4570
+ } else if (run) {
4571
+ run.push(entry);
4572
+ } else {
4573
+ run = [entry];
4574
+ groups.push({ group: label, pages: run });
4575
+ }
4576
+ }
4577
+ return groups;
4578
+ }
4579
+ var groupOperationKeys = (groups) => groups.flatMap(({ pages }) => pages.flatMap((page) => typeof page === "string" ? [page] : groupOperationKeys([page])));
4580
+ function mergeApiGroups(existing, incoming) {
4581
+ const identity = (key) => key.replace(/^(\S+)/, (method) => method.toLowerCase());
4582
+ const seen = new Set(groupOperationKeys(existing ?? []).map(identity));
4583
+ const unseen = (groups) => groups.flatMap((group) => {
4584
+ const pages = group.pages.flatMap((page) => {
4585
+ if (typeof page !== "string") return unseen([page]);
4586
+ if (seen.has(identity(page))) return [];
4587
+ seen.add(identity(page));
4588
+ return [page];
4589
+ });
4590
+ return pages.length > 0 ? [{ ...group, pages }] : [];
4591
+ });
4592
+ const merged = [...existing ?? [], ...unseen(incoming ?? [])];
4593
+ return merged.length > 0 ? merged : void 0;
4594
+ }
4459
4595
  var hasNavigationChildren = (node) => ["groups", "pages"].some((key) => Array.isArray(node[key]) && node[key].length > 0);
4460
4596
  function mintlifyNavigationApiReferences(config) {
4461
4597
  const navigation = objectValue2(config.navigation) ?? config;
4462
4598
  const references = [];
4599
+ const defaultLanguage = pickDefaultLanguage(
4600
+ (Array.isArray(navigation.languages) ? navigation.languages : []).flatMap((value) => {
4601
+ const language = objectValue2(value);
4602
+ return language && withLanguageCode(language) ? [language] : [];
4603
+ })
4604
+ );
4463
4605
  const containerKeys = ["tabs", "anchors", "products", "dropdowns", "versions", "menus", "languages"];
4464
4606
  const nestedKeys = ["groups", "pages"];
4465
- function collect(node, tabLabel, sibling) {
4607
+ function collect(node, tabLabel, gated, otherLanguage, hidden, sibling) {
4466
4608
  for (const kind of ["openapi", "asyncapi"]) {
4467
4609
  const value = specSource(node[kind]);
4468
4610
  const directory = objectValue2(node[kind])?.directory;
4469
4611
  if (value) {
4470
- const operations = typeof directory === "string" && directory.trim() ? listedOperations(node) : [];
4612
+ const listed = listedOperations(node);
4613
+ const operations = typeof directory === "string" && directory.trim() ? listed : [];
4614
+ const groups = gated || otherLanguage || hidden ? [] : operationGroups(node);
4615
+ const tooDeepOnly = groups.length === 0 && listed.length > 0 && !gated && !hidden && groupOperationKeys(operationGroups(node, SWEEP_DEPTH_LIMIT)).length === listed.length;
4616
+ const operationsWithheld = !otherLanguage && (hidden || groups.length === 0 && listed.length > 0 && !tooDeepOnly);
4471
4617
  references.push({
4472
4618
  value,
4473
4619
  kind,
4474
4620
  tabLabel,
4475
4621
  ...sibling,
4476
4622
  ...typeof directory === "string" && directory.trim() ? { directory: directory.trim() } : {},
4477
- ...operations.length > 0 ? { operations } : {}
4623
+ ...operations.length > 0 ? { operations } : {},
4624
+ ...groups.length > 0 ? { groups } : {},
4625
+ ...operationsWithheld ? { operationsWithheld: true } : {},
4626
+ ...otherLanguage ? { otherLanguage: true } : {}
4478
4627
  });
4479
4628
  }
4480
4629
  }
@@ -4486,7 +4635,7 @@ function mintlifyNavigationApiReferences(config) {
4486
4635
  siblingSources.set(candidate, source);
4487
4636
  return candidate;
4488
4637
  }
4489
- function visitChildren(rawNode, tabLabel) {
4638
+ function visitChildren(rawNode, tabLabel, gated, otherLanguage, hidden) {
4490
4639
  const node = withFlattenedProductGroups(rawNode);
4491
4640
  for (const key of containerKeys) {
4492
4641
  const entries = node[key];
@@ -4494,7 +4643,7 @@ function mintlifyNavigationApiReferences(config) {
4494
4643
  for (const entry of entries) {
4495
4644
  const object2 = objectValue2(entry);
4496
4645
  if (!object2) continue;
4497
- visit(object2, labelFor(object2, tabLabel ?? "Documentation"));
4646
+ visit(object2, labelFor(object2, tabLabel ?? "Documentation"), gated, otherLanguage || key === "languages" && rawNode === navigation && object2 !== defaultLanguage, hidden);
4498
4647
  }
4499
4648
  }
4500
4649
  for (const key of nestedKeys) {
@@ -4503,7 +4652,7 @@ function mintlifyNavigationApiReferences(config) {
4503
4652
  for (const entry of entries) {
4504
4653
  if (typeof entry === "string") continue;
4505
4654
  const object2 = objectValue2(entry);
4506
- if (object2) visit(object2, tabLabel);
4655
+ if (object2) visit(object2, tabLabel, gated, otherLanguage, hidden || object2.hidden === true);
4507
4656
  }
4508
4657
  }
4509
4658
  if (Array.isArray(node.menu)) {
@@ -4515,24 +4664,26 @@ function mintlifyNavigationApiReferences(config) {
4515
4664
  const apiItems = items.filter((item) => specSource(item.openapi) || specSource(item.asyncapi));
4516
4665
  const soleContent = apiItems.length === 1 && !hasNavigationChildren(node) && !items.some(hasNavigationChildren);
4517
4666
  items.forEach((item, index) => {
4518
- if (soleContent && apiItems[0] === item) collect(item, tab);
4667
+ const itemGated = gated || Boolean(navigationGateReason(item));
4668
+ if (soleContent && apiItems[0] === item) collect(item, tab, itemGated, otherLanguage, hidden);
4519
4669
  else if (apiItems.includes(item)) {
4520
4670
  const label = uniqueSiblingLabel(
4521
4671
  `${tab}: ${labelFor(item, `Menu item ${index + 1}`)}`,
4522
4672
  `${specSource(item.openapi) ?? ""}|${specSource(item.asyncapi) ?? ""}`
4523
4673
  );
4524
4674
  const icon = iconName(item.icon);
4525
- collect(item, label, { parentTab: tab, ...icon ? { icon } : {}, ...item.hidden === true ? { hidden: true } : {} });
4675
+ collect(item, label, itemGated, otherLanguage, hidden, { parentTab: tab, ...icon ? { icon } : {}, ...item.hidden === true ? { hidden: true } : {} });
4526
4676
  }
4527
- visitChildren(item, tabLabel);
4677
+ visitChildren(item, tabLabel, itemGated, otherLanguage, hidden);
4528
4678
  });
4529
4679
  }
4530
4680
  }
4531
- function visit(node, tabLabel) {
4532
- collect(node, tabLabel);
4533
- visitChildren(node, tabLabel);
4681
+ function visit(node, tabLabel, outerGated, otherLanguage, hidden) {
4682
+ const gated = outerGated || Boolean(navigationGateReason(node));
4683
+ collect(node, tabLabel, gated, otherLanguage, hidden);
4684
+ visitChildren(node, tabLabel, gated, otherLanguage, hidden);
4534
4685
  }
4535
- visit(navigation, void 0);
4686
+ visit(navigation, void 0, false, false, false);
4536
4687
  return references;
4537
4688
  }
4538
4689
  function insertApiTab(tabs, tab, parentTab) {
@@ -4555,6 +4706,17 @@ function normalizePageRef(value, pathPrefix = "") {
4555
4706
  ref = ref.replace(/\.(?:mdx?|rst|txt)$/i, "");
4556
4707
  return pageIdFromReference(ref, true);
4557
4708
  }
4709
+ var UNBOUND_OPERATIONS = { bound: false, gated: false, hidden: false, level: 0 };
4710
+ function descendOperationScope(scope, node, nested) {
4711
+ const binds = bindsSpec(node);
4712
+ return {
4713
+ bound: !nested && scope.bound || binds,
4714
+ gated: scope.gated || Boolean(navigationGateReason(node)),
4715
+ hidden: scope.hidden || !nested && node.hidden === true,
4716
+ // A container that binds a spec roots a tree of its own, where it is the first level when it is a group (as `operationGroups` reads it).
4717
+ level: binds ? typeof node.group === "string" ? 1 : 0 : !nested && scope.bound && hasGroupShape(node) ? scope.level + 1 : scope.level
4718
+ };
4719
+ }
4558
4720
  var SWEEP_DEPTH_LIMIT = 256;
4559
4721
  function collectGatedSourceRefs(node, gate, out, limits, depth = 0) {
4560
4722
  if (depth > SWEEP_DEPTH_LIMIT) {
@@ -4588,15 +4750,24 @@ function collectReferenceAccess(node, access, pathPrefix, out, depth = 0) {
4588
4750
  }
4589
4751
  for (const value of Object.values(object2)) collectReferenceAccess(value, nested, pathPrefix, out, depth + 1);
4590
4752
  }
4591
- function withContainerAccess(object2, context, walk2) {
4753
+ function withContainerAccess(object2, context, walk2, nested = false) {
4592
4754
  const outer = context.access;
4593
4755
  context.access = descendNavigationAccess(outer, navigationAccess(object2));
4594
4756
  try {
4595
- return walk2();
4757
+ return withOperationScope(object2, context, nested, walk2);
4596
4758
  } finally {
4597
4759
  context.access = outer;
4598
4760
  }
4599
4761
  }
4762
+ function withOperationScope(object2, context, nested, walk2) {
4763
+ const outer = context.operations;
4764
+ context.operations = descendOperationScope(outer, object2, nested);
4765
+ try {
4766
+ return walk2();
4767
+ } finally {
4768
+ context.operations = outer;
4769
+ }
4770
+ }
4600
4771
  function warnOnce(context, key, message) {
4601
4772
  if (context.warningKeys.has(key)) return;
4602
4773
  context.warningKeys.add(key);
@@ -4634,11 +4805,32 @@ var OPENAPI_OPERATION_REF = /^(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+\S/;
4634
4805
  function convertPage(value, context) {
4635
4806
  if (typeof value === "string") {
4636
4807
  if (OPENAPI_OPERATION_REF.test(value)) {
4637
- warnOnce(
4638
- context,
4639
- "openapi-operation-list",
4640
- 'One or more navigation groups hand-pick or reorder individual OpenAPI operations (e.g. "GET /users"); Thally always renders every operation of a bound spec and has no manual-selection equivalent, so that list was dropped. The operations themselves are still available through the bound API reference.'
4641
- );
4808
+ const { bound, gated, hidden, level } = context.operations;
4809
+ if (!context.operationGroups || !bound) {
4810
+ warnOnce(
4811
+ context,
4812
+ "openapi-operation-list",
4813
+ 'Navigation lists individual OpenAPI operations (e.g. "GET /users") that were not added to the API navigation: an operation list is carried over only from a tab or group that names an openapi spec in a migrated docs.json. The operations themselves still have pages in the API reference.'
4814
+ );
4815
+ } else if (gated) {
4816
+ warnOnce(
4817
+ context,
4818
+ "openapi-operation-list-gated",
4819
+ 'Navigation lists individual OpenAPI operations (e.g. "GET /users") under access-restricted groups, so they were not added to the API navigation. The operations themselves still have pages in the API reference.'
4820
+ );
4821
+ } else if (context.otherLanguage) {
4822
+ warnOnce(
4823
+ context,
4824
+ "openapi-operation-list-language",
4825
+ 'Navigation lists individual OpenAPI operations (e.g. "GET /users") under a language other than the default, so they were not added to the API navigation: the API tab follows the default language. The operations themselves still have pages in the API reference.'
4826
+ );
4827
+ } else if (!hidden && level > API_GROUPS_MAX_DEPTH) {
4828
+ warnOnce(
4829
+ context,
4830
+ "openapi-operation-list-depth",
4831
+ `Navigation lists individual OpenAPI operations (e.g. "GET /users") in groups nested more than ${API_GROUPS_MAX_DEPTH} levels deep, so they were not added to the API navigation: the API navigation follows no deeper groups. The operations themselves still have pages in the API reference.`
4832
+ );
4833
+ }
4642
4834
  return null;
4643
4835
  }
4644
4836
  return registerReference(value, context);
@@ -4749,10 +4941,10 @@ function convertMenuItems(items, context) {
4749
4941
  `Mintlify menu item "${label}" has an openapi/asyncapi value without a "source", so its API reference was not migrated.`
4750
4942
  );
4751
4943
  }
4752
- const children = convertNavigationValues([
4944
+ const children = withOperationScope(item, context, true, () => convertNavigationValues([
4753
4945
  ...Array.isArray(item.groups) ? item.groups : [],
4754
4946
  ...Array.isArray(item.pages) ? item.pages : []
4755
- ], context);
4947
+ ], context));
4756
4948
  if (typeof item.href === "string" && item.href) {
4757
4949
  warnOnce(
4758
4950
  context,
@@ -4812,7 +5004,7 @@ function convertContainerToTabs(containerValue, context, fallbackTab, trace, dep
4812
5004
  ...object2.hidden === true ? { hidden: true } : {}
4813
5005
  }];
4814
5006
  }
4815
- const nested = withContainerAccess(object2, context, () => convertContainerToTabs(object2, context, tab, trace, depth + 1));
5007
+ const nested = withContainerAccess(object2, context, () => convertContainerToTabs(object2, context, tab, trace, depth + 1), true);
4816
5008
  if (nested.length > 0) {
4817
5009
  if (nested.length === 1) {
4818
5010
  return [{
@@ -5036,6 +5228,8 @@ function projectedCompatibleConfig(config, warnings) {
5036
5228
  return {
5037
5229
  // Mintlify pages show a group eyebrow above the title and no breadcrumb trail.
5038
5230
  breadcrumbs: false,
5231
+ // `styling.eyebrows: "breadcrumbs"` shows the whole group path; "section" (the default) shows the nearest group.
5232
+ ...objectValue2(config.styling)?.eyebrows === "breadcrumbs" ? { eyebrow: "path" } : {},
5039
5233
  ...projectedTheme(config.theme) ? { theme: projectedTheme(config.theme) } : {},
5040
5234
  ...projectedContextual.length > 0 ? { contextual: { options: projectedContextual } } : {},
5041
5235
  ...projectedColorsValue ? { colors: projectedColorsValue } : {},
@@ -5181,6 +5375,16 @@ function mintlifyAllVersionPrefixes(config) {
5181
5375
  collect(navigation);
5182
5376
  return prefixes;
5183
5377
  }
5378
+ function withLanguageCode(language) {
5379
+ try {
5380
+ return { ...language, language: Intl.getCanonicalLocales(String(language.language ?? language.locale ?? "en"))[0] };
5381
+ } catch {
5382
+ return void 0;
5383
+ }
5384
+ }
5385
+ function pickDefaultLanguage(languages) {
5386
+ return languages.find((entry) => entry.default === true) ?? languages[0];
5387
+ }
5184
5388
  function projectMintlifyNavigation(config, options = {}) {
5185
5389
  const warnings = [];
5186
5390
  const references = [];
@@ -5191,6 +5395,8 @@ function projectMintlifyNavigation(config, options = {}) {
5191
5395
  const gatedNodes = /* @__PURE__ */ new Map();
5192
5396
  const navigation = objectValue2(config.navigation) ?? config;
5193
5397
  const rootAccess = descendNavigationAccess(OPEN_NAVIGATION_ACCESS, navigationAccess(navigation));
5398
+ const rootOperations = descendOperationScope(UNBOUND_OPERATIONS, navigation, true);
5399
+ const operationGroups2 = options.operationGroups === true;
5194
5400
  const limits = { tooDeep: false };
5195
5401
  collectGatedSourceRefs(navigation, void 0, gatedNodes, limits);
5196
5402
  if (limits.tooDeep) {
@@ -5202,20 +5408,16 @@ function projectMintlifyNavigation(config, options = {}) {
5202
5408
  const languages = Array.isArray(navigation.languages) ? navigation.languages.flatMap((value) => {
5203
5409
  const language = objectValue2(value);
5204
5410
  if (!language) return [];
5205
- try {
5206
- const code = Intl.getCanonicalLocales(String(language.language ?? language.locale ?? "en"))[0];
5207
- return [{ ...language, language: code }];
5208
- } catch {
5209
- warnings.push({ code: "unsupported-config", message: "A navigation language with an invalid locale code was skipped." });
5210
- return [];
5211
- }
5411
+ const withCode = withLanguageCode(language);
5412
+ if (!withCode) warnings.push({ code: "unsupported-config", message: "A navigation language with an invalid locale code was skipped." });
5413
+ return withCode ? [withCode] : [];
5212
5414
  }) : [];
5415
+ const defaultLanguage = pickDefaultLanguage(languages);
5213
5416
  let tabs = [];
5214
5417
  let i18n;
5215
5418
  const localizedNavigation = {};
5216
5419
  const projectionTrace = {};
5217
- if (languages.length > 0) {
5218
- const defaultLanguage = languages.find((entry) => entry.default === true) ?? languages[0];
5420
+ if (defaultLanguage) {
5219
5421
  const defaultLocale = String(defaultLanguage.language);
5220
5422
  const locales = languages.map((entry) => {
5221
5423
  const code = String(entry.language);
@@ -5241,6 +5443,9 @@ function projectMintlifyNavigation(config, options = {}) {
5241
5443
  warningKeys,
5242
5444
  appearances,
5243
5445
  access: descendNavigationAccess(rootAccess, navigationAccess(language)),
5446
+ operations: descendOperationScope(rootOperations, language, true),
5447
+ operationGroups: operationGroups2,
5448
+ otherLanguage: language !== defaultLanguage,
5244
5449
  gatedSourceRefs,
5245
5450
  gatedNodes,
5246
5451
  limits
@@ -5260,9 +5465,10 @@ function projectMintlifyNavigation(config, options = {}) {
5260
5465
  }
5261
5466
  if (Object.keys(localizedNavigation).length > 0) i18n.navigation = localizedNavigation;
5262
5467
  } else {
5263
- const context = { references, seenReferences, warnings, warningKeys, appearances, access: rootAccess, gatedSourceRefs, gatedNodes, limits, pathPrefix: options.pathPrefix, resolveSourceRef: options.resolveSourceRef };
5468
+ const context = { references, seenReferences, warnings, warningKeys, appearances, access: rootAccess, operations: rootOperations, operationGroups: operationGroups2, gatedSourceRefs, gatedNodes, limits, pathPrefix: options.pathPrefix, resolveSourceRef: options.resolveSourceRef };
5264
5469
  tabs = convertContainerToTabs(navigation, context, "Documentation", projectionTrace);
5265
5470
  if (tabs.length === 0 && Array.isArray(config.navigation)) {
5471
+ context.operations = UNBOUND_OPERATIONS;
5266
5472
  const children = convertNavigationValues(config.navigation, context);
5267
5473
  if (children.length > 0) {
5268
5474
  const hasRootPages = children.some((page) => typeof page === "string");
@@ -5279,7 +5485,7 @@ function projectMintlifyNavigation(config, options = {}) {
5279
5485
  message: "Mintlify navigation could not be projected; generated navigation will be used."
5280
5486
  });
5281
5487
  }
5282
- const versionOwner = languages.length > 0 ? languages.find((entry) => entry.default === true) ?? languages[0] : navigation;
5488
+ const versionOwner = defaultLanguage ?? navigation;
5283
5489
  const globalNavigation = objectValue2(versionOwner.global) ?? objectValue2(navigation.global);
5284
5490
  const shortcuts = Array.isArray(globalNavigation?.anchors) ? globalNavigation.anchors.flatMap((value) => {
5285
5491
  const anchor = objectValue2(value);
@@ -7196,8 +7402,49 @@ function splitOpenApiRef(raw) {
7196
7402
  const specRef = match[1] ? unquote(match[1]) : void 0;
7197
7403
  return { ...specRef ? { specRef } : {}, operation: match[2] };
7198
7404
  }
7405
+ function canonicalSpecRef(specRef) {
7406
+ return specRef.trim().split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^(?:\.?\/+)+/, "");
7407
+ }
7408
+ function isRemoteSpecRef(specRef) {
7409
+ return /^[a-z][a-z0-9+.-]*:/i.test(specRef);
7410
+ }
7199
7411
  function specRefBaseName(specRef) {
7200
- return specRef.trim().replace(/\\/g, "/").split(/[?#]/, 1)[0].split("/").filter(Boolean).pop()?.toLowerCase() ?? "";
7412
+ return canonicalSpecRef(specRef).split("/").filter(Boolean).pop()?.toLowerCase() ?? "";
7413
+ }
7414
+ function withoutSpecExtension(value) {
7415
+ return value.replace(/\.(?:json|ya?ml)$/i, "");
7416
+ }
7417
+ function resolveSpecRef(specRef, candidates, anyFolder = true) {
7418
+ const key = canonicalSpecRef(specRef);
7419
+ const byId = /* @__PURE__ */ new Map();
7420
+ for (const candidate of candidates) {
7421
+ const id = candidate.sourcePath ?? candidate.filename;
7422
+ if (!byId.has(id)) byId.set(id, candidate);
7423
+ }
7424
+ const unique = [...byId.values()];
7425
+ const exact = unique.find((candidate) => candidate.sourcePath === key);
7426
+ if (exact) return { kind: "match", spec: exact };
7427
+ if (!anyFolder && key.includes("/")) return { kind: "none" };
7428
+ const name = specRefBaseName(key);
7429
+ const names = unique.map((candidate) => specRefBaseName(candidate.sourcePath ?? candidate.filename));
7430
+ for (const fits of [unique.filter((_, index) => names[index] === name), unique.filter((_, index) => withoutSpecExtension(names[index]) === name)]) {
7431
+ if (fits.length === 1) return { kind: "match", spec: fits[0] };
7432
+ if (fits.length > 1) return { kind: "ambiguous", candidates: fits };
7433
+ }
7434
+ return { kind: "none" };
7435
+ }
7436
+ function localSpecRef(openapi) {
7437
+ const specRef = openapi ? splitOpenApiRef(openapi)?.specRef : void 0;
7438
+ return specRef && !isRemoteSpecRef(specRef) ? specRef : void 0;
7439
+ }
7440
+ function specNamedByPage(openapi, specs) {
7441
+ const specRef = localSpecRef(openapi);
7442
+ const resolved = specRef ? resolveSpecRef(specRef, specs) : void 0;
7443
+ return resolved?.kind === "match" ? resolved.spec : void 0;
7444
+ }
7445
+ function namesSeveralSpecs(openapi, specs) {
7446
+ const specRef = localSpecRef(openapi);
7447
+ return Boolean(specRef) && resolveSpecRef(specRef, specs).kind === "ambiguous";
7201
7448
  }
7202
7449
  function withSpecRef(ref, specRef) {
7203
7450
  return `${specRef} ${ref.operation}`;
@@ -7205,25 +7452,44 @@ function withSpecRef(ref, specRef) {
7205
7452
 
7206
7453
  // src/spec-exclude.ts
7207
7454
  import { parse as parseYaml4, stringify as stringifyYaml } from "yaml";
7208
- function specRefMatches(ref, specPath, specFilename, isDefault) {
7209
- const prefix = /^(?:(\S+)\s+)?(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|WEBHOOK)\s/i.exec(`${ref} `)?.[1];
7455
+ function specRefMatches(ref, specPath, isDefault) {
7456
+ const prefix = splitOpenApiRef(ref)?.specRef;
7210
7457
  if (!prefix) return isDefault;
7211
- const wanted = prefix.replace(/^\/+/, "").toLowerCase();
7458
+ const wanted = (isRemoteSpecRef(prefix) ? prefix.trim() : canonicalSpecRef(prefix)).toLowerCase();
7212
7459
  const lowerPath = specPath.toLowerCase();
7213
- return lowerPath === wanted || lowerPath.endsWith(`/${wanted}`) || specFilename.toLowerCase() === wanted;
7214
- }
7215
- function operationKey(operation) {
7460
+ const stem = withoutSpecExtension(lowerPath);
7461
+ return lowerPath === wanted || lowerPath.endsWith(`/${wanted}`) || stem === wanted || stem.endsWith(`/${wanted}`);
7462
+ }
7463
+ function resolvedSources(ref, specs) {
7464
+ const specRef = splitOpenApiRef(ref)?.specRef;
7465
+ if (!specRef || isRemoteSpecRef(specRef)) return { unique: false, sources: [] };
7466
+ const resolved = resolveSpecRef(specRef, specs);
7467
+ if (resolved.kind === "match") return { unique: true, sources: [resolved.spec.sourcePath] };
7468
+ return { unique: false, sources: resolved.kind === "ambiguous" ? resolved.candidates.map((candidate) => candidate.sourcePath) : [] };
7469
+ }
7470
+ function refWithholdsFrom(ref, spec, specs) {
7471
+ const { unique, sources } = resolvedSources(ref, specs);
7472
+ return unique ? sources[0] === spec.sourcePath : specRefMatches(ref, spec.sourcePath, true) || sources.includes(spec.sourcePath);
7473
+ }
7474
+ function refKeepsIn(ref, spec, specs) {
7475
+ const specRef = splitOpenApiRef(ref)?.specRef;
7476
+ if (!specRef) return false;
7477
+ if (isRemoteSpecRef(specRef)) return specRefMatches(ref, spec.sourcePath, false);
7478
+ const { unique, sources } = resolvedSources(ref, specs);
7479
+ return unique && sources[0] === spec.sourcePath;
7480
+ }
7481
+ function operationKey2(operation) {
7216
7482
  const [first, ...rest] = operation.trim().split(/\s+/);
7217
7483
  return `${first.toLowerCase()} ${rest.join(" ")}`;
7218
7484
  }
7219
- function keysFor(spec, refs) {
7485
+ function keysFor(refs, names) {
7220
7486
  return [...new Set(
7221
- refs.filter((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true)).map((ref) => splitOpenApiRef(ref)?.operation).filter((op) => !!op).map(operationKey)
7487
+ refs.filter(names).map((ref) => splitOpenApiRef(ref)?.operation).filter((op) => !!op).map(operationKey2)
7222
7488
  )];
7223
7489
  }
7224
- function withheldOperationKeys(spec, withheldRefs, keptRefs) {
7225
- const kept = keysFor(spec, keptRefs);
7226
- return { withheld: keysFor(spec, withheldRefs).filter((key) => !kept.includes(key)), kept };
7490
+ function withheldOperationKeys(spec, withheldRefs, keptRefs, specs = [spec]) {
7491
+ const kept = keysFor(keptRefs, (ref) => refKeepsIn(ref, spec, specs));
7492
+ return { withheld: keysFor(withheldRefs, (ref) => refWithholdsFrom(ref, spec, specs)).filter((key) => !kept.includes(key)), kept };
7227
7493
  }
7228
7494
  function withoutByteOrderMark(content) {
7229
7495
  return content.length >= 3 && content[0] === 239 && content[1] === 187 && content[2] === 191 ? content.subarray(3) : content;
@@ -10474,22 +10740,56 @@ function normalizedReferenceKey(value) {
10474
10740
  function exactReferenceKey(value) {
10475
10741
  return value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/").replace(/\.(?:mdx?|rst|txt)$/i, "");
10476
10742
  }
10477
- function navigationOperationRefs(config) {
10478
- const refs = [];
10479
- const visit = (node) => {
10743
+ function containerSpec(node) {
10744
+ const value = node.openapi;
10745
+ if (value === void 0) return void 0;
10746
+ const source = value && typeof value === "object" && !Array.isArray(value) ? value.source : value;
10747
+ return typeof source === "string" && source.trim() && !splitOpenApiRef(source) ? source.trim() : null;
10748
+ }
10749
+ function navigationSpecContext(config, pageIdentity) {
10750
+ const context = { operations: [], pageOwners: /* @__PURE__ */ new Map(), pageIdentity };
10751
+ const visit = (node, owner, listed) => {
10480
10752
  if (typeof node === "string") {
10481
- if (splitOpenApiRef(node)) refs.push(node);
10482
- } else if (Array.isArray(node)) node.forEach(visit);
10753
+ if (splitOpenApiRef(node)) context.operations.push({ ref: node, owner });
10754
+ else if (listed) {
10755
+ const page = pageIdentity(node);
10756
+ if (page !== null) context.pageOwners.set(page, (context.pageOwners.get(page) ?? /* @__PURE__ */ new Set()).add(owner));
10757
+ }
10758
+ } else if (Array.isArray(node)) node.forEach((child) => visit(child, owner, true));
10483
10759
  else if (node && typeof node === "object") {
10484
10760
  if (navigationGateReason(node)) return;
10485
- Object.values(node).forEach(visit);
10486
- }
10487
- };
10488
- visit(config?.navigation);
10489
- return refs;
10490
- }
10491
- function excludeWithheldOperations(spec, withheldRefs, keptRefs) {
10492
- const { withheld, kept } = withheldOperationKeys(spec, withheldRefs, keptRefs);
10761
+ const own2 = containerSpec(node);
10762
+ Object.values(node).forEach((child) => visit(child, own2 === void 0 ? owner : own2, false));
10763
+ }
10764
+ };
10765
+ visit(config?.navigation, void 0, false);
10766
+ return context;
10767
+ }
10768
+ function keptOperationRefs(published, navigation, defaultSpec, specs) {
10769
+ const kept = [];
10770
+ const unowned = [];
10771
+ const source = (owner) => {
10772
+ if (!owner || isRemoteSpecRef(owner)) return owner;
10773
+ const resolved = resolveSpecRef(owner, specs);
10774
+ return resolved.kind === "match" ? resolved.spec.sourcePath ?? resolved.spec.filename : owner;
10775
+ };
10776
+ const add = (raw, owner) => {
10777
+ const ref = splitOpenApiRef(raw);
10778
+ if (!ref) return;
10779
+ if (ref.specRef) kept.push(raw);
10780
+ else if (owner) kept.push(withSpecRef(ref, owner));
10781
+ else unowned.push(ref.operation);
10782
+ };
10783
+ for (const { ref, file } of published) {
10784
+ const page = navigation.pageIdentity(file);
10785
+ const owners = new Set([...(page === null ? void 0 : navigation.pageOwners.get(page)) ?? [void 0]].map((owner) => source(owner === void 0 ? defaultSpec : owner)));
10786
+ add(ref, owners.size === 1 ? [...owners][0] : null);
10787
+ }
10788
+ for (const { ref, owner } of navigation.operations) add(ref, source(owner === void 0 ? defaultSpec : owner));
10789
+ return { kept, unowned };
10790
+ }
10791
+ function excludeWithheldOperations(spec, withheldRefs, keptRefs, specs) {
10792
+ const { withheld, kept } = withheldOperationKeys(spec, withheldRefs, keptRefs, specs);
10493
10793
  return markExcluded(spec.content, withheld, kept);
10494
10794
  }
10495
10795
  var TOKEN_SEPARATORS = /[\s"'()<>[\]{}=,;:|\\/`*!?#&]+/;
@@ -10590,6 +10890,23 @@ function specAssetPath(filename) {
10590
10890
  function specAsset(filename, content) {
10591
10891
  return { path: specAssetPath(filename), content, projectRelative: true };
10592
10892
  }
10893
+ function createSpecAssetNamer() {
10894
+ const nameBySource = /* @__PURE__ */ new Map();
10895
+ const sourceByName = /* @__PURE__ */ new Map();
10896
+ return ({ sourcePath, filename, tabLabel }) => {
10897
+ let name = nameBySource.get(sourcePath);
10898
+ if (name === void 0) {
10899
+ name = filename;
10900
+ if (sourceByName.has(name.toLowerCase())) {
10901
+ name = sourcePath.replace(/\//g, "-");
10902
+ while (sourceByName.has(name.toLowerCase())) name = `${(tabLabel ?? "default").replace(/\W+/g, "-")}-${name}`;
10903
+ }
10904
+ nameBySource.set(sourcePath, name);
10905
+ sourceByName.set(name.toLowerCase(), sourcePath);
10906
+ }
10907
+ return name;
10908
+ };
10909
+ }
10593
10910
  function mintlifyTopLevelApiReferences(config) {
10594
10911
  const api = config?.api && typeof config.api === "object" && !Array.isArray(config.api) ? config.api : null;
10595
10912
  if (!api) return [];
@@ -10601,19 +10918,25 @@ function mintlifyTopLevelApiReferences(config) {
10601
10918
  }
10602
10919
  return references;
10603
10920
  }
10604
- function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, pages = []) {
10921
+ function specFileCandidates(files) {
10922
+ return files.filter((file) => /\.(?:json|ya?ml)$/i.test(file.relativePath)).map((file) => ({ sourcePath: file.relativePath, filename: specRefBaseName(file.relativePath) }));
10923
+ }
10924
+ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, nameSpec, pages = []) {
10605
10925
  if (!mintlifyConfig) return [];
10606
10926
  const references = [
10607
10927
  ...mintlifyTopLevelApiReferences(mintlifyConfig),
10608
10928
  ...mintlifyNavigationApiReferences(mintlifyConfig)
10609
10929
  ];
10610
- const specKey = (value) => value.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/^\/+/, "");
10611
- const known = new Set(references.map((reference) => specKey(reference.value)));
10930
+ const known = new Set(references.map((reference) => canonicalSpecRef(reference.value)));
10931
+ const specFiles = specFileCandidates(files);
10932
+ const namedFiles = /* @__PURE__ */ new Map();
10612
10933
  for (const page of pages) {
10613
10934
  const specRef = page.openapi ? splitOpenApiRef(page.openapi)?.specRef : void 0;
10614
10935
  if (!specRef || /^[a-z][a-z0-9+.-]*:/i.test(specRef)) continue;
10615
- const key = specKey(specRef);
10616
- if (known.has(key) || !files.some((file) => file.relativePath === key)) continue;
10936
+ const named = namedFiles.get(specRef) ?? resolveSpecRef(specRef, specFiles, false);
10937
+ namedFiles.set(specRef, named);
10938
+ const key = named.kind === "match" ? named.spec.sourcePath : "";
10939
+ if (!key || known.has(key)) continue;
10617
10940
  known.add(key);
10618
10941
  references.push({ value: key, kind: "openapi", tabLabel: `OpenAPI: ${key}`, hidden: true, pageOnly: true });
10619
10942
  }
@@ -10631,14 +10954,28 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, p
10631
10954
  delete mount.operations;
10632
10955
  }
10633
10956
  };
10957
+ const listsNothing = (reference) => !reference.groups && !reference.operationsWithheld && !reference.otherLanguage;
10958
+ const groupTargets = /* @__PURE__ */ new Map();
10634
10959
  const specs = [];
10635
10960
  for (const reference of references) {
10636
10961
  const dedupeKey = `${reference.kind}:${reference.value}:${reference.tabLabel ?? ""}`;
10637
10962
  if (seen.has(dedupeKey)) {
10638
10963
  mountDirectory(remoteByKey.get(dedupeKey), reference);
10964
+ const shared = groupTargets.get(dedupeKey);
10965
+ if (shared && !shared.unlisted) {
10966
+ if (reference.operationsWithheld) shared.withheld = true;
10967
+ if (reference.groups) {
10968
+ const merged = mergeApiGroups(shared.target.groups, reference.groups);
10969
+ if (merged) shared.target.groups = merged;
10970
+ } else if (listsNothing(reference)) {
10971
+ shared.unlisted = true;
10972
+ delete shared.target.groups;
10973
+ }
10974
+ }
10639
10975
  continue;
10640
10976
  }
10641
10977
  seen.add(dedupeKey);
10978
+ const groups = mergeApiGroups(void 0, reference.groups);
10642
10979
  const tabSuffix = reference.tabLabel ? ` (tab "${reference.tabLabel}")` : "";
10643
10980
  if (reference.kind === "asyncapi") {
10644
10981
  warnings.push({
@@ -10648,10 +10985,11 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, p
10648
10985
  continue;
10649
10986
  }
10650
10987
  if (/^https:\/\//i.test(reference.value)) {
10651
- const remote = { url: reference.value, ...reference.tabLabel ? { tabLabel: reference.tabLabel } : {}, ...reference.parentTab ? { parentTab: reference.parentTab } : {}, ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {} };
10988
+ const remote = { url: reference.value, ...reference.tabLabel ? { tabLabel: reference.tabLabel } : {}, ...reference.parentTab ? { parentTab: reference.parentTab } : {}, ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, ...groups ? { groups } : {} };
10652
10989
  mountDirectory(remote, reference);
10653
10990
  remoteSpecs.push(remote);
10654
10991
  remoteByKey.set(dedupeKey, remote);
10992
+ groupTargets.set(dedupeKey, { target: remote, unlisted: listsNothing(reference), withheld: reference.operationsWithheld === true });
10655
10993
  warnings.push({
10656
10994
  code: "unsupported-config",
10657
10995
  message: `The remote OpenAPI spec "${reference.value}"${tabSuffix} requires a network download before this import is complete.`,
@@ -10663,7 +11001,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, p
10663
11001
  warnings.push({ code: "unsupported-config", message: `The OpenAPI spec "${reference.value}"${tabSuffix} uses insecure HTTP and was not imported.`, source: reference.value });
10664
11002
  continue;
10665
11003
  }
10666
- const key = reference.value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/");
11004
+ const key = canonicalSpecRef(reference.value);
10667
11005
  const match = files.find((file) => file.relativePath === key);
10668
11006
  if (!match) {
10669
11007
  warnings.push({
@@ -10680,7 +11018,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, p
10680
11018
  }
10681
11019
  const content = readSpecFile(match.absolutePath, match.relativePath, warnings);
10682
11020
  if (!content) continue;
10683
- specs.push({
11021
+ const spec = {
10684
11022
  filename: basename3(match.relativePath),
10685
11023
  content,
10686
11024
  tabLabel: reference.tabLabel,
@@ -10689,24 +11027,16 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, p
10689
11027
  hidden: reference.hidden,
10690
11028
  ...reference.pageOnly ? { pageOnly: true } : {},
10691
11029
  sourcePath: match.relativePath,
10692
- directory: reference.directory
10693
- });
11030
+ directory: reference.directory,
11031
+ ...groups ? { groups } : {}
11032
+ };
11033
+ specs.push(spec);
11034
+ groupTargets.set(dedupeKey, { target: spec, unlisted: listsNothing(reference), withheld: reference.operationsWithheld === true });
10694
11035
  }
10695
- const filenameOwners = /* @__PURE__ */ new Map();
10696
- const filenameSources = /* @__PURE__ */ new Map();
10697
- for (const spec of specs) {
10698
- const groupKey = spec.tabLabel ?? "\0default";
10699
- const owner = filenameOwners.get(spec.filename);
10700
- if (owner !== void 0 && owner !== groupKey && filenameSources.get(spec.filename) !== spec.sourcePath) {
10701
- let disambiguated = spec.sourcePath.replace(/\//g, "-");
10702
- while (filenameOwners.has(disambiguated) && filenameOwners.get(disambiguated) !== groupKey) {
10703
- disambiguated = `${groupKey.replace(/\W+/g, "-")}-${disambiguated}`;
10704
- }
10705
- spec.filename = disambiguated;
10706
- }
10707
- filenameOwners.set(spec.filename, groupKey);
10708
- filenameSources.set(spec.filename, spec.sourcePath);
11036
+ for (const { target: target2, unlisted, withheld } of groupTargets.values()) {
11037
+ if (withheld && !unlisted && !target2.groups) target2.navigation = false;
10709
11038
  }
11039
+ for (const spec of specs) spec.filename = nameSpec(spec);
10710
11040
  return specs;
10711
11041
  }
10712
11042
  function rewriteMintlifyPageSpecRefs(pages, specs, remoteUrls, warnings, explainMissing = () => "no such file was found") {
@@ -10719,12 +11049,13 @@ function rewriteMintlifyPageSpecRefs(pages, specs, remoteUrls, warnings, explain
10719
11049
  }
10720
11050
  continue;
10721
11051
  }
10722
- const key = ref.specRef.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/^\/+/, "");
10723
- const byPath = specs.find((spec) => spec.sourcePath === key);
10724
- const byName = specs.filter((spec) => spec.filename.toLowerCase() === specRefBaseName(key));
10725
- const match = byPath ?? (byName.length === 1 ? byName[0] : void 0);
10726
- if (match) {
10727
- page.openapi = withSpecRef(ref, specAssetPath(match.filename));
11052
+ const key = canonicalSpecRef(ref.specRef);
11053
+ const resolved = resolveSpecRef(ref.specRef, specs);
11054
+ if (resolved.kind === "match") {
11055
+ page.openapi = withSpecRef(ref, specAssetPath(resolved.spec.filename));
11056
+ } else if (resolved.kind === "ambiguous") {
11057
+ const candidates = resolved.candidates.map((spec) => spec.sourcePath ?? spec.filename).join(", ");
11058
+ warnings.push({ code: "unsupported-config", message: `Page "${page.id}" references the OpenAPI spec "${ref.specRef}", which fits more than one spec (${candidates}), so it was left as written and the page will not render an endpoint. Write the full path of the spec in the page's openapi frontmatter.`, source: page.source });
10728
11059
  } else {
10729
11060
  warnings.push({ code: "unsupported-config", message: `Page "${page.id}" references the OpenAPI spec "${ref.specRef}", which was not migrated (${explainMissing(key)}); the page will not render an endpoint.`, source: page.source });
10730
11061
  }
@@ -12055,9 +12386,12 @@ function injectOpenApiSpecs(config, specs, warnings) {
12055
12386
  const apiTab = spec.tabLabel ? tabs.find((tab) => tab.tab === spec.tabLabel) : tabs.find((tab) => tab.tab.toLowerCase().includes("api"));
12056
12387
  if (apiTab) {
12057
12388
  if (apiTab.api && warnings) {
12389
+ const source = specAssetPath(spec.filename);
12390
+ const pageBound = spec.pageReferenced && !tabs.some((tab) => tab.api?.source === source);
12391
+ if (pageBound) insertApiTab(tabs, { tab: `OpenAPI: ${spec.sourcePath ?? spec.filename}`, hidden: true, api: { source } });
12058
12392
  warnings.push({
12059
12393
  code: "unsupported-config",
12060
- 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.`
12394
+ message: `Tab "${apiTab.tab}" already uses another OpenAPI spec, and Thally supports one API spec per tab, so "${source}" ${pageBound ? "gets no endpoint list of its own; the pages that name it still show their endpoint. Put it in its own tab to list all its endpoints." : "was not added. Put it in its own tab to include it."}`
12061
12395
  });
12062
12396
  continue;
12063
12397
  }
@@ -12066,15 +12400,18 @@ function injectOpenApiSpecs(config, specs, warnings) {
12066
12400
  // An API-only tab needs generated endpoint navigation; an authored
12067
12401
  // page tab keeps its own groups alongside the bound spec, unless
12068
12402
  // Mintlify generated the endpoint pages (the renderer appends the
12069
- // endpoint groups after the authored ones).
12070
- ...(apiTab.groups?.length || apiTab.pages?.length) && !spec.generatedNavigation ? { navigation: false } : {}
12403
+ // endpoint groups after the authored ones) or docs.json lists the
12404
+ // operations itself (those authored groups are the API navigation).
12405
+ // A spec whose listed operations are all withheld has no endpoint navigation either way.
12406
+ ...spec.navigation === false || (apiTab.groups?.length || apiTab.pages?.length) && !spec.generatedNavigation && !spec.groups ? { navigation: false } : {},
12407
+ ...spec.groups ? { groups: spec.groups } : {}
12071
12408
  };
12072
12409
  } else {
12073
12410
  insertApiTab(tabs, {
12074
12411
  tab: spec.tabLabel ?? "API Reference",
12075
12412
  ...spec.icon ? { icon: spec.icon } : {},
12076
12413
  ...spec.hidden ? { hidden: true } : {},
12077
- api: { source: specAssetPath(spec.filename) }
12414
+ api: { source: specAssetPath(spec.filename), ...spec.navigation === false ? { navigation: false } : {}, ...spec.groups ? { groups: spec.groups } : {} }
12078
12415
  }, spec.parentTab);
12079
12416
  }
12080
12417
  }
@@ -12159,6 +12496,8 @@ function migrateRepository(options) {
12159
12496
  defaultVersionPrefixes = mintlifyDefaultVersionPrefixes(config);
12160
12497
  allVersionPrefixes = mintlifyAllVersionPrefixes(config);
12161
12498
  const projected = projectMintlifyNavigation(config, {
12499
+ // injectOpenApiSpecs / hydrateRemoteApiSpecs bind the authored operation lists to their API tabs.
12500
+ operationGroups: true,
12162
12501
  resolveSourceRef: (repo) => {
12163
12502
  const ref = activeSourceRefs.find((candidate) => candidate.repo.toLowerCase() === repo.toLowerCase());
12164
12503
  if (!ref) return null;
@@ -12316,6 +12655,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12316
12655
  const gateByPath = /* @__PURE__ */ new Map();
12317
12656
  const withheldSpecRefs = [];
12318
12657
  const publishedSpecRefs = [];
12658
+ const publishedSpecPages = [];
12319
12659
  const withheldPaths = /* @__PURE__ */ new Set();
12320
12660
  const isDocFile = (file) => [".md", ".mdx"].includes(extname6(file.relativePath).toLowerCase());
12321
12661
  const isGateCandidate = (file) => isDocFile(file) && !file.relativePath.split("/").some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase()));
@@ -12352,6 +12692,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12352
12692
  const verdict = classifyPageGate(file);
12353
12693
  gateByPath.set(file.absolutePath, verdict);
12354
12694
  if (verdict.openapi) (verdict.reason ? withheldSpecRefs : publishedSpecRefs).push(verdict.openapi);
12695
+ if (verdict.openapi && !verdict.reason) publishedSpecPages.push({ ref: verdict.openapi, file: file.relativePath });
12355
12696
  if (verdict.reason) withheldPaths.add(file.absolutePath);
12356
12697
  }
12357
12698
  for (const file of mintignoreFilteredFiles) {
@@ -12455,6 +12796,23 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12455
12796
  let discovered = files.length;
12456
12797
  const localeConfig = docsConfig.i18n;
12457
12798
  const defaultPageIds = new Set([...referenceMap.values()].filter((reference) => !reference.locale || reference.locale === localeConfig?.defaultLocale).map((reference) => reference.navigationId));
12799
+ const resolvePageIdentity = (path) => {
12800
+ const referenced = exactReferenceMap.get(exactReferenceKey(path)) ?? referenceMap.get(normalizedReferenceKey(path));
12801
+ let locale = referenced?.locale;
12802
+ let navigationId = referenced?.navigationId ?? pageIdFromReference(path, platform === "mintlify");
12803
+ if (!referenced && localeConfig) {
12804
+ for (const entry of localeConfig.locales) {
12805
+ const localized = mintlifyLocalizedReference(path, entry.code, defaultPageIds);
12806
+ if (localized === path) continue;
12807
+ locale = entry.code;
12808
+ navigationId = pageIdFromReference(localized, platform === "mintlify");
12809
+ break;
12810
+ }
12811
+ }
12812
+ if (!navigationId) return null;
12813
+ const isDefaultLocale = !locale || locale === localeConfig?.defaultLocale;
12814
+ return { locale, navigationId, id: isDefaultLocale ? navigationId : `${locale}/${navigationId}` };
12815
+ };
12458
12816
  const routeAliases = [];
12459
12817
  const pageFiles = [...files].sort((left, right) => {
12460
12818
  const leftOrder = referenceOrder.get(normalizedReferenceKey(left.relativePath)) ?? Number.MAX_SAFE_INTEGER;
@@ -12550,7 +12908,6 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12550
12908
  }
12551
12909
  continue;
12552
12910
  }
12553
- const key = normalizedReferenceKey(file.relativePath);
12554
12911
  let pageGate;
12555
12912
  {
12556
12913
  pageGate = gateByPath.get(file.absolutePath);
@@ -12600,25 +12957,13 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12600
12957
  }
12601
12958
  }
12602
12959
  const isRestrictedPage = Boolean(pageGate?.reason);
12603
- const referenced = exactReferenceMap.get(exactReferenceKey(file.relativePath)) ?? referenceMap.get(key);
12604
- let locale = referenced?.locale;
12605
- let navigationId = referenced?.navigationId ?? pageIdFromReference(file.relativePath, platform === "mintlify");
12606
- if (!referenced && localeConfig) {
12607
- for (const entry of localeConfig.locales) {
12608
- const localized = mintlifyLocalizedReference(file.relativePath, entry.code, defaultPageIds);
12609
- if (localized === file.relativePath) continue;
12610
- locale = entry.code;
12611
- navigationId = pageIdFromReference(localized, platform === "mintlify");
12612
- break;
12613
- }
12614
- }
12615
- if (!navigationId) {
12960
+ const identity = resolvePageIdentity(file.relativePath);
12961
+ if (!identity) {
12616
12962
  skipped++;
12617
12963
  warnings.push({ code: "invalid-page", message: "Page path could not be normalized safely.", source: file.relativePath });
12618
12964
  continue;
12619
12965
  }
12620
- const isDefaultLocale = !locale || locale === localeConfig?.defaultLocale;
12621
- const id = isDefaultLocale ? navigationId : `${locale}/${navigationId}`;
12966
+ const { locale, navigationId, id } = identity;
12622
12967
  let raw = inlineMdxSnippets(
12623
12968
  mintlifyProjectRoot ? importUndeclaredSnippetComponents(readFileSync6(file.absolutePath, "utf8"), file.absolutePath, repositoryDir, mintlifyProjectRoot, snippetExports, snippetAliases, inlineGate, warnings) : injectDocusaurusGlobalImports(readFileSync6(file.absolutePath, "utf8"), globalDocusaurusImports),
12624
12969
  file.absolutePath,
@@ -13267,8 +13612,21 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13267
13612
  const warnSharedSpec = (path, excluded = []) => {
13268
13613
  warnings.push({ code: "gated-page", message: sharedSpecMessage(path, excluded), source: path });
13269
13614
  };
13270
- const keptSpecRefs = [...publishedSpecRefs, ...navigationOperationRefs(mintlifyConfig)];
13271
- const allSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs, pages.filter((page) => !restrictedPages.has(page)));
13615
+ const nameSpec = createSpecAssetNamer();
13616
+ const allSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs, nameSpec, pages.filter((page) => !restrictedPages.has(page)));
13617
+ const navigation = navigationSpecContext(mintlifyConfig, (path) => resolvePageIdentity(path)?.id ?? null);
13618
+ const defaultSources = /* @__PURE__ */ new Set([
13619
+ ...allSpecs.filter((spec) => !spec.pageOnly && spec.tabLabel === void 0).map((spec) => spec.sourcePath),
13620
+ ...remoteApiSpecs.filter((spec) => spec.tabLabel === void 0).map((spec) => spec.url)
13621
+ ]);
13622
+ const keptOperations = (fallbackSource) => {
13623
+ const defaultSpec = defaultSources.size === 0 ? fallbackSource : defaultSources.size === 1 ? [...defaultSources][0] : void 0;
13624
+ const candidates = fallbackSource ? [{ sourcePath: fallbackSource, filename: basename3(fallbackSource) }, ...allSpecs] : allSpecs;
13625
+ const { kept, unowned } = keptOperationRefs(publishedSpecPages, navigation, defaultSpec, candidates);
13626
+ return { kept: kept.filter((ref) => !namesSeveralSpecs(ref, allSpecs)), unowned };
13627
+ };
13628
+ let { kept: keptSpecRefs, unowned: unownedOperations } = keptOperations();
13629
+ const withheldKeys = /* @__PURE__ */ new Set();
13272
13630
  for (const remote of remoteApiSpecs) {
13273
13631
  const { withheld, kept } = withheldOperationKeys({ sourcePath: remote.url, filename: remote.url }, withheldSpecRefs, keptSpecRefs);
13274
13632
  if (withheld.length > 0) {
@@ -13278,9 +13636,10 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13278
13636
  }
13279
13637
  const withheldOperations = /* @__PURE__ */ new Map();
13280
13638
  for (const spec of allSpecs) {
13281
- const result = excludeWithheldOperations(spec, withheldSpecRefs, keptSpecRefs);
13639
+ const result = excludeWithheldOperations(spec, withheldSpecRefs, keptSpecRefs, allSpecs);
13282
13640
  spec.content = result.content;
13283
13641
  withheldOperations.set(spec, result.excluded);
13642
+ result.excluded.forEach((operation) => withheldKeys.add(operationKey2(operation)));
13284
13643
  }
13285
13644
  const resolvedSpecs = allSpecs.filter((spec) => !spec.pageOnly);
13286
13645
  const pageOnlySpecs = allSpecs.filter((spec) => spec.pageOnly);
@@ -13288,12 +13647,14 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13288
13647
  if (!assets.some((asset) => asset.path === specAssetPath(spec.filename))) {
13289
13648
  assets.push(specAsset(spec.filename, spec.content));
13290
13649
  }
13291
- if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath, withheldOperations.get(spec));
13650
+ if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, true))) warnSharedSpec(spec.sourcePath, withheldOperations.get(spec));
13292
13651
  }
13293
13652
  let pageSpecs = resolvedSpecs;
13294
13653
  if (resolvedSpecs.length > 0) {
13654
+ const namedByPages = new Set(pages.filter((page) => !restrictedPages.has(page)).map((page) => specNamedByPage(page.openapi, resolvedSpecs)));
13295
13655
  for (const spec of resolvedSpecs) {
13296
- spec.generatedNavigation = Boolean(spec.directory) && !publishedSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true));
13656
+ spec.pageReferenced = namedByPages.has(spec);
13657
+ spec.generatedNavigation = Boolean(spec.directory) && !publishedSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, true));
13297
13658
  }
13298
13659
  docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs, warnings);
13299
13660
  if (remoteApiSpecs.length > 0) {
@@ -13311,13 +13672,16 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13311
13672
  const fallback = findOpenApi(files);
13312
13673
  const fallbackContent = fallback ? readSpecFile(fallback.absolutePath, fallback.relativePath, warnings) : void 0;
13313
13674
  if (fallback && fallbackContent) {
13314
- const filename = basename3(fallback.relativePath);
13315
- const matches = (ref) => specRefMatches(ref, fallback.relativePath, filename, true);
13675
+ const matches = (ref) => specRefMatches(ref, fallback.relativePath, true);
13316
13676
  if (withheldSpecRefs.some(matches) && !publishedSpecRefs.some(matches)) {
13317
- quarantinedFiles.push({ path: `${QUARANTINE_DIRECTORY}/assets/${specAssetPath(filename)}`, content: fallbackContent });
13677
+ quarantinedFiles.push({ path: `${QUARANTINE_DIRECTORY}/assets/${specAssetPath(basename3(fallback.relativePath))}`, content: fallbackContent });
13318
13678
  withheldAssetCount++;
13319
13679
  } else {
13320
- const pruned = excludeWithheldOperations({ sourcePath: fallback.relativePath, filename, content: fallbackContent }, withheldSpecRefs, keptSpecRefs);
13680
+ const filename = nameSpec({ sourcePath: fallback.relativePath, filename: basename3(fallback.relativePath) });
13681
+ const fallbackSpec = { sourcePath: fallback.relativePath, filename, content: fallbackContent };
13682
+ ({ kept: keptSpecRefs, unowned: unownedOperations } = keptOperations(fallback.relativePath));
13683
+ const pruned = excludeWithheldOperations(fallbackSpec, withheldSpecRefs, keptSpecRefs, [fallbackSpec, ...allSpecs]);
13684
+ pruned.excluded.forEach((operation) => withheldKeys.add(operationKey2(operation)));
13321
13685
  if (!assets.some((asset) => asset.path === specAssetPath(filename))) {
13322
13686
  assets.push(specAsset(filename, pruned.content));
13323
13687
  }
@@ -13329,13 +13693,22 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13329
13693
  }
13330
13694
  for (const spec of pageOnlySpecs) {
13331
13695
  if (!assets.some((asset) => asset.path === specAssetPath(spec.filename))) assets.push(specAsset(spec.filename, spec.content));
13332
- if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath, withheldOperations.get(spec));
13696
+ if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, true))) warnSharedSpec(spec.sourcePath, withheldOperations.get(spec));
13333
13697
  }
13334
13698
  if (pageOnlySpecs.length > 0) {
13335
13699
  docsConfig = injectOpenApiSpecs(docsConfig, pageOnlySpecs, warnings);
13336
13700
  pageSpecs = [...pageSpecs, ...pageOnlySpecs];
13337
13701
  }
13702
+ const notKeptPublic = [...new Set(unownedOperations.filter((operation) => withheldKeys.has(operationKey2(operation))))];
13703
+ if (notKeptPublic.length > 0) {
13704
+ warnings.push({
13705
+ code: "gated-page",
13706
+ message: `${notKeptPublic.length} operation(s) withheld from a published OpenAPI spec (${notKeptPublic.join(", ")}) are also named by a published page or navigation entry that does not say which spec it means, so that reference did not keep them public. Write the spec in its openapi value (for example "openapi.json GET /x") to publish the operation.`
13707
+ });
13708
+ }
13338
13709
  const explainMissingSpec = (key) => {
13710
+ const fits = resolveSpecRef(key, specFileCandidates(files), false);
13711
+ if (fits.kind === "ambiguous") return `several spec files fit that name (${fits.candidates.map((spec) => spec.sourcePath).join(", ")}); write the full path in the page's openapi frontmatter`;
13339
13712
  const sameName = scannedFiles.find((file) => file.relativePath.toLowerCase() === key.toLowerCase());
13340
13713
  if (sameName && sameName.relativePath !== key) return `paths are case-sensitive; the file is at "${sameName.relativePath}"`;
13341
13714
  if (sameName && mintignoreMatcher?.ignores(sameName.relativePath)) return "the file is excluded by .mintignore";
@@ -13675,10 +14048,10 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
13675
14048
  const sourceFavicon = mintlifyConfig?.favicon ?? fernRawConfig?.favicon ?? docusaurusSettings?.favicon;
13676
14049
  const logo = brandVariants(sourceLogo);
13677
14050
  const favicon = brandVariants(sourceFavicon);
13678
- const logoLight = publicBrandPath(logo.light);
13679
14051
  const logoDark = publicBrandPath(logo.dark);
13680
- const faviconLight = publicBrandPath(favicon.light);
14052
+ const logoLight = publicBrandPath(logo.light) ?? logoDark;
13681
14053
  const faviconDark = publicBrandPath(favicon.dark);
14054
+ const faviconLight = publicBrandPath(favicon.light) ?? faviconDark;
13682
14055
  if (logoLight) docsConfig = {
13683
14056
  ...docsConfig,
13684
14057
  navbar: {
@@ -13707,15 +14080,30 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
13707
14080
  const decoration = background.decoration;
13708
14081
  const image = publicBrandPath(background.image);
13709
14082
  const imageDark = publicBrandPath(background.imageDark);
13710
- docsConfig = {
13711
- ...docsConfig,
13712
- background: {
13713
- ...docsConfig.background,
13714
- ...decoration === "none" || decoration === "grid" || decoration === "gradient" ? { decoration } : {},
13715
- ...image ? { image } : {},
13716
- ...imageDark ? { imageDark } : {}
13717
- }
14083
+ const mapped = {
14084
+ ...docsConfig.background,
14085
+ ...decoration === "none" || decoration === "grid" || decoration === "gradient" ? { decoration } : {},
14086
+ ...image ? { image } : {},
14087
+ ...imageDark ? { imageDark } : {}
13718
14088
  };
14089
+ if (Object.keys(mapped).length > 0) docsConfig = { ...docsConfig, background: mapped };
14090
+ const canvas = background.color && typeof background.color === "object" ? background.color : {};
14091
+ for (const mode of ["light", "dark"]) {
14092
+ if (canvas[mode] === void 0) continue;
14093
+ const hex = typeof canvas[mode] === "string" ? cssColorToHex(canvas[mode]) : void 0;
14094
+ if (!hex) {
14095
+ warnings.push({
14096
+ code: "unsupported-config",
14097
+ message: `background.color.${mode} ${JSON.stringify(canvas[mode])?.slice(0, 40)} is not a hex or rgb() colour (for example #121317) and was dropped, so that mode keeps Thally's default canvas. Use a hex value in docs.json to set it.`,
14098
+ source: "docs.json"
14099
+ });
14100
+ continue;
14101
+ }
14102
+ docsConfig = {
14103
+ ...docsConfig,
14104
+ colors: { ...docsConfig.colors, [mode]: { ...docsConfig.colors?.[mode], background: hex } }
14105
+ };
14106
+ }
13719
14107
  }
13720
14108
  }
13721
14109
  if (platform === "docusaurus") {
@@ -13725,13 +14113,23 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
13725
14113
  ...docsConfig,
13726
14114
  favicon: { light: faviconLight, ...faviconDark ? { dark: faviconDark } : {} }
13727
14115
  };
13728
- const missingBrand = [
13729
- ...[logo.light, logo.dark].filter((value) => typeof value === "string" && !publicBrandPath(value)),
13730
- ...[favicon.light, favicon.dark].filter((value) => typeof value === "string" && !publicBrandPath(value))
13731
- ];
14116
+ const notImported = (variants) => [variants.light, variants.dark].filter((value) => typeof value === "string" && !publicBrandPath(value));
14117
+ const missingLogo = notImported(logo);
14118
+ const missingFavicon = notImported(favicon);
14119
+ const missingBrand = [...missingLogo, ...missingFavicon];
14120
+ const substituted = [
14121
+ ...missingLogo.length > 0 && logoLight ? [`logo (${logoLight})`] : [],
14122
+ ...missingFavicon.length > 0 && faviconLight ? [`favicon (${faviconLight})`] : []
14123
+ ].map((asset) => ` The imported ${asset} is used for both light and dark mode.`).join("");
13732
14124
  if (missingBrand.length > 0) warnings.push({
13733
14125
  code: "unsupported-config",
13734
- message: `Brand asset(s) were referenced but not imported: ${[...new Set(missingBrand)].join(", ")}. Add these files under public/ or update docs.json.`
14126
+ message: `Brand asset(s) were referenced but not imported: ${[...new Set(missingBrand)].join(", ")}. Add these files under public/ or update docs.json.${substituted}`
14127
+ });
14128
+ const logoHref = sourceLogo && typeof sourceLogo === "object" ? sourceLogo.href : void 0;
14129
+ if (typeof logoHref === "string" && logoHref.trim() && logoHref.trim() !== "/") warnings.push({
14130
+ code: "unsupported-config",
14131
+ message: `logo.href ${JSON.stringify(logoHref.trim().slice(0, 120))} was dropped: the header logo always links to the docs home page. Add it as a navbar link in docs.json if readers need it.`,
14132
+ source: mintlifyConfig ? "docs.json" : "docs.yml"
13735
14133
  });
13736
14134
  if (platform === "mintlify") {
13737
14135
  docsConfig = { ...docsConfig, markdown: { enabled: true } };
@@ -13764,7 +14162,7 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
13764
14162
  message: `Fern global theme ${JSON.stringify(fernRawConfig["global-theme"])} could not be fetched during local migration; local overrides and Thally default styling tokens were applied.`,
13765
14163
  source: "global-theme"
13766
14164
  });
13767
- if (fernRawConfig?.logo && !fernRawConfig["global-theme"] && !logoLight && !logo.light) warnings.push({
14165
+ if (fernRawConfig?.logo && !fernRawConfig["global-theme"] && !logoLight && !logo.light && !logo.dark) warnings.push({
13768
14166
  code: "unsupported-config",
13769
14167
  message: "Fern supplied a logo through its global theme without a local asset; add a logo path to docs.json after import."
13770
14168
  });
@@ -15714,8 +16112,9 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
15714
16112
  if (ref?.specRef === reference.url) page.openapi = withSpecRef(ref, `openapi/${filename}`);
15715
16113
  }
15716
16114
  const tab = reference.tabLabel ? tabs.find((candidate) => candidate.tab === reference.tabLabel) : tabs.find((candidate) => candidate.tab.toLowerCase().includes("api"));
15717
- if (tab) tab.api = { source: `openapi/${filename}` };
15718
- else insertApiTab(tabs, { tab: reference.tabLabel ?? "API Reference", ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, api: { source: `openapi/${filename}` } }, reference.parentTab);
16115
+ const api = { source: `openapi/${filename}`, ...reference.navigation === false ? { navigation: false } : {}, ...reference.groups ? { groups: reference.groups } : {} };
16116
+ if (tab) tab.api = api;
16117
+ else insertApiTab(tabs, { tab: reference.tabLabel ?? "API Reference", ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, api }, reference.parentTab);
15719
16118
  bound.push({ reference, filename, content: body, operations, tabName: tab?.tab ?? reference.tabLabel ?? "API Reference" });
15720
16119
  } catch (error) {
15721
16120
  warnings.push({