rspress-plugin-api-extractor 0.10.0 → 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/build-program.js +33 -31
- package/build-stages.js +47 -39
- package/errors.js +0 -1
- package/layers/ConfigServiceLive.js +339 -433
- package/layers/HighlighterServiceLive.js +52 -0
- package/layers/OgServiceLive.js +134 -0
- package/layers/TwoslashCacheServiceLive.js +74 -19
- package/layers/TwoslashEnvironmentsLive.js +33 -0
- package/layers/TypeRegistryServiceLive.js +54 -47
- 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 -7
- package/observability/spans.js +3 -1
- package/observability/sync-emitter.js +78 -0
- package/og-resolver.js +46 -287
- package/package.json +2 -3
- package/path-derivation.js +19 -1
- package/plugin.js +48 -73
- package/prettier-formatter.js +4 -10
- package/remark-api-codeblocks.js +10 -18
- package/remark-with-api.js +11 -21
- package/services/HighlighterService.js +30 -0
- package/services/OgService.js +23 -0
- package/services/PluginConfig.js +26 -0
- package/services/TwoslashEnvironments.js +7 -0
- package/shiki-transformer.js +53 -234
- package/twoslash-access.js +48 -0
- package/twoslash-transformer.js +106 -83
- package/vfs-registry.js +1 -31
- package/layers/PathDerivationServiceLive.js +0 -16
- 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": [
|
|
@@ -53,7 +53,7 @@
|
|
|
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.
|
|
56
|
+
"@tsdoctor/model": "0.3.0",
|
|
57
57
|
"@tsdoctor/registry": "0.2.1",
|
|
58
58
|
"@tsdoctor/snapshot": "0.1.1",
|
|
59
59
|
"@typescript/vfs": "^1.6.4",
|
|
@@ -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,39 +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";
|
|
3
4
|
import { codeBlockReport } from "./observability/metric-report.js";
|
|
4
5
|
import { runHeartbeat } from "./observability/heartbeat.js";
|
|
5
6
|
import { writeIssuesJson } from "./observability/sinks/issues-sink.js";
|
|
6
7
|
import { writeRenderPhaseJson } from "./observability/sinks/render-sink.js";
|
|
7
8
|
import { buildEventBus, logBuildSummary, makeSummaryLoggerLayer } from "./layers/ObservabilityLive.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
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";
|
|
12
12
|
import { VfsRegistry } from "./vfs-registry.js";
|
|
13
13
|
import { generateApiDocs } from "./build-program.js";
|
|
14
14
|
import { deriveOutputPaths, normalizeBaseRoute, unscopedName } from "./path-derivation.js";
|
|
15
15
|
import { fromDir, fromParentDir } from "./config-helpers.js";
|
|
16
16
|
import { classifyApiConfig, mergeLlmsPluginConfig } from "./config-utils.js";
|
|
17
|
-
import {
|
|
17
|
+
import { collectShikiThemes, normalizeThemeConfig } from "./markdown/shiki-utils.js";
|
|
18
18
|
import { resolveObservability } from "./schemas/observability.js";
|
|
19
19
|
import { PluginOptions } from "./schemas/config.js";
|
|
20
20
|
import "./schemas/index.js";
|
|
21
21
|
import { ConfigService } from "./services/ConfigService.js";
|
|
22
|
+
import { PluginConfig } from "./services/PluginConfig.js";
|
|
22
23
|
import { TwoslashCacheService } from "./services/TwoslashCacheService.js";
|
|
23
24
|
import { ConfigServiceLive } from "./layers/ConfigServiceLive.js";
|
|
24
|
-
import {
|
|
25
|
+
import { HighlighterServiceLive } from "./layers/HighlighterServiceLive.js";
|
|
26
|
+
import { OgServiceLive } from "./layers/OgServiceLive.js";
|
|
27
|
+
import { PlatformLive } from "./layers/xdg.js";
|
|
25
28
|
import { TwoslashCacheServiceLive } from "./layers/TwoslashCacheServiceLive.js";
|
|
29
|
+
import { TwoslashEnvironmentsLive } from "./layers/TwoslashEnvironmentsLive.js";
|
|
26
30
|
import { TypeRegistryServiceLive } from "./layers/TypeRegistryServiceLive.js";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
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";
|
|
30
34
|
import { createRequire } from "node:module";
|
|
31
35
|
import fs from "node:fs";
|
|
36
|
+
import os from "node:os";
|
|
32
37
|
import path from "node:path";
|
|
33
38
|
import { fileURLToPath } from "node:url";
|
|
34
39
|
import { NodeFileSystem } from "@effect/platform-node";
|
|
35
40
|
import { SnapshotServiceLive } from "@tsdoctor/snapshot";
|
|
36
|
-
import { Effect, FileSystem, Layer, ManagedRuntime, Ref, Schema } from "effect";
|
|
41
|
+
import { Effect, FileSystem, Layer, ManagedRuntime, Option, Ref, Schema } from "effect";
|
|
37
42
|
|
|
38
43
|
//#region src/plugin.ts
|
|
39
44
|
/* v8 ignore start -- RSPress plugin adapter, requires RSPress runtime */
|
|
@@ -53,30 +58,11 @@ const readSitePackageName = Effect.gen(function* () {
|
|
|
53
58
|
return "unknown";
|
|
54
59
|
});
|
|
55
60
|
/**
|
|
56
|
-
* Normalize theme configuration from user input to a consistent format.
|
|
57
|
-
*/
|
|
58
|
-
function normalizeThemeConfig(theme) {
|
|
59
|
-
if (!theme) return { ...DEFAULT_SHIKI_THEMES };
|
|
60
|
-
if (typeof theme === "string") return {
|
|
61
|
-
light: theme,
|
|
62
|
-
dark: theme
|
|
63
|
-
};
|
|
64
|
-
if ("light" in theme && "dark" in theme && typeof theme.light === "string" && typeof theme.dark === "string") return {
|
|
65
|
-
light: theme.light,
|
|
66
|
-
dark: theme.dark
|
|
67
|
-
};
|
|
68
|
-
return {
|
|
69
|
-
light: theme,
|
|
70
|
-
dark: theme
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
61
|
* RSPress plugin for generating API documentation from API Extractor model files
|
|
75
62
|
*/
|
|
76
63
|
function ApiExtractorPluginImpl(rawOptions) {
|
|
77
64
|
const options = Schema.decodeUnknownSync(PluginOptions)(rawOptions);
|
|
78
65
|
const isInert = classifyApiConfig(options) === "disabled";
|
|
79
|
-
const shikiCrossLinker = new ShikiCrossLinker();
|
|
80
66
|
const envLogLevel = process.env.LOG_LEVEL?.toLowerCase();
|
|
81
67
|
const buildId = `${process.pid}-${performance.now().toString(36)}`;
|
|
82
68
|
const { resolved: obs, deprecations } = resolveObservability({
|
|
@@ -90,22 +76,14 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
90
76
|
const { layer: eventBusLayer, trace: traceSink, issues: issuesSink, render: renderSink, metrics: metricStore } = buildEventBus(obs);
|
|
91
77
|
const dbPath = path.resolve(process.cwd(), ".api-docs", "snapshot", "api-docs.db");
|
|
92
78
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
93
|
-
const
|
|
94
|
-
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);
|
|
95
84
|
const effectRuntime = ManagedRuntime.make(EffectAppLayer);
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
setShikiUtilsEventEmitter(emitSync, buildId);
|
|
99
|
-
setPrettierEventEmitter(emitSync, buildId);
|
|
100
|
-
setOgResolverEventEmitter(emitSync, buildId);
|
|
101
|
-
setRemarkWithApiEventEmitter(emitSync, buildId, obs.thresholds.slowCodeBlock);
|
|
102
|
-
setRemarkApiCodeblocksEventEmitter(emitSync, buildId, obs.thresholds.slowCodeBlock);
|
|
103
|
-
setBuildStagesEventEmitter(emitSync, buildId);
|
|
104
|
-
/**
|
|
105
|
-
* The build's Twoslash result cache, captured in `config()` and persisted in
|
|
106
|
-
* `afterBuild` — the render phase that populates it runs in between.
|
|
107
|
-
*/
|
|
108
|
-
let twoslashCacheHandle = null;
|
|
85
|
+
const emitterRuntime = ManagedRuntime.make(Layer.mergeAll(eventBusLayer, metricStore.layer, makeSummaryLoggerLayer(obs.logLevel), BuildEnvLayer));
|
|
86
|
+
installSyncEmitter(emitterRuntime);
|
|
109
87
|
const fileContextMap = /* @__PURE__ */ new Map();
|
|
110
88
|
let docsRoot;
|
|
111
89
|
let isFirstBuild = true;
|
|
@@ -121,22 +99,20 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
121
99
|
if (isFirstBuild && !isInert) {
|
|
122
100
|
const renderSamples = renderSink.snapshot();
|
|
123
101
|
const report = await effectRuntime.runPromise(codeBlockReport);
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
persisted: stats.dirty
|
|
137
|
-
}));
|
|
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
|
|
138
114
|
}));
|
|
139
|
-
}
|
|
115
|
+
}));
|
|
140
116
|
await effectRuntime.runPromise(logBuildSummary(obs.thresholds.slowCodeBlock, report));
|
|
141
117
|
if (isProd) await effectRuntime.runPromise(Effect.gen(function* () {
|
|
142
118
|
const packageName = yield* readSitePackageName;
|
|
@@ -165,7 +141,10 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
165
141
|
isFirstBuild = false;
|
|
166
142
|
}
|
|
167
143
|
if (traceSink) traceSink.flush();
|
|
168
|
-
if (isProd)
|
|
144
|
+
if (isProd) {
|
|
145
|
+
await effectRuntime.dispose();
|
|
146
|
+
await emitterRuntime.dispose();
|
|
147
|
+
}
|
|
169
148
|
},
|
|
170
149
|
async config(_config, _utils, isProd) {
|
|
171
150
|
const buildStartTime = performance.now();
|
|
@@ -206,6 +185,8 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
206
185
|
for (const dp of derivedPaths) fs.mkdirSync(dp.outputDir, { recursive: true });
|
|
207
186
|
}
|
|
208
187
|
VfsRegistry.clear();
|
|
188
|
+
clearTwoslashAccess();
|
|
189
|
+
clearTypeRoutes();
|
|
209
190
|
fileContextMap.clear();
|
|
210
191
|
issuesSink.reset();
|
|
211
192
|
for (const dep of deprecations) emitSync(PluginEvent.DeprecatedConfigUsed({
|
|
@@ -237,17 +218,11 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
237
218
|
apisTotal: apiCount,
|
|
238
219
|
buildId
|
|
239
220
|
}));
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
cache: buildContext.twoslashCache,
|
|
243
|
-
envHash: buildContext.twoslashEnvHash
|
|
244
|
-
};
|
|
221
|
+
installTwoslashAccess(yield* TwoslashEnvironments);
|
|
222
|
+
const apiConfigs = yield* (yield* ConfigService).resolve(rspressConfigSubset);
|
|
245
223
|
buildResults.length = 0;
|
|
246
224
|
yield* Ref.set(phaseRef, "generate");
|
|
247
|
-
yield* Effect.forEach(
|
|
248
|
-
...apiConfig,
|
|
249
|
-
suppressExampleErrors: buildContext.suppressExampleErrors
|
|
250
|
-
}, buildContext, fileContextMap).pipe(Effect.tap((result) => {
|
|
225
|
+
yield* Effect.forEach(apiConfigs, (apiConfig) => generateApiDocs(apiConfig, fileContextMap).pipe(Effect.tap((result) => {
|
|
251
226
|
buildResults.push(result);
|
|
252
227
|
return emit(PluginEvent.ApiDocsCompleted({
|
|
253
228
|
ctx: { buildId },
|
|
@@ -305,10 +280,10 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
305
280
|
if (!existingInclude.includes("rspress-plugin-api-extractor/runtime")) updatedConfig.builderConfig.source.include = [...existingInclude, "rspress-plugin-api-extractor/runtime"];
|
|
306
281
|
if (!updatedConfig.markdown) updatedConfig.markdown = {};
|
|
307
282
|
if (!updatedConfig.markdown.remarkPlugins) updatedConfig.markdown.remarkPlugins = [];
|
|
308
|
-
const
|
|
283
|
+
const firstApiTheme = options.api?.theme ?? options.apis?.[0]?.theme;
|
|
284
|
+
const remarkTheme = normalizeThemeConfig(firstApiTheme);
|
|
309
285
|
updatedConfig.markdown.remarkPlugins.push([remarkWithApi, {
|
|
310
|
-
|
|
311
|
-
getTransformer: (apiScope) => TwoslashManager.getInstance().getTransformer(apiScope),
|
|
286
|
+
getTransformer: (apiScope) => twoslashTransformerFor(apiScope),
|
|
312
287
|
theme: remarkTheme
|
|
313
288
|
}]);
|
|
314
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 };
|