@thallylabs/migrate 0.2.34 → 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/README.md CHANGED
@@ -92,6 +92,40 @@ instead of silently dropping that API tab's reference. AsyncAPI has no Thally
92
92
  renderer, so it always produces a clear warning naming the spec instead of
93
93
  disappearing.
94
94
 
95
+ `METHOD /path` entries (`"POST /v2/keys.createKey"`) under a container that
96
+ binds an `openapi` spec become `api.groups` on the tab that spec binds to:
97
+ nested groups, icons and authored order are kept, and page entries stay in the
98
+ tab's own groups (a group that mixes pages and operations is split, so the
99
+ sidebar lists the API groups after the page groups). Operations written
100
+ straight under a tab form a group named after the tab, at their place among the
101
+ tab's own groups (each uninterrupted run is its own group, so authored order is
102
+ kept). Hidden groups (and containers under one), groups under access-restricted
103
+ containers, and groups left empty are left out. Groups nest at most 32 levels
104
+ deep, as the renderer follows no deeper: operations below that are not migrated,
105
+ and one warning says so. With `navigation.languages`, only the default language's
106
+ containers contribute groups; operation entries under another language are
107
+ reported, since its API tab is the default language's.
108
+ When several containers bind one spec to the same tab (one API tab per product,
109
+ say), their groups are concatenated in order and an operation already listed
110
+ earlier in that tab is dropped (the method is compared without case, the path
111
+ exactly), unless one of those containers lists no operation (Mintlify generated
112
+ every operation there), in which case the tab gets no `api.groups` and keeps the
113
+ generated, tag-grouped navigation. A container whose operations are all hidden or
114
+ access-restricted adds no groups but still counts as listing operations; when
115
+ every container bound to a tab is like that, the tab gets `navigation: false`
116
+ (the source sidebar showed none of those operations, so no generated list is
117
+ added). Operations dropped only for nesting too deep are Thally's limit, not the
118
+ author's choice: such a container counts as listing nothing, so the tab keeps
119
+ the generated navigation. Groups with the same title are not folded together. With `api.groups`
120
+ the authored list is the tab's navigation, so `navigation: false` is not
121
+ written. Operation entries with no `openapi` container above them, and every one in a
122
+ site imported by URL, cannot be placed: one warning says they were not added to the API navigation, and the
123
+ operations still have their pages. This is separate from a `directory`'s flat
124
+ operation list, which still only drives the endpoint redirects.
125
+
126
+ Mintlify `styling.eyebrows: "breadcrumbs"` becomes `eyebrow: "path"` (the full
127
+ group path above the page title); `"section"` or no setting writes nothing.
128
+
95
129
  Page `openapi:` frontmatter with a spec prefix
96
130
  (`openapi: "specs/openapi-b.yaml GET /widgets/{id}"`) is rewritten to where the
97
131
  spec lands in the output (`/openapi-b.yaml GET /widgets/{id}`). As in Mintlify,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { M as MigrationPage, a as MigrationAsset, b as MigrationNavigationGroup, R as RenderedMigrationFile, c as MigrationWarning, d as MigrationFetcher, e as MigrationPlatform, f as MigrationBundle, g as MigrationDocsConfig } from './url-migration-EwtII4P-.js';
2
- export { h as MigrationCapture, i as MigrationCaptureProvider, j as MigrationCaptureState, k as MigrationCapturedAsset, l as MigrationEvidence, m as MigrationFetchRequest, n as MigrationFetchResponse, o as MigrationJournalAsset, p as MigrationNavigationOccurrence, q as MigrationNavigationTab, U as UrlMigrationCheckpoint, r as UrlMigrationCheckpointDelta, s as UrlMigrationIssue, t as UrlMigrationRecord, u as UrlMigrationRun, v as UrlMigrationRunOptions, w as UrlMigrationValidation, x as classifyMigrationResponse, y as mergeMigrationConfig, z as renderMigrationFiles, A as runUrlMigration, B as validateUrlMigration } from './url-migration-EwtII4P-.js';
1
+ import { M as MigrationPage, a as MigrationAsset, b as MigrationNavigationGroup, R as RenderedMigrationFile, c as MigrationWarning, d as MigrationFetcher, e as MigrationPlatform, f as MigrationBundle, g as MigrationDocsConfig } from './url-migration-peVzTLjL.js';
2
+ export { h as MigrationCapture, i as MigrationCaptureProvider, j as MigrationCaptureState, k as MigrationCapturedAsset, l as MigrationEvidence, m as MigrationFetchRequest, n as MigrationFetchResponse, o as MigrationJournalAsset, p as MigrationNavigationOccurrence, q as MigrationNavigationTab, U as UrlMigrationCheckpoint, r as UrlMigrationCheckpointDelta, s as UrlMigrationIssue, t as UrlMigrationRecord, u as UrlMigrationRun, v as UrlMigrationRunOptions, w as UrlMigrationValidation, x as classifyMigrationResponse, y as mergeMigrationConfig, z as renderMigrationFiles, A as runUrlMigration, B as validateUrlMigration } from './url-migration-peVzTLjL.js';
3
3
  export { isPassiveSvg } from './svg-policy.js';
4
4
 
5
5
  /**
@@ -182,6 +182,11 @@ interface MintlifyProjectionOptions {
182
182
  * when the repository was not mapped with `--source-ref`.
183
183
  */
184
184
  resolveSourceRef?: (repo: string) => Array<string | MigrationNavigationGroup> | null;
185
+ /**
186
+ * Set by a caller that binds the authored operation lists (`mintlifyNavigationApiReferences`
187
+ * `groups`) to their API tabs. Without it every "METHOD /path" entry is reported as not carried over.
188
+ */
189
+ operationGroups?: boolean;
185
190
  }
186
191
  /** Preserve Mintlify's implicit `/section` route for section landing pages. */
187
192
  declare function addMintlifyDirectoryRedirects(config: MigrationDocsConfig, pages: Array<MigrationPage>): MigrationDocsConfig;
package/dist/index.js CHANGED
@@ -4524,39 +4524,106 @@ function specSource(value) {
4524
4524
  }
4525
4525
  function listedOperations(container) {
4526
4526
  const operations = [];
4527
- const visit = (entry, root) => {
4527
+ const visit = (entry, depth) => {
4528
4528
  if (typeof entry === "string") {
4529
4529
  if (OPENAPI_OPERATION_REF.test(entry)) operations.push(entry.trim());
4530
4530
  return;
4531
4531
  }
4532
4532
  const object2 = objectValue2(entry);
4533
- 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;
4534
4534
  for (const key of ["groups", "pages"]) {
4535
- 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));
4536
4536
  }
4537
4537
  };
4538
- visit(container, true);
4538
+ visit(container, 0);
4539
4539
  return operations;
4540
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
+ }
4541
4595
  var hasNavigationChildren = (node) => ["groups", "pages"].some((key) => Array.isArray(node[key]) && node[key].length > 0);
4542
4596
  function mintlifyNavigationApiReferences(config) {
4543
4597
  const navigation = objectValue2(config.navigation) ?? config;
4544
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
+ );
4545
4605
  const containerKeys = ["tabs", "anchors", "products", "dropdowns", "versions", "menus", "languages"];
4546
4606
  const nestedKeys = ["groups", "pages"];
4547
- function collect(node, tabLabel, sibling) {
4607
+ function collect(node, tabLabel, gated, otherLanguage, hidden, sibling) {
4548
4608
  for (const kind of ["openapi", "asyncapi"]) {
4549
4609
  const value = specSource(node[kind]);
4550
4610
  const directory = objectValue2(node[kind])?.directory;
4551
4611
  if (value) {
4552
- 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);
4553
4617
  references.push({
4554
4618
  value,
4555
4619
  kind,
4556
4620
  tabLabel,
4557
4621
  ...sibling,
4558
4622
  ...typeof directory === "string" && directory.trim() ? { directory: directory.trim() } : {},
4559
- ...operations.length > 0 ? { operations } : {}
4623
+ ...operations.length > 0 ? { operations } : {},
4624
+ ...groups.length > 0 ? { groups } : {},
4625
+ ...operationsWithheld ? { operationsWithheld: true } : {},
4626
+ ...otherLanguage ? { otherLanguage: true } : {}
4560
4627
  });
4561
4628
  }
4562
4629
  }
@@ -4568,7 +4635,7 @@ function mintlifyNavigationApiReferences(config) {
4568
4635
  siblingSources.set(candidate, source);
4569
4636
  return candidate;
4570
4637
  }
4571
- function visitChildren(rawNode, tabLabel) {
4638
+ function visitChildren(rawNode, tabLabel, gated, otherLanguage, hidden) {
4572
4639
  const node = withFlattenedProductGroups(rawNode);
4573
4640
  for (const key of containerKeys) {
4574
4641
  const entries = node[key];
@@ -4576,7 +4643,7 @@ function mintlifyNavigationApiReferences(config) {
4576
4643
  for (const entry of entries) {
4577
4644
  const object2 = objectValue2(entry);
4578
4645
  if (!object2) continue;
4579
- visit(object2, labelFor(object2, tabLabel ?? "Documentation"));
4646
+ visit(object2, labelFor(object2, tabLabel ?? "Documentation"), gated, otherLanguage || key === "languages" && rawNode === navigation && object2 !== defaultLanguage, hidden);
4580
4647
  }
4581
4648
  }
4582
4649
  for (const key of nestedKeys) {
@@ -4585,7 +4652,7 @@ function mintlifyNavigationApiReferences(config) {
4585
4652
  for (const entry of entries) {
4586
4653
  if (typeof entry === "string") continue;
4587
4654
  const object2 = objectValue2(entry);
4588
- if (object2) visit(object2, tabLabel);
4655
+ if (object2) visit(object2, tabLabel, gated, otherLanguage, hidden || object2.hidden === true);
4589
4656
  }
4590
4657
  }
4591
4658
  if (Array.isArray(node.menu)) {
@@ -4597,24 +4664,26 @@ function mintlifyNavigationApiReferences(config) {
4597
4664
  const apiItems = items.filter((item) => specSource(item.openapi) || specSource(item.asyncapi));
4598
4665
  const soleContent = apiItems.length === 1 && !hasNavigationChildren(node) && !items.some(hasNavigationChildren);
4599
4666
  items.forEach((item, index) => {
4600
- 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);
4601
4669
  else if (apiItems.includes(item)) {
4602
4670
  const label = uniqueSiblingLabel(
4603
4671
  `${tab}: ${labelFor(item, `Menu item ${index + 1}`)}`,
4604
4672
  `${specSource(item.openapi) ?? ""}|${specSource(item.asyncapi) ?? ""}`
4605
4673
  );
4606
4674
  const icon = iconName(item.icon);
4607
- 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 } : {} });
4608
4676
  }
4609
- visitChildren(item, tabLabel);
4677
+ visitChildren(item, tabLabel, itemGated, otherLanguage, hidden);
4610
4678
  });
4611
4679
  }
4612
4680
  }
4613
- function visit(node, tabLabel) {
4614
- collect(node, tabLabel);
4615
- 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);
4616
4685
  }
4617
- visit(navigation, void 0);
4686
+ visit(navigation, void 0, false, false, false);
4618
4687
  return references;
4619
4688
  }
4620
4689
  function insertApiTab(tabs, tab, parentTab) {
@@ -4637,6 +4706,17 @@ function normalizePageRef(value, pathPrefix = "") {
4637
4706
  ref = ref.replace(/\.(?:mdx?|rst|txt)$/i, "");
4638
4707
  return pageIdFromReference(ref, true);
4639
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
+ }
4640
4720
  var SWEEP_DEPTH_LIMIT = 256;
4641
4721
  function collectGatedSourceRefs(node, gate, out, limits, depth = 0) {
4642
4722
  if (depth > SWEEP_DEPTH_LIMIT) {
@@ -4670,15 +4750,24 @@ function collectReferenceAccess(node, access, pathPrefix, out, depth = 0) {
4670
4750
  }
4671
4751
  for (const value of Object.values(object2)) collectReferenceAccess(value, nested, pathPrefix, out, depth + 1);
4672
4752
  }
4673
- function withContainerAccess(object2, context, walk2) {
4753
+ function withContainerAccess(object2, context, walk2, nested = false) {
4674
4754
  const outer = context.access;
4675
4755
  context.access = descendNavigationAccess(outer, navigationAccess(object2));
4676
4756
  try {
4677
- return walk2();
4757
+ return withOperationScope(object2, context, nested, walk2);
4678
4758
  } finally {
4679
4759
  context.access = outer;
4680
4760
  }
4681
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
+ }
4682
4771
  function warnOnce(context, key, message) {
4683
4772
  if (context.warningKeys.has(key)) return;
4684
4773
  context.warningKeys.add(key);
@@ -4716,11 +4805,32 @@ var OPENAPI_OPERATION_REF = /^(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+\S/;
4716
4805
  function convertPage(value, context) {
4717
4806
  if (typeof value === "string") {
4718
4807
  if (OPENAPI_OPERATION_REF.test(value)) {
4719
- warnOnce(
4720
- context,
4721
- "openapi-operation-list",
4722
- '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.'
4723
- );
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
+ }
4724
4834
  return null;
4725
4835
  }
4726
4836
  return registerReference(value, context);
@@ -4831,10 +4941,10 @@ function convertMenuItems(items, context) {
4831
4941
  `Mintlify menu item "${label}" has an openapi/asyncapi value without a "source", so its API reference was not migrated.`
4832
4942
  );
4833
4943
  }
4834
- const children = convertNavigationValues([
4944
+ const children = withOperationScope(item, context, true, () => convertNavigationValues([
4835
4945
  ...Array.isArray(item.groups) ? item.groups : [],
4836
4946
  ...Array.isArray(item.pages) ? item.pages : []
4837
- ], context);
4947
+ ], context));
4838
4948
  if (typeof item.href === "string" && item.href) {
4839
4949
  warnOnce(
4840
4950
  context,
@@ -4894,7 +5004,7 @@ function convertContainerToTabs(containerValue, context, fallbackTab, trace, dep
4894
5004
  ...object2.hidden === true ? { hidden: true } : {}
4895
5005
  }];
4896
5006
  }
4897
- 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);
4898
5008
  if (nested.length > 0) {
4899
5009
  if (nested.length === 1) {
4900
5010
  return [{
@@ -5118,6 +5228,8 @@ function projectedCompatibleConfig(config, warnings) {
5118
5228
  return {
5119
5229
  // Mintlify pages show a group eyebrow above the title and no breadcrumb trail.
5120
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" } : {},
5121
5233
  ...projectedTheme(config.theme) ? { theme: projectedTheme(config.theme) } : {},
5122
5234
  ...projectedContextual.length > 0 ? { contextual: { options: projectedContextual } } : {},
5123
5235
  ...projectedColorsValue ? { colors: projectedColorsValue } : {},
@@ -5263,6 +5375,16 @@ function mintlifyAllVersionPrefixes(config) {
5263
5375
  collect(navigation);
5264
5376
  return prefixes;
5265
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
+ }
5266
5388
  function projectMintlifyNavigation(config, options = {}) {
5267
5389
  const warnings = [];
5268
5390
  const references = [];
@@ -5273,6 +5395,8 @@ function projectMintlifyNavigation(config, options = {}) {
5273
5395
  const gatedNodes = /* @__PURE__ */ new Map();
5274
5396
  const navigation = objectValue2(config.navigation) ?? config;
5275
5397
  const rootAccess = descendNavigationAccess(OPEN_NAVIGATION_ACCESS, navigationAccess(navigation));
5398
+ const rootOperations = descendOperationScope(UNBOUND_OPERATIONS, navigation, true);
5399
+ const operationGroups2 = options.operationGroups === true;
5276
5400
  const limits = { tooDeep: false };
5277
5401
  collectGatedSourceRefs(navigation, void 0, gatedNodes, limits);
5278
5402
  if (limits.tooDeep) {
@@ -5284,20 +5408,16 @@ function projectMintlifyNavigation(config, options = {}) {
5284
5408
  const languages = Array.isArray(navigation.languages) ? navigation.languages.flatMap((value) => {
5285
5409
  const language = objectValue2(value);
5286
5410
  if (!language) return [];
5287
- try {
5288
- const code = Intl.getCanonicalLocales(String(language.language ?? language.locale ?? "en"))[0];
5289
- return [{ ...language, language: code }];
5290
- } catch {
5291
- warnings.push({ code: "unsupported-config", message: "A navigation language with an invalid locale code was skipped." });
5292
- return [];
5293
- }
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] : [];
5294
5414
  }) : [];
5415
+ const defaultLanguage = pickDefaultLanguage(languages);
5295
5416
  let tabs = [];
5296
5417
  let i18n;
5297
5418
  const localizedNavigation = {};
5298
5419
  const projectionTrace = {};
5299
- if (languages.length > 0) {
5300
- const defaultLanguage = languages.find((entry) => entry.default === true) ?? languages[0];
5420
+ if (defaultLanguage) {
5301
5421
  const defaultLocale = String(defaultLanguage.language);
5302
5422
  const locales = languages.map((entry) => {
5303
5423
  const code = String(entry.language);
@@ -5323,6 +5443,9 @@ function projectMintlifyNavigation(config, options = {}) {
5323
5443
  warningKeys,
5324
5444
  appearances,
5325
5445
  access: descendNavigationAccess(rootAccess, navigationAccess(language)),
5446
+ operations: descendOperationScope(rootOperations, language, true),
5447
+ operationGroups: operationGroups2,
5448
+ otherLanguage: language !== defaultLanguage,
5326
5449
  gatedSourceRefs,
5327
5450
  gatedNodes,
5328
5451
  limits
@@ -5342,9 +5465,10 @@ function projectMintlifyNavigation(config, options = {}) {
5342
5465
  }
5343
5466
  if (Object.keys(localizedNavigation).length > 0) i18n.navigation = localizedNavigation;
5344
5467
  } else {
5345
- 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 };
5346
5469
  tabs = convertContainerToTabs(navigation, context, "Documentation", projectionTrace);
5347
5470
  if (tabs.length === 0 && Array.isArray(config.navigation)) {
5471
+ context.operations = UNBOUND_OPERATIONS;
5348
5472
  const children = convertNavigationValues(config.navigation, context);
5349
5473
  if (children.length > 0) {
5350
5474
  const hasRootPages = children.some((page) => typeof page === "string");
@@ -5361,7 +5485,7 @@ function projectMintlifyNavigation(config, options = {}) {
5361
5485
  message: "Mintlify navigation could not be projected; generated navigation will be used."
5362
5486
  });
5363
5487
  }
5364
- const versionOwner = languages.length > 0 ? languages.find((entry) => entry.default === true) ?? languages[0] : navigation;
5488
+ const versionOwner = defaultLanguage ?? navigation;
5365
5489
  const globalNavigation = objectValue2(versionOwner.global) ?? objectValue2(navigation.global);
5366
5490
  const shortcuts = Array.isArray(globalNavigation?.anchors) ? globalNavigation.anchors.flatMap((value) => {
5367
5491
  const anchor = objectValue2(value);
@@ -7354,13 +7478,13 @@ function refKeepsIn(ref, spec, specs) {
7354
7478
  const { unique, sources } = resolvedSources(ref, specs);
7355
7479
  return unique && sources[0] === spec.sourcePath;
7356
7480
  }
7357
- function operationKey(operation) {
7481
+ function operationKey2(operation) {
7358
7482
  const [first, ...rest] = operation.trim().split(/\s+/);
7359
7483
  return `${first.toLowerCase()} ${rest.join(" ")}`;
7360
7484
  }
7361
7485
  function keysFor(refs, names) {
7362
7486
  return [...new Set(
7363
- refs.filter(names).map((ref) => splitOpenApiRef(ref)?.operation).filter((op) => !!op).map(operationKey)
7487
+ refs.filter(names).map((ref) => splitOpenApiRef(ref)?.operation).filter((op) => !!op).map(operationKey2)
7364
7488
  )];
7365
7489
  }
7366
7490
  function withheldOperationKeys(spec, withheldRefs, keptRefs, specs = [spec]) {
@@ -10830,14 +10954,28 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, n
10830
10954
  delete mount.operations;
10831
10955
  }
10832
10956
  };
10957
+ const listsNothing = (reference) => !reference.groups && !reference.operationsWithheld && !reference.otherLanguage;
10958
+ const groupTargets = /* @__PURE__ */ new Map();
10833
10959
  const specs = [];
10834
10960
  for (const reference of references) {
10835
10961
  const dedupeKey = `${reference.kind}:${reference.value}:${reference.tabLabel ?? ""}`;
10836
10962
  if (seen.has(dedupeKey)) {
10837
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
+ }
10838
10975
  continue;
10839
10976
  }
10840
10977
  seen.add(dedupeKey);
10978
+ const groups = mergeApiGroups(void 0, reference.groups);
10841
10979
  const tabSuffix = reference.tabLabel ? ` (tab "${reference.tabLabel}")` : "";
10842
10980
  if (reference.kind === "asyncapi") {
10843
10981
  warnings.push({
@@ -10847,10 +10985,11 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, n
10847
10985
  continue;
10848
10986
  }
10849
10987
  if (/^https:\/\//i.test(reference.value)) {
10850
- 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 } : {} };
10851
10989
  mountDirectory(remote, reference);
10852
10990
  remoteSpecs.push(remote);
10853
10991
  remoteByKey.set(dedupeKey, remote);
10992
+ groupTargets.set(dedupeKey, { target: remote, unlisted: listsNothing(reference), withheld: reference.operationsWithheld === true });
10854
10993
  warnings.push({
10855
10994
  code: "unsupported-config",
10856
10995
  message: `The remote OpenAPI spec "${reference.value}"${tabSuffix} requires a network download before this import is complete.`,
@@ -10879,7 +11018,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, n
10879
11018
  }
10880
11019
  const content = readSpecFile(match.absolutePath, match.relativePath, warnings);
10881
11020
  if (!content) continue;
10882
- specs.push({
11021
+ const spec = {
10883
11022
  filename: basename3(match.relativePath),
10884
11023
  content,
10885
11024
  tabLabel: reference.tabLabel,
@@ -10888,8 +11027,14 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, n
10888
11027
  hidden: reference.hidden,
10889
11028
  ...reference.pageOnly ? { pageOnly: true } : {},
10890
11029
  sourcePath: match.relativePath,
10891
- directory: reference.directory
10892
- });
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 });
11035
+ }
11036
+ for (const { target: target2, unlisted, withheld } of groupTargets.values()) {
11037
+ if (withheld && !unlisted && !target2.groups) target2.navigation = false;
10893
11038
  }
10894
11039
  for (const spec of specs) spec.filename = nameSpec(spec);
10895
11040
  return specs;
@@ -12255,15 +12400,18 @@ function injectOpenApiSpecs(config, specs, warnings) {
12255
12400
  // An API-only tab needs generated endpoint navigation; an authored
12256
12401
  // page tab keeps its own groups alongside the bound spec, unless
12257
12402
  // Mintlify generated the endpoint pages (the renderer appends the
12258
- // endpoint groups after the authored ones).
12259
- ...(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 } : {}
12260
12408
  };
12261
12409
  } else {
12262
12410
  insertApiTab(tabs, {
12263
12411
  tab: spec.tabLabel ?? "API Reference",
12264
12412
  ...spec.icon ? { icon: spec.icon } : {},
12265
12413
  ...spec.hidden ? { hidden: true } : {},
12266
- api: { source: specAssetPath(spec.filename) }
12414
+ api: { source: specAssetPath(spec.filename), ...spec.navigation === false ? { navigation: false } : {}, ...spec.groups ? { groups: spec.groups } : {} }
12267
12415
  }, spec.parentTab);
12268
12416
  }
12269
12417
  }
@@ -12348,6 +12496,8 @@ function migrateRepository(options) {
12348
12496
  defaultVersionPrefixes = mintlifyDefaultVersionPrefixes(config);
12349
12497
  allVersionPrefixes = mintlifyAllVersionPrefixes(config);
12350
12498
  const projected = projectMintlifyNavigation(config, {
12499
+ // injectOpenApiSpecs / hydrateRemoteApiSpecs bind the authored operation lists to their API tabs.
12500
+ operationGroups: true,
12351
12501
  resolveSourceRef: (repo) => {
12352
12502
  const ref = activeSourceRefs.find((candidate) => candidate.repo.toLowerCase() === repo.toLowerCase());
12353
12503
  if (!ref) return null;
@@ -13489,7 +13639,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13489
13639
  const result = excludeWithheldOperations(spec, withheldSpecRefs, keptSpecRefs, allSpecs);
13490
13640
  spec.content = result.content;
13491
13641
  withheldOperations.set(spec, result.excluded);
13492
- result.excluded.forEach((operation) => withheldKeys.add(operationKey(operation)));
13642
+ result.excluded.forEach((operation) => withheldKeys.add(operationKey2(operation)));
13493
13643
  }
13494
13644
  const resolvedSpecs = allSpecs.filter((spec) => !spec.pageOnly);
13495
13645
  const pageOnlySpecs = allSpecs.filter((spec) => spec.pageOnly);
@@ -13531,7 +13681,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13531
13681
  const fallbackSpec = { sourcePath: fallback.relativePath, filename, content: fallbackContent };
13532
13682
  ({ kept: keptSpecRefs, unowned: unownedOperations } = keptOperations(fallback.relativePath));
13533
13683
  const pruned = excludeWithheldOperations(fallbackSpec, withheldSpecRefs, keptSpecRefs, [fallbackSpec, ...allSpecs]);
13534
- pruned.excluded.forEach((operation) => withheldKeys.add(operationKey(operation)));
13684
+ pruned.excluded.forEach((operation) => withheldKeys.add(operationKey2(operation)));
13535
13685
  if (!assets.some((asset) => asset.path === specAssetPath(filename))) {
13536
13686
  assets.push(specAsset(filename, pruned.content));
13537
13687
  }
@@ -13549,7 +13699,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13549
13699
  docsConfig = injectOpenApiSpecs(docsConfig, pageOnlySpecs, warnings);
13550
13700
  pageSpecs = [...pageSpecs, ...pageOnlySpecs];
13551
13701
  }
13552
- const notKeptPublic = [...new Set(unownedOperations.filter((operation) => withheldKeys.has(operationKey(operation))))];
13702
+ const notKeptPublic = [...new Set(unownedOperations.filter((operation) => withheldKeys.has(operationKey2(operation))))];
13553
13703
  if (notKeptPublic.length > 0) {
13554
13704
  warnings.push({
13555
13705
  code: "gated-page",
@@ -15962,8 +16112,9 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
15962
16112
  if (ref?.specRef === reference.url) page.openapi = withSpecRef(ref, `openapi/${filename}`);
15963
16113
  }
15964
16114
  const tab = reference.tabLabel ? tabs.find((candidate) => candidate.tab === reference.tabLabel) : tabs.find((candidate) => candidate.tab.toLowerCase().includes("api"));
15965
- if (tab) tab.api = { source: `openapi/${filename}` };
15966
- 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);
15967
16118
  bound.push({ reference, filename, content: body, operations, tabName: tab?.tab ?? reference.tabLabel ?? "API Reference" });
15968
16119
  } catch (error) {
15969
16120
  warnings.push({