rspress-plugin-api-extractor 0.1.2 → 0.2.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 (109) hide show
  1. package/README.md +2 -2
  2. package/api-extracted-package.js +426 -0
  3. package/build-program.js +120 -0
  4. package/build-stages.js +664 -0
  5. package/category-resolver.js +50 -0
  6. package/code-post-processor.js +38 -0
  7. package/config-helpers.js +106 -0
  8. package/config-utils.js +258 -0
  9. package/content-hash.js +79 -0
  10. package/errors.js +29 -0
  11. package/formatter.js +69 -0
  12. package/hide-cut-transformer.js +96 -0
  13. package/index.d.ts +901 -831
  14. package/index.js +4 -6128
  15. package/layers/ConfigServiceLive.js +377 -0
  16. package/layers/ObservabilityLive.js +136 -0
  17. package/layers/PathDerivationServiceLive.js +16 -0
  18. package/layers/SnapshotServiceLive.js +94 -0
  19. package/layers/TypeRegistryServiceLive.js +46 -0
  20. package/llms-processing.js +270 -0
  21. package/llms-program.js +262 -0
  22. package/loader.js +186 -0
  23. package/markdown/cross-linker.js +156 -0
  24. package/markdown/helpers.js +364 -0
  25. package/markdown/index.js +11 -0
  26. package/markdown/page-generators/class-page.js +357 -0
  27. package/markdown/page-generators/enum-page.js +152 -0
  28. package/markdown/page-generators/function-page.js +127 -0
  29. package/markdown/page-generators/index-pages.js +27 -0
  30. package/markdown/page-generators/interface-page.js +307 -0
  31. package/markdown/page-generators/namespace-page.js +280 -0
  32. package/markdown/page-generators/type-alias-page.js +110 -0
  33. package/markdown/page-generators/variable-page.js +110 -0
  34. package/markdown/shiki-utils.js +48 -0
  35. package/migrations/001_create_snapshots.js +25 -0
  36. package/model-loader.js +95 -0
  37. package/multi-entry-resolver.js +70 -0
  38. package/og-resolver.js +271 -0
  39. package/package.json +63 -73
  40. package/path-derivation.js +48 -0
  41. package/plugin.js +218 -0
  42. package/prettier-formatter.js +73 -0
  43. package/public/tsconfig/rspress.json +44 -0
  44. package/remark-api-codeblocks.js +130 -0
  45. package/remark-with-api.js +172 -0
  46. package/route-collisions.js +52 -0
  47. package/runtime/components/ApiExample/index.js +29 -25
  48. package/runtime/components/ApiLlmsPackageActions/index.js +252 -323
  49. package/runtime/components/ApiLlmsViewOptions/index.js +269 -340
  50. package/runtime/components/ApiMember/index.js +49 -47
  51. package/runtime/components/ApiSignature/index.js +32 -28
  52. package/runtime/components/EnumMembersTable/{index_module.css → index.css} +18 -19
  53. package/runtime/components/EnumMembersTable/index.js +36 -67
  54. package/runtime/components/EnumMembersTable/index.module.js +9 -6
  55. package/runtime/components/ExampleBlock/{index_module.css → index.css} +2 -3
  56. package/runtime/components/ExampleBlock/index.js +23 -28
  57. package/runtime/components/ExampleBlock/index.module.js +8 -5
  58. package/runtime/components/MarkdownContent/index.js +26 -18
  59. package/runtime/components/MarkdownText/index.js +28 -22
  60. package/runtime/components/MemberSignature/{index_module.css → index.css} +5 -6
  61. package/runtime/components/MemberSignature/index.js +46 -46
  62. package/runtime/components/MemberSignature/index.module.js +9 -6
  63. package/runtime/components/ParametersTable/{index_module.css → index.css} +19 -20
  64. package/runtime/components/ParametersTable/index.js +36 -67
  65. package/runtime/components/ParametersTable/index.module.js +9 -6
  66. package/runtime/components/SignatureBlock/{index_module.css → index.css} +5 -6
  67. package/runtime/components/SignatureBlock/index.js +30 -29
  68. package/runtime/components/SignatureBlock/index.module.js +9 -6
  69. package/runtime/components/SignatureCode/{index_module.css → index.css} +9 -10
  70. package/runtime/components/SignatureCode/index.js +40 -32
  71. package/runtime/components/SignatureCode/index.module.js +9 -6
  72. package/runtime/components/SignatureToolbar/{index_module.css → index.css} +18 -20
  73. package/runtime/components/SignatureToolbar/index.js +48 -51
  74. package/runtime/components/SignatureToolbar/index.module.js +13 -10
  75. package/runtime/components/buttons/ButtonGroup.js +13 -6
  76. package/runtime/components/buttons/CopyCodeButton.js +40 -38
  77. package/runtime/components/buttons/WrapSignatureButton.js +20 -16
  78. package/runtime/components/buttons/{index_module.css → index.css} +5 -6
  79. package/runtime/components/buttons/index.module.js +8 -5
  80. package/runtime/components/icons/CheckIcon/index.js +20 -17
  81. package/runtime/components/icons/CopyIcon/index.js +20 -17
  82. package/runtime/components/icons/UnwrapIcon/index.js +22 -18
  83. package/runtime/components/icons/WrapIcon/index.js +20 -17
  84. package/runtime/components/shared/_twoslash.css +3 -10
  85. package/runtime/components/shared/variables.css +0 -3
  86. package/runtime/hooks/useWrapToggle.js +32 -9
  87. package/runtime/index.d.ts +513 -173
  88. package/runtime/index.js +11 -9
  89. package/runtime/utils/decode-hast.js +31 -16
  90. package/runtime/utils/hast-renderer.js +21 -7
  91. package/schemas/config.js +199 -0
  92. package/schemas/index.js +5 -0
  93. package/schemas/opengraph.js +26 -0
  94. package/schemas/performance.js +19 -0
  95. package/serve.js +133 -0
  96. package/services/ConfigService.js +7 -0
  97. package/services/PathDerivationService.js +7 -0
  98. package/services/SnapshotService.js +7 -0
  99. package/services/TypeRegistryService.js +7 -0
  100. package/shiki-transformer.js +758 -0
  101. package/tsconfig-parser.js +127 -0
  102. package/tsdoc-metadata.json +11 -11
  103. package/twoslash-patterns.js +87 -0
  104. package/twoslash-transformer.js +316 -0
  105. package/type-reference-extractor.js +201 -0
  106. package/typescript-config.js +168 -0
  107. package/vfs-registry.js +121 -0
  108. package/0~llms-program.js +0 -344
  109. package/runtime/components/ApiLlmsPackageActions/index.module.js +0 -2
@@ -0,0 +1,664 @@
1
+ import { hashContent, hashFrontmatter } from "./content-hash.js";
2
+ import { BuildMetrics } from "./layers/ObservabilityLive.js";
3
+ import { ApiParser } from "./loader.js";
4
+ import { generateFrontmatter } from "./markdown/helpers.js";
5
+ import { ClassPageGenerator } from "./markdown/page-generators/class-page.js";
6
+ import { EnumPageGenerator } from "./markdown/page-generators/enum-page.js";
7
+ import { FunctionPageGenerator } from "./markdown/page-generators/function-page.js";
8
+ import { MainIndexPageGenerator } from "./markdown/page-generators/index-pages.js";
9
+ import { InterfacePageGenerator } from "./markdown/page-generators/interface-page.js";
10
+ import { NamespacePageGenerator } from "./markdown/page-generators/namespace-page.js";
11
+ import { TypeAliasPageGenerator } from "./markdown/page-generators/type-alias-page.js";
12
+ import { VariablePageGenerator } from "./markdown/page-generators/variable-page.js";
13
+ import "./markdown/index.js";
14
+ import { resolveEntryPoints } from "./multi-entry-resolver.js";
15
+ import { OpenGraphResolver } from "./og-resolver.js";
16
+ import { assertNoRouteCollisions } from "./route-collisions.js";
17
+ import { SnapshotService } from "./services/SnapshotService.js";
18
+ import path from "node:path";
19
+ import { Effect, Metric, Stream } from "effect";
20
+ import { FileSystem } from "@effect/platform";
21
+ import { ApiItemKind } from "@microsoft/api-extractor-model";
22
+ import matter from "gray-matter";
23
+
24
+ //#region src/build-stages.ts
25
+ /**
26
+ * Cross-link priority by API item kind (lower = higher priority). When a bare
27
+ * name maps to multiple pages (the const+type companion pattern), the bare
28
+ * cross-link resolves to the higher-priority kind — value declarations win over
29
+ * type-only declarations, so `Foo` links to the importable schema, not the type.
30
+ */
31
+ const CROSS_LINK_KIND_PRIORITY = {
32
+ Class: 0,
33
+ Function: 1,
34
+ Variable: 2,
35
+ Enum: 3,
36
+ Interface: 4,
37
+ TypeAlias: 5,
38
+ Namespace: 6
39
+ };
40
+ /** Lower number = higher priority for which page a bare cross-link name resolves to. */
41
+ function crossLinkKindPriority(kind) {
42
+ return CROSS_LINK_KIND_PRIORITY[kind] ?? 100;
43
+ }
44
+ /**
45
+ * Sanitize a display name to create a valid HTML ID.
46
+ * Mirrors the logic in MarkdownCrossLinker.sanitizeId().
47
+ */
48
+ function sanitizeId(displayName) {
49
+ return displayName.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "");
50
+ }
51
+ /**
52
+ * Prepare the flat list of WorkItems to process and the cross-link data maps.
53
+ *
54
+ * This function:
55
+ * 1. Categorizes API items from the model
56
+ * 2. Builds cross-link routes and kinds maps (replicating MarkdownCrossLinker.initialize())
57
+ * 3. Extracts namespace members and adds their routes (with collision detection)
58
+ * 4. Flattens all items into a single WorkItem[]
59
+ *
60
+ * NOTE: This function does NOT call the markdownCrossLinker singleton. The caller
61
+ * is responsible for passing the returned crossLinkData to the cross-linker and
62
+ * Shiki cross-linker as needed.
63
+ */
64
+ function prepareWorkItems(input) {
65
+ const { apiPackage, categories, baseRoute } = input;
66
+ const resolvedItems = resolveEntryPoints(apiPackage);
67
+ const resolvedLookup = /* @__PURE__ */ new Map();
68
+ for (const resolved of resolvedItems) {
69
+ const key = `${resolved.item.displayName}::${resolved.item.kind}`;
70
+ resolvedLookup.set(key, resolved);
71
+ }
72
+ const items = ApiParser.categorizeApiItems(resolvedItems, categories);
73
+ const namespaceMembers = ApiParser.extractNamespaceMembers(resolvedItems);
74
+ const candidates = [];
75
+ for (const [categoryKey, categoryConfig] of Object.entries(categories)) for (const item of items[categoryKey] || []) candidates.push({
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({
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
+ assertNoRouteCollisions(candidates, baseRoute);
97
+ const routes = /* @__PURE__ */ new Map();
98
+ const kinds = /* @__PURE__ */ new Map();
99
+ const routeOwnerPriority = /* @__PURE__ */ new Map();
100
+ for (const [categoryKey, categoryConfig] of Object.entries(categories)) {
101
+ const categoryItems = items[categoryKey] || [];
102
+ for (const item of categoryItems) {
103
+ const itemRoute = `${baseRoute}/${categoryConfig.folderName}/${item.displayName.toLowerCase()}`;
104
+ const priority = crossLinkKindPriority(String(item.kind));
105
+ const existingPriority = routeOwnerPriority.get(item.displayName);
106
+ if (existingPriority === void 0 || priority < existingPriority) {
107
+ routes.set(item.displayName, itemRoute);
108
+ kinds.set(item.displayName, item.kind);
109
+ routeOwnerPriority.set(item.displayName, priority);
110
+ }
111
+ if (item.kind === "Class" || item.kind === "Interface") {
112
+ const itemWithMembers = item;
113
+ for (const member of itemWithMembers.members) {
114
+ const memberName = member.displayName;
115
+ const memberId = sanitizeId(memberName);
116
+ const fullMemberName = `${item.displayName}.${memberName}`;
117
+ const memberRoute = `${itemRoute}#${memberId}`;
118
+ routes.set(fullMemberName, memberRoute);
119
+ kinds.set(fullMemberName, member.kind);
120
+ }
121
+ }
122
+ }
123
+ }
124
+ const unqualifiedNameCounts = /* @__PURE__ */ new Map();
125
+ for (const nsMember of namespaceMembers) {
126
+ const name = nsMember.item.displayName;
127
+ unqualifiedNameCounts.set(name, (unqualifiedNameCounts.get(name) || 0) + 1);
128
+ }
129
+ for (const nsMember of namespaceMembers) {
130
+ const categoryEntry = Object.entries(categories).find(([, config]) => config.itemKinds?.includes(nsMember.item.kind));
131
+ if (!categoryEntry) continue;
132
+ const [, categoryConfig] = categoryEntry;
133
+ const qualifiedRoute = `${baseRoute}/${categoryConfig.folderName}/${nsMember.qualifiedName.toLowerCase()}`;
134
+ routes.set(nsMember.qualifiedName, qualifiedRoute);
135
+ kinds.set(nsMember.qualifiedName, nsMember.item.kind);
136
+ const displayName = nsMember.item.displayName;
137
+ if (/^[A-Z]/.test(displayName) && (unqualifiedNameCounts.get(displayName) || 0) <= 1 && !routes.has(displayName)) {
138
+ routes.set(displayName, qualifiedRoute);
139
+ kinds.set(displayName, nsMember.item.kind);
140
+ }
141
+ }
142
+ const workItems = [];
143
+ for (const [categoryKey, categoryConfig] of Object.entries(categories)) {
144
+ const categoryItems = items[categoryKey] || [];
145
+ for (const item of categoryItems) {
146
+ const lookupKey = `${item.displayName}::${item.kind}`;
147
+ const resolved = resolvedLookup.get(lookupKey);
148
+ workItems.push({
149
+ item,
150
+ categoryKey,
151
+ categoryConfig,
152
+ ...resolved?.availableFrom != null ? { availableFrom: resolved.availableFrom } : {}
153
+ });
154
+ }
155
+ }
156
+ for (const nsMember of namespaceMembers) {
157
+ const categoryEntry = Object.entries(categories).find(([, config]) => config.itemKinds?.includes(nsMember.item.kind));
158
+ if (categoryEntry) {
159
+ const [categoryKey, categoryConfig] = categoryEntry;
160
+ workItems.push({
161
+ item: nsMember.item,
162
+ categoryKey,
163
+ categoryConfig,
164
+ namespaceMember: nsMember
165
+ });
166
+ }
167
+ }
168
+ return {
169
+ workItems,
170
+ crossLinkData: {
171
+ routes,
172
+ kinds
173
+ }
174
+ };
175
+ }
176
+ /**
177
+ * Normalize markdown spacing by removing excessive blank lines.
178
+ * - Remove extra blank lines between headings and code blocks
179
+ * - Ensure single blank line between sections
180
+ */
181
+ function normalizeMarkdownSpacing(content) {
182
+ return content.replace(/\n\n\n+/g, "\n\n").replace(/^(#{1,6}\s+.+?)\n+(?=````)/gm, "$1\n").replace(/^(#{2}\s+.+?)\n\n+/gm, "$1\n\n");
183
+ }
184
+ /**
185
+ * Generate a single page from a work item. Returns null for unsupported kinds.
186
+ */
187
+ function generateSinglePage(workItem, ctx) {
188
+ return Effect.gen(function* () {
189
+ const fileSystem = yield* FileSystem.FileSystem;
190
+ const { existingSnapshots, baseRoute, packageName, apiScope, apiName, source, buildTime, resolvedOutputDir, suppressExampleErrors, llmsPlugin } = ctx;
191
+ const { item, categoryConfig, namespaceMember } = workItem;
192
+ let page = null;
193
+ switch (item.kind) {
194
+ case ApiItemKind.Class: {
195
+ const generator = new ClassPageGenerator();
196
+ page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom));
197
+ page = {
198
+ routePath: page.routePath.replace("/class/", `/${categoryConfig.folderName}/`),
199
+ content: page.content
200
+ };
201
+ break;
202
+ }
203
+ case ApiItemKind.Interface: {
204
+ const generator = new InterfacePageGenerator();
205
+ page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom));
206
+ page = {
207
+ routePath: page.routePath.replace("/interface/", `/${categoryConfig.folderName}/`),
208
+ content: page.content
209
+ };
210
+ break;
211
+ }
212
+ case ApiItemKind.Function: {
213
+ const generator = new FunctionPageGenerator();
214
+ page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom));
215
+ page = {
216
+ routePath: page.routePath.replace("/function/", `/${categoryConfig.folderName}/`),
217
+ content: page.content
218
+ };
219
+ break;
220
+ }
221
+ case ApiItemKind.TypeAlias: {
222
+ const generator = new TypeAliasPageGenerator();
223
+ page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom));
224
+ page = {
225
+ routePath: page.routePath.replace("/type/", `/${categoryConfig.folderName}/`),
226
+ content: page.content
227
+ };
228
+ break;
229
+ }
230
+ case ApiItemKind.Enum: {
231
+ const generator = new EnumPageGenerator();
232
+ page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom));
233
+ page = {
234
+ routePath: page.routePath.replace("/enum/", `/${categoryConfig.folderName}/`),
235
+ content: page.content
236
+ };
237
+ break;
238
+ }
239
+ case ApiItemKind.Variable: {
240
+ const generator = new VariablePageGenerator();
241
+ page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom));
242
+ page = {
243
+ routePath: page.routePath.replace("/variable/", `/${categoryConfig.folderName}/`),
244
+ content: page.content
245
+ };
246
+ break;
247
+ }
248
+ case ApiItemKind.Namespace: {
249
+ const generator = new NamespacePageGenerator();
250
+ page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom));
251
+ page = {
252
+ routePath: page.routePath.replace("/namespace/", `/${categoryConfig.folderName}/`),
253
+ content: page.content
254
+ };
255
+ break;
256
+ }
257
+ default:
258
+ yield* Effect.logDebug(`Skipping item "${item.displayName}" with unsupported kind: ${item.kind} (${ApiItemKind[item.kind] || "unknown"}) in category "${categoryConfig.displayName}"`);
259
+ return null;
260
+ }
261
+ if (!page) return null;
262
+ if (namespaceMember) {
263
+ const simpleName = item.displayName.toLowerCase();
264
+ const qualifiedNameLower = namespaceMember.qualifiedName.toLowerCase();
265
+ page = {
266
+ routePath: page.routePath.replace(`/${simpleName}`, `/${qualifiedNameLower}`),
267
+ content: page.content
268
+ };
269
+ }
270
+ yield* Metric.increment(BuildMetrics.pagesGenerated);
271
+ const parsed = matter(page.content);
272
+ const bodyContent = normalizeMarkdownSpacing(parsed.content);
273
+ const frontmatterData = parsed.data;
274
+ const relativePathWithExt = `${page.routePath.replace(baseRoute, "").replace(/^\//, "")}.mdx`;
275
+ const contentHash = hashContent(bodyContent);
276
+ const frontmatterHash = hashFrontmatter(frontmatterData);
277
+ let publishedTime;
278
+ let modifiedTime;
279
+ let isUnchanged = false;
280
+ const oldSnapshot = existingSnapshots.get(relativePathWithExt);
281
+ if (!oldSnapshot) {
282
+ const absolutePath = path.join(resolvedOutputDir, relativePathWithExt);
283
+ if (yield* fileSystem.exists(absolutePath).pipe(Effect.orElseSucceed(() => false))) {
284
+ const existingContent = yield* fileSystem.readFileString(absolutePath).pipe(Effect.orElseSucceed(() => null));
285
+ if (existingContent !== null) {
286
+ const { data: existingFrontmatter, content: existingBody } = matter(existingContent);
287
+ const existingContentHash = hashContent(normalizeMarkdownSpacing(existingBody));
288
+ const existingFrontmatterHash = hashFrontmatter(existingFrontmatter);
289
+ if (existingContentHash === contentHash && existingFrontmatterHash === frontmatterHash) {
290
+ publishedTime = existingFrontmatter["article:published_time"] || buildTime;
291
+ modifiedTime = existingFrontmatter["article:modified_time"] || buildTime;
292
+ isUnchanged = true;
293
+ } else {
294
+ publishedTime = existingFrontmatter["article:published_time"] || buildTime;
295
+ modifiedTime = buildTime;
296
+ }
297
+ } else {
298
+ publishedTime = buildTime;
299
+ modifiedTime = buildTime;
300
+ }
301
+ } else {
302
+ publishedTime = buildTime;
303
+ modifiedTime = buildTime;
304
+ }
305
+ } else if (oldSnapshot.contentHash === contentHash && oldSnapshot.frontmatterHash === frontmatterHash) {
306
+ publishedTime = oldSnapshot.publishedTime;
307
+ modifiedTime = oldSnapshot.modifiedTime;
308
+ isUnchanged = true;
309
+ } else {
310
+ publishedTime = oldSnapshot.publishedTime;
311
+ modifiedTime = buildTime;
312
+ }
313
+ return {
314
+ workItem,
315
+ content: page.content,
316
+ bodyContent,
317
+ frontmatter: frontmatterData,
318
+ contentHash,
319
+ frontmatterHash,
320
+ routePath: page.routePath,
321
+ relativePathWithExt,
322
+ publishedTime,
323
+ modifiedTime,
324
+ isUnchanged
325
+ };
326
+ });
327
+ }
328
+ /**
329
+ * Write a single generated page to disk. No-op for unchanged pages.
330
+ */
331
+ function writeSingleFile(result, ctx) {
332
+ return Effect.gen(function* () {
333
+ const fileSystem = yield* FileSystem.FileSystem;
334
+ const { resolvedOutputDir, buildTime, ogResolver, siteUrl, ogImage, packageName, apiName } = ctx;
335
+ const { workItem, bodyContent, frontmatter, contentHash, frontmatterHash, publishedTime, modifiedTime, isUnchanged, routePath, relativePathWithExt } = result;
336
+ const { item, categoryKey, categoryConfig, namespaceMember } = workItem;
337
+ const absolutePath = path.join(resolvedOutputDir, relativePathWithExt);
338
+ const label = namespaceMember ? namespaceMember.qualifiedName : item.displayName;
339
+ const snapshot = {
340
+ outputDir: resolvedOutputDir,
341
+ filePath: relativePathWithExt,
342
+ publishedTime,
343
+ modifiedTime,
344
+ contentHash,
345
+ frontmatterHash,
346
+ buildTime
347
+ };
348
+ if (isUnchanged) {
349
+ yield* Metric.increment(BuildMetrics.filesTotal);
350
+ yield* Metric.increment(BuildMetrics.filesUnchanged);
351
+ return {
352
+ relativePathWithExt,
353
+ absolutePath,
354
+ status: "unchanged",
355
+ snapshot,
356
+ categoryKey,
357
+ label,
358
+ routePath
359
+ };
360
+ }
361
+ let finalContent = matter.stringify(bodyContent, frontmatter);
362
+ if (ogResolver && siteUrl && packageName) {
363
+ const ogImageMetadata = yield* Effect.promise(() => ogResolver.resolve(ogImage, packageName, apiName));
364
+ const ogMetadataOptions = {
365
+ siteUrl,
366
+ pageRoute: routePath,
367
+ description: frontmatter.description,
368
+ publishedTime,
369
+ modifiedTime,
370
+ section: categoryConfig.displayName,
371
+ packageName
372
+ };
373
+ if (ogImageMetadata) ogMetadataOptions.ogImage = ogImageMetadata;
374
+ const ogMetadata = OpenGraphResolver.createPageMetadata(ogMetadataOptions);
375
+ finalContent = generateFrontmatter(item.displayName, frontmatter.description, categoryConfig.singularName, apiName, ogMetadata) + bodyContent;
376
+ }
377
+ const fileExisted = yield* fileSystem.exists(absolutePath).pipe(Effect.orElseSucceed(() => false));
378
+ const dirPath = path.dirname(absolutePath);
379
+ yield* fileSystem.makeDirectory(dirPath, { recursive: true }).pipe(Effect.orDie);
380
+ yield* fileSystem.writeFileString(absolutePath, finalContent).pipe(Effect.orDie);
381
+ const status = fileExisted ? "modified" : "new";
382
+ yield* Metric.increment(BuildMetrics.filesTotal);
383
+ if (status === "new") yield* Metric.increment(BuildMetrics.filesNew);
384
+ else yield* Metric.increment(BuildMetrics.filesModified);
385
+ return {
386
+ relativePathWithExt,
387
+ absolutePath,
388
+ status,
389
+ snapshot,
390
+ categoryKey,
391
+ label,
392
+ routePath
393
+ };
394
+ });
395
+ }
396
+ /**
397
+ * Write all metadata files (_meta.json and index.mdx) for the generated API docs.
398
+ *
399
+ * This function handles three groups of metadata:
400
+ * 1. Root API _meta.json — category folder entries with collapsible/collapsed settings
401
+ * 2. Main index page (index.mdx) — API landing page, skipped if already exists
402
+ * 3. Category _meta.json files — sorted navigation entries per category folder
403
+ *
404
+ * All writes use snapshot tracking (hash comparison, disk fallback, timestamp
405
+ * preservation) to avoid unnecessary disk writes.
406
+ *
407
+ * The `generatedFiles` Set is mutated — entries are added for each metadata file
408
+ * written. This is required for stale file cleanup by the caller.
409
+ */
410
+ function writeMetadata(input) {
411
+ return Effect.gen(function* () {
412
+ const fileSystem = yield* FileSystem.FileSystem;
413
+ const snapshotSvc = yield* SnapshotService;
414
+ const { fileResults, categories, resolvedOutputDir, existingSnapshots, buildTime, baseRoute, packageName, generatedFiles } = input;
415
+ const categoriesWithItems = /* @__PURE__ */ new Set();
416
+ for (const result of fileResults) categoriesWithItems.add(result.categoryKey);
417
+ const apiMetaEntries = [];
418
+ for (const [categoryKey, categoryConfig] of Object.entries(categories)) if (categoriesWithItems.has(categoryKey)) apiMetaEntries.push({
419
+ type: "dir",
420
+ name: categoryConfig.folderName,
421
+ label: categoryConfig.displayName,
422
+ collapsible: categoryConfig.collapsible ?? true,
423
+ collapsed: categoryConfig.collapsed ?? true,
424
+ overviewHeaders: categoryConfig.overviewHeaders ?? [2]
425
+ });
426
+ const apiMetaJsonPath = path.join(resolvedOutputDir, "_meta.json");
427
+ const apiMetaJsonRelPath = "_meta.json";
428
+ const apiMetaJsonContent = JSON.stringify(apiMetaEntries, null, " ");
429
+ const apiMetaContentHash = hashContent(apiMetaJsonContent);
430
+ const apiMetaOldSnapshot = existingSnapshots.get(apiMetaJsonRelPath);
431
+ let apiMetaUnchanged = false;
432
+ let apiMetaPublished;
433
+ let apiMetaModified;
434
+ if (!(yield* fileSystem.exists(apiMetaJsonPath).pipe(Effect.orElseSucceed(() => false)))) {
435
+ apiMetaPublished = apiMetaOldSnapshot?.publishedTime || buildTime;
436
+ apiMetaModified = buildTime;
437
+ apiMetaUnchanged = false;
438
+ } else if (!apiMetaOldSnapshot) {
439
+ const existingContent = yield* fileSystem.readFileString(apiMetaJsonPath).pipe(Effect.orElseSucceed(() => null));
440
+ const existingData = existingContent ? yield* Effect.try(() => JSON.parse(existingContent)).pipe(Effect.orElseSucceed(() => null)) : null;
441
+ if ((existingData ? JSON.stringify(existingData, null, " ") : null) === apiMetaJsonContent) {
442
+ apiMetaPublished = "2024-01-01T00:00:00.000Z";
443
+ apiMetaModified = "2024-01-01T00:00:00.000Z";
444
+ apiMetaUnchanged = true;
445
+ } else {
446
+ apiMetaPublished = "2024-01-01T00:00:00.000Z";
447
+ apiMetaModified = buildTime;
448
+ }
449
+ } else if (apiMetaOldSnapshot.contentHash === apiMetaContentHash) {
450
+ apiMetaPublished = apiMetaOldSnapshot.publishedTime;
451
+ apiMetaModified = apiMetaOldSnapshot.modifiedTime;
452
+ apiMetaUnchanged = true;
453
+ } else {
454
+ apiMetaPublished = apiMetaOldSnapshot.publishedTime;
455
+ apiMetaModified = buildTime;
456
+ }
457
+ if (!apiMetaUnchanged) {
458
+ yield* fileSystem.writeFileString(apiMetaJsonPath, apiMetaJsonContent).pipe(Effect.orDie);
459
+ yield* Metric.increment(BuildMetrics.filesTotal);
460
+ if (apiMetaOldSnapshot) yield* Metric.increment(BuildMetrics.filesModified);
461
+ else yield* Metric.increment(BuildMetrics.filesNew);
462
+ } else {
463
+ yield* Metric.increment(BuildMetrics.filesTotal);
464
+ yield* Metric.increment(BuildMetrics.filesUnchanged);
465
+ }
466
+ yield* snapshotSvc.upsert({
467
+ outputDir: resolvedOutputDir,
468
+ filePath: apiMetaJsonRelPath,
469
+ publishedTime: apiMetaPublished,
470
+ modifiedTime: apiMetaModified,
471
+ contentHash: apiMetaContentHash,
472
+ frontmatterHash: "",
473
+ buildTime
474
+ }).pipe(Effect.ignore);
475
+ generatedFiles.add(apiMetaJsonRelPath);
476
+ const categoryCounts = {};
477
+ for (const result of fileResults) categoryCounts[result.categoryKey] = (categoryCounts[result.categoryKey] || 0) + 1;
478
+ const mainIndex = new MainIndexPageGenerator().generate(packageName, baseRoute, categoryCounts);
479
+ const indexRelativePath = `${mainIndex.routePath.replace(baseRoute, "").replace(/^\//, "")}.mdx`;
480
+ const indexAbsolutePath = path.join(resolvedOutputDir, indexRelativePath);
481
+ if (!(yield* fileSystem.exists(indexAbsolutePath).pipe(Effect.orElseSucceed(() => false)))) {
482
+ const indexDirPath = path.dirname(indexAbsolutePath);
483
+ yield* fileSystem.makeDirectory(indexDirPath, { recursive: true }).pipe(Effect.orDie);
484
+ yield* fileSystem.writeFileString(indexAbsolutePath, mainIndex.content).pipe(Effect.orDie);
485
+ yield* Metric.increment(BuildMetrics.filesTotal);
486
+ yield* Metric.increment(BuildMetrics.filesNew);
487
+ } else {
488
+ yield* Metric.increment(BuildMetrics.filesTotal);
489
+ yield* Metric.increment(BuildMetrics.filesUnchanged);
490
+ }
491
+ generatedFiles.add("index.mdx");
492
+ const categoryMetaEntriesMap = /* @__PURE__ */ new Map();
493
+ for (const result of fileResults) {
494
+ const baseName = path.basename(result.relativePathWithExt, ".mdx");
495
+ const entries = categoryMetaEntriesMap.get(result.categoryKey) || [];
496
+ entries.push({
497
+ name: baseName,
498
+ label: result.label
499
+ });
500
+ categoryMetaEntriesMap.set(result.categoryKey, entries);
501
+ }
502
+ const metaSnapshotsToUpdate = (yield* Effect.forEach(Array.from(categoryMetaEntriesMap.entries()), ([categoryKey, entries]) => Effect.gen(function* () {
503
+ const categoryConfig = categories[categoryKey];
504
+ if (!categoryConfig || entries.length === 0) return null;
505
+ entries.sort((a, b) => a.label.localeCompare(b.label));
506
+ const categoryMeta = entries.map((entry) => ({
507
+ type: "file",
508
+ name: entry.name,
509
+ label: entry.label
510
+ }));
511
+ const categoryMetaPath = path.join(resolvedOutputDir, categoryConfig.folderName, "_meta.json");
512
+ const relPath = path.join(categoryConfig.folderName, "_meta.json");
513
+ const content = JSON.stringify(categoryMeta, null, " ");
514
+ const contentHash = hashContent(content);
515
+ const oldSnapshot = existingSnapshots.get(relPath);
516
+ let isUnchanged = false;
517
+ let publishedTime;
518
+ let modifiedTime;
519
+ if (!(yield* fileSystem.exists(categoryMetaPath).pipe(Effect.orElseSucceed(() => false)))) {
520
+ publishedTime = oldSnapshot?.publishedTime || buildTime;
521
+ modifiedTime = buildTime;
522
+ isUnchanged = false;
523
+ } else if (!oldSnapshot) {
524
+ const existingContent = yield* fileSystem.readFileString(categoryMetaPath).pipe(Effect.orElseSucceed(() => null));
525
+ const existingData = existingContent ? yield* Effect.try(() => JSON.parse(existingContent)).pipe(Effect.orElseSucceed(() => null)) : null;
526
+ if ((existingData ? JSON.stringify(existingData, null, " ") : null) === content) {
527
+ publishedTime = "2024-01-01T00:00:00.000Z";
528
+ modifiedTime = "2024-01-01T00:00:00.000Z";
529
+ isUnchanged = true;
530
+ } else {
531
+ publishedTime = "2024-01-01T00:00:00.000Z";
532
+ modifiedTime = buildTime;
533
+ }
534
+ } else if (oldSnapshot.contentHash === contentHash) {
535
+ publishedTime = oldSnapshot.publishedTime;
536
+ modifiedTime = oldSnapshot.modifiedTime;
537
+ isUnchanged = true;
538
+ } else {
539
+ publishedTime = oldSnapshot.publishedTime;
540
+ modifiedTime = buildTime;
541
+ }
542
+ if (!isUnchanged) {
543
+ const categoryDir = path.dirname(categoryMetaPath);
544
+ yield* fileSystem.makeDirectory(categoryDir, { recursive: true }).pipe(Effect.orDie);
545
+ yield* fileSystem.writeFileString(categoryMetaPath, content).pipe(Effect.orDie);
546
+ yield* Metric.increment(BuildMetrics.filesTotal);
547
+ if (oldSnapshot) yield* Metric.increment(BuildMetrics.filesModified);
548
+ else yield* Metric.increment(BuildMetrics.filesNew);
549
+ } else {
550
+ yield* Metric.increment(BuildMetrics.filesTotal);
551
+ yield* Metric.increment(BuildMetrics.filesUnchanged);
552
+ }
553
+ generatedFiles.add(relPath);
554
+ if (isUnchanged) return null;
555
+ return {
556
+ outputDir: resolvedOutputDir,
557
+ filePath: relPath,
558
+ publishedTime,
559
+ modifiedTime,
560
+ contentHash,
561
+ frontmatterHash: "",
562
+ buildTime
563
+ };
564
+ }), { concurrency: "unbounded" })).filter((s) => s !== null);
565
+ if (metaSnapshotsToUpdate.length > 0) yield* snapshotSvc.batchUpsert(metaSnapshotsToUpdate).pipe(Effect.ignore);
566
+ });
567
+ }
568
+ /**
569
+ * Batch-upsert snapshots for written files, then delete stale and orphaned files
570
+ * from disk and the snapshot database. Finally, remove any empty subdirectories.
571
+ *
572
+ * Steps:
573
+ * 1. Filter fileResults to written files (status !== "unchanged"), extract snapshots,
574
+ * and batch-upsert them into the snapshot DB.
575
+ * 2. Call snapshotManager.cleanupStaleFiles() to find files tracked in DB but not
576
+ * generated in this build, then delete them from disk.
577
+ * 3. Read the output directory recursively; for each .mdx or _meta.json file not in
578
+ * generatedFiles, delete it from disk and remove its snapshot.
579
+ * 4. After deleting orphans, remove empty subdirectories deepest-first.
580
+ */
581
+ function cleanupAndCommit(input) {
582
+ return Effect.gen(function* () {
583
+ const fileSystem = yield* FileSystem.FileSystem;
584
+ const snapshotSvc = yield* SnapshotService;
585
+ const { fileResults, resolvedOutputDir, generatedFiles } = input;
586
+ const snapshotsToUpdate = fileResults.filter((r) => r.status !== "unchanged").map((r) => r.snapshot);
587
+ if (snapshotsToUpdate.length > 0) yield* snapshotSvc.batchUpsert(snapshotsToUpdate).pipe(Effect.ignore);
588
+ const staleFiles = yield* snapshotSvc.cleanupStale(resolvedOutputDir, generatedFiles).pipe(Effect.orElseSucceed(() => []));
589
+ yield* Effect.forEach(staleFiles, (staleFile) => Effect.gen(function* () {
590
+ const fullPath = path.join(resolvedOutputDir, staleFile);
591
+ yield* fileSystem.remove(fullPath).pipe(Effect.ignore);
592
+ yield* Effect.logDebug(`🗑️ DELETED STALE: ${staleFile}`);
593
+ }), { concurrency: "unbounded" });
594
+ const allFiles = yield* fileSystem.readDirectory(resolvedOutputDir, { recursive: true }).pipe(Effect.orElseSucceed(() => []));
595
+ const orphanedFiles = [];
596
+ for (const entry of allFiles) {
597
+ const relPath = typeof entry === "string" ? entry : String(entry);
598
+ if (!relPath.endsWith(".mdx") && !relPath.endsWith("_meta.json")) continue;
599
+ const normalizedRelPath = relPath.replace(/\\/g, "/");
600
+ if (!generatedFiles.has(normalizedRelPath)) orphanedFiles.push(normalizedRelPath);
601
+ }
602
+ yield* Effect.forEach(orphanedFiles, (orphan) => Effect.gen(function* () {
603
+ const fullPath = path.join(resolvedOutputDir, orphan);
604
+ yield* fileSystem.remove(fullPath).pipe(Effect.ignore);
605
+ yield* snapshotSvc.deleteSnapshot(resolvedOutputDir, orphan).pipe(Effect.ignore);
606
+ yield* Effect.logDebug(`🗑️ DELETED ORPHAN: ${orphan}`);
607
+ }), { concurrency: "unbounded" });
608
+ if (orphanedFiles.length > 0) {
609
+ const dirs = /* @__PURE__ */ new Set();
610
+ for (const orphan of orphanedFiles) {
611
+ const dir = path.dirname(orphan);
612
+ if (dir !== ".") dirs.add(dir);
613
+ }
614
+ const sortedDirs = [...dirs].sort((a, b) => b.split("/").length - a.split("/").length);
615
+ for (const dir of sortedDirs) {
616
+ const fullDir = path.join(resolvedOutputDir, dir);
617
+ if ((yield* fileSystem.readDirectory(fullDir).pipe(Effect.orElseSucceed(() => ["placeholder"]))).length === 0) {
618
+ yield* fileSystem.remove(fullDir).pipe(Effect.ignore);
619
+ yield* Effect.logDebug(`🗑️ REMOVED EMPTY DIR: ${dir}`);
620
+ }
621
+ }
622
+ }
623
+ });
624
+ }
625
+ /**
626
+ * Effect Stream pipeline: workItems → generate → write (no-op for unchanged) → fold
627
+ *
628
+ * Unchanged files are NOT filtered out. They flow through the write stage as
629
+ * no-ops and appear in the fold output with status: "unchanged". This is
630
+ * required because ALL generated files must be tracked for:
631
+ * - generatedFiles set (stale/orphan cleanup)
632
+ * - fileContextMap (remark plugin Twoslash error attribution)
633
+ * - _meta.json navigation entries
634
+ *
635
+ * The Stream.filter only removes nulls (unsupported ApiItemKind). All other
636
+ * items — including unchanged ones — flow through to the fold accumulator.
637
+ */
638
+ function buildPipelineForApi(input) {
639
+ const generateCtx = {
640
+ existingSnapshots: input.existingSnapshots,
641
+ baseRoute: input.baseRoute,
642
+ packageName: input.packageName,
643
+ apiScope: input.apiScope,
644
+ ...input.apiName != null ? { apiName: input.apiName } : {},
645
+ ...input.source != null ? { source: input.source } : {},
646
+ buildTime: input.buildTime,
647
+ resolvedOutputDir: input.resolvedOutputDir,
648
+ ...input.suppressExampleErrors != null ? { suppressExampleErrors: input.suppressExampleErrors } : {},
649
+ ...input.llmsPlugin != null ? { llmsPlugin: input.llmsPlugin } : {}
650
+ };
651
+ const writeCtx = {
652
+ resolvedOutputDir: input.resolvedOutputDir,
653
+ buildTime: input.buildTime,
654
+ ...input.ogResolver !== void 0 ? { ogResolver: input.ogResolver } : {},
655
+ ...input.siteUrl != null ? { siteUrl: input.siteUrl } : {},
656
+ ...input.ogImage != null ? { ogImage: input.ogImage } : {},
657
+ ...input.packageName != null ? { packageName: input.packageName } : {},
658
+ ...input.apiName != null ? { apiName: input.apiName } : {}
659
+ };
660
+ return Stream.fromIterable(input.workItems).pipe(Stream.mapEffect((workItem) => generateSinglePage(workItem, generateCtx), { concurrency: input.pageConcurrency }), Stream.filter((result) => result !== null), Stream.mapEffect((result) => writeSingleFile(result, writeCtx), { concurrency: input.pageConcurrency }), Stream.runFold([], (acc, result) => [...acc, result]));
661
+ }
662
+
663
+ //#endregion
664
+ export { buildPipelineForApi, cleanupAndCommit, prepareWorkItems, writeMetadata };