@fern-api/fdr-sdk 1.2.42-0c024f5b0d → 1.2.42-23647d2912
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/navigation/ledger-root-builder.js +134 -30
- package/dist/js/navigation/ledger-root-builder.js.map +1 -1
- package/dist/js/navigation/ledger-root-builder.mjs +134 -30
- package/dist/js/navigation/ledger-root-builder.mjs.map +1 -1
- package/dist/navigation/__test__/ledger-root-builder.crossProductDedup.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.crossProductDedup.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.crossProductDedup.test.js +267 -0
- package/dist/navigation/__test__/ledger-root-builder.crossProductDedup.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.ledgerParity.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.ledgerParity.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.ledgerParity.test.js +306 -0
- package/dist/navigation/__test__/ledger-root-builder.ledgerParity.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.js +109 -4
- package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.js.map +1 -1
- package/dist/navigation/__test__/ledger-root-builder.rootChildOrder.test.js +102 -0
- package/dist/navigation/__test__/ledger-root-builder.rootChildOrder.test.js.map +1 -1
- package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.js +256 -0
- package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.js.map +1 -0
- package/dist/navigation/ledger-root-builder.d.ts.map +1 -1
- package/dist/navigation/ledger-root-builder.js +192 -33
- package/dist/navigation/ledger-root-builder.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/navigation/__test__/ledger-root-builder.crossProductDedup.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.crossProductDedup.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.ledgerParity.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.ledgerParity.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts.map +1 -0
- package/dist/types/navigation/ledger-root-builder.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -60,6 +60,41 @@ function metaStr(metadata, key) {
|
|
|
60
60
|
}
|
|
61
61
|
return undefined;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* V1 availability values stored in ledger metadata are lowercase (e.g. "deprecated"),
|
|
65
|
+
* but the latest Availability type uses PascalCase (e.g. "Deprecated"). This map
|
|
66
|
+
* normalises both formats so the badge component can look up display names correctly.
|
|
67
|
+
*/
|
|
68
|
+
const AVAILABILITY_LOOKUP = {
|
|
69
|
+
// V1 lowercase values (stored by docsPublishTransform from V1 navigation tree)
|
|
70
|
+
alpha: FernNavigation.Availability.Alpha,
|
|
71
|
+
stable: FernNavigation.Availability.Stable,
|
|
72
|
+
"generally-available": FernNavigation.Availability.GenerallyAvailable,
|
|
73
|
+
"in-development": FernNavigation.Availability.InDevelopment,
|
|
74
|
+
"pre-release": FernNavigation.Availability.PreRelease,
|
|
75
|
+
deprecated: FernNavigation.Availability.Deprecated,
|
|
76
|
+
beta: FernNavigation.Availability.Beta,
|
|
77
|
+
preview: FernNavigation.Availability.Preview,
|
|
78
|
+
legacy: FernNavigation.Availability.Legacy,
|
|
79
|
+
// Latest PascalCase values (pass-through)
|
|
80
|
+
Alpha: FernNavigation.Availability.Alpha,
|
|
81
|
+
Stable: FernNavigation.Availability.Stable,
|
|
82
|
+
GenerallyAvailable: FernNavigation.Availability.GenerallyAvailable,
|
|
83
|
+
InDevelopment: FernNavigation.Availability.InDevelopment,
|
|
84
|
+
PreRelease: FernNavigation.Availability.PreRelease,
|
|
85
|
+
Deprecated: FernNavigation.Availability.Deprecated,
|
|
86
|
+
Beta: FernNavigation.Availability.Beta,
|
|
87
|
+
Preview: FernNavigation.Availability.Preview,
|
|
88
|
+
Legacy: FernNavigation.Availability.Legacy
|
|
89
|
+
};
|
|
90
|
+
/** Read an availability field from metadata, converting V1 lowercase to latest PascalCase. */
|
|
91
|
+
function metaAvailability(metadata) {
|
|
92
|
+
const raw = metaStr(metadata, "availability");
|
|
93
|
+
if (raw == null) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
return AVAILABILITY_LOOKUP[raw];
|
|
97
|
+
}
|
|
63
98
|
function metaBool(metadata, key) {
|
|
64
99
|
if (metadata != null && typeof metadata === "object" && key in metadata) {
|
|
65
100
|
const v = metadata[key];
|
|
@@ -198,7 +233,7 @@ function artifactToNode(ctx, route) {
|
|
|
198
233
|
}),
|
|
199
234
|
pageId: FernNavigation.PageId(pageId),
|
|
200
235
|
noindex: metaBool(md, "noindex"),
|
|
201
|
-
availability:
|
|
236
|
+
availability: metaAvailability(md)
|
|
202
237
|
};
|
|
203
238
|
return node;
|
|
204
239
|
}
|
|
@@ -269,7 +304,7 @@ function apiLeafNodeFromNavRouteInternal(ctx, route, segmentApiDefinitionId) {
|
|
|
269
304
|
endpointId: FernNavigation.EndpointId(endpointId),
|
|
270
305
|
apiDefinitionId,
|
|
271
306
|
isResponseStream: metaBool(md, "isResponseStream"),
|
|
272
|
-
availability:
|
|
307
|
+
availability: metaAvailability(md),
|
|
273
308
|
playground: undefined
|
|
274
309
|
};
|
|
275
310
|
}
|
|
@@ -285,7 +320,7 @@ function apiLeafNodeFromNavRouteInternal(ctx, route, segmentApiDefinitionId) {
|
|
|
285
320
|
method: method,
|
|
286
321
|
webhookId: FernNavigation.WebhookId(webhookId),
|
|
287
322
|
apiDefinitionId,
|
|
288
|
-
availability:
|
|
323
|
+
availability: metaAvailability(md)
|
|
289
324
|
};
|
|
290
325
|
}
|
|
291
326
|
if (route.type === "asyncapi") {
|
|
@@ -298,7 +333,7 @@ function apiLeafNodeFromNavRouteInternal(ctx, route, segmentApiDefinitionId) {
|
|
|
298
333
|
...meta,
|
|
299
334
|
webSocketId: FernNavigation.WebSocketId(webSocketId),
|
|
300
335
|
apiDefinitionId,
|
|
301
|
-
availability:
|
|
336
|
+
availability: metaAvailability(md),
|
|
302
337
|
playground: undefined
|
|
303
338
|
};
|
|
304
339
|
}
|
|
@@ -314,7 +349,7 @@ function apiLeafNodeFromNavRouteInternal(ctx, route, segmentApiDefinitionId) {
|
|
|
314
349
|
method: method,
|
|
315
350
|
grpcId: FernNavigation.GrpcId(grpcId),
|
|
316
351
|
apiDefinitionId,
|
|
317
|
-
availability:
|
|
352
|
+
availability: metaAvailability(md)
|
|
318
353
|
};
|
|
319
354
|
}
|
|
320
355
|
if (route.type === "graphql") {
|
|
@@ -330,7 +365,7 @@ function apiLeafNodeFromNavRouteInternal(ctx, route, segmentApiDefinitionId) {
|
|
|
330
365
|
graphqlOperationId: FernNavigation.GraphQlOperationId(graphqlOperationId),
|
|
331
366
|
graphqlOperationIds: undefined,
|
|
332
367
|
apiDefinitionId,
|
|
333
|
-
availability:
|
|
368
|
+
availability: metaAvailability(md),
|
|
334
369
|
playground: undefined
|
|
335
370
|
};
|
|
336
371
|
}
|
|
@@ -534,6 +569,30 @@ function stripLeadingScopePrefix(section, scopePrefix) {
|
|
|
534
569
|
}
|
|
535
570
|
return section;
|
|
536
571
|
}
|
|
572
|
+
function computeDuplicateSiblingSortBoundaries(siblings) {
|
|
573
|
+
const result = new Map();
|
|
574
|
+
const byPath = new Map();
|
|
575
|
+
for (const sibling of siblings) {
|
|
576
|
+
if (metaStr(sibling.seg.metadata, "type") !== "section") {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const group = byPath.get(sibling.seg.section) ?? [];
|
|
580
|
+
group.push(sibling);
|
|
581
|
+
byPath.set(sibling.seg.section, group);
|
|
582
|
+
}
|
|
583
|
+
for (const [, group] of byPath) {
|
|
584
|
+
if (group.length <= 1) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
for (let i = 0; i < group.length; i++) {
|
|
588
|
+
result.set(group[i], {
|
|
589
|
+
lo: group[i].seg.sortOrder,
|
|
590
|
+
hi: i + 1 < group.length ? group[i + 1].seg.sortOrder : Number.MAX_SAFE_INTEGER
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return result;
|
|
595
|
+
}
|
|
537
596
|
/**
|
|
538
597
|
* Merge leaf children and section children by their original V1 tree position.
|
|
539
598
|
* Leaves carry position via `displaySortOrder` (stored in nav routes), sections
|
|
@@ -597,7 +656,7 @@ function sectionNode(ctx, scoped, sectionChildren, scopePrefix) {
|
|
|
597
656
|
noindex: metaBool(overview?.metadata, "noindex") ?? metaBool(seg.metadata, "noindex"),
|
|
598
657
|
collapsible: metaBool(seg.metadata, "collapsible") ?? (metaCollapsed(seg.metadata) === true ? true : undefined),
|
|
599
658
|
collapsedByDefault: metaBool(seg.metadata, "collapsedByDefault") ?? (metaCollapsed(seg.metadata) === true ? true : undefined),
|
|
600
|
-
availability:
|
|
659
|
+
availability: metaAvailability(seg.metadata),
|
|
601
660
|
pointsTo: metaPointsTo != null ? pointsToSlug(metaPointsTo) : undefined,
|
|
602
661
|
children: mergeChildrenByPosition(ctx, nav, sectionChildren)
|
|
603
662
|
};
|
|
@@ -607,13 +666,16 @@ function sectionNode(ctx, scoped, sectionChildren, scopePrefix) {
|
|
|
607
666
|
* `parentSectionPath(seg.section) === parentSection`, or is an orphaned segment
|
|
608
667
|
* whose nearest existing ancestor is `parentSection`.
|
|
609
668
|
*/
|
|
610
|
-
function directChildApiPackages(parentSection, scopeSegs) {
|
|
669
|
+
function directChildApiPackages(parentSection, scopeSegs, apiDefinitionId) {
|
|
611
670
|
const sectionPaths = new Set(scopeSegs.map((s) => s.seg.section));
|
|
612
671
|
return scopeSegs
|
|
613
672
|
.filter((s) => {
|
|
614
673
|
if (metaStr(s.seg.metadata, "type") !== "apiPackage") {
|
|
615
674
|
return false;
|
|
616
675
|
}
|
|
676
|
+
if (metaStr(s.seg.metadata, "apiDefinitionId") !== apiDefinitionId) {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
617
679
|
const parent = parentSectionPath(s.seg.section);
|
|
618
680
|
if (parent === parentSection) {
|
|
619
681
|
return true;
|
|
@@ -638,7 +700,7 @@ function buildApiChildren(ctx, scoped, scopeSegs, apiDefinitionId, scopePrefix)
|
|
|
638
700
|
// API leaf types.
|
|
639
701
|
const leafNodes = positionedApiLeafChildren(ctx, nav, apiDefinitionId);
|
|
640
702
|
// Build nested ApiPackageNodes from direct child apiPackage segments.
|
|
641
|
-
const childPackages = directChildApiPackages(seg.section, scopeSegs);
|
|
703
|
+
const childPackages = directChildApiPackages(seg.section, scopeSegs, apiDefinitionId);
|
|
642
704
|
const packageNodes = childPackages.map((child) => ({
|
|
643
705
|
node: apiPackageChildNode(ctx, child, scopeSegs, apiDefinitionId, scopePrefix),
|
|
644
706
|
childIndex: metaNum(child.seg.metadata, "childIndex")
|
|
@@ -686,7 +748,7 @@ function apiPackageChildNode(ctx, scoped, scopeSegs, parentApiDefinitionId, scop
|
|
|
686
748
|
featureFlags: metaFeatureFlags(seg.metadata)
|
|
687
749
|
}),
|
|
688
750
|
apiDefinitionId: FernNavigation.ApiDefinitionId(apiDefinitionId),
|
|
689
|
-
availability:
|
|
751
|
+
availability: metaAvailability(seg.metadata),
|
|
690
752
|
overviewPageId: overviewPageId != null ? FernNavigation.PageId(overviewPageId) : undefined,
|
|
691
753
|
noindex: metaBool(overview?.metadata, "noindex") ?? metaBool(seg.metadata, "noindex"),
|
|
692
754
|
pointsTo: metaPointsTo != null ? pointsToSlug(metaPointsTo) : undefined,
|
|
@@ -796,10 +858,10 @@ function apiReferenceNode(ctx, scoped, scopeSegs, scopePrefix) {
|
|
|
796
858
|
featureFlags: metaFeatureFlags(seg.metadata)
|
|
797
859
|
}),
|
|
798
860
|
apiDefinitionId: FernNavigation.ApiDefinitionId(apiDefinitionId),
|
|
799
|
-
availability:
|
|
861
|
+
availability: metaAvailability(seg.metadata),
|
|
800
862
|
overviewPageId: overviewPageId != null ? FernNavigation.PageId(overviewPageId) : undefined,
|
|
801
863
|
noindex: metaBool(overview?.metadata, "noindex") ?? metaBool(seg.metadata, "noindex"),
|
|
802
|
-
paginated:
|
|
864
|
+
paginated: metaBool(seg.metadata, "paginated"),
|
|
803
865
|
showErrors: metaBool(seg.metadata, "showErrors"),
|
|
804
866
|
hideTitle: metaBool(seg.metadata, "hideTitle"),
|
|
805
867
|
pointsTo: metaPointsTo != null ? pointsToSlug(metaPointsTo) : undefined,
|
|
@@ -817,7 +879,12 @@ function attachedApiChangelogNode(ctx, scoped, scopeSegs, apiDefinitionId, scope
|
|
|
817
879
|
}
|
|
818
880
|
return metaStr(candidate.seg.metadata, "apiDefinitionId") === apiDefinitionId;
|
|
819
881
|
});
|
|
820
|
-
const
|
|
882
|
+
const apiReferenceCount = scopeSegs.filter((candidate) => {
|
|
883
|
+
return (metaStr(candidate.seg.metadata, "type") === "apiReference" &&
|
|
884
|
+
metaStr(candidate.seg.metadata, "apiDefinitionId") === apiDefinitionId);
|
|
885
|
+
}).length;
|
|
886
|
+
const changelogSeg = candidates.find((candidate) => parentSectionPath(candidate.seg.section) === seg.section) ??
|
|
887
|
+
(apiReferenceCount === 1 ? candidates[0] : undefined);
|
|
821
888
|
return changelogSeg != null ? changelogNode(ctx, changelogSeg, scopePrefix) : undefined;
|
|
822
889
|
}
|
|
823
890
|
function hasOwningApiReference(scoped, scopeSegs) {
|
|
@@ -825,12 +892,14 @@ function hasOwningApiReference(scoped, scopeSegs) {
|
|
|
825
892
|
if (apiDefinitionId == null || metaStr(scoped.seg.metadata, "type") !== "changelog") {
|
|
826
893
|
return false;
|
|
827
894
|
}
|
|
828
|
-
|
|
895
|
+
const candidates = scopeSegs.filter((candidate) => {
|
|
829
896
|
if (metaStr(candidate.seg.metadata, "type") !== "apiReference") {
|
|
830
897
|
return false;
|
|
831
898
|
}
|
|
832
899
|
return metaStr(candidate.seg.metadata, "apiDefinitionId") === apiDefinitionId;
|
|
833
900
|
});
|
|
901
|
+
return (candidates.some((candidate) => parentSectionPath(scoped.seg.section) === candidate.seg.section) ||
|
|
902
|
+
candidates.length === 1);
|
|
834
903
|
}
|
|
835
904
|
/**
|
|
836
905
|
* True when this segment is an `apiPackage` that will be nested under an in-scope
|
|
@@ -1023,6 +1092,7 @@ function buildSectionTree(ctx, scopeSegs, parentPath, scopePrefix, changelogScop
|
|
|
1023
1092
|
return false;
|
|
1024
1093
|
})
|
|
1025
1094
|
.sort((a, b) => a.seg.sortOrder - b.seg.sortOrder);
|
|
1095
|
+
const siblingBoundaries = computeDuplicateSiblingSortBoundaries(direct);
|
|
1026
1096
|
return direct.map((s) => {
|
|
1027
1097
|
const type = metaStr(s.seg.metadata, "type");
|
|
1028
1098
|
const childIndex = metaNum(s.seg.metadata, "childIndex");
|
|
@@ -1035,7 +1105,11 @@ function buildSectionTree(ctx, scopeSegs, parentPath, scopePrefix, changelogScop
|
|
|
1035
1105
|
}
|
|
1036
1106
|
else {
|
|
1037
1107
|
// section (or unrecognised) → recurse for nested sections.
|
|
1038
|
-
|
|
1108
|
+
const boundary = siblingBoundaries.get(s);
|
|
1109
|
+
const childScopeSegs = boundary != null
|
|
1110
|
+
? scopeSegs.filter((candidate) => candidate.seg.sortOrder >= boundary.lo && candidate.seg.sortOrder < boundary.hi)
|
|
1111
|
+
: scopeSegs;
|
|
1112
|
+
node = sectionNode(ctx, s, buildSectionTree(ctx, childScopeSegs, s.seg.section, scopePrefix, changelogScopePrefix), scopePrefix);
|
|
1039
1113
|
}
|
|
1040
1114
|
return { node, childIndex };
|
|
1041
1115
|
});
|
|
@@ -1055,12 +1129,19 @@ function buildSectionTree(ctx, scopeSegs, parentPath, scopePrefix, changelogScop
|
|
|
1055
1129
|
function computeSiblingSortBoundaries(ordered) {
|
|
1056
1130
|
const result = new Map();
|
|
1057
1131
|
// Collect root-level sections grouped by their section path.
|
|
1132
|
+
// Sections with sec==="" (skip-slug sections) are included: when multiple
|
|
1133
|
+
// skip-slug sections exist at the root, they share path "" and need
|
|
1134
|
+
// boundaries so each only adopts its own children.
|
|
1058
1135
|
const sectionPaths = new Set(ordered.map((s) => s.seg.section));
|
|
1059
1136
|
const rootSections = [];
|
|
1060
1137
|
for (const s of ordered) {
|
|
1061
1138
|
const type = metaStr(s.seg.metadata, "type");
|
|
1062
1139
|
const sec = s.seg.section;
|
|
1063
|
-
if (type !== "section"
|
|
1140
|
+
if (type !== "section") {
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
if (sec === "") {
|
|
1144
|
+
rootSections.push(s);
|
|
1064
1145
|
continue;
|
|
1065
1146
|
}
|
|
1066
1147
|
const parent = parentSectionPath(sec);
|
|
@@ -1109,6 +1190,17 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
|
|
|
1109
1190
|
// segments. Both index into the SAME child list at write time, so they
|
|
1110
1191
|
// interleave directly (e.g. [page 0, page 1, section 2…15, page 16, 17]).
|
|
1111
1192
|
const annotated = [];
|
|
1193
|
+
// ── Sibling section ownership ────────────────────────────────────────
|
|
1194
|
+
//
|
|
1195
|
+
// Multiple root-level sections can share the same section path (e.g.
|
|
1196
|
+
// three sections all with path equal to the version slug). When this
|
|
1197
|
+
// happens, buildSectionTree would incorrectly give ALL child subsections
|
|
1198
|
+
// to each sibling. We resolve ownership via sortOrder: a section owns
|
|
1199
|
+
// segments whose sortOrder falls between its own and the next sibling's.
|
|
1200
|
+
//
|
|
1201
|
+
// Computed first because hasRootSection (below) uses it to decide
|
|
1202
|
+
// whether bounded skip-slug siblings should activate adoption.
|
|
1203
|
+
const siblingBoundaries = computeSiblingSortBoundaries(ordered);
|
|
1112
1204
|
// ── Root-section adoption flow ──────────────────────────────────────
|
|
1113
1205
|
//
|
|
1114
1206
|
// A scope may contain a "root section" — a segment with type "section"
|
|
@@ -1127,15 +1219,20 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
|
|
|
1127
1219
|
// → SKIP — already adopted by (B) via buildSectionTree
|
|
1128
1220
|
//
|
|
1129
1221
|
// Without a root section, depth-0 sections are emitted directly.
|
|
1130
|
-
const hasRootSection = ordered.some((s) => metaStr(s.seg.metadata, "type") === "section" && s.seg.section === "");
|
|
1131
|
-
// ── Sibling section ownership ────────────────────────────────────────
|
|
1132
1222
|
//
|
|
1133
|
-
//
|
|
1134
|
-
//
|
|
1135
|
-
//
|
|
1136
|
-
//
|
|
1137
|
-
//
|
|
1138
|
-
|
|
1223
|
+
// A section with sec=="" AND `childIndex` in its metadata is a sibling
|
|
1224
|
+
// inside a V1 sidebarGroup that has skipUrlSlug — not a true structural
|
|
1225
|
+
// wrapper. `walkNavSubTrees` always passes `childIndex`; direct
|
|
1226
|
+
// sidebarRoot children (true wrappers) never receive one.
|
|
1227
|
+
//
|
|
1228
|
+
// Such siblings only activate adoption when they have sortOrder
|
|
1229
|
+
// boundaries (multiple sec="" sections sharing the same path), which
|
|
1230
|
+
// correctly scope each section's children. A *single* unbounded
|
|
1231
|
+
// skip-slug sibling (e.g. mangopay's "Introduction") must NOT adopt
|
|
1232
|
+
// its depth-0 peers — doing so produces spurious breadcrumb entries.
|
|
1233
|
+
const hasRootSection = ordered.some((s) => metaStr(s.seg.metadata, "type") === "section" &&
|
|
1234
|
+
s.seg.section === "" &&
|
|
1235
|
+
(metaNum(s.seg.metadata, "childIndex") == null || siblingBoundaries.has(s)));
|
|
1139
1236
|
for (const s of ordered) {
|
|
1140
1237
|
const type = metaStr(s.seg.metadata, "type");
|
|
1141
1238
|
const sec = s.seg.section;
|
|
@@ -1158,8 +1255,16 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
|
|
|
1158
1255
|
continue;
|
|
1159
1256
|
}
|
|
1160
1257
|
// (B) Root section wrapper — adopts depth-0 children via buildSectionTree.
|
|
1161
|
-
|
|
1162
|
-
|
|
1258
|
+
// Only applies to true root wrappers (no childIndex); sibling
|
|
1259
|
+
// sections with skipUrlSlug (have childIndex) fall through to (C).
|
|
1260
|
+
// When multiple sections share sec==="" (skip-slug), scope children
|
|
1261
|
+
// to each section's sortOrder range so subsections aren't duplicated.
|
|
1262
|
+
if (type === "section" && sec === "" && hasRootSection) {
|
|
1263
|
+
const bounds = siblingBoundaries.get(s);
|
|
1264
|
+
const ownedSegs = bounds != null
|
|
1265
|
+
? scopeSegs.filter((seg) => seg.seg.sortOrder >= bounds.lo && seg.seg.sortOrder < bounds.hi)
|
|
1266
|
+
: scopeSegs;
|
|
1267
|
+
const sectionChildren = buildSectionTree(ctx, ownedSegs, "", scopePrefix, changelogScopePrefix);
|
|
1163
1268
|
const node = sectionNode(ctx, s, sectionChildren, scopePrefix);
|
|
1164
1269
|
annotated.push({ node, pos: metaNum(s.seg.metadata, "childIndex"), isLeaf: false });
|
|
1165
1270
|
continue;
|
|
@@ -1190,7 +1295,20 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
|
|
|
1190
1295
|
const ownedSegs = bounds != null
|
|
1191
1296
|
? scopeSegs.filter((seg) => seg.seg.sortOrder >= bounds.lo && seg.seg.sortOrder < bounds.hi)
|
|
1192
1297
|
: scopeSegs;
|
|
1193
|
-
|
|
1298
|
+
// A section with sec="" that reaches here (not adopted as a
|
|
1299
|
+
// root wrapper in case B) is a skipUrlSlug sibling.
|
|
1300
|
+
// When sortOrder boundaries exist (multiple sec="" sections),
|
|
1301
|
+
// the scoped ownedSegs correctly limits which subsections
|
|
1302
|
+
// each section can adopt. When there are NO boundaries
|
|
1303
|
+
// (single sec="" section, e.g. mangopay's "Introduction"),
|
|
1304
|
+
// buildSectionTree("") would re-adopt all depth-0 peers;
|
|
1305
|
+
// pass an empty child list instead — its leaf artifacts are
|
|
1306
|
+
// already in its own segment nav.
|
|
1307
|
+
const isUnboundedSkipSlugSibling = sec === "" && metaNum(s.seg.metadata, "childIndex") != null && bounds == null;
|
|
1308
|
+
const sectionChildren = isUnboundedSkipSlugSibling
|
|
1309
|
+
? []
|
|
1310
|
+
: buildSectionTree(ctx, ownedSegs, sec, scopePrefix, changelogScopePrefix);
|
|
1311
|
+
node = sectionNode(ctx, s, sectionChildren, scopePrefix);
|
|
1194
1312
|
}
|
|
1195
1313
|
// Shared/merged sections (paths not rooted in scopePrefix) must not
|
|
1196
1314
|
// carry childIndex — their source-version positions would sort them
|
|
@@ -1260,11 +1378,6 @@ function groupSidebarRootChildren(ctx, children) {
|
|
|
1260
1378
|
flushRun();
|
|
1261
1379
|
out.push(child);
|
|
1262
1380
|
}
|
|
1263
|
-
else if (child.type === "changelog") {
|
|
1264
|
-
flushRun();
|
|
1265
|
-
run.push(child);
|
|
1266
|
-
flushRun();
|
|
1267
|
-
}
|
|
1268
1381
|
else {
|
|
1269
1382
|
run.push(child);
|
|
1270
1383
|
}
|
|
@@ -1431,7 +1544,7 @@ function buildVariantedNode(ctx, variants, scopeSegs, scopePrefix) {
|
|
|
1431
1544
|
? asSlug(FernNavigation.slugjoin(scopePrefix, variantIndividualSlug))
|
|
1432
1545
|
: scopePrefix;
|
|
1433
1546
|
const metaPointsTo = metaStr(variant.metadata, "pointsTo");
|
|
1434
|
-
const children = buildSidebarRootChildren(ctx, variantSegs,
|
|
1547
|
+
const children = buildSidebarRootChildren(ctx, variantSegs, variantSlug);
|
|
1435
1548
|
return {
|
|
1436
1549
|
type: "variant",
|
|
1437
1550
|
...withMetadataDefaults({
|
|
@@ -1967,6 +2080,14 @@ function buildProductGroup(ctx, products, scoped, rootSlug, skipSharedSectionMer
|
|
|
1967
2080
|
};
|
|
1968
2081
|
return node;
|
|
1969
2082
|
});
|
|
2083
|
+
// Cross-product canonical slug deduplication (V2 parity).
|
|
2084
|
+
// V2's migrator deduplicates pages across products via a global pageId map:
|
|
2085
|
+
// the first product to register a pageId wins, and subsequent products'
|
|
2086
|
+
// pages with the same pageId receive a canonicalSlug pointing to the
|
|
2087
|
+
// canonical (default-product) page. Without this, shared content that is
|
|
2088
|
+
// materialized under every product scope (e.g. merge-api-basics pages)
|
|
2089
|
+
// appears as separate sitemap entries per product.
|
|
2090
|
+
markCrossProductCanonicalSlugs(children);
|
|
1970
2091
|
return {
|
|
1971
2092
|
type: "productgroup",
|
|
1972
2093
|
id: ctx.nodeId("productgroup"),
|
|
@@ -1975,5 +2096,43 @@ function buildProductGroup(ctx, products, scoped, rootSlug, skipSharedSectionMer
|
|
|
1975
2096
|
children
|
|
1976
2097
|
};
|
|
1977
2098
|
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Deduplicate pages across products using pageId as a content-identity key.
|
|
2101
|
+
* Pages with the same pageId in different products are the same content
|
|
2102
|
+
* materialized per-scope; the default product's slug becomes canonical and
|
|
2103
|
+
* non-default copies receive `canonicalSlug` so the sitemap collapses them.
|
|
2104
|
+
*
|
|
2105
|
+
* API leaf nodes (endpoints, websockets, etc.) are unaffected — `getPageId`
|
|
2106
|
+
* returns undefined for those types, so product-specific API surface is
|
|
2107
|
+
* never suppressed.
|
|
2108
|
+
*/
|
|
2109
|
+
function markCrossProductCanonicalSlugs(productNodes) {
|
|
2110
|
+
const pageIdToCanonical = new Map();
|
|
2111
|
+
// Process default product first so its slugs become the canonical targets.
|
|
2112
|
+
const sorted = [...productNodes].sort((a, b) => {
|
|
2113
|
+
if (a.default !== b.default) {
|
|
2114
|
+
return a.default ? -1 : 1;
|
|
2115
|
+
}
|
|
2116
|
+
return 0;
|
|
2117
|
+
});
|
|
2118
|
+
for (const product of sorted) {
|
|
2119
|
+
FernNavigation.traverseDF(product, (child) => {
|
|
2120
|
+
if (FernNavigation.hasMetadata(child) && FernNavigation.isPage(child)) {
|
|
2121
|
+
const pid = FernNavigation.getPageId(child);
|
|
2122
|
+
if (pid == null) {
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
const key = `pid:${pid}`;
|
|
2126
|
+
const canonical = pageIdToCanonical.get(key);
|
|
2127
|
+
if (canonical != null && !product.default) {
|
|
2128
|
+
child.canonicalSlug = canonical;
|
|
2129
|
+
}
|
|
2130
|
+
else if (canonical == null) {
|
|
2131
|
+
pageIdToCanonical.set(key, child.canonicalSlug ?? child.slug);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
1978
2137
|
// (root keeps the bare "root" id; it is the single tree root.)
|
|
1979
2138
|
//# sourceMappingURL=ledger-root-builder.js.map
|