@fern-api/fdr-sdk 1.2.39-cc5ce5c5b6 → 1.2.39-dc3805e7f9
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/js/client/FdrClient.js +1 -0
- package/dist/js/client/FdrClient.js.map +1 -1
- package/dist/js/client/FdrClient.mjs +1 -0
- package/dist/js/client/FdrClient.mjs.map +1 -1
- package/dist/js/converters/index.js +1 -6
- package/dist/js/converters/index.js.map +1 -1
- package/dist/js/converters/index.mjs +1 -6
- package/dist/js/converters/index.mjs.map +1 -1
- package/dist/js/index.js +2 -6
- package/dist/js/index.js.map +1 -1
- package/dist/js/index.mjs +2 -6
- package/dist/js/index.mjs.map +1 -1
- package/dist/js/navigation/index.js +1 -6
- package/dist/js/navigation/index.js.map +1 -1
- package/dist/js/navigation/index.mjs +1 -6
- package/dist/js/navigation/index.mjs.map +1 -1
- package/dist/js/navigation/ledger-root-builder.js +228 -58
- package/dist/js/navigation/ledger-root-builder.js.map +1 -1
- package/dist/js/navigation/ledger-root-builder.mjs +228 -58
- package/dist/js/navigation/ledger-root-builder.mjs.map +1 -1
- package/dist/js/orpc-client.js +1 -0
- package/dist/js/orpc-client.js.map +1 -1
- package/dist/js/orpc-client.mjs +1 -0
- package/dist/js/orpc-client.mjs.map +1 -1
- package/dist/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.js +85 -0
- package/dist/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.js +85 -0
- package/dist/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.landingPage.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.landingPage.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.landingPage.test.js +82 -0
- package/dist/navigation/__test__/ledger-root-builder.landingPage.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.js +1031 -0
- package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.rootChildOrder.test.js +22 -9
- package/dist/navigation/__test__/ledger-root-builder.rootChildOrder.test.js.map +1 -1
- package/dist/navigation/__test__/ledger-root-builder.rootSlug.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.rootSlug.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.rootSlug.test.js +47 -0
- package/dist/navigation/__test__/ledger-root-builder.rootSlug.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.skipSlugTabs.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.skipSlugTabs.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.skipSlugTabs.test.js +284 -0
- package/dist/navigation/__test__/ledger-root-builder.skipSlugTabs.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.variantSharedSegments.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.variantSharedSegments.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.variantSharedSegments.test.js +262 -0
- package/dist/navigation/__test__/ledger-root-builder.variantSharedSegments.test.js.map +1 -0
- package/dist/navigation/ledger-root-builder.d.ts +12 -0
- package/dist/navigation/ledger-root-builder.d.ts.map +1 -1
- package/dist/navigation/ledger-root-builder.js +298 -69
- package/dist/navigation/ledger-root-builder.js.map +1 -1
- package/dist/navigation/utils/findNode.d.ts.map +1 -1
- package/dist/navigation/utils/findNode.js +0 -8
- package/dist/navigation/utils/findNode.js.map +1 -1
- package/dist/navigation/utils/followRedirect.d.ts.map +1 -1
- package/dist/navigation/utils/followRedirect.js +2 -0
- package/dist/navigation/utils/followRedirect.js.map +1 -1
- package/dist/orpc-client/docs-ledger/contract.d.ts +50 -0
- package/dist/orpc-client/docs-ledger/contract.d.ts.map +1 -1
- package/dist/orpc-client/docs-ledger/contract.js +1 -0
- package/dist/orpc-client/docs-ledger/contract.js.map +1 -1
- package/dist/orpc-client/docs-ledger/ledger-manifest.d.ts +12 -0
- package/dist/orpc-client/docs-ledger/ledger-manifest.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.landingPage.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.landingPage.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.metadataPreservation.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.metadataPreservation.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.rootSlug.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.rootSlug.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.skipSlugTabs.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.skipSlugTabs.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.variantSharedSegments.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.variantSharedSegments.test.d.ts.map +1 -0
- package/dist/types/navigation/ledger-root-builder.d.ts +12 -0
- package/dist/types/navigation/ledger-root-builder.d.ts.map +1 -1
- package/dist/types/navigation/utils/findNode.d.ts.map +1 -1
- package/dist/types/navigation/utils/followRedirect.d.ts.map +1 -1
- package/dist/types/orpc-client/docs-ledger/contract.d.ts +50 -0
- package/dist/types/orpc-client/docs-ledger/contract.d.ts.map +1 -1
- package/dist/types/orpc-client/docs-ledger/ledger-manifest.d.ts +12 -0
- package/dist/types/orpc-client/docs-ledger/ledger-manifest.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -330,6 +330,63 @@ function apiLeafNodeFromNavRouteInternal(ctx, route, segmentApiDefinitionId) {
|
|
|
330
330
|
}
|
|
331
331
|
return undefined;
|
|
332
332
|
}
|
|
333
|
+
function apiLeafPositionKey(node) {
|
|
334
|
+
switch (node.type) {
|
|
335
|
+
case "endpoint":
|
|
336
|
+
return `endpoint:${node.endpointId}`;
|
|
337
|
+
case "webhook":
|
|
338
|
+
return `webhook:${node.webhookId}`;
|
|
339
|
+
case "webSocket":
|
|
340
|
+
return `webSocket:${node.webSocketId}`;
|
|
341
|
+
case "grpc":
|
|
342
|
+
return `grpc:${node.grpcId}`;
|
|
343
|
+
case "graphql":
|
|
344
|
+
return `graphql:${node.graphqlOperationId}`;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function apiChildPositionKey(node) {
|
|
348
|
+
switch (node.type) {
|
|
349
|
+
case "endpoint":
|
|
350
|
+
case "webhook":
|
|
351
|
+
case "webSocket":
|
|
352
|
+
case "grpc":
|
|
353
|
+
case "graphql":
|
|
354
|
+
return apiLeafPositionKey(node);
|
|
355
|
+
default:
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function positionedApiLeafChildren(ctx, nav, apiDefinitionId) {
|
|
360
|
+
const rawLeafNodes = [];
|
|
361
|
+
const leafPositions = new Map();
|
|
362
|
+
const nonApiLeafNodes = [];
|
|
363
|
+
for (let i = 0; i < nav.length; i++) {
|
|
364
|
+
const route = nav[i];
|
|
365
|
+
const pos = route.displaySortOrder ?? i;
|
|
366
|
+
const apiLeaf = apiLeafNodeFromNavRouteInternal(ctx, route, apiDefinitionId);
|
|
367
|
+
if (apiLeaf != null) {
|
|
368
|
+
rawLeafNodes.push(apiLeaf);
|
|
369
|
+
const key = apiLeafPositionKey(apiLeaf);
|
|
370
|
+
leafPositions.set(key, Math.min(leafPositions.get(key) ?? pos, pos));
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const node = artifactToNode(ctx, route);
|
|
374
|
+
if (node?.type === "page" || node?.type === "link") {
|
|
375
|
+
nonApiLeafNodes.push({ node, pos });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const apiLeafNodes = mergeEndpointPairs(ctx, rawLeafNodes, nav);
|
|
379
|
+
const positionedApiLeafNodes = apiLeafNodes.map((node, index) => {
|
|
380
|
+
if (node.type === "endpointPair") {
|
|
381
|
+
const streamPos = leafPositions.get(apiLeafPositionKey(node.stream)) ?? index;
|
|
382
|
+
const nonStreamPos = leafPositions.get(apiLeafPositionKey(node.nonStream)) ?? index;
|
|
383
|
+
return { node, pos: Math.min(streamPos, nonStreamPos) };
|
|
384
|
+
}
|
|
385
|
+
const key = apiChildPositionKey(node);
|
|
386
|
+
return { node, pos: key != null ? (leafPositions.get(key) ?? index) : index };
|
|
387
|
+
});
|
|
388
|
+
return [...positionedApiLeafNodes, ...nonApiLeafNodes];
|
|
389
|
+
}
|
|
333
390
|
/**
|
|
334
391
|
* Collect every endpoint/webhook navigation node from a single API segment's
|
|
335
392
|
* nav routes. The root builder now populates apiReference children directly,
|
|
@@ -524,14 +581,10 @@ function sectionNode(ctx, scoped, sectionChildren, scopePrefix) {
|
|
|
524
581
|
return {
|
|
525
582
|
type: "section",
|
|
526
583
|
...defaults,
|
|
527
|
-
|
|
528
|
-
// so getInitiallyOpenByDefaultNodes picks it up and the UI starts open.
|
|
529
|
-
collapsed: metaCollapsed(seg.metadata) === false
|
|
530
|
-
? "open-by-default"
|
|
531
|
-
: (metaCollapsed(seg.metadata) ?? defaults.collapsed),
|
|
584
|
+
collapsed: metaCollapsed(seg.metadata) ?? defaults.collapsed,
|
|
532
585
|
overviewPageId: overviewPageId != null ? FernNavigation.PageId(overviewPageId) : undefined,
|
|
533
586
|
noindex: metaBool(overview?.metadata, "noindex") ?? metaBool(seg.metadata, "noindex"),
|
|
534
|
-
collapsible: metaBool(seg.metadata, "collapsible") ?? (metaCollapsed(seg.metadata)
|
|
587
|
+
collapsible: metaBool(seg.metadata, "collapsible") ?? (metaCollapsed(seg.metadata) === true ? true : undefined),
|
|
535
588
|
collapsedByDefault: metaBool(seg.metadata, "collapsedByDefault") ?? (metaCollapsed(seg.metadata) === true ? true : undefined),
|
|
536
589
|
availability: metaStr(seg.metadata, "availability"),
|
|
537
590
|
pointsTo: metaPointsTo != null ? pointsToSlug(metaPointsTo) : undefined,
|
|
@@ -569,11 +622,10 @@ function directChildApiPackages(parentSection, scopeSegs) {
|
|
|
569
622
|
function buildApiChildren(ctx, scoped, scopeSegs, apiDefinitionId, scopePrefix) {
|
|
570
623
|
const { seg, nav } = scoped;
|
|
571
624
|
// Build leaf nodes from this segment's own nav routes (not descendants).
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const leafNodes = mergeEndpointPairs(ctx, rawLeafNodes, nav);
|
|
625
|
+
// API references can contain normal docs pages and links in addition to
|
|
626
|
+
// endpoint leaves, so include markdown/link artifacts here instead of only
|
|
627
|
+
// API leaf types.
|
|
628
|
+
const leafNodes = positionedApiLeafChildren(ctx, nav, apiDefinitionId);
|
|
577
629
|
// Build nested ApiPackageNodes from direct child apiPackage segments.
|
|
578
630
|
const childPackages = directChildApiPackages(seg.section, scopeSegs);
|
|
579
631
|
const packageNodes = childPackages.map((child) => ({
|
|
@@ -583,18 +635,12 @@ function buildApiChildren(ctx, scoped, scopeSegs, apiDefinitionId, scopePrefix)
|
|
|
583
635
|
// Interleave by position when childIndex metadata is available
|
|
584
636
|
const hasChildIndex = packageNodes.some((p) => p.childIndex != null);
|
|
585
637
|
if (!hasChildIndex) {
|
|
586
|
-
return [...leafNodes, ...packageNodes.map((p) => p.node)]
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
for (const route of nav) {
|
|
590
|
-
if (route.displaySortOrder != null) {
|
|
591
|
-
leafPositions.push(route.displaySortOrder);
|
|
592
|
-
}
|
|
638
|
+
return [...leafNodes, ...packageNodes.map((p) => ({ node: p.node, pos: Number.MAX_SAFE_INTEGER }))]
|
|
639
|
+
.sort((a, b) => a.pos - b.pos)
|
|
640
|
+
.map((p) => p.node);
|
|
593
641
|
}
|
|
594
642
|
const positioned = [];
|
|
595
|
-
|
|
596
|
-
positioned.push({ node: leafNodes[i], pos: leafPositions[i] ?? i });
|
|
597
|
-
}
|
|
643
|
+
positioned.push(...leafNodes);
|
|
598
644
|
for (const p of packageNodes) {
|
|
599
645
|
positioned.push({ node: p.node, pos: p.childIndex ?? Number.MAX_SAFE_INTEGER });
|
|
600
646
|
}
|
|
@@ -803,7 +849,7 @@ function changelogNode(ctx, scoped, scopePrefix) {
|
|
|
803
849
|
pageId: FernNavigation.PageId(metaStr(r.metadata, "pageId") ?? ""),
|
|
804
850
|
noindex: metaBool(r.metadata, "noindex"),
|
|
805
851
|
date: dateStr ?? "",
|
|
806
|
-
tags:
|
|
852
|
+
tags: metaStrArr(r.metadata, "tags")
|
|
807
853
|
};
|
|
808
854
|
let months = byYear.get(year);
|
|
809
855
|
if (months == null) {
|
|
@@ -930,6 +976,54 @@ function buildSectionTree(ctx, scopeSegs, parentPath, scopePrefix, changelogScop
|
|
|
930
976
|
});
|
|
931
977
|
}
|
|
932
978
|
// ── build a sidebarRoot for a single scope ─────────────────────────────────
|
|
979
|
+
/**
|
|
980
|
+
* When multiple root-level sections share the same section path (common for
|
|
981
|
+
* versioned docs where all tab-level sections receive the version slug as
|
|
982
|
+
* their path), we need to scope buildSectionTree so each section only adopts
|
|
983
|
+
* the child segments that physically belong to it.
|
|
984
|
+
*
|
|
985
|
+
* Ownership is determined by sortOrder ranges: a section owns segments whose
|
|
986
|
+
* sortOrder falls between its own and the next sibling section's sortOrder.
|
|
987
|
+
* Returns a map only for sections that have duplicates; unique sections won't
|
|
988
|
+
* appear (and should pass unfiltered scopeSegs to buildSectionTree).
|
|
989
|
+
*/
|
|
990
|
+
function computeSiblingSortBoundaries(ordered) {
|
|
991
|
+
const result = new Map();
|
|
992
|
+
// Collect root-level sections grouped by their section path.
|
|
993
|
+
const sectionPaths = new Set(ordered.map((s) => s.seg.section));
|
|
994
|
+
const rootSections = [];
|
|
995
|
+
for (const s of ordered) {
|
|
996
|
+
const type = metaStr(s.seg.metadata, "type");
|
|
997
|
+
const sec = s.seg.section;
|
|
998
|
+
if (type !== "section" || sec === "") {
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const parent = parentSectionPath(sec);
|
|
1002
|
+
const isRootLevel = parent === "" || (!sectionPaths.has(parent) && nearestExistingAncestor(sec, sectionPaths) === "");
|
|
1003
|
+
if (isRootLevel) {
|
|
1004
|
+
rootSections.push(s);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
// Group by section path — only paths with >1 section need boundaries.
|
|
1008
|
+
const byPath = new Map();
|
|
1009
|
+
for (const s of rootSections) {
|
|
1010
|
+
const group = byPath.get(s.seg.section) ?? [];
|
|
1011
|
+
group.push(s);
|
|
1012
|
+
byPath.set(s.seg.section, group);
|
|
1013
|
+
}
|
|
1014
|
+
for (const [, siblings] of byPath) {
|
|
1015
|
+
if (siblings.length <= 1) {
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
// Siblings are already in sortOrder (from `ordered`).
|
|
1019
|
+
for (let i = 0; i < siblings.length; i++) {
|
|
1020
|
+
const lo = siblings[i].seg.sortOrder;
|
|
1021
|
+
const hi = i + 1 < siblings.length ? siblings[i + 1].seg.sortOrder : Number.MAX_SAFE_INTEGER;
|
|
1022
|
+
result.set(siblings[i], { lo, hi });
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return result;
|
|
1026
|
+
}
|
|
933
1027
|
/**
|
|
934
1028
|
* Assemble the children of a scope's sidebar root in cross-segment order.
|
|
935
1029
|
* `tabRoot` segments contribute their page/link artifacts inline at their
|
|
@@ -969,12 +1063,23 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
|
|
|
969
1063
|
//
|
|
970
1064
|
// Without a root section, depth-0 sections are emitted directly.
|
|
971
1065
|
const hasRootSection = ordered.some((s) => metaStr(s.seg.metadata, "type") === "section" && s.seg.section === "");
|
|
1066
|
+
// ── Sibling section ownership ────────────────────────────────────────
|
|
1067
|
+
//
|
|
1068
|
+
// Multiple root-level sections can share the same section path (e.g.
|
|
1069
|
+
// three sections all with path equal to the version slug). When this
|
|
1070
|
+
// happens, buildSectionTree would incorrectly give ALL child subsections
|
|
1071
|
+
// to each sibling. We resolve ownership via sortOrder: a section owns
|
|
1072
|
+
// segments whose sortOrder falls between its own and the next sibling's.
|
|
1073
|
+
const siblingBoundaries = computeSiblingSortBoundaries(ordered);
|
|
972
1074
|
for (const s of ordered) {
|
|
973
1075
|
const type = metaStr(s.seg.metadata, "type");
|
|
974
1076
|
const sec = s.seg.section;
|
|
975
1077
|
if (hasOwningApiReference(s, scopeSegs)) {
|
|
976
1078
|
continue;
|
|
977
1079
|
}
|
|
1080
|
+
if (type === "tabRoot" && metaBool(s.seg.metadata, "landingPage") === true) {
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
978
1083
|
// (A) Loose artifacts: tabRoot or non-section segments at section path "".
|
|
979
1084
|
if (type === "tabRoot" || (sec === "" && type !== "section")) {
|
|
980
1085
|
for (const r of s.nav) {
|
|
@@ -1008,9 +1113,25 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
|
|
|
1008
1113
|
node = changelogNode(ctx, s, changelogScopePrefix ?? scopePrefix);
|
|
1009
1114
|
}
|
|
1010
1115
|
else {
|
|
1011
|
-
|
|
1116
|
+
// Scope child segments to this section's sortOrder range when
|
|
1117
|
+
// sibling sections share the same path.
|
|
1118
|
+
const bounds = siblingBoundaries.get(s);
|
|
1119
|
+
const ownedSegs = bounds != null
|
|
1120
|
+
? scopeSegs.filter((seg) => seg.seg.sortOrder >= bounds.lo && seg.seg.sortOrder < bounds.hi)
|
|
1121
|
+
: scopeSegs;
|
|
1122
|
+
node = sectionNode(ctx, s, buildSectionTree(ctx, ownedSegs, sec, scopePrefix, changelogScopePrefix), scopePrefix);
|
|
1012
1123
|
}
|
|
1013
|
-
|
|
1124
|
+
// Shared/merged sections (paths not rooted in scopePrefix) must not
|
|
1125
|
+
// carry childIndex — their source-version positions would sort them
|
|
1126
|
+
// ahead of native sections. Only applies when sibling sections exist
|
|
1127
|
+
// (multiple sections sharing same path), indicating a versioned site
|
|
1128
|
+
// where shared sections were merged from a sibling version.
|
|
1129
|
+
const isSharedSection = siblingBoundaries.size > 0 && scopePrefix !== "" && sec !== "" && !sec.startsWith(scopePrefix);
|
|
1130
|
+
annotated.push({
|
|
1131
|
+
node,
|
|
1132
|
+
pos: isSharedSection ? undefined : metaNum(s.seg.metadata, "childIndex"),
|
|
1133
|
+
isLeaf: false
|
|
1134
|
+
});
|
|
1014
1135
|
}
|
|
1015
1136
|
}
|
|
1016
1137
|
// Interleave by V1 position only when BOTH leaves and segments carry one.
|
|
@@ -1094,7 +1215,7 @@ function buildSidebarRoot(ctx, scopeSegs, scopePrefix, changelogScopePrefix) {
|
|
|
1094
1215
|
* Build a full (un-pruned) `RootNode` from the decomposed ledger model.
|
|
1095
1216
|
*/
|
|
1096
1217
|
export function buildFullRootFromSegments(options) {
|
|
1097
|
-
const { basePath, title, segments, navBySegment } = options;
|
|
1218
|
+
const { basePath, rootSlug: sourceRootSlug, title, segments, navBySegment, skipSharedSectionMerge } = options;
|
|
1098
1219
|
// Per-call state — never shared across concurrent builds.
|
|
1099
1220
|
const ctx = createBuildContext(basePath);
|
|
1100
1221
|
const scoped = segments
|
|
@@ -1105,7 +1226,7 @@ export function buildFullRootFromSegments(options) {
|
|
|
1105
1226
|
return t !== "files" && t !== "redirects";
|
|
1106
1227
|
})
|
|
1107
1228
|
.map((seg) => ({ seg, nav: navBySegment.get(seg.segmentHash) ?? [] }));
|
|
1108
|
-
const rootSlug = asSlug(basePath);
|
|
1229
|
+
const rootSlug = asSlug(sourceRootSlug ?? basePath);
|
|
1109
1230
|
// Group by scope identity.
|
|
1110
1231
|
const scopesByKey = new Map();
|
|
1111
1232
|
for (const s of scoped) {
|
|
@@ -1119,10 +1240,10 @@ export function buildFullRootFromSegments(options) {
|
|
|
1119
1240
|
const tabs = uniqueDetails(scoped, "tab");
|
|
1120
1241
|
let child;
|
|
1121
1242
|
if (products.length > 0) {
|
|
1122
|
-
child = buildProductGroup(ctx, products, scoped, rootSlug);
|
|
1243
|
+
child = buildProductGroup(ctx, products, scoped, rootSlug, skipSharedSectionMerge);
|
|
1123
1244
|
}
|
|
1124
1245
|
else if (versions.length > 0) {
|
|
1125
|
-
child = buildVersioned(ctx, versions, scoped, rootSlug, undefined);
|
|
1246
|
+
child = buildVersioned(ctx, versions, scoped, rootSlug, undefined, skipSharedSectionMerge);
|
|
1126
1247
|
}
|
|
1127
1248
|
else {
|
|
1128
1249
|
child = buildUnversioned(ctx, scoped, rootSlug, tabs.length > 0);
|
|
@@ -1183,6 +1304,10 @@ export function buildFullRootFromSegments(options) {
|
|
|
1183
1304
|
* back to `detail.id` so that distinct skip-slug tabs/products aren't
|
|
1184
1305
|
* collapsed into a single entry.
|
|
1185
1306
|
*
|
|
1307
|
+
* Tabs always key by `d.id` because skip-slug tabs inherit their
|
|
1308
|
+
* parent's slug (e.g. the version slug "v-2"), so multiple distinct
|
|
1309
|
+
* tabs share the same slug — keying by slug would collapse them.
|
|
1310
|
+
*
|
|
1186
1311
|
* Among duplicates at the same key we prefer the one whose metadata
|
|
1187
1312
|
* carries a `pointsTo` value (richer / more recent publish).
|
|
1188
1313
|
*/
|
|
@@ -1194,7 +1319,9 @@ function uniqueDetails(scoped, dim) {
|
|
|
1194
1319
|
continue;
|
|
1195
1320
|
}
|
|
1196
1321
|
const slug = detailSlug(d);
|
|
1197
|
-
|
|
1322
|
+
// Tabs always key by id: skip-slug tabs share the parent slug
|
|
1323
|
+
// (e.g. "v-2") and would otherwise collapse into one entry.
|
|
1324
|
+
const key = dim === "tab" ? d.id : slug !== "" ? slug : d.id;
|
|
1198
1325
|
const existing = byKey.get(key);
|
|
1199
1326
|
if (existing == null) {
|
|
1200
1327
|
byKey.set(key, d);
|
|
@@ -1271,22 +1398,36 @@ function buildSidebarRootWithVariants(ctx, scopeSegs, scopePrefix, changelogScop
|
|
|
1271
1398
|
if (variants.length <= 1) {
|
|
1272
1399
|
return buildSidebarRoot(ctx, scopeSegs, scopePrefix, changelogScopePrefix);
|
|
1273
1400
|
}
|
|
1401
|
+
const sharedSegs = scopeSegs.filter((s) => s.seg.variant == null);
|
|
1274
1402
|
const variantedNode = buildVariantedNode(ctx, variants, scopeSegs, scopePrefix);
|
|
1403
|
+
const sharedChildren = sharedSegs.length > 0 ? buildSidebarRootChildren(ctx, sharedSegs, scopePrefix, changelogScopePrefix) : [];
|
|
1275
1404
|
const first = scopeSegs[0];
|
|
1276
1405
|
return {
|
|
1277
1406
|
type: "sidebarRoot",
|
|
1278
1407
|
id: ctx.nodeId(`sidebar:${first != null ? scopeKey(first.seg) : "root"}`),
|
|
1279
1408
|
collapsed: undefined,
|
|
1280
|
-
children: [variantedNode]
|
|
1409
|
+
children: [...sharedChildren, variantedNode]
|
|
1281
1410
|
};
|
|
1282
1411
|
}
|
|
1283
1412
|
function buildTabbedOrSidebar(ctx, scopeSegs, tabsPresent, scopePrefix) {
|
|
1284
1413
|
if (!tabsPresent) {
|
|
1285
|
-
// When no tabs exist but variants do,
|
|
1286
|
-
//
|
|
1414
|
+
// When no tabs exist but variants do, wrap variant-scoped segments
|
|
1415
|
+
// in a VariantedNode and include non-variant segments alongside it.
|
|
1287
1416
|
const variants = uniqueDetails(scopeSegs, "variant");
|
|
1288
1417
|
if (variants.length > 1) {
|
|
1289
|
-
|
|
1418
|
+
const sharedSegs = scopeSegs.filter((s) => s.seg.variant == null);
|
|
1419
|
+
const variantedNode = buildVariantedNode(ctx, variants, scopeSegs, scopePrefix);
|
|
1420
|
+
if (sharedSegs.length > 0) {
|
|
1421
|
+
const sharedChildren = buildSidebarRootChildren(ctx, sharedSegs, scopePrefix);
|
|
1422
|
+
const first = scopeSegs[0];
|
|
1423
|
+
return {
|
|
1424
|
+
type: "sidebarRoot",
|
|
1425
|
+
id: ctx.nodeId(`sidebar:${first != null ? scopeKey(first.seg) : "root"}`),
|
|
1426
|
+
collapsed: undefined,
|
|
1427
|
+
children: [...sharedChildren, variantedNode]
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
return variantedNode;
|
|
1290
1431
|
}
|
|
1291
1432
|
return buildSidebarRoot(ctx, scopeSegs, scopePrefix);
|
|
1292
1433
|
}
|
|
@@ -1394,16 +1535,101 @@ function buildUnversioned(ctx, scopeSegs, scopePrefix, tabsPresent) {
|
|
|
1394
1535
|
type: "unversioned",
|
|
1395
1536
|
id: ctx.nodeId(`unversioned:${first?.seg.product?.id ?? "root"}`),
|
|
1396
1537
|
collapsed: undefined,
|
|
1397
|
-
landingPage:
|
|
1538
|
+
landingPage: buildLandingPage(ctx, scopeSegs),
|
|
1398
1539
|
// The enclosing scope prefix (product slug, or rootSlug at the top
|
|
1399
1540
|
// level) roots structural sections so they share the scope's slug
|
|
1400
1541
|
// namespace.
|
|
1401
1542
|
child: buildTabbedOrSidebar(ctx, scopeSegs, tabsPresent, scopePrefix)
|
|
1402
1543
|
};
|
|
1403
1544
|
}
|
|
1404
|
-
|
|
1545
|
+
/**
|
|
1546
|
+
* Merge shared (tab-level) sections from sibling versions into the default
|
|
1547
|
+
* version's segment list. A section is considered "shared" when its section
|
|
1548
|
+
* path does NOT start with any version slug — indicating it was authored at
|
|
1549
|
+
* the tab level and the publish transform assigned it to only one version.
|
|
1550
|
+
*/
|
|
1551
|
+
function mergeSharedSections(defaultSegs, allScoped, defaultVersion, versionSlugs) {
|
|
1552
|
+
// Build a map of the default version's tabs by displayName for
|
|
1553
|
+
// cross-version matching. Tab slugs can't be used because the "default"
|
|
1554
|
+
// tab's slug equals the version slug (e.g. "2026-02-09" vs "2025-04-21"
|
|
1555
|
+
// for the same logical "Documentation" tab across versions).
|
|
1556
|
+
const defaultTabsByName = new Map();
|
|
1557
|
+
for (const s of defaultSegs) {
|
|
1558
|
+
if (s.seg.tab != null) {
|
|
1559
|
+
const tabName = s.seg.tab.displayName;
|
|
1560
|
+
if (!defaultTabsByName.has(tabName)) {
|
|
1561
|
+
defaultTabsByName.set(tabName, s.seg.tab);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
// Collect section paths already present in the default version.
|
|
1566
|
+
const existingSections = new Set(defaultSegs.filter((s) => metaStr(s.seg.metadata, "type") === "section").map((s) => s.seg.section));
|
|
1567
|
+
// Merged sections must sort AFTER all native default segments so they
|
|
1568
|
+
// appear in a trailing sidebarGroup (after non-collapsible sections like
|
|
1569
|
+
// "Getting Started", "Dev Tools", "Capabilities") matching v2 ordering.
|
|
1570
|
+
const maxSortOrder = defaultSegs.reduce((max, s) => Math.max(max, s.seg.sortOrder), 0);
|
|
1571
|
+
let nextSortOrder = maxSortOrder + 1;
|
|
1572
|
+
const merged = [...defaultSegs];
|
|
1573
|
+
for (const s of allScoped) {
|
|
1574
|
+
if (s.seg.version?.id === defaultVersion.id) {
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
const type = metaStr(s.seg.metadata, "type");
|
|
1578
|
+
if (type !== "section") {
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
const sectionPath = s.seg.section;
|
|
1582
|
+
// Skip version-specific sections (path starts with a version slug).
|
|
1583
|
+
if (versionSlugs.some((vs) => sectionPath === vs || sectionPath.startsWith(vs + "/"))) {
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
// Skip if the default version already has this section.
|
|
1587
|
+
if (existingSections.has(sectionPath)) {
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
// Match the source tab to the default version's corresponding tab
|
|
1591
|
+
// by displayName (tab slugs differ across versions for the default tab).
|
|
1592
|
+
const sourceTabName = s.seg.tab?.displayName ?? "";
|
|
1593
|
+
const targetTab = defaultTabsByName.get(sourceTabName);
|
|
1594
|
+
if (targetTab == null) {
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
// Re-map the segment's tab and version to the default version's.
|
|
1598
|
+
// Strip childIndex from metadata so merged sections don't sort before
|
|
1599
|
+
// native sections in the annotated interleave step, and assign a
|
|
1600
|
+
// sortOrder after all native segments to place them in the trailing
|
|
1601
|
+
// sidebarGroup.
|
|
1602
|
+
const metadata = s.seg.metadata != null ? { ...s.seg.metadata, childIndex: undefined } : s.seg.metadata;
|
|
1603
|
+
merged.push({
|
|
1604
|
+
seg: {
|
|
1605
|
+
...s.seg,
|
|
1606
|
+
tab: targetTab,
|
|
1607
|
+
version: defaultVersion,
|
|
1608
|
+
sortOrder: nextSortOrder++,
|
|
1609
|
+
metadata
|
|
1610
|
+
},
|
|
1611
|
+
nav: s.nav
|
|
1612
|
+
});
|
|
1613
|
+
existingSections.add(sectionPath);
|
|
1614
|
+
}
|
|
1615
|
+
return merged;
|
|
1616
|
+
}
|
|
1617
|
+
function buildVersioned(ctx, versions, scoped, rootSlug, productPrefix, skipSharedSectionMerge) {
|
|
1618
|
+
// Collect version slugs for detecting version-prefixed section paths.
|
|
1619
|
+
const versionSlugs = versions.map((v) => detailSlug(v)).filter((s) => s !== "");
|
|
1405
1620
|
const children = versions.map((version) => {
|
|
1406
|
-
|
|
1621
|
+
let versionSegs = scoped.filter((s) => s.seg.version?.id === version.id);
|
|
1622
|
+
// Merge shared tab-level sections from sibling versions into the
|
|
1623
|
+
// default version. The publish transform may assign tab-level content
|
|
1624
|
+
// (sections without a version-prefixed path) to a single version;
|
|
1625
|
+
// v2's monolith root includes them in every version's sidebar.
|
|
1626
|
+
// Skipped for scoped roots: the scoped root only loads nav for a
|
|
1627
|
+
// single route's scope, so merged sections carry the source version's
|
|
1628
|
+
// fullPaths into the default tree, creating duplicate slugs that
|
|
1629
|
+
// orphan the real version's pages in NodeCollector.
|
|
1630
|
+
if (detailIsDefault(version) && !skipSharedSectionMerge) {
|
|
1631
|
+
versionSegs = mergeSharedSections(versionSegs, scoped, version, versionSlugs);
|
|
1632
|
+
}
|
|
1407
1633
|
const tabsPresent = uniqueDetails(versionSegs, "tab").length > 0;
|
|
1408
1634
|
const prefix = productPrefix != null ? productPrefix : rootSlug;
|
|
1409
1635
|
// The version node always carries its FULL versioned slug
|
|
@@ -1440,7 +1666,7 @@ function buildVersioned(ctx, versions, scoped, rootSlug, productPrefix) {
|
|
|
1440
1666
|
default: detailIsDefault(version),
|
|
1441
1667
|
versionId: FernNavigation.VersionId(version.id),
|
|
1442
1668
|
availability: undefined,
|
|
1443
|
-
landingPage:
|
|
1669
|
+
landingPage: buildLandingPage(ctx, versionSegs),
|
|
1444
1670
|
announcement: undefined,
|
|
1445
1671
|
pointsTo: metaStr(version.metadata, "pointsTo") != null
|
|
1446
1672
|
? pointsToSlug(metaStr(version.metadata, "pointsTo"))
|
|
@@ -1546,38 +1772,41 @@ function crossVersionDedupKeys(node, parents) {
|
|
|
1546
1772
|
keys.push([...parentTitles, node.title].join("###"));
|
|
1547
1773
|
return keys;
|
|
1548
1774
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
* In V1, these are version-level landing pages that sit outside any tab.
|
|
1552
|
-
*/
|
|
1553
|
-
function buildVersionLandingPage(ctx, versionSegs, versionSlug) {
|
|
1554
|
-
const tablessSegs = versionSegs.filter((s) => s.seg.tab == null);
|
|
1555
|
-
if (tablessSegs.length === 0) {
|
|
1775
|
+
function routeToLandingPage(ctx, route) {
|
|
1776
|
+
if (route == null || route.type !== "markdown" || route.fullPath == null) {
|
|
1556
1777
|
return undefined;
|
|
1557
1778
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
const pageId = metaStr(md, "pageId") ?? "";
|
|
1563
|
-
const slug = ctx.contentSlug(r.fullPath);
|
|
1564
|
-
return {
|
|
1565
|
-
type: "landingPage",
|
|
1566
|
-
...withMetadataDefaults({
|
|
1567
|
-
id: ctx.nodeId(`landing:${slug}`),
|
|
1568
|
-
title: metaStr(md, "title") ?? "",
|
|
1569
|
-
slug,
|
|
1570
|
-
icon: metaStr(md, "icon"),
|
|
1571
|
-
hidden: r.hidden,
|
|
1572
|
-
viewers: toViewers(metaStrArr(md, "viewers"))
|
|
1573
|
-
}),
|
|
1574
|
-
pageId: FernNavigation.PageId(pageId),
|
|
1575
|
-
noindex: metaBool(md, "noindex")
|
|
1576
|
-
};
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1779
|
+
const md = route.metadata;
|
|
1780
|
+
const pageId = metaStr(md, "pageId");
|
|
1781
|
+
if (pageId == null) {
|
|
1782
|
+
return undefined;
|
|
1579
1783
|
}
|
|
1580
|
-
|
|
1784
|
+
const slug = ctx.contentSlug(route.fullPath);
|
|
1785
|
+
return {
|
|
1786
|
+
type: "landingPage",
|
|
1787
|
+
...withMetadataDefaults({
|
|
1788
|
+
id: ctx.nodeId(`landing:${slug}`),
|
|
1789
|
+
title: metaStr(md, "title") ?? "",
|
|
1790
|
+
slug,
|
|
1791
|
+
icon: metaStr(md, "icon"),
|
|
1792
|
+
hidden: route.hidden,
|
|
1793
|
+
authed: metaBool(md, "authed"),
|
|
1794
|
+
viewers: toViewers(metaStrArr(md, "viewers")),
|
|
1795
|
+
orphaned: metaBool(md, "orphaned"),
|
|
1796
|
+
featureFlags: metaFeatureFlags(md)
|
|
1797
|
+
}),
|
|
1798
|
+
pageId: FernNavigation.PageId(pageId),
|
|
1799
|
+
noindex: metaBool(md, "noindex")
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Build a LandingPageNode from the explicit landing-page segment emitted by
|
|
1804
|
+
* the publish transform. This avoids confusing ordinary loose tabRoot pages
|
|
1805
|
+
* with root/product/version landing pages.
|
|
1806
|
+
*/
|
|
1807
|
+
function buildLandingPage(ctx, scopeSegs) {
|
|
1808
|
+
const landingSegment = scopeSegs.find((s) => metaStr(s.seg.metadata, "type") === "tabRoot" && metaBool(s.seg.metadata, "landingPage") === true);
|
|
1809
|
+
return routeToLandingPage(ctx, landingSegment?.nav.find((route) => route.type === "markdown"));
|
|
1581
1810
|
}
|
|
1582
1811
|
/**
|
|
1583
1812
|
* After default-version canonical slugs are marked, propagate them across
|
|
@@ -1623,7 +1852,7 @@ function markCrossVersionCanonicalSlugs(versionNodes) {
|
|
|
1623
1852
|
});
|
|
1624
1853
|
}
|
|
1625
1854
|
}
|
|
1626
|
-
function buildProductGroup(ctx, products, scoped, rootSlug) {
|
|
1855
|
+
function buildProductGroup(ctx, products, scoped, rootSlug, skipSharedSectionMerge) {
|
|
1627
1856
|
const children = products.map((product) => {
|
|
1628
1857
|
const productSegs = scoped.filter((s) => s.seg.product?.id === product.id);
|
|
1629
1858
|
const versions = uniqueDetails(productSegs, "version");
|
|
@@ -1632,7 +1861,7 @@ function buildProductGroup(ctx, products, scoped, rootSlug) {
|
|
|
1632
1861
|
// ProductChild is VersionedNode | UnversionedNode — a version-less
|
|
1633
1862
|
// product wraps its sidebar/tabs in an UnversionedNode.
|
|
1634
1863
|
const productChild = versions.length > 0
|
|
1635
|
-
? buildVersioned(ctx, versions, productSegs, rootSlug, productPrefix)
|
|
1864
|
+
? buildVersioned(ctx, versions, productSegs, rootSlug, productPrefix, skipSharedSectionMerge)
|
|
1636
1865
|
: buildUnversioned(ctx, productSegs, asSlug(productPrefix), tabsPresent);
|
|
1637
1866
|
const node = {
|
|
1638
1867
|
type: "product",
|
|
@@ -1667,7 +1896,7 @@ function buildProductGroup(ctx, products, scoped, rootSlug) {
|
|
|
1667
1896
|
type: "productgroup",
|
|
1668
1897
|
id: ctx.nodeId("productgroup"),
|
|
1669
1898
|
collapsed: undefined,
|
|
1670
|
-
landingPage:
|
|
1899
|
+
landingPage: buildLandingPage(ctx, scoped.filter((s) => s.seg.product == null)),
|
|
1671
1900
|
children
|
|
1672
1901
|
};
|
|
1673
1902
|
}
|