rspress-plugin-api-extractor 0.9.2 → 0.11.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/BuildEnv.js +58 -0
- package/README.md +2 -1
- package/build-program.js +33 -30
- package/build-stages.js +47 -39
- package/errors.js +0 -1
- package/index.d.ts +22 -14
- package/layers/ConfigServiceLive.js +349 -400
- package/layers/HighlighterServiceLive.js +52 -0
- package/layers/ObservabilityLive.js +26 -7
- package/layers/OgServiceLive.js +134 -0
- package/layers/TwoslashCacheServiceLive.js +108 -0
- package/layers/TwoslashEnvironmentsLive.js +33 -0
- package/layers/TypeRegistryServiceLive.js +54 -47
- package/layers/build-metrics.js +32 -5
- package/layers/xdg.js +44 -0
- package/markdown/helpers.js +9 -55
- package/markdown/page-generators/class-page.js +8 -31
- package/markdown/page-generators/index-pages.js +6 -8
- package/markdown/page-generators/interface-page.js +7 -7
- package/markdown/shiki-utils.js +65 -10
- package/observability/EventBus.js +29 -9
- package/observability/heartbeat.js +1 -1
- package/observability/metric-report.js +124 -0
- package/observability/sinks/console-sink.js +6 -0
- package/observability/sinks/metrics-sink.js +64 -21
- package/observability/sinks/render-sink.js +86 -0
- package/observability/sinks/trace-sink.js +10 -17
- package/observability/spans.js +4 -2
- package/observability/sync-emitter.js +78 -0
- package/og-resolver.js +46 -287
- package/package.json +4 -5
- package/path-derivation.js +19 -1
- package/plugin.js +64 -52
- package/prettier-formatter.js +4 -10
- package/remark-api-codeblocks.js +33 -15
- package/remark-with-api.js +24 -27
- package/schemas/config.js +11 -7
- package/services/HighlighterService.js +30 -0
- package/services/OgService.js +23 -0
- package/services/PluginConfig.js +26 -0
- package/services/TwoslashCacheService.js +15 -0
- package/services/TwoslashEnvironments.js +7 -0
- package/shiki-transformer.js +55 -256
- package/twoslash-access.js +48 -0
- package/twoslash-cache.js +174 -0
- package/twoslash-patterns.js +1 -1
- package/twoslash-timing-wrapper.js +23 -0
- package/twoslash-transformer.js +153 -89
- package/vfs-registry.js +1 -31
- package/layers/PathDerivationServiceLive.js +0 -16
- package/runtime/components/MarkdownText/index.js +0 -34
- package/services/PathDerivationService.js +0 -7
package/og-resolver.js
CHANGED
|
@@ -1,19 +1,6 @@
|
|
|
1
|
-
import { PluginEvent } from "./observability/events.js";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { imageSizeFromFile } from "image-size/fromFile";
|
|
5
|
-
|
|
6
1
|
//#region src/og-resolver.ts
|
|
7
|
-
/** Module-level emitter injected by plugin.ts at startup. */
|
|
8
|
-
let emitEvent = () => {};
|
|
9
|
-
let currentBuildId = "";
|
|
10
|
-
function setOgResolverEventEmitter(fn, buildId = "") {
|
|
11
|
-
emitEvent = fn;
|
|
12
|
-
currentBuildId = buildId;
|
|
13
|
-
}
|
|
14
2
|
/**
|
|
15
|
-
* MIME type mappings for common image formats
|
|
16
|
-
* Used to determine the `og:image:type` meta tag value.
|
|
3
|
+
* MIME type mappings for common image formats, used for `og:image:type`.
|
|
17
4
|
*/
|
|
18
5
|
const IMAGE_MIME_TYPES = {
|
|
19
6
|
jpg: "image/jpeg",
|
|
@@ -24,282 +11,54 @@ const IMAGE_MIME_TYPES = {
|
|
|
24
11
|
svg: "image/svg+xml"
|
|
25
12
|
};
|
|
26
13
|
/**
|
|
27
|
-
*
|
|
14
|
+
* The `og:image:type` value for a detected image format, or `undefined` for a
|
|
15
|
+
* format with no mapping.
|
|
16
|
+
*/
|
|
17
|
+
function imageMimeType(type) {
|
|
18
|
+
if (type == null) return void 0;
|
|
19
|
+
return IMAGE_MIME_TYPES[type.toLowerCase()];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Turn a configured image URL into an absolute one.
|
|
28
23
|
*
|
|
29
|
-
*
|
|
30
|
-
* (
|
|
31
|
-
*
|
|
24
|
+
* @returns The absolute URL, or `undefined` when the input is neither an
|
|
25
|
+
* absolute `http(s)` URL nor a site-root-relative path. A bare relative path
|
|
26
|
+
* is deliberately rejected rather than guessed at — there is no base to
|
|
27
|
+
* resolve it against that would not silently produce a broken link.
|
|
28
|
+
*/
|
|
29
|
+
function resolveOgUrl(siteUrl, url) {
|
|
30
|
+
if (url.startsWith("http://") || url.startsWith("https://")) return url;
|
|
31
|
+
if (url.startsWith("/")) return `${siteUrl}${url}`;
|
|
32
|
+
}
|
|
33
|
+
/** Descriptive alt text for a package's (or one API's) OG image. */
|
|
34
|
+
function ogAltText(packageName, apiName) {
|
|
35
|
+
return apiName ? `${apiName} - ${packageName} API Documentation` : `${packageName} API Documentation`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Assemble the complete Open Graph metadata for one documentation page.
|
|
32
39
|
*
|
|
33
40
|
* @remarks
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* - **Object format**: Detailed metadata with explicit properties that will be
|
|
38
|
-
* validated and URL-resolved.
|
|
39
|
-
*
|
|
40
|
-
* @example Basic usage with a relative path
|
|
41
|
-
* ```typescript
|
|
42
|
-
* const resolver = new OpenGraphResolver({
|
|
43
|
-
* siteUrl: "https://example.com",
|
|
44
|
-
* docsRoot: "/path/to/docs"
|
|
45
|
-
* });
|
|
46
|
-
*
|
|
47
|
-
* const metadata = await resolver.resolve(
|
|
48
|
-
* "/images/og-api.png",
|
|
49
|
-
* "my-package",
|
|
50
|
-
* "MyClass"
|
|
51
|
-
* );
|
|
52
|
-
* // Result: { url: "https://example.com/images/og-api.png", width: 1200, height: 630, ... }
|
|
53
|
-
* ```
|
|
54
|
-
*
|
|
55
|
-
* @example Using detailed configuration
|
|
56
|
-
* ```typescript
|
|
57
|
-
* const metadata = await resolver.resolve(
|
|
58
|
-
* {
|
|
59
|
-
* url: "/images/og.png",
|
|
60
|
-
* alt: "Custom alt text",
|
|
61
|
-
* width: 1200,
|
|
62
|
-
* height: 630
|
|
63
|
-
* },
|
|
64
|
-
* "my-package"
|
|
65
|
-
* );
|
|
66
|
-
* ```
|
|
41
|
+
* Was `OpenGraphResolver.createPageMetadata`. It never touched the resolver's
|
|
42
|
+
* instance state, so it is a free function now rather than a static on a class
|
|
43
|
+
* that no longer exists.
|
|
67
44
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Resolves an Open Graph image configuration into complete metadata.
|
|
87
|
-
*
|
|
88
|
-
* Handles both string URLs/paths and detailed metadata objects, converting them
|
|
89
|
-
* into fully-qualified `OpenGraphImageMetadata` with absolute URLs.
|
|
90
|
-
*
|
|
91
|
-
* @param config - The OG image configuration to resolve. Can be:
|
|
92
|
-
* - A string URL (absolute or relative path starting with `/`)
|
|
93
|
-
* - An `OpenGraphImageMetadata` object with explicit properties
|
|
94
|
-
* - `undefined` to indicate no OG image
|
|
95
|
-
* @param packageName - The package name for generating default alt text
|
|
96
|
-
* @param apiName - Optional API name for more descriptive alt text
|
|
97
|
-
* @returns Resolved metadata with absolute URLs, or `undefined` if:
|
|
98
|
-
* - `config` is `undefined`
|
|
99
|
-
* - The URL format is invalid
|
|
100
|
-
*
|
|
101
|
-
* @example Resolve a relative path
|
|
102
|
-
* ```typescript
|
|
103
|
-
* const metadata = await resolver.resolve("/images/og.png", "my-lib");
|
|
104
|
-
* // Returns: { url: "https://example.com/images/og.png", alt: "my-lib API Documentation", ... }
|
|
105
|
-
* ```
|
|
106
|
-
*
|
|
107
|
-
* @example Resolve an absolute URL
|
|
108
|
-
* ```typescript
|
|
109
|
-
* const metadata = await resolver.resolve("https://cdn.example.com/og.png", "my-lib");
|
|
110
|
-
* // Returns: { url: "https://cdn.example.com/og.png", alt: "my-lib API Documentation" }
|
|
111
|
-
* ```
|
|
112
|
-
*/
|
|
113
|
-
async resolve(config, packageName, apiName) {
|
|
114
|
-
if (!config) return;
|
|
115
|
-
if (typeof config === "object") return this.resolveFromMetadata(config, packageName, apiName);
|
|
116
|
-
return this.resolveFromString(config, packageName, apiName);
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Resolves a metadata object configuration into complete OG image metadata.
|
|
120
|
-
*
|
|
121
|
-
* Validates and resolves URLs in the provided metadata object, ensuring all
|
|
122
|
-
* URLs are absolute and properly formatted.
|
|
123
|
-
*
|
|
124
|
-
* @param metadata - The metadata object containing OG image properties
|
|
125
|
-
* @param packageName - Package name for default alt text generation
|
|
126
|
-
* @param apiName - Optional API name for more descriptive alt text
|
|
127
|
-
* @returns Resolved metadata with absolute URLs, or `undefined` if URL is invalid
|
|
128
|
-
*/
|
|
129
|
-
async resolveFromMetadata(metadata, packageName, apiName) {
|
|
130
|
-
const { url, secureUrl, type, width, height, alt } = metadata;
|
|
131
|
-
const resolvedUrl = this.resolveUrl(url);
|
|
132
|
-
if (!resolvedUrl) {
|
|
133
|
-
emitEvent(PluginEvent.ConfigValidationWarning({
|
|
134
|
-
ctx: { buildId: currentBuildId },
|
|
135
|
-
field: "ogImage.url",
|
|
136
|
-
value: url,
|
|
137
|
-
reason: "invalid URL format",
|
|
138
|
-
level: "warn"
|
|
139
|
-
}));
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
let resolvedSecureUrl;
|
|
143
|
-
if (secureUrl) {
|
|
144
|
-
if (secureUrl.startsWith("https://")) resolvedSecureUrl = secureUrl;
|
|
145
|
-
else emitEvent(PluginEvent.ConfigValidationWarning({
|
|
146
|
-
ctx: { buildId: currentBuildId },
|
|
147
|
-
field: "ogImage.secureUrl",
|
|
148
|
-
value: secureUrl,
|
|
149
|
-
reason: "secureUrl must be absolute HTTPS",
|
|
150
|
-
level: "warn"
|
|
151
|
-
}));
|
|
152
|
-
}
|
|
153
|
-
return {
|
|
154
|
-
url: resolvedUrl,
|
|
155
|
-
secureUrl: resolvedSecureUrl,
|
|
156
|
-
type,
|
|
157
|
-
width,
|
|
158
|
-
height,
|
|
159
|
-
alt: alt ?? this.generateAltText(packageName, apiName)
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Resolves a string URL/path into complete OG image metadata.
|
|
164
|
-
*
|
|
165
|
-
* For relative paths pointing to local files, this method will attempt to:
|
|
166
|
-
* 1. Locate the file in the docs `public` directory
|
|
167
|
-
* 2. Read the image dimensions using `image-size`
|
|
168
|
-
* 3. Determine the MIME type from the file extension
|
|
169
|
-
*
|
|
170
|
-
* @param imageUrl - The image URL or path to resolve
|
|
171
|
-
* @param packageName - Package name for alt text generation
|
|
172
|
-
* @param apiName - Optional API name for more descriptive alt text
|
|
173
|
-
* @returns Resolved metadata with auto-detected dimensions for local files,
|
|
174
|
-
* or `undefined` if the URL format is invalid
|
|
175
|
-
*/
|
|
176
|
-
async resolveFromString(imageUrl, packageName, apiName) {
|
|
177
|
-
const resolvedUrl = this.resolveUrl(imageUrl);
|
|
178
|
-
if (!resolvedUrl) {
|
|
179
|
-
emitEvent(PluginEvent.ConfigValidationWarning({
|
|
180
|
-
ctx: { buildId: currentBuildId },
|
|
181
|
-
field: "ogImage",
|
|
182
|
-
value: imageUrl,
|
|
183
|
-
reason: "invalid URL format",
|
|
184
|
-
level: "warn"
|
|
185
|
-
}));
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
const localPath = this.findLocalImage(imageUrl);
|
|
189
|
-
const dimensions = localPath ? await this.readImageDimensions(localPath) : void 0;
|
|
190
|
-
return {
|
|
191
|
-
url: resolvedUrl,
|
|
192
|
-
type: dimensions?.type,
|
|
193
|
-
width: dimensions?.width,
|
|
194
|
-
height: dimensions?.height,
|
|
195
|
-
alt: this.generateAltText(packageName, apiName)
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Resolves a URL string to an absolute URL.
|
|
200
|
-
*
|
|
201
|
-
* @param url - The URL to resolve (absolute URL or relative path)
|
|
202
|
-
* @returns The absolute URL, or `undefined` if the format is invalid
|
|
203
|
-
*/
|
|
204
|
-
resolveUrl(url) {
|
|
205
|
-
if (url.startsWith("http://") || url.startsWith("https://")) return url;
|
|
206
|
-
if (url.startsWith("/")) return `${this.siteUrl}${url}`;
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Attempts to find a local image file in the docs public directory.
|
|
210
|
-
*
|
|
211
|
-
* @param imagePath - The relative image path (starting with `/`)
|
|
212
|
-
* @returns The absolute file path if found, or `undefined` if not found
|
|
213
|
-
* or if `docsRoot` is not configured
|
|
214
|
-
*/
|
|
215
|
-
findLocalImage(imagePath) {
|
|
216
|
-
if (!this.docsRoot || !imagePath.startsWith("/")) return;
|
|
217
|
-
const publicPath = path.join(this.docsRoot, "public", imagePath);
|
|
218
|
-
if (fs.existsSync(publicPath)) return publicPath;
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Reads image dimensions and type from a local file.
|
|
222
|
-
*
|
|
223
|
-
* @param filePath - Absolute path to the image file
|
|
224
|
-
* @returns Object containing width, height, and MIME type if successful,
|
|
225
|
-
* or `undefined` if the file cannot be read or analyzed
|
|
226
|
-
*/
|
|
227
|
-
async readImageDimensions(filePath) {
|
|
228
|
-
try {
|
|
229
|
-
const dimensions = await imageSizeFromFile(filePath);
|
|
230
|
-
let mimeType;
|
|
231
|
-
if (dimensions.type) mimeType = IMAGE_MIME_TYPES[dimensions.type.toLowerCase()];
|
|
232
|
-
return {
|
|
233
|
-
width: dimensions.width,
|
|
234
|
-
height: dimensions.height,
|
|
235
|
-
...mimeType != null ? { type: mimeType } : {}
|
|
236
|
-
};
|
|
237
|
-
} catch (error) {
|
|
238
|
-
emitEvent(PluginEvent.ConfigValidationWarning({
|
|
239
|
-
ctx: { buildId: currentBuildId },
|
|
240
|
-
field: "ogImage",
|
|
241
|
-
value: filePath,
|
|
242
|
-
reason: error.message ?? String(error),
|
|
243
|
-
level: "warn"
|
|
244
|
-
}));
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Generates descriptive alt text for the OG image.
|
|
250
|
-
*
|
|
251
|
-
* @param packageName - The package name
|
|
252
|
-
* @param apiName - Optional API name for more specific text
|
|
253
|
-
* @returns Generated alt text string
|
|
254
|
-
*/
|
|
255
|
-
generateAltText(packageName, apiName) {
|
|
256
|
-
if (apiName) return `${apiName} - ${packageName} API Documentation`;
|
|
257
|
-
return `${packageName} API Documentation`;
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Creates complete Open Graph metadata for an API documentation page.
|
|
261
|
-
*
|
|
262
|
-
* This static factory method builds a complete `OpenGraphMetadata` object
|
|
263
|
-
* suitable for inclusion in page frontmatter, combining resolved image
|
|
264
|
-
* metadata with article-specific information.
|
|
265
|
-
*
|
|
266
|
-
* @param options - Configuration for the OG metadata.
|
|
267
|
-
* Includes `siteUrl`, `pageRoute`, `description`, `publishedTime`,
|
|
268
|
-
* `modifiedTime`, `section`, `packageName`, and optional `ogImage`.
|
|
269
|
-
* @returns Complete Open Graph metadata object
|
|
270
|
-
*
|
|
271
|
-
* @example
|
|
272
|
-
* ```typescript
|
|
273
|
-
* const ogMetadata = OpenGraphResolver.createPageMetadata({
|
|
274
|
-
* siteUrl: "https://example.com",
|
|
275
|
-
* pageRoute: "/api/classes/MyClass",
|
|
276
|
-
* description: "MyClass provides...",
|
|
277
|
-
* publishedTime: "2024-01-15T10:00:00Z",
|
|
278
|
-
* modifiedTime: "2024-01-20T15:30:00Z",
|
|
279
|
-
* section: "Classes",
|
|
280
|
-
* packageName: "my-library",
|
|
281
|
-
* ogImage: resolvedImageMetadata
|
|
282
|
-
* });
|
|
283
|
-
* ```
|
|
284
|
-
*/
|
|
285
|
-
static createPageMetadata(options) {
|
|
286
|
-
return {
|
|
287
|
-
siteUrl: options.siteUrl,
|
|
288
|
-
pageRoute: options.pageRoute,
|
|
289
|
-
description: options.description,
|
|
290
|
-
publishedTime: options.publishedTime,
|
|
291
|
-
modifiedTime: options.modifiedTime,
|
|
292
|
-
section: options.section,
|
|
293
|
-
tags: [
|
|
294
|
-
"TypeScript",
|
|
295
|
-
"API",
|
|
296
|
-
options.packageName
|
|
297
|
-
],
|
|
298
|
-
...options.ogImage != null ? { ogImage: options.ogImage } : {},
|
|
299
|
-
ogType: "article"
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
};
|
|
45
|
+
function createPageMetadata(options) {
|
|
46
|
+
return {
|
|
47
|
+
siteUrl: options.siteUrl,
|
|
48
|
+
pageRoute: options.pageRoute,
|
|
49
|
+
description: options.description,
|
|
50
|
+
publishedTime: options.publishedTime,
|
|
51
|
+
modifiedTime: options.modifiedTime,
|
|
52
|
+
section: options.section,
|
|
53
|
+
tags: [
|
|
54
|
+
"TypeScript",
|
|
55
|
+
"API",
|
|
56
|
+
options.packageName
|
|
57
|
+
],
|
|
58
|
+
...options.ogImage != null ? { ogImage: options.ogImage } : {},
|
|
59
|
+
ogType: "article"
|
|
60
|
+
};
|
|
61
|
+
}
|
|
303
62
|
|
|
304
63
|
//#endregion
|
|
305
|
-
export {
|
|
64
|
+
export { createPageMetadata, imageMimeType, ogAltText, resolveOgUrl };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rspress-plugin-api-extractor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "RSPress plugin for generating API documentation from TypeScript API Extractor models",
|
|
6
6
|
"keywords": [
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@effected/glob": "^0.4.0",
|
|
43
43
|
"@effected/jsonc": "^0.8.0",
|
|
44
44
|
"@effected/markdown": "^0.7.0",
|
|
45
|
-
"@effected/npm": "^0.12.
|
|
45
|
+
"@effected/npm": "^0.12.1",
|
|
46
46
|
"@effected/package-json": "^0.12.0",
|
|
47
47
|
"@effected/semver": "^0.5.0",
|
|
48
48
|
"@effected/store": "^0.5.0",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"@microsoft/api-extractor-model": "^7.33.11",
|
|
54
54
|
"@shikijs/twoslash": "^4.4.3",
|
|
55
55
|
"@tsdoctor/bundle": "0.2.0",
|
|
56
|
-
"@tsdoctor/model": "0.
|
|
57
|
-
"@tsdoctor/registry": "0.2.
|
|
56
|
+
"@tsdoctor/model": "0.3.0",
|
|
57
|
+
"@tsdoctor/registry": "0.2.1",
|
|
58
58
|
"@tsdoctor/snapshot": "0.1.1",
|
|
59
59
|
"@typescript/vfs": "^1.6.4",
|
|
60
60
|
"clsx": "^2.1.1",
|
|
@@ -62,7 +62,6 @@
|
|
|
62
62
|
"hast-util-to-jsx-runtime": "^2.3.6",
|
|
63
63
|
"image-size": "^2.0.2",
|
|
64
64
|
"ioredis": "^5.7.0",
|
|
65
|
-
"mdast-util-from-markdown": "^2.0.3",
|
|
66
65
|
"mdast-util-to-hast": "^13.2.1",
|
|
67
66
|
"open": "^11.0.0",
|
|
68
67
|
"prettier": "^3.9.6",
|
package/path-derivation.js
CHANGED
|
@@ -43,6 +43,24 @@ function deriveOutputPaths(input) {
|
|
|
43
43
|
}
|
|
44
44
|
return results;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The API scope key derived from a base route.
|
|
48
|
+
*
|
|
49
|
+
* @remarks
|
|
50
|
+
* Load-bearing and previously duplicated. Config resolution registers each
|
|
51
|
+
* API's Twoslash environment under this key and the build program looks it up
|
|
52
|
+
* by the same key; if the two derivations disagree, every lookup misses and
|
|
53
|
+
* `getTransformer` falls back to the build-wide environment. Per-scope
|
|
54
|
+
* type-checking degrades to build-wide with no error and nothing visibly
|
|
55
|
+
* wrong in the output — the failure mode is silent, which is why one
|
|
56
|
+
* definition matters more here than the duplication was costing.
|
|
57
|
+
*
|
|
58
|
+
* Falls back to the package name so a single-API site mounted at `/` still
|
|
59
|
+
* gets a non-empty scope.
|
|
60
|
+
*/
|
|
61
|
+
function apiScopeOf(baseRoute, packageName) {
|
|
62
|
+
return baseRoute.replace(/^\//, "").split("/")[0] || packageName;
|
|
63
|
+
}
|
|
46
64
|
|
|
47
65
|
//#endregion
|
|
48
|
-
export { deriveOutputPaths, normalizeBaseRoute, unscopedName };
|
|
66
|
+
export { apiScopeOf, deriveOutputPaths, normalizeBaseRoute, unscopedName };
|
package/plugin.js
CHANGED
|
@@ -1,35 +1,44 @@
|
|
|
1
|
+
import { BuildId, PageConcurrency, SuppressExampleErrors, Thresholds } from "./BuildEnv.js";
|
|
1
2
|
import { PluginEvent } from "./observability/events.js";
|
|
2
|
-
import { emit
|
|
3
|
+
import { emit } from "./observability/EventBus.js";
|
|
4
|
+
import { codeBlockReport } from "./observability/metric-report.js";
|
|
3
5
|
import { runHeartbeat } from "./observability/heartbeat.js";
|
|
4
6
|
import { writeIssuesJson } from "./observability/sinks/issues-sink.js";
|
|
7
|
+
import { writeRenderPhaseJson } from "./observability/sinks/render-sink.js";
|
|
5
8
|
import { buildEventBus, logBuildSummary, makeSummaryLoggerLayer } from "./layers/ObservabilityLive.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { TwoslashManager, setEventEmitter } from "./twoslash-transformer.js";
|
|
9
|
+
import { emitSync, installSyncEmitter } from "./observability/sync-emitter.js";
|
|
10
|
+
import { TwoslashEnvironments } from "./services/TwoslashEnvironments.js";
|
|
11
|
+
import { clearTypeRoutes } from "./twoslash-transformer.js";
|
|
10
12
|
import { VfsRegistry } from "./vfs-registry.js";
|
|
11
13
|
import { generateApiDocs } from "./build-program.js";
|
|
12
14
|
import { deriveOutputPaths, normalizeBaseRoute, unscopedName } from "./path-derivation.js";
|
|
13
15
|
import { fromDir, fromParentDir } from "./config-helpers.js";
|
|
14
16
|
import { classifyApiConfig, mergeLlmsPluginConfig } from "./config-utils.js";
|
|
15
|
-
import {
|
|
17
|
+
import { collectShikiThemes, normalizeThemeConfig } from "./markdown/shiki-utils.js";
|
|
16
18
|
import { resolveObservability } from "./schemas/observability.js";
|
|
17
19
|
import { PluginOptions } from "./schemas/config.js";
|
|
18
20
|
import "./schemas/index.js";
|
|
19
21
|
import { ConfigService } from "./services/ConfigService.js";
|
|
22
|
+
import { PluginConfig } from "./services/PluginConfig.js";
|
|
23
|
+
import { TwoslashCacheService } from "./services/TwoslashCacheService.js";
|
|
20
24
|
import { ConfigServiceLive } from "./layers/ConfigServiceLive.js";
|
|
21
|
-
import {
|
|
25
|
+
import { HighlighterServiceLive } from "./layers/HighlighterServiceLive.js";
|
|
26
|
+
import { OgServiceLive } from "./layers/OgServiceLive.js";
|
|
27
|
+
import { PlatformLive } from "./layers/xdg.js";
|
|
28
|
+
import { TwoslashCacheServiceLive } from "./layers/TwoslashCacheServiceLive.js";
|
|
29
|
+
import { TwoslashEnvironmentsLive } from "./layers/TwoslashEnvironmentsLive.js";
|
|
22
30
|
import { TypeRegistryServiceLive } from "./layers/TypeRegistryServiceLive.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
31
|
+
import { clearTwoslashAccess, installTwoslashAccess, twoslashTransformerFor } from "./twoslash-access.js";
|
|
32
|
+
import { remarkApiCodeblocks } from "./remark-api-codeblocks.js";
|
|
33
|
+
import { remarkWithApi } from "./remark-with-api.js";
|
|
26
34
|
import { createRequire } from "node:module";
|
|
27
35
|
import fs from "node:fs";
|
|
36
|
+
import os from "node:os";
|
|
28
37
|
import path from "node:path";
|
|
29
38
|
import { fileURLToPath } from "node:url";
|
|
30
39
|
import { NodeFileSystem } from "@effect/platform-node";
|
|
31
40
|
import { SnapshotServiceLive } from "@tsdoctor/snapshot";
|
|
32
|
-
import { Effect, FileSystem, Layer, ManagedRuntime, Ref, Schema } from "effect";
|
|
41
|
+
import { Effect, FileSystem, Layer, ManagedRuntime, Option, Ref, Schema } from "effect";
|
|
33
42
|
|
|
34
43
|
//#region src/plugin.ts
|
|
35
44
|
/* v8 ignore start -- RSPress plugin adapter, requires RSPress runtime */
|
|
@@ -49,30 +58,11 @@ const readSitePackageName = Effect.gen(function* () {
|
|
|
49
58
|
return "unknown";
|
|
50
59
|
});
|
|
51
60
|
/**
|
|
52
|
-
* Normalize theme configuration from user input to a consistent format.
|
|
53
|
-
*/
|
|
54
|
-
function normalizeThemeConfig(theme) {
|
|
55
|
-
if (!theme) return { ...DEFAULT_SHIKI_THEMES };
|
|
56
|
-
if (typeof theme === "string") return {
|
|
57
|
-
light: theme,
|
|
58
|
-
dark: theme
|
|
59
|
-
};
|
|
60
|
-
if ("light" in theme && "dark" in theme && typeof theme.light === "string" && typeof theme.dark === "string") return {
|
|
61
|
-
light: theme.light,
|
|
62
|
-
dark: theme.dark
|
|
63
|
-
};
|
|
64
|
-
return {
|
|
65
|
-
light: theme,
|
|
66
|
-
dark: theme
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
61
|
* RSPress plugin for generating API documentation from API Extractor model files
|
|
71
62
|
*/
|
|
72
63
|
function ApiExtractorPluginImpl(rawOptions) {
|
|
73
64
|
const options = Schema.decodeUnknownSync(PluginOptions)(rawOptions);
|
|
74
65
|
const isInert = classifyApiConfig(options) === "disabled";
|
|
75
|
-
const shikiCrossLinker = new ShikiCrossLinker();
|
|
76
66
|
const envLogLevel = process.env.LOG_LEVEL?.toLowerCase();
|
|
77
67
|
const buildId = `${process.pid}-${performance.now().toString(36)}`;
|
|
78
68
|
const { resolved: obs, deprecations } = resolveObservability({
|
|
@@ -83,20 +73,17 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
83
73
|
cwd: process.cwd(),
|
|
84
74
|
buildId
|
|
85
75
|
});
|
|
86
|
-
const { layer: eventBusLayer, trace: traceSink, issues: issuesSink } = buildEventBus(obs);
|
|
76
|
+
const { layer: eventBusLayer, trace: traceSink, issues: issuesSink, render: renderSink, metrics: metricStore } = buildEventBus(obs);
|
|
87
77
|
const dbPath = path.resolve(process.cwd(), ".api-docs", "snapshot", "api-docs.db");
|
|
88
78
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
89
|
-
const
|
|
90
|
-
const
|
|
79
|
+
const BuildEnvLayer = Layer.mergeAll(Layer.succeed(BuildId, buildId), Layer.succeed(Thresholds, obs.thresholds), Layer.succeed(PageConcurrency, os.cpus().length), Layer.succeed(SuppressExampleErrors, options.errors?.example !== "show"));
|
|
80
|
+
const PluginConfigLive = Layer.succeed(PluginConfig, options);
|
|
81
|
+
const HighlighterLive = HighlighterServiceLive(collectShikiThemes(options.api ? [options.api] : options.apis ?? []));
|
|
82
|
+
const BaseLayer = Layer.mergeAll(eventBusLayer, PluginConfigLive, HighlighterLive, TwoslashEnvironmentsLive, Layer.provide(OgServiceLive, PlatformLive), BuildEnvLayer, metricStore.layer, TypeRegistryServiceLive, NodeFileSystem.layer, SnapshotServiceLive(dbPath), TwoslashCacheServiceLive, makeSummaryLoggerLayer(obs.logLevel));
|
|
83
|
+
const EffectAppLayer = Layer.provideMerge(ConfigServiceLive, BaseLayer);
|
|
91
84
|
const effectRuntime = ManagedRuntime.make(EffectAppLayer);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
setShikiUtilsEventEmitter(emitSync, buildId);
|
|
95
|
-
setPrettierEventEmitter(emitSync, buildId);
|
|
96
|
-
setOgResolverEventEmitter(emitSync, buildId);
|
|
97
|
-
setRemarkWithApiEventEmitter(emitSync, buildId, obs.thresholds.slowCodeBlock);
|
|
98
|
-
setRemarkApiCodeblocksEventEmitter(emitSync, buildId);
|
|
99
|
-
setBuildStagesEventEmitter(emitSync, buildId);
|
|
85
|
+
const emitterRuntime = ManagedRuntime.make(Layer.mergeAll(eventBusLayer, metricStore.layer, makeSummaryLoggerLayer(obs.logLevel), BuildEnvLayer));
|
|
86
|
+
installSyncEmitter(emitterRuntime);
|
|
100
87
|
const fileContextMap = /* @__PURE__ */ new Map();
|
|
101
88
|
let docsRoot;
|
|
102
89
|
let isFirstBuild = true;
|
|
@@ -110,13 +97,35 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
110
97
|
async beforeBuild(_config, _isProd) {},
|
|
111
98
|
async afterBuild(_config, isProd) {
|
|
112
99
|
if (isFirstBuild && !isInert) {
|
|
113
|
-
|
|
100
|
+
const renderSamples = renderSink.snapshot();
|
|
101
|
+
const report = await effectRuntime.runPromise(codeBlockReport);
|
|
102
|
+
await effectRuntime.runPromise(Effect.gen(function* () {
|
|
103
|
+
const saved = yield* (yield* TwoslashCacheService).persist();
|
|
104
|
+
if (Option.isNone(saved)) return;
|
|
105
|
+
const stats = saved.value;
|
|
106
|
+
yield* emit(PluginEvent.TwoslashCacheSaved({
|
|
107
|
+
ctx: { buildId },
|
|
108
|
+
level: "info",
|
|
109
|
+
envHash: stats.envHash,
|
|
110
|
+
hits: stats.hits,
|
|
111
|
+
misses: stats.misses,
|
|
112
|
+
entries: stats.entries,
|
|
113
|
+
persisted: stats.dirty
|
|
114
|
+
}));
|
|
115
|
+
}));
|
|
116
|
+
await effectRuntime.runPromise(logBuildSummary(obs.thresholds.slowCodeBlock, report));
|
|
114
117
|
if (isProd) await effectRuntime.runPromise(Effect.gen(function* () {
|
|
115
118
|
const packageName = yield* readSitePackageName;
|
|
119
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
116
120
|
yield* writeIssuesJson(issuesSink.snapshot(), {
|
|
117
121
|
cwd: process.cwd(),
|
|
118
122
|
packageName,
|
|
119
|
-
generatedAt
|
|
123
|
+
generatedAt
|
|
124
|
+
});
|
|
125
|
+
yield* writeRenderPhaseJson(report, renderSamples, {
|
|
126
|
+
cwd: process.cwd(),
|
|
127
|
+
packageName,
|
|
128
|
+
generatedAt
|
|
120
129
|
});
|
|
121
130
|
}));
|
|
122
131
|
if (rspressLlmsEnabled && resolvedLlmsPlugin.enabled) {
|
|
@@ -132,7 +141,10 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
132
141
|
isFirstBuild = false;
|
|
133
142
|
}
|
|
134
143
|
if (traceSink) traceSink.flush();
|
|
135
|
-
if (isProd)
|
|
144
|
+
if (isProd) {
|
|
145
|
+
await effectRuntime.dispose();
|
|
146
|
+
await emitterRuntime.dispose();
|
|
147
|
+
}
|
|
136
148
|
},
|
|
137
149
|
async config(_config, _utils, isProd) {
|
|
138
150
|
const buildStartTime = performance.now();
|
|
@@ -173,6 +185,8 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
173
185
|
for (const dp of derivedPaths) fs.mkdirSync(dp.outputDir, { recursive: true });
|
|
174
186
|
}
|
|
175
187
|
VfsRegistry.clear();
|
|
188
|
+
clearTwoslashAccess();
|
|
189
|
+
clearTypeRoutes();
|
|
176
190
|
fileContextMap.clear();
|
|
177
191
|
issuesSink.reset();
|
|
178
192
|
for (const dep of deprecations) emitSync(PluginEvent.DeprecatedConfigUsed({
|
|
@@ -204,13 +218,11 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
204
218
|
apisTotal: apiCount,
|
|
205
219
|
buildId
|
|
206
220
|
}));
|
|
207
|
-
|
|
221
|
+
installTwoslashAccess(yield* TwoslashEnvironments);
|
|
222
|
+
const apiConfigs = yield* (yield* ConfigService).resolve(rspressConfigSubset);
|
|
208
223
|
buildResults.length = 0;
|
|
209
224
|
yield* Ref.set(phaseRef, "generate");
|
|
210
|
-
yield* Effect.forEach(
|
|
211
|
-
...apiConfig,
|
|
212
|
-
suppressExampleErrors: buildContext.suppressExampleErrors
|
|
213
|
-
}, buildContext, fileContextMap).pipe(Effect.tap((result) => {
|
|
225
|
+
yield* Effect.forEach(apiConfigs, (apiConfig) => generateApiDocs(apiConfig, fileContextMap).pipe(Effect.tap((result) => {
|
|
214
226
|
buildResults.push(result);
|
|
215
227
|
return emit(PluginEvent.ApiDocsCompleted({
|
|
216
228
|
ctx: { buildId },
|
|
@@ -268,10 +280,10 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
268
280
|
if (!existingInclude.includes("rspress-plugin-api-extractor/runtime")) updatedConfig.builderConfig.source.include = [...existingInclude, "rspress-plugin-api-extractor/runtime"];
|
|
269
281
|
if (!updatedConfig.markdown) updatedConfig.markdown = {};
|
|
270
282
|
if (!updatedConfig.markdown.remarkPlugins) updatedConfig.markdown.remarkPlugins = [];
|
|
271
|
-
const
|
|
283
|
+
const firstApiTheme = options.api?.theme ?? options.apis?.[0]?.theme;
|
|
284
|
+
const remarkTheme = normalizeThemeConfig(firstApiTheme);
|
|
272
285
|
updatedConfig.markdown.remarkPlugins.push([remarkWithApi, {
|
|
273
|
-
|
|
274
|
-
getTransformer: () => TwoslashManager.getInstance().getTransformer(),
|
|
286
|
+
getTransformer: (apiScope) => twoslashTransformerFor(apiScope),
|
|
275
287
|
theme: remarkTheme
|
|
276
288
|
}]);
|
|
277
289
|
updatedConfig.markdown.remarkPlugins.push([remarkApiCodeblocks]);
|
package/prettier-formatter.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import { PluginEvent } from "./observability/events.js";
|
|
2
2
|
import { addLogicalBlankLines } from "./code-post-processor.js";
|
|
3
|
+
import { emitSync, syncBuildId } from "./observability/sync-emitter.js";
|
|
3
4
|
import { format } from "prettier";
|
|
4
5
|
|
|
5
6
|
//#region src/prettier-formatter.ts
|
|
6
7
|
/* v8 ignore start -- Prettier integration wrapper, tested via page generator integration tests */
|
|
7
|
-
/** Module-level emitter injected by plugin.ts at startup. */
|
|
8
|
-
let emitEvent = () => {};
|
|
9
|
-
let currentBuildId = "";
|
|
10
|
-
function setPrettierEventEmitter(fn, buildId = "") {
|
|
11
|
-
emitEvent = fn;
|
|
12
|
-
currentBuildId = buildId;
|
|
13
|
-
}
|
|
14
8
|
/**
|
|
15
9
|
* Map code fence languages to Prettier parsers
|
|
16
10
|
*/
|
|
@@ -65,8 +59,8 @@ async function formatCode(code, language) {
|
|
|
65
59
|
} catch (error) {
|
|
66
60
|
const formatTime = performance.now() - start;
|
|
67
61
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
68
|
-
|
|
69
|
-
ctx: { buildId:
|
|
62
|
+
emitSync(PluginEvent.PrettierError({
|
|
63
|
+
ctx: { buildId: syncBuildId() },
|
|
70
64
|
file: "unknown",
|
|
71
65
|
reason: errorMsg,
|
|
72
66
|
level: "warn"
|
|
@@ -81,4 +75,4 @@ async function formatCode(code, language) {
|
|
|
81
75
|
}
|
|
82
76
|
|
|
83
77
|
//#endregion
|
|
84
|
-
export { formatCode
|
|
78
|
+
export { formatCode };
|