rspress-plugin-api-extractor 0.14.0 → 0.15.0

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.
Files changed (37) hide show
  1. package/BuildEnv.js +0 -1
  2. package/build-program.js +4 -4
  3. package/build-stages.js +98 -281
  4. package/config-helpers.js +1 -1
  5. package/emit/mdx.js +311 -0
  6. package/emit/meta.js +62 -0
  7. package/index.d.ts +1 -42
  8. package/layers/build-metrics.js +1 -2
  9. package/layers/config-resolution.js +3 -5
  10. package/layers/type-environment.js +1 -2
  11. package/llms-program.js +3 -3
  12. package/markdown/helpers.js +10 -176
  13. package/observability/sinks/console-sink.js +0 -1
  14. package/observability/sinks/metrics-sink.js +0 -2
  15. package/package.json +6 -5
  16. package/path-derivation.js +1 -29
  17. package/plugin.js +2 -1
  18. package/prettier-formatter.js +27 -59
  19. package/remark-with-api.js +3 -2
  20. package/schemas/config.js +1 -18
  21. package/schemas/observability.js +0 -2
  22. package/schemas/performance.js +0 -1
  23. package/services/TwoslashCacheService.js +1 -1
  24. package/twoslash-transformer.js +14 -2
  25. package/code-post-processor.js +0 -38
  26. package/llms-processing.js +0 -270
  27. package/markdown/page-generators/class-page.js +0 -363
  28. package/markdown/page-generators/enum-page.js +0 -152
  29. package/markdown/page-generators/function-page.js +0 -127
  30. package/markdown/page-generators/index-pages.js +0 -25
  31. package/markdown/page-generators/interface-page.js +0 -310
  32. package/markdown/page-generators/namespace-page.js +0 -277
  33. package/markdown/page-generators/type-alias-page.js +0 -110
  34. package/markdown/page-generators/variable-page.js +0 -110
  35. package/markdown/prose-linker.js +0 -22
  36. package/twoslash-cache.js +0 -174
  37. package/twoslash-patterns.js +0 -87
package/BuildEnv.js CHANGED
@@ -39,7 +39,6 @@ const Thresholds = Context.Reference("rspress-plugin-api-extractor/Thresholds",
39
39
  slowPageGeneration: 500,
40
40
  slowApiLoad: 1e3,
41
41
  slowFileOperation: 50,
42
- slowHttpRequest: 2e3,
43
42
  slowDbOperation: 100
44
43
  }) });
45
44
  /**
package/build-program.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { BuildId, PageConcurrency, SuppressExampleErrors } from "./BuildEnv.js";
2
- import { setProseLinker } from "./markdown/prose-linker.js";
3
2
  import { buildPipelineForApi, cleanupAndCommit, prepareWorkItems, writeMetadata } from "./build-stages.js";
4
3
  import { HideCutLinesTransformer, MemberFormatTransformer } from "./hide-cut-transformer.js";
5
4
  import { withPhase } from "./observability/spans.js";
@@ -10,6 +9,7 @@ import { ShikiCrossLinker } from "./shiki-transformer.js";
10
9
  import { VfsRegistry } from "./vfs-registry.js";
11
10
  import path from "node:path";
12
11
  import { Effect, FileSystem } from "effect";
12
+ import { CrossLinker } from "@tsdoctor/model";
13
13
  import { attributionFacts, packageContext } from "@tsdoctor/seo";
14
14
  import { SnapshotService } from "@tsdoctor/snapshot";
15
15
 
@@ -54,10 +54,9 @@ function generateApiDocs(apiConfig, fileContextMap) {
54
54
  const { workItems, crossLinkData } = yield* withPhase("resolve", phaseCtx, Effect.sync(() => prepareWorkItems({
55
55
  apiPackage,
56
56
  categories,
57
- baseRoute,
58
- packageName
57
+ baseRoute
59
58
  })));
60
- setProseLinker(crossLinkData.routes);
59
+ const linker = CrossLinker.fromRoutes(crossLinkData.routes);
61
60
  const apiScope = baseRoute.replace(/^\//, "").split("/")[0] || packageName;
62
61
  const shikiCrossLinker = ShikiCrossLinker.fromRoutes(crossLinkData.routes, apiScope);
63
62
  addTypeRoutes(crossLinkData.routes);
@@ -88,6 +87,7 @@ function generateApiDocs(apiConfig, fileContextMap) {
88
87
  existingSnapshots,
89
88
  ...suppressExampleErrors != null ? { suppressExampleErrors } : {},
90
89
  ...llmsPlugin != null ? { llmsPlugin } : {},
90
+ linker,
91
91
  ...apiConfig.docsRoot != null ? { docsRoot: apiConfig.docsRoot } : {},
92
92
  ...siteUrl != null ? { siteUrl } : {},
93
93
  ...ogImage != null ? { ogImage } : {},
package/build-stages.js CHANGED
@@ -1,102 +1,50 @@
1
+ import { emitMdxBody } from "./emit/mdx.js";
2
+ import { emitIndexPage, renderCategoryMeta, renderRootMeta } from "./emit/meta.js";
1
3
  import { BuildMetrics } from "./layers/build-metrics.js";
4
+ import { generateFrontmatter } from "./markdown/helpers.js";
2
5
  import { PluginEvent } from "./observability/events.js";
3
6
  import { emit } from "./observability/EventBus.js";
4
7
  import { emitSync, syncBuildId } from "./observability/sync-emitter.js";
5
- import { generateFrontmatter } from "./markdown/helpers.js";
6
- import { ClassPageGenerator } from "./markdown/page-generators/class-page.js";
7
- import { EnumPageGenerator } from "./markdown/page-generators/enum-page.js";
8
- import { FunctionPageGenerator } from "./markdown/page-generators/function-page.js";
9
- import { MainIndexPageGenerator } from "./markdown/page-generators/index-pages.js";
10
- import { InterfacePageGenerator } from "./markdown/page-generators/interface-page.js";
11
- import { NamespacePageGenerator } from "./markdown/page-generators/namespace-page.js";
12
- import { TypeAliasPageGenerator } from "./markdown/page-generators/type-alias-page.js";
13
- import { VariablePageGenerator } from "./markdown/page-generators/variable-page.js";
14
8
  import { OgService } from "./services/OgService.js";
15
9
  import path from "node:path";
10
+ import { NavEntry, buildIndexPage, buildNav, buildPage, prepareWorkItems } from "@tsdoctor/pages";
16
11
  import { Effect, FileSystem, Metric, Option, Stream } from "effect";
12
+ import { Routes, parseFrontmatter, stringifyFrontmatter } from "@tsdoctor/model";
17
13
  import { deriveScriptBody, headTags } from "@tsdoctor/seo";
18
14
  import { SnapshotService, hashContent, hashFrontmatter } from "@tsdoctor/snapshot";
19
- import { ApiItemKind } from "@microsoft/api-extractor-model";
20
- import { ApiItems, EntryPoints, Routes, SyntheticBases, parseFrontmatter, stringifyFrontmatter } from "@tsdoctor/model";
21
15
 
22
16
  //#region src/build-stages.ts
23
17
  /**
24
- * Cross-link priority by API item kind (lower = higher priority). When a bare
25
- * name maps to multiple pages (the const+type companion pattern), the bare
26
- * cross-link resolves to the higher-priority kind — value declarations win over
27
- * type-only declarations, so `Foo` links to the importable schema, not the type.
28
- */
29
- const CROSS_LINK_KIND_PRIORITY = {
30
- Class: 0,
31
- Function: 1,
32
- Variable: 2,
33
- Enum: 3,
34
- Interface: 4,
35
- TypeAlias: 5,
36
- Namespace: 6
37
- };
38
- /** Lower number = higher priority for which page a bare cross-link name resolves to. */
39
- function crossLinkKindPriority(kind) {
40
- return CROSS_LINK_KIND_PRIORITY[kind] ?? 100;
41
- }
42
- /**
43
18
  * Prepare the flat list of WorkItems to process and the cross-link data maps.
44
19
  *
45
- * This function:
46
- * 1. Categorizes API items from the model
47
- * 2. Builds cross-link routes and kinds maps for the prose and Shiki cross-linkers
48
- * 3. Extracts namespace members and adds their routes (with collision detection)
49
- * 4. Flattens all items into a single WorkItem[]
20
+ * The computation lives in `@tsdoctor/pages`' `prepareWorkItems`; this is
21
+ * the adapter's reporting over its result: an `ItemSkipped` warning per
22
+ * uncategorized item through the sync-island seam, and a typed
23
+ * `RouteCollisionDetected` event per collision before the fatal
24
+ * `Routes.RouteCollisionError` so the fatal build path still surfaces the
25
+ * collision in .api-docs/build/issues.json (see plugin.ts's config() catch).
50
26
  *
51
27
  * NOTE: This function does NOT install the prose linker. The caller
52
28
  * is responsible for passing the returned crossLinkData to the cross-linker and
53
29
  * Shiki cross-linker as needed.
54
30
  */
55
- function prepareWorkItems(input) {
31
+ function prepareWorkItems$1(input) {
56
32
  const { apiPackage, categories, baseRoute } = input;
57
- const resolvedItems = EntryPoints.resolve(apiPackage);
58
- const syntheticBases = SyntheticBases.detect(resolvedItems.map((r) => r.item));
59
- const docItems = syntheticBases.bases.size ? resolvedItems.filter((r) => !syntheticBases.bases.has(r.item)) : resolvedItems;
60
- const resolvedLookup = /* @__PURE__ */ new Map();
61
- for (const resolved of docItems) {
62
- const key = `${resolved.item.displayName}::${resolved.item.kind}`;
63
- resolvedLookup.set(key, resolved);
64
- }
65
- const { items, uncategorized } = ApiItems.categorize(docItems, categories);
66
- for (const skipped of uncategorized) emitSync(PluginEvent.ItemSkipped({
33
+ const prepared = prepareWorkItems({
34
+ apiPackage,
35
+ categories,
36
+ baseRoute
37
+ });
38
+ for (const skipped of prepared.uncategorized) emitSync(PluginEvent.ItemSkipped({
67
39
  ctx: { buildId: syncBuildId() },
68
40
  item: skipped.displayName,
69
41
  kind: String(skipped.kind),
70
42
  reason: "uncategorized",
71
43
  level: "warn"
72
44
  }));
73
- const namespaceMembers = ApiItems.namespaceMembers(docItems);
74
- const candidates = [];
75
- for (const [categoryKey, categoryConfig] of Object.entries(categories)) for (const item of items[categoryKey] || []) candidates.push(new Routes.RouteCandidate({
76
- id: `${item.displayName}::${item.kind}`,
77
- displayName: item.displayName,
78
- folder: categoryConfig.folderName,
79
- baseName: item.displayName.toLowerCase(),
80
- kind: String(item.kind),
81
- canonicalRef: item.canonicalReference?.toString() ?? item.displayName
82
- }));
83
- for (const nsMember of namespaceMembers) {
84
- const nsCategoryEntry = Object.entries(categories).find(([, config]) => config.itemKinds?.includes(nsMember.item.kind));
85
- if (!nsCategoryEntry) continue;
86
- const [, nsCategoryConfig] = nsCategoryEntry;
87
- candidates.push(new Routes.RouteCandidate({
88
- id: nsMember.qualifiedName,
89
- displayName: nsMember.qualifiedName,
90
- folder: nsCategoryConfig.folderName,
91
- baseName: nsMember.qualifiedName.toLowerCase(),
92
- kind: String(nsMember.item.kind),
93
- canonicalRef: nsMember.item.canonicalReference?.toString() ?? nsMember.qualifiedName
94
- }));
95
- }
96
- const collisions = Routes.detectCollisions(candidates);
97
- if (collisions.length > 0) {
45
+ if (prepared.collisions.length > 0) {
98
46
  try {
99
- for (const collision of collisions) emitSync(PluginEvent.RouteCollisionDetected({
47
+ for (const collision of prepared.collisions) emitSync(PluginEvent.RouteCollisionDetected({
100
48
  ctx: {
101
49
  buildId: syncBuildId(),
102
50
  route: collision.route
@@ -107,100 +55,12 @@ function prepareWorkItems(input) {
107
55
  } catch {}
108
56
  throw new Routes.RouteCollisionError({
109
57
  baseRoute,
110
- collisions
58
+ collisions: prepared.collisions
111
59
  });
112
60
  }
113
- const routes = /* @__PURE__ */ new Map();
114
- const kinds = /* @__PURE__ */ new Map();
115
- const routeOwnerPriority = /* @__PURE__ */ new Map();
116
- for (const [categoryKey, categoryConfig] of Object.entries(categories)) {
117
- const categoryItems = items[categoryKey] || [];
118
- for (const item of categoryItems) {
119
- const itemRoute = `${baseRoute}/${categoryConfig.folderName}/${item.displayName.toLowerCase()}`;
120
- const priority = crossLinkKindPriority(String(item.kind));
121
- const existingPriority = routeOwnerPriority.get(item.displayName);
122
- if (existingPriority === void 0 || priority < existingPriority) {
123
- routes.set(item.displayName, itemRoute);
124
- kinds.set(item.displayName, item.kind);
125
- routeOwnerPriority.set(item.displayName, priority);
126
- }
127
- if (item.kind === "Class" || item.kind === "Interface") {
128
- const itemWithMembers = item;
129
- const anchors = ApiItems.memberAnchors(itemWithMembers);
130
- const byCanonicalRef = new Map(itemWithMembers.members.map((member) => [member.canonicalReference?.toString() ?? member.displayName, member]));
131
- for (const [routeKey, memberId] of ApiItems.memberRouteKeys(itemWithMembers)) {
132
- const member = byCanonicalRef.get(memberId);
133
- if (!member) continue;
134
- const anchor = anchors.get(memberId) ?? Routes.memberAnchor(member.displayName);
135
- routes.set(routeKey, `${itemRoute}#${anchor}`);
136
- kinds.set(routeKey, member.kind);
137
- }
138
- }
139
- }
140
- }
141
- const unqualifiedNameCounts = /* @__PURE__ */ new Map();
142
- for (const nsMember of namespaceMembers) {
143
- const name = nsMember.item.displayName;
144
- unqualifiedNameCounts.set(name, (unqualifiedNameCounts.get(name) || 0) + 1);
145
- }
146
- for (const nsMember of namespaceMembers) {
147
- const categoryEntry = Object.entries(categories).find(([, config]) => config.itemKinds?.includes(nsMember.item.kind));
148
- if (!categoryEntry) continue;
149
- const [, categoryConfig] = categoryEntry;
150
- const qualifiedRoute = `${baseRoute}/${categoryConfig.folderName}/${nsMember.qualifiedName.toLowerCase()}`;
151
- routes.set(nsMember.qualifiedName, qualifiedRoute);
152
- kinds.set(nsMember.qualifiedName, nsMember.item.kind);
153
- const displayName = nsMember.item.displayName;
154
- if (/^[A-Z]/.test(displayName) && (unqualifiedNameCounts.get(displayName) || 0) <= 1 && !routes.has(displayName)) {
155
- routes.set(displayName, qualifiedRoute);
156
- kinds.set(displayName, nsMember.item.kind);
157
- }
158
- }
159
- for (const [baseItem, syntheticBase] of syntheticBases.bases) {
160
- const baseName = baseItem.displayName;
161
- if (routes.has(baseName)) continue;
162
- const owner = syntheticBase.ownerClasses[0];
163
- const ownerRoute = owner ? routes.get(owner.displayName) : void 0;
164
- if (!ownerRoute) continue;
165
- routes.set(baseName, `${ownerRoute}#${SyntheticBases.BASE_CLASS_ANCHOR}`);
166
- kinds.set(baseName, baseItem.kind);
167
- }
168
- const workItems = [];
169
- for (const [categoryKey, categoryConfig] of Object.entries(categories)) {
170
- const categoryItems = items[categoryKey] || [];
171
- for (const item of categoryItems) {
172
- const lookupKey = `${item.displayName}::${item.kind}`;
173
- const resolved = resolvedLookup.get(lookupKey);
174
- const syntheticBase = syntheticBases.baseByOwner.get(item);
175
- const memberAnchors = item.kind === "Class" || item.kind === "Interface" ? ApiItems.memberAnchors(item) : void 0;
176
- workItems.push({
177
- item,
178
- categoryKey,
179
- categoryConfig,
180
- ...resolved?.availableFrom != null ? { availableFrom: resolved.availableFrom } : {},
181
- ...syntheticBase != null ? { syntheticBase } : {},
182
- ...memberAnchors != null ? { memberAnchors } : {}
183
- });
184
- }
185
- }
186
- for (const nsMember of namespaceMembers) {
187
- const categoryEntry = Object.entries(categories).find(([, config]) => config.itemKinds?.includes(nsMember.item.kind));
188
- if (categoryEntry) {
189
- const [categoryKey, categoryConfig] = categoryEntry;
190
- workItems.push({
191
- item: nsMember.item,
192
- categoryKey,
193
- categoryConfig,
194
- namespaceMember: nsMember
195
- });
196
- }
197
- }
198
61
  return {
199
- workItems,
200
- crossLinkData: {
201
- routes,
202
- kinds
203
- }
62
+ workItems: prepared.workItems,
63
+ crossLinkData: prepared.crossLinkData
204
64
  };
205
65
  }
206
66
  /**
@@ -220,94 +80,58 @@ function generateSinglePage(workItem, ctx) {
220
80
  const { buildId, existingSnapshots, baseRoute, packageName, apiScope, apiName, source, buildTime, resolvedOutputDir, suppressExampleErrors, llmsPlugin } = ctx;
221
81
  const { item, categoryConfig, namespaceMember } = workItem;
222
82
  const pageGenStart = performance.now();
223
- let page = null;
224
- switch (item.kind) {
225
- case ApiItemKind.Class: {
226
- const generator = new ClassPageGenerator();
227
- page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom, workItem.syntheticBase, workItem.memberAnchors));
228
- page = {
229
- routePath: page.routePath.replace("/class/", `/${categoryConfig.folderName}/`),
230
- content: page.content
231
- };
232
- break;
233
- }
234
- case ApiItemKind.Interface: {
235
- const generator = new InterfacePageGenerator();
236
- page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom));
237
- page = {
238
- routePath: page.routePath.replace("/interface/", `/${categoryConfig.folderName}/`),
239
- content: page.content
240
- };
241
- break;
242
- }
243
- case ApiItemKind.Function: {
244
- const generator = new FunctionPageGenerator();
245
- page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom));
246
- page = {
247
- routePath: page.routePath.replace("/function/", `/${categoryConfig.folderName}/`),
248
- content: page.content
249
- };
250
- break;
251
- }
252
- case ApiItemKind.TypeAlias: {
253
- const generator = new TypeAliasPageGenerator();
254
- page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom));
255
- page = {
256
- routePath: page.routePath.replace("/type/", `/${categoryConfig.folderName}/`),
257
- content: page.content
258
- };
259
- break;
260
- }
261
- case ApiItemKind.Enum: {
262
- const generator = new EnumPageGenerator();
263
- page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom));
264
- page = {
265
- routePath: page.routePath.replace("/enum/", `/${categoryConfig.folderName}/`),
266
- content: page.content
267
- };
268
- break;
269
- }
270
- case ApiItemKind.Variable: {
271
- const generator = new VariablePageGenerator();
272
- page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom));
273
- page = {
274
- routePath: page.routePath.replace("/variable/", `/${categoryConfig.folderName}/`),
275
- content: page.content
276
- };
277
- break;
278
- }
279
- case ApiItemKind.Namespace: {
280
- const generator = new NamespacePageGenerator();
281
- page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom));
282
- page = {
283
- routePath: page.routePath.replace("/namespace/", `/${categoryConfig.folderName}/`),
284
- content: page.content
285
- };
286
- break;
287
- }
288
- default:
289
- yield* emit(PluginEvent.ItemSkipped({
83
+ const built = yield* buildPage({
84
+ item,
85
+ categoryKey: workItem.categoryKey,
86
+ singularName: categoryConfig.singularName,
87
+ folderName: categoryConfig.folderName,
88
+ baseRoute,
89
+ packageName,
90
+ apiName,
91
+ namespaceMember,
92
+ availableFrom: workItem.availableFrom,
93
+ syntheticBase: workItem.syntheticBase,
94
+ memberAnchors: workItem.memberAnchors,
95
+ source,
96
+ suppressExampleErrors,
97
+ linker: ctx.linker,
98
+ onExampleFormatError: (error) => {
99
+ const cause = error.cause;
100
+ return emit(PluginEvent.PrettierError({
290
101
  ctx: {
291
102
  buildId,
292
103
  packageName,
293
104
  apiScope
294
105
  },
295
- item: item.displayName,
296
- kind: String(item.kind),
297
- reason: "unsupported kind",
298
- level: "trace"
106
+ file: "unknown",
107
+ reason: cause instanceof Error ? cause.message : String(cause),
108
+ level: "warn"
299
109
  }));
300
- return null;
301
- }
302
- if (!page) return null;
303
- if (namespaceMember) {
304
- const qualifiedNameLower = namespaceMember.qualifiedName.toLowerCase();
305
- const lastSlash = page.routePath.lastIndexOf("/");
306
- page = {
307
- routePath: `${page.routePath.slice(0, lastSlash + 1)}${qualifiedNameLower}`,
308
- content: page.content
309
- };
110
+ }
111
+ });
112
+ if (Option.isNone(built)) {
113
+ yield* emit(PluginEvent.ItemSkipped({
114
+ ctx: {
115
+ buildId,
116
+ packageName,
117
+ apiScope
118
+ },
119
+ item: item.displayName,
120
+ kind: String(item.kind),
121
+ reason: "unsupported kind",
122
+ level: "trace"
123
+ }));
124
+ return null;
310
125
  }
126
+ const irPage = built.value;
127
+ const body = yield* Effect.fromResult(emitMdxBody(irPage, {
128
+ apiScope,
129
+ llmsEnabled: llmsPlugin?.enabled === true
130
+ })).pipe(Effect.orDie);
131
+ const page = {
132
+ routePath: irPage.route,
133
+ content: generateFrontmatter(irPage.entityName, irPage.description, irPage.singularName, apiName) + body
134
+ };
311
135
  const codeblockCount = (page.content.match(/<(ApiSignature|ApiMember|ApiExample)\b/g) ?? []).length;
312
136
  yield* emit(PluginEvent.PageGenerated({
313
137
  ctx: {
@@ -543,20 +367,27 @@ function writeMetadata(input) {
543
367
  const fileSystem = yield* FileSystem.FileSystem;
544
368
  const snapshotSvc = yield* SnapshotService;
545
369
  const { buildId, fileResults, categories, resolvedOutputDir, existingSnapshots, buildTime, baseRoute, packageName, generatedFiles } = input;
546
- const categoriesWithItems = /* @__PURE__ */ new Set();
547
- for (const result of fileResults) categoriesWithItems.add(result.categoryKey);
548
- const apiMetaEntries = [];
549
- for (const [categoryKey, categoryConfig] of Object.entries(categories)) if (categoriesWithItems.has(categoryKey)) apiMetaEntries.push({
550
- type: "dir",
551
- name: categoryConfig.folderName,
552
- label: categoryConfig.displayName,
553
- collapsible: categoryConfig.collapsible ?? true,
554
- collapsed: categoryConfig.collapsed ?? true,
555
- overviewHeaders: categoryConfig.overviewHeaders ?? [2]
370
+ const navCategories = {};
371
+ for (const [categoryKey, categoryConfig] of Object.entries(categories)) navCategories[categoryKey] = {
372
+ displayName: categoryConfig.displayName,
373
+ folderName: categoryConfig.folderName,
374
+ ...categoryConfig.collapsible !== void 0 ? { collapsible: categoryConfig.collapsible } : {},
375
+ ...categoryConfig.collapsed !== void 0 ? { collapsed: categoryConfig.collapsed } : {},
376
+ ...categoryConfig.overviewHeaders !== void 0 ? { overviewHeaders: categoryConfig.overviewHeaders } : {}
377
+ };
378
+ const navTree = buildNav({
379
+ baseRoute,
380
+ categories: navCategories,
381
+ entries: fileResults.map((result) => NavEntry.make({
382
+ categoryKey: result.categoryKey,
383
+ label: result.label,
384
+ name: path.basename(result.relativePathWithExt, ".mdx"),
385
+ route: result.routePath
386
+ }))
556
387
  });
557
388
  const apiMetaJsonPath = path.join(resolvedOutputDir, "_meta.json");
558
389
  const apiMetaJsonRelPath = "_meta.json";
559
- const apiMetaJsonContent = JSON.stringify(apiMetaEntries, null, " ");
390
+ const apiMetaJsonContent = renderRootMeta(navTree);
560
391
  const apiMetaContentHash = hashContent(apiMetaJsonContent);
561
392
  const apiMetaOldSnapshot = existingSnapshots.get(apiMetaJsonRelPath);
562
393
  let apiMetaUnchanged = false;
@@ -621,15 +452,17 @@ function writeMetadata(input) {
621
452
  buildTime
622
453
  }).pipe(Effect.ignore);
623
454
  generatedFiles.add(apiMetaJsonRelPath);
624
- const categoryCounts = {};
625
- for (const result of fileResults) categoryCounts[result.categoryKey] = (categoryCounts[result.categoryKey] || 0) + 1;
626
- const mainIndex = new MainIndexPageGenerator().generate(packageName, baseRoute, categoryCounts);
627
- const indexRelativePath = `${mainIndex.routePath.replace(baseRoute, "").replace(/^\//, "")}.mdx`;
455
+ const mainIndex = buildIndexPage({
456
+ packageName,
457
+ baseRoute
458
+ });
459
+ const mainIndexContent = emitIndexPage(mainIndex);
460
+ const indexRelativePath = `${mainIndex.route.replace(baseRoute, "").replace(/^\//, "")}.mdx`;
628
461
  const indexAbsolutePath = path.join(resolvedOutputDir, indexRelativePath);
629
462
  if (!(yield* fileSystem.exists(indexAbsolutePath).pipe(Effect.orElseSucceed(() => false)))) {
630
463
  const indexDirPath = path.dirname(indexAbsolutePath);
631
464
  yield* fileSystem.makeDirectory(indexDirPath, { recursive: true }).pipe(Effect.orDie);
632
- yield* fileSystem.writeFileString(indexAbsolutePath, mainIndex.content).pipe(Effect.orDie);
465
+ yield* fileSystem.writeFileString(indexAbsolutePath, mainIndexContent).pipe(Effect.orDie);
633
466
  yield* Metric.update(BuildMetrics.filesTotal, 1);
634
467
  yield* Metric.update(BuildMetrics.filesNew, 1);
635
468
  } else {
@@ -637,28 +470,11 @@ function writeMetadata(input) {
637
470
  yield* Metric.update(BuildMetrics.filesUnchanged, 1);
638
471
  }
639
472
  generatedFiles.add("index.mdx");
640
- const categoryMetaEntriesMap = /* @__PURE__ */ new Map();
641
- for (const result of fileResults) {
642
- const baseName = path.basename(result.relativePathWithExt, ".mdx");
643
- const entries = categoryMetaEntriesMap.get(result.categoryKey) || [];
644
- entries.push({
645
- name: baseName,
646
- label: result.label
647
- });
648
- categoryMetaEntriesMap.set(result.categoryKey, entries);
649
- }
650
- const metaSnapshotsToUpdate = (yield* Effect.forEach(Array.from(categoryMetaEntriesMap.entries()), ([categoryKey, entries]) => Effect.gen(function* () {
651
- const categoryConfig = categories[categoryKey];
652
- if (!categoryConfig || entries.length === 0) return null;
653
- entries.sort((a, b) => a.label.localeCompare(b.label));
654
- const categoryMeta = entries.map((entry) => ({
655
- type: "file",
656
- name: entry.name,
657
- label: entry.label
658
- }));
473
+ const metaSnapshotsToUpdate = (yield* Effect.forEach(navTree.groups, (group) => Effect.gen(function* () {
474
+ const categoryConfig = group.category;
659
475
  const categoryMetaPath = path.join(resolvedOutputDir, categoryConfig.folderName, "_meta.json");
660
476
  const relPath = path.join(categoryConfig.folderName, "_meta.json");
661
- const content = JSON.stringify(categoryMeta, null, " ");
477
+ const content = renderCategoryMeta(group);
662
478
  const contentHash = hashContent(content);
663
479
  const oldSnapshot = existingSnapshots.get(relPath);
664
480
  let isUnchanged = false;
@@ -829,6 +645,7 @@ function buildPipelineForApi(input) {
829
645
  resolvedOutputDir: input.resolvedOutputDir,
830
646
  ...input.suppressExampleErrors != null ? { suppressExampleErrors: input.suppressExampleErrors } : {},
831
647
  ...input.llmsPlugin != null ? { llmsPlugin: input.llmsPlugin } : {},
648
+ linker: input.linker,
832
649
  ...input.docsRoot !== void 0 ? { docsRoot: input.docsRoot } : {},
833
650
  ...input.siteUrl != null ? { siteUrl: input.siteUrl } : {},
834
651
  ...input.ogImage != null ? { ogImage: input.ogImage } : {},
@@ -849,4 +666,4 @@ function buildPipelineForApi(input) {
849
666
  }
850
667
 
851
668
  //#endregion
852
- export { buildPipelineForApi, cleanupAndCommit, crossLinkKindPriority, generateSinglePage, prepareWorkItems, writeMetadata, writeSingleFile };
669
+ export { buildPipelineForApi, cleanupAndCommit, generateSinglePage, prepareWorkItems$1 as prepareWorkItems, writeMetadata, writeSingleFile };
package/config-helpers.js CHANGED
@@ -1,7 +1,7 @@
1
- import { normalizeBaseRoute } from "./path-derivation.js";
2
1
  import { SyncDiscoveryLayer } from "./sync-node-fs.js";
3
2
  import fsSync from "node:fs";
4
3
  import path from "node:path";
4
+ import { normalizeBaseRoute } from "@tsdoctor/pages";
5
5
  import { Effect, Result } from "effect";
6
6
  import { discoverBundle } from "@tsdoctor/bundle";
7
7