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.
- package/README.md +2 -2
- package/api-extracted-package.js +426 -0
- package/build-program.js +120 -0
- package/build-stages.js +664 -0
- package/category-resolver.js +50 -0
- package/code-post-processor.js +38 -0
- package/config-helpers.js +106 -0
- package/config-utils.js +258 -0
- package/content-hash.js +79 -0
- package/errors.js +29 -0
- package/formatter.js +69 -0
- package/hide-cut-transformer.js +96 -0
- package/index.d.ts +901 -831
- package/index.js +4 -6128
- package/layers/ConfigServiceLive.js +377 -0
- package/layers/ObservabilityLive.js +136 -0
- package/layers/PathDerivationServiceLive.js +16 -0
- package/layers/SnapshotServiceLive.js +94 -0
- package/layers/TypeRegistryServiceLive.js +46 -0
- package/llms-processing.js +270 -0
- package/llms-program.js +262 -0
- package/loader.js +186 -0
- package/markdown/cross-linker.js +156 -0
- package/markdown/helpers.js +364 -0
- package/markdown/index.js +11 -0
- package/markdown/page-generators/class-page.js +357 -0
- package/markdown/page-generators/enum-page.js +152 -0
- package/markdown/page-generators/function-page.js +127 -0
- package/markdown/page-generators/index-pages.js +27 -0
- package/markdown/page-generators/interface-page.js +307 -0
- package/markdown/page-generators/namespace-page.js +280 -0
- package/markdown/page-generators/type-alias-page.js +110 -0
- package/markdown/page-generators/variable-page.js +110 -0
- package/markdown/shiki-utils.js +48 -0
- package/migrations/001_create_snapshots.js +25 -0
- package/model-loader.js +95 -0
- package/multi-entry-resolver.js +70 -0
- package/og-resolver.js +271 -0
- package/package.json +63 -73
- package/path-derivation.js +48 -0
- package/plugin.js +218 -0
- package/prettier-formatter.js +73 -0
- package/public/tsconfig/rspress.json +44 -0
- package/remark-api-codeblocks.js +130 -0
- package/remark-with-api.js +172 -0
- package/route-collisions.js +52 -0
- package/runtime/components/ApiExample/index.js +29 -25
- package/runtime/components/ApiLlmsPackageActions/index.js +252 -323
- package/runtime/components/ApiLlmsViewOptions/index.js +269 -340
- package/runtime/components/ApiMember/index.js +49 -47
- package/runtime/components/ApiSignature/index.js +32 -28
- package/runtime/components/EnumMembersTable/{index_module.css → index.css} +18 -19
- package/runtime/components/EnumMembersTable/index.js +36 -67
- package/runtime/components/EnumMembersTable/index.module.js +9 -6
- package/runtime/components/ExampleBlock/{index_module.css → index.css} +2 -3
- package/runtime/components/ExampleBlock/index.js +23 -28
- package/runtime/components/ExampleBlock/index.module.js +8 -5
- package/runtime/components/MarkdownContent/index.js +26 -18
- package/runtime/components/MarkdownText/index.js +28 -22
- package/runtime/components/MemberSignature/{index_module.css → index.css} +5 -6
- package/runtime/components/MemberSignature/index.js +46 -46
- package/runtime/components/MemberSignature/index.module.js +9 -6
- package/runtime/components/ParametersTable/{index_module.css → index.css} +19 -20
- package/runtime/components/ParametersTable/index.js +36 -67
- package/runtime/components/ParametersTable/index.module.js +9 -6
- package/runtime/components/SignatureBlock/{index_module.css → index.css} +5 -6
- package/runtime/components/SignatureBlock/index.js +30 -29
- package/runtime/components/SignatureBlock/index.module.js +9 -6
- package/runtime/components/SignatureCode/{index_module.css → index.css} +9 -10
- package/runtime/components/SignatureCode/index.js +40 -32
- package/runtime/components/SignatureCode/index.module.js +9 -6
- package/runtime/components/SignatureToolbar/{index_module.css → index.css} +18 -20
- package/runtime/components/SignatureToolbar/index.js +48 -51
- package/runtime/components/SignatureToolbar/index.module.js +13 -10
- package/runtime/components/buttons/ButtonGroup.js +13 -6
- package/runtime/components/buttons/CopyCodeButton.js +40 -38
- package/runtime/components/buttons/WrapSignatureButton.js +20 -16
- package/runtime/components/buttons/{index_module.css → index.css} +5 -6
- package/runtime/components/buttons/index.module.js +8 -5
- package/runtime/components/icons/CheckIcon/index.js +20 -17
- package/runtime/components/icons/CopyIcon/index.js +20 -17
- package/runtime/components/icons/UnwrapIcon/index.js +22 -18
- package/runtime/components/icons/WrapIcon/index.js +20 -17
- package/runtime/components/shared/_twoslash.css +3 -10
- package/runtime/components/shared/variables.css +0 -3
- package/runtime/hooks/useWrapToggle.js +32 -9
- package/runtime/index.d.ts +513 -173
- package/runtime/index.js +11 -9
- package/runtime/utils/decode-hast.js +31 -16
- package/runtime/utils/hast-renderer.js +21 -7
- package/schemas/config.js +199 -0
- package/schemas/index.js +5 -0
- package/schemas/opengraph.js +26 -0
- package/schemas/performance.js +19 -0
- package/serve.js +133 -0
- package/services/ConfigService.js +7 -0
- package/services/PathDerivationService.js +7 -0
- package/services/SnapshotService.js +7 -0
- package/services/TypeRegistryService.js +7 -0
- package/shiki-transformer.js +758 -0
- package/tsconfig-parser.js +127 -0
- package/tsdoc-metadata.json +11 -11
- package/twoslash-patterns.js +87 -0
- package/twoslash-transformer.js +316 -0
- package/type-reference-extractor.js +201 -0
- package/typescript-config.js +168 -0
- package/vfs-registry.js +121 -0
- package/0~llms-program.js +0 -344
- package/runtime/components/ApiLlmsPackageActions/index.module.js +0 -2
package/build-stages.js
ADDED
|
@@ -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 };
|