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