rspress-plugin-api-extractor 0.1.2 → 0.2.1
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 +64 -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/og-resolver.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { imageSizeFromFile } from "image-size/fromFile";
|
|
4
|
+
|
|
5
|
+
//#region src/og-resolver.ts
|
|
6
|
+
/**
|
|
7
|
+
* MIME type mappings for common image formats.
|
|
8
|
+
* Used to determine the `og:image:type` meta tag value.
|
|
9
|
+
*/
|
|
10
|
+
const IMAGE_MIME_TYPES = {
|
|
11
|
+
jpg: "image/jpeg",
|
|
12
|
+
jpeg: "image/jpeg",
|
|
13
|
+
png: "image/png",
|
|
14
|
+
gif: "image/gif",
|
|
15
|
+
webp: "image/webp",
|
|
16
|
+
svg: "image/svg+xml"
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Resolves Open Graph image configurations into fully-qualified metadata.
|
|
20
|
+
*
|
|
21
|
+
* This class handles the conversion of flexible OG image configuration formats
|
|
22
|
+
* (strings or metadata objects) into complete `OpenGraphImageMetadata` objects
|
|
23
|
+
* with resolved URLs and auto-detected dimensions for local images.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* The resolver supports two input formats:
|
|
27
|
+
* - **String format**: A URL or path that will be resolved and optionally enhanced
|
|
28
|
+
* with auto-detected dimensions if pointing to a local file.
|
|
29
|
+
* - **Object format**: Detailed metadata with explicit properties that will be
|
|
30
|
+
* validated and URL-resolved.
|
|
31
|
+
*
|
|
32
|
+
* @example Basic usage with a relative path
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const resolver = new OpenGraphResolver({
|
|
35
|
+
* siteUrl: "https://example.com",
|
|
36
|
+
* docsRoot: "/path/to/docs"
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* const metadata = await resolver.resolve(
|
|
40
|
+
* "/images/og-api.png",
|
|
41
|
+
* "my-package",
|
|
42
|
+
* "MyClass"
|
|
43
|
+
* );
|
|
44
|
+
* // Result: { url: "https://example.com/images/og-api.png", width: 1200, height: 630, ... }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @example Using detailed configuration
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const metadata = await resolver.resolve(
|
|
50
|
+
* {
|
|
51
|
+
* url: "/images/og.png",
|
|
52
|
+
* alt: "Custom alt text",
|
|
53
|
+
* width: 1200,
|
|
54
|
+
* height: 630
|
|
55
|
+
* },
|
|
56
|
+
* "my-package"
|
|
57
|
+
* );
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
var OpenGraphResolver = class {
|
|
61
|
+
siteUrl;
|
|
62
|
+
docsRoot;
|
|
63
|
+
/**
|
|
64
|
+
* Creates a new OpenGraphResolver instance.
|
|
65
|
+
*
|
|
66
|
+
* @param options - Configuration options for the resolver.
|
|
67
|
+
* `siteUrl`: Base URL for the website (e.g., "https://example.com"),
|
|
68
|
+
* used to construct absolute URLs from relative paths.
|
|
69
|
+
* `docsRoot`: Optional root directory for documentation files;
|
|
70
|
+
* when provided, enables auto-detection of image dimensions for local files
|
|
71
|
+
* by looking in the `public` subdirectory.
|
|
72
|
+
*/
|
|
73
|
+
constructor(options) {
|
|
74
|
+
this.siteUrl = options.siteUrl;
|
|
75
|
+
if (options.docsRoot != null) this.docsRoot = options.docsRoot;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolves an Open Graph image configuration into complete metadata.
|
|
79
|
+
*
|
|
80
|
+
* Handles both string URLs/paths and detailed metadata objects, converting them
|
|
81
|
+
* into fully-qualified `OpenGraphImageMetadata` with absolute URLs.
|
|
82
|
+
*
|
|
83
|
+
* @param config - The OG image configuration to resolve. Can be:
|
|
84
|
+
* - A string URL (absolute or relative path starting with `/`)
|
|
85
|
+
* - An `OpenGraphImageMetadata` object with explicit properties
|
|
86
|
+
* - `undefined` to indicate no OG image
|
|
87
|
+
* @param packageName - The package name for generating default alt text
|
|
88
|
+
* @param apiName - Optional API name for more descriptive alt text
|
|
89
|
+
* @returns Resolved metadata with absolute URLs, or `undefined` if:
|
|
90
|
+
* - `config` is `undefined`
|
|
91
|
+
* - The URL format is invalid
|
|
92
|
+
*
|
|
93
|
+
* @example Resolve a relative path
|
|
94
|
+
* ```typescript
|
|
95
|
+
* const metadata = await resolver.resolve("/images/og.png", "my-lib");
|
|
96
|
+
* // Returns: { url: "https://example.com/images/og.png", alt: "my-lib API Documentation", ... }
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @example Resolve an absolute URL
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const metadata = await resolver.resolve("https://cdn.example.com/og.png", "my-lib");
|
|
102
|
+
* // Returns: { url: "https://cdn.example.com/og.png", alt: "my-lib API Documentation" }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
async resolve(config, packageName, apiName) {
|
|
106
|
+
if (!config) return;
|
|
107
|
+
if (typeof config === "object") return this.resolveFromMetadata(config, packageName, apiName);
|
|
108
|
+
return this.resolveFromString(config, packageName, apiName);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resolves a metadata object configuration into complete OG image metadata.
|
|
112
|
+
*
|
|
113
|
+
* Validates and resolves URLs in the provided metadata object, ensuring all
|
|
114
|
+
* URLs are absolute and properly formatted.
|
|
115
|
+
*
|
|
116
|
+
* @param metadata - The metadata object containing OG image properties
|
|
117
|
+
* @param packageName - Package name for default alt text generation
|
|
118
|
+
* @param apiName - Optional API name for more descriptive alt text
|
|
119
|
+
* @returns Resolved metadata with absolute URLs, or `undefined` if URL is invalid
|
|
120
|
+
*/
|
|
121
|
+
async resolveFromMetadata(metadata, packageName, apiName) {
|
|
122
|
+
const { url, secureUrl, type, width, height, alt } = metadata;
|
|
123
|
+
const resolvedUrl = this.resolveUrl(url);
|
|
124
|
+
if (!resolvedUrl) {
|
|
125
|
+
console.warn(`[og-resolver] Invalid ogImage URL format: "${url}"`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
let resolvedSecureUrl;
|
|
129
|
+
if (secureUrl) if (secureUrl.startsWith("https://")) resolvedSecureUrl = secureUrl;
|
|
130
|
+
else console.warn(`[og-resolver] ogImage secureUrl must be an absolute HTTPS URL: "${secureUrl}"`);
|
|
131
|
+
return {
|
|
132
|
+
url: resolvedUrl,
|
|
133
|
+
secureUrl: resolvedSecureUrl,
|
|
134
|
+
type,
|
|
135
|
+
width,
|
|
136
|
+
height,
|
|
137
|
+
alt: alt ?? this.generateAltText(packageName, apiName)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolves a string URL/path into complete OG image metadata.
|
|
142
|
+
*
|
|
143
|
+
* For relative paths pointing to local files, this method will attempt to:
|
|
144
|
+
* 1. Locate the file in the docs `public` directory
|
|
145
|
+
* 2. Read the image dimensions using `image-size`
|
|
146
|
+
* 3. Determine the MIME type from the file extension
|
|
147
|
+
*
|
|
148
|
+
* @param imageUrl - The image URL or path to resolve
|
|
149
|
+
* @param packageName - Package name for alt text generation
|
|
150
|
+
* @param apiName - Optional API name for more descriptive alt text
|
|
151
|
+
* @returns Resolved metadata with auto-detected dimensions for local files,
|
|
152
|
+
* or `undefined` if the URL format is invalid
|
|
153
|
+
*/
|
|
154
|
+
async resolveFromString(imageUrl, packageName, apiName) {
|
|
155
|
+
const resolvedUrl = this.resolveUrl(imageUrl);
|
|
156
|
+
if (!resolvedUrl) {
|
|
157
|
+
console.warn(`[og-resolver] Invalid ogImage format: "${imageUrl}" (must be absolute URL or path starting with /)`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const localPath = this.findLocalImage(imageUrl);
|
|
161
|
+
const dimensions = localPath ? await this.readImageDimensions(localPath) : void 0;
|
|
162
|
+
return {
|
|
163
|
+
url: resolvedUrl,
|
|
164
|
+
type: dimensions?.type,
|
|
165
|
+
width: dimensions?.width,
|
|
166
|
+
height: dimensions?.height,
|
|
167
|
+
alt: this.generateAltText(packageName, apiName)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Resolves a URL string to an absolute URL.
|
|
172
|
+
*
|
|
173
|
+
* @param url - The URL to resolve (absolute URL or relative path)
|
|
174
|
+
* @returns The absolute URL, or `undefined` if the format is invalid
|
|
175
|
+
*/
|
|
176
|
+
resolveUrl(url) {
|
|
177
|
+
if (url.startsWith("http://") || url.startsWith("https://")) return url;
|
|
178
|
+
if (url.startsWith("/")) return `${this.siteUrl}${url}`;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Attempts to find a local image file in the docs public directory.
|
|
182
|
+
*
|
|
183
|
+
* @param imagePath - The relative image path (starting with `/`)
|
|
184
|
+
* @returns The absolute file path if found, or `undefined` if not found
|
|
185
|
+
* or if `docsRoot` is not configured
|
|
186
|
+
*/
|
|
187
|
+
findLocalImage(imagePath) {
|
|
188
|
+
if (!this.docsRoot || !imagePath.startsWith("/")) return;
|
|
189
|
+
const publicPath = path.join(this.docsRoot, "public", imagePath);
|
|
190
|
+
if (fs.existsSync(publicPath)) return publicPath;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Reads image dimensions and type from a local file.
|
|
194
|
+
*
|
|
195
|
+
* @param filePath - Absolute path to the image file
|
|
196
|
+
* @returns Object containing width, height, and MIME type if successful,
|
|
197
|
+
* or `undefined` if the file cannot be read or analyzed
|
|
198
|
+
*/
|
|
199
|
+
async readImageDimensions(filePath) {
|
|
200
|
+
try {
|
|
201
|
+
const dimensions = await imageSizeFromFile(filePath);
|
|
202
|
+
let mimeType;
|
|
203
|
+
if (dimensions.type) mimeType = IMAGE_MIME_TYPES[dimensions.type.toLowerCase()];
|
|
204
|
+
return {
|
|
205
|
+
width: dimensions.width,
|
|
206
|
+
height: dimensions.height,
|
|
207
|
+
...mimeType != null ? { type: mimeType } : {}
|
|
208
|
+
};
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.warn(`[og-resolver] Failed to read image dimensions from ${filePath}:`, error.message);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Generates descriptive alt text for the OG image.
|
|
216
|
+
*
|
|
217
|
+
* @param packageName - The package name
|
|
218
|
+
* @param apiName - Optional API name for more specific text
|
|
219
|
+
* @returns Generated alt text string
|
|
220
|
+
*/
|
|
221
|
+
generateAltText(packageName, apiName) {
|
|
222
|
+
if (apiName) return `${apiName} - ${packageName} API Documentation`;
|
|
223
|
+
return `${packageName} API Documentation`;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Creates complete Open Graph metadata for an API documentation page.
|
|
227
|
+
*
|
|
228
|
+
* This static factory method builds a complete `OpenGraphMetadata` object
|
|
229
|
+
* suitable for inclusion in page frontmatter, combining resolved image
|
|
230
|
+
* metadata with article-specific information.
|
|
231
|
+
*
|
|
232
|
+
* @param options - Configuration for the OG metadata.
|
|
233
|
+
* Includes `siteUrl`, `pageRoute`, `description`, `publishedTime`,
|
|
234
|
+
* `modifiedTime`, `section`, `packageName`, and optional `ogImage`.
|
|
235
|
+
* @returns Complete Open Graph metadata object
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```typescript
|
|
239
|
+
* const ogMetadata = OpenGraphResolver.createPageMetadata({
|
|
240
|
+
* siteUrl: "https://example.com",
|
|
241
|
+
* pageRoute: "/api/classes/MyClass",
|
|
242
|
+
* description: "MyClass provides...",
|
|
243
|
+
* publishedTime: "2024-01-15T10:00:00Z",
|
|
244
|
+
* modifiedTime: "2024-01-20T15:30:00Z",
|
|
245
|
+
* section: "Classes",
|
|
246
|
+
* packageName: "my-library",
|
|
247
|
+
* ogImage: resolvedImageMetadata
|
|
248
|
+
* });
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
static createPageMetadata(options) {
|
|
252
|
+
return {
|
|
253
|
+
siteUrl: options.siteUrl,
|
|
254
|
+
pageRoute: options.pageRoute,
|
|
255
|
+
description: options.description,
|
|
256
|
+
publishedTime: options.publishedTime,
|
|
257
|
+
modifiedTime: options.modifiedTime,
|
|
258
|
+
section: options.section,
|
|
259
|
+
tags: [
|
|
260
|
+
"TypeScript",
|
|
261
|
+
"API",
|
|
262
|
+
options.packageName
|
|
263
|
+
],
|
|
264
|
+
...options.ogImage != null ? { ogImage: options.ogImage } : {},
|
|
265
|
+
ogType: "article"
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
//#endregion
|
|
271
|
+
export { OpenGraphResolver };
|
package/package.json
CHANGED
|
@@ -1,75 +1,66 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
},
|
|
67
|
-
"peerDependencies": {
|
|
68
|
-
"@rspress/core": "^2.0.0",
|
|
69
|
-
"react": "^19.2.0",
|
|
70
|
-
"react-dom": "^19.2.0"
|
|
71
|
-
},
|
|
72
|
-
"engines": {
|
|
73
|
-
"node": ">=24.1.0"
|
|
74
|
-
}
|
|
2
|
+
"name": "rspress-plugin-api-extractor",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "RSPress plugin for generating API documentation from TypeScript API Extractor models",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"rspress",
|
|
8
|
+
"plugin",
|
|
9
|
+
"api-extractor",
|
|
10
|
+
"documentation",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/spencerbeggs/rspress-plugin-api-extractor#readme",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/spencerbeggs/rspress-plugin-api-extractor.git",
|
|
17
|
+
"directory": "plugin"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Spencer Beggs",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./index.d.ts",
|
|
25
|
+
"import": "./index.js"
|
|
26
|
+
},
|
|
27
|
+
"./runtime": {
|
|
28
|
+
"types": "./runtime/index.d.ts",
|
|
29
|
+
"import": "./runtime/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./tsconfig/rspress.json": "./public/tsconfig/rspress.json",
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@effect/platform": "^0.96.1",
|
|
36
|
+
"@effect/platform-node": "^0.107.0",
|
|
37
|
+
"@effect/sql": "^0.51.1",
|
|
38
|
+
"@effect/sql-sqlite-node": "^0.52.0",
|
|
39
|
+
"@microsoft/api-extractor-model": "^7.33.8",
|
|
40
|
+
"@shikijs/twoslash": "^4.1.0",
|
|
41
|
+
"api-extractor-llms": "0.1.0",
|
|
42
|
+
"clsx": "^2.1.1",
|
|
43
|
+
"effect": "^3.21.3",
|
|
44
|
+
"gray-matter": "^4.0.3",
|
|
45
|
+
"hast-util-to-jsx-runtime": "^2.3.6",
|
|
46
|
+
"image-size": "^2.0.2",
|
|
47
|
+
"mdast-util-from-markdown": "^2.0.3",
|
|
48
|
+
"mdast-util-to-hast": "^13.2.1",
|
|
49
|
+
"open": "^11.0.0",
|
|
50
|
+
"prettier": "^3.8.3",
|
|
51
|
+
"react-markdown": "^10.1.0",
|
|
52
|
+
"semver-effect": "^0.2.1",
|
|
53
|
+
"shiki": "^4.1.0",
|
|
54
|
+
"type-registry-effect": "^0.2.3",
|
|
55
|
+
"typescript": "^6.0.3",
|
|
56
|
+
"unist-util-visit": "^5.1.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@rspress/core": "^2.0.0",
|
|
60
|
+
"react": "^19.2.0",
|
|
61
|
+
"react-dom": "^19.2.0"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=24.11.0"
|
|
65
|
+
}
|
|
75
66
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
//#region src/path-derivation.ts
|
|
4
|
+
/** Extract unscoped name from a potentially scoped package name */
|
|
5
|
+
function unscopedName(packageName) {
|
|
6
|
+
return packageName.startsWith("@") ? packageName.split("/")[1] ?? packageName : packageName;
|
|
7
|
+
}
|
|
8
|
+
/** Normalize baseRoute: ensure leading slash, strip trailing slash, preserve root "/" */
|
|
9
|
+
function normalizeBaseRoute(route) {
|
|
10
|
+
const withSlash = route.startsWith("/") ? route : `/${route}`;
|
|
11
|
+
const stripped = withSlash.endsWith("/") ? withSlash.slice(0, -1) : withSlash;
|
|
12
|
+
return stripped === "" ? "/" : stripped;
|
|
13
|
+
}
|
|
14
|
+
function deriveOutputPaths(input) {
|
|
15
|
+
const { docsRoot, baseRoute, apiFolder, locales, defaultLang, versions, defaultVersion } = input;
|
|
16
|
+
const results = [];
|
|
17
|
+
const folder = apiFolder ?? void 0;
|
|
18
|
+
const baseSegment = baseRoute === "/" ? void 0 : baseRoute.replace(/^\//, "");
|
|
19
|
+
const versionList = versions.length > 0 ? versions : [void 0];
|
|
20
|
+
const localeList = locales.length > 0 ? locales : [void 0];
|
|
21
|
+
for (const version of versionList) for (const locale of localeList) {
|
|
22
|
+
const dirParts = [
|
|
23
|
+
docsRoot,
|
|
24
|
+
version,
|
|
25
|
+
locale,
|
|
26
|
+
baseSegment,
|
|
27
|
+
folder
|
|
28
|
+
].filter((p) => p !== void 0);
|
|
29
|
+
const outputDir = dirParts.length > 0 ? path.join(...dirParts) : docsRoot;
|
|
30
|
+
const routeParts = [
|
|
31
|
+
!(version === defaultVersion) ? version : void 0,
|
|
32
|
+
!(locale === defaultLang) ? locale : void 0,
|
|
33
|
+
baseSegment,
|
|
34
|
+
folder
|
|
35
|
+
].filter((p) => p !== void 0);
|
|
36
|
+
const routeBase = routeParts.length > 0 ? `/${routeParts.join("/")}` : "/";
|
|
37
|
+
results.push({
|
|
38
|
+
outputDir,
|
|
39
|
+
routeBase,
|
|
40
|
+
version,
|
|
41
|
+
locale
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return results;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { deriveOutputPaths, normalizeBaseRoute, unscopedName };
|
package/plugin.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { PluginLoggerLayer, logBuildSummary } from "./layers/ObservabilityLive.js";
|
|
2
|
+
import { TwoslashManager } from "./twoslash-transformer.js";
|
|
3
|
+
import { VfsRegistry } from "./vfs-registry.js";
|
|
4
|
+
import { generateApiDocs } from "./build-program.js";
|
|
5
|
+
import { deriveOutputPaths, normalizeBaseRoute, unscopedName } from "./path-derivation.js";
|
|
6
|
+
import { fromDir, fromParentDir } from "./config-helpers.js";
|
|
7
|
+
import { mergeLlmsPluginConfig } from "./config-utils.js";
|
|
8
|
+
import { DEFAULT_SHIKI_THEMES } from "./markdown/shiki-utils.js";
|
|
9
|
+
import { PluginOptions } from "./schemas/config.js";
|
|
10
|
+
import "./schemas/index.js";
|
|
11
|
+
import { ConfigService } from "./services/ConfigService.js";
|
|
12
|
+
import { ConfigServiceLive } from "./layers/ConfigServiceLive.js";
|
|
13
|
+
import { PathDerivationServiceLive } from "./layers/PathDerivationServiceLive.js";
|
|
14
|
+
import { SnapshotServiceLive } from "./layers/SnapshotServiceLive.js";
|
|
15
|
+
import { TypeRegistryServiceLive } from "./layers/TypeRegistryServiceLive.js";
|
|
16
|
+
import { remarkApiCodeblocks } from "./remark-api-codeblocks.js";
|
|
17
|
+
import { remarkWithApi } from "./remark-with-api.js";
|
|
18
|
+
import { ShikiCrossLinker } from "./shiki-transformer.js";
|
|
19
|
+
import { createRequire } from "node:module";
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { NodeFileSystem } from "@effect/platform-node";
|
|
24
|
+
import { Effect, Layer, ManagedRuntime, Schema } from "effect";
|
|
25
|
+
|
|
26
|
+
//#region src/plugin.ts
|
|
27
|
+
/* v8 ignore start -- RSPress plugin adapter, requires RSPress runtime */
|
|
28
|
+
/**
|
|
29
|
+
* Normalize theme configuration from user input to a consistent format.
|
|
30
|
+
*/
|
|
31
|
+
function normalizeThemeConfig(theme) {
|
|
32
|
+
if (!theme) return { ...DEFAULT_SHIKI_THEMES };
|
|
33
|
+
if (typeof theme === "string") return {
|
|
34
|
+
light: theme,
|
|
35
|
+
dark: theme
|
|
36
|
+
};
|
|
37
|
+
if ("light" in theme && "dark" in theme && typeof theme.light === "string" && typeof theme.dark === "string") return {
|
|
38
|
+
light: theme.light,
|
|
39
|
+
dark: theme.dark
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
light: theme,
|
|
43
|
+
dark: theme
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* RSPress plugin for generating API documentation from API Extractor model files
|
|
48
|
+
*/
|
|
49
|
+
function ApiExtractorPluginImpl(rawOptions) {
|
|
50
|
+
const options = Schema.decodeUnknownSync(PluginOptions)(rawOptions);
|
|
51
|
+
const shikiCrossLinker = new ShikiCrossLinker();
|
|
52
|
+
const logLevel = process.env.LOG_LEVEL?.toLowerCase() || options.logLevel || "info";
|
|
53
|
+
const dbPath = path.resolve(process.cwd(), "api-docs-snapshot.db");
|
|
54
|
+
const BaseLayer = Layer.mergeAll(PathDerivationServiceLive, PluginLoggerLayer(logLevel), TypeRegistryServiceLive, NodeFileSystem.layer, SnapshotServiceLive(dbPath));
|
|
55
|
+
const EffectAppLayer = Layer.provideMerge(ConfigServiceLive(options, shikiCrossLinker), BaseLayer);
|
|
56
|
+
const effectRuntime = ManagedRuntime.make(EffectAppLayer);
|
|
57
|
+
const fileContextMap = /* @__PURE__ */ new Map();
|
|
58
|
+
const isVerbose = logLevel === "verbose" || logLevel === "debug";
|
|
59
|
+
let docsRoot;
|
|
60
|
+
let isFirstBuild = true;
|
|
61
|
+
let rspressLlmsEnabled = false;
|
|
62
|
+
let rspressOutDir = "dist";
|
|
63
|
+
const buildResults = [];
|
|
64
|
+
const resolvedLlmsPlugin = mergeLlmsPluginConfig(options.llmsPlugin);
|
|
65
|
+
const packageRoutes = /* @__PURE__ */ new Map();
|
|
66
|
+
return {
|
|
67
|
+
name: "rspress-plugin-api-docs",
|
|
68
|
+
async beforeBuild(_config, _isProd) {},
|
|
69
|
+
async afterBuild(_config, isProd) {
|
|
70
|
+
if (isFirstBuild) {
|
|
71
|
+
await effectRuntime.runPromise(logBuildSummary);
|
|
72
|
+
if (rspressLlmsEnabled && resolvedLlmsPlugin.enabled) {
|
|
73
|
+
const { processLlmsFiles } = await import("./llms-program.js");
|
|
74
|
+
await effectRuntime.runPromise(processLlmsFiles({
|
|
75
|
+
outDir: path.resolve(process.cwd(), rspressOutDir),
|
|
76
|
+
buildResults,
|
|
77
|
+
llmsPlugin: resolvedLlmsPlugin,
|
|
78
|
+
packageRoutes
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
isFirstBuild = false;
|
|
82
|
+
}
|
|
83
|
+
if (isProd) await effectRuntime.dispose();
|
|
84
|
+
},
|
|
85
|
+
async config(_config) {
|
|
86
|
+
const buildStartTime = performance.now();
|
|
87
|
+
if (_config.root) docsRoot = path.isAbsolute(_config.root) ? _config.root : path.resolve(process.cwd(), _config.root);
|
|
88
|
+
const rspressRoot = docsRoot || process.cwd();
|
|
89
|
+
const rspressLocales = _config.locales?.map((l) => l.lang) ?? [];
|
|
90
|
+
const rspressLang = _config.lang;
|
|
91
|
+
const rspressMultiVersion = _config.multiVersion;
|
|
92
|
+
rspressLlmsEnabled = Boolean(_config.llms);
|
|
93
|
+
rspressOutDir = _config.outDir ?? "dist";
|
|
94
|
+
if (options.api) {
|
|
95
|
+
const api = options.api;
|
|
96
|
+
const baseRoute = normalizeBaseRoute(api.baseRoute ?? "/");
|
|
97
|
+
const versions = rspressMultiVersion?.versions ?? [];
|
|
98
|
+
const derivedPaths = deriveOutputPaths({
|
|
99
|
+
mode: "single",
|
|
100
|
+
docsRoot: rspressRoot,
|
|
101
|
+
baseRoute,
|
|
102
|
+
apiFolder: api.apiFolder ?? "api",
|
|
103
|
+
locales: rspressLocales,
|
|
104
|
+
defaultLang: rspressLang,
|
|
105
|
+
versions,
|
|
106
|
+
defaultVersion: rspressMultiVersion?.default
|
|
107
|
+
});
|
|
108
|
+
for (const dp of derivedPaths) fs.mkdirSync(dp.outputDir, { recursive: true });
|
|
109
|
+
} else if (options.apis) for (const api of options.apis) {
|
|
110
|
+
const derivedPaths = deriveOutputPaths({
|
|
111
|
+
mode: "multi",
|
|
112
|
+
docsRoot: rspressRoot,
|
|
113
|
+
baseRoute: normalizeBaseRoute(api.baseRoute ?? `/${unscopedName(api.packageName)}`),
|
|
114
|
+
apiFolder: api.apiFolder ?? "api",
|
|
115
|
+
locales: rspressLocales,
|
|
116
|
+
defaultLang: rspressLang,
|
|
117
|
+
versions: [],
|
|
118
|
+
defaultVersion: void 0
|
|
119
|
+
});
|
|
120
|
+
for (const dp of derivedPaths) fs.mkdirSync(dp.outputDir, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
VfsRegistry.clear();
|
|
123
|
+
fileContextMap.clear();
|
|
124
|
+
if (isVerbose) console.log("🚀 RSPress API Extractor Plugin");
|
|
125
|
+
try {
|
|
126
|
+
const rspressConfigSubset = {
|
|
127
|
+
...rspressMultiVersion != null ? { multiVersion: rspressMultiVersion } : {},
|
|
128
|
+
...rspressLocales.length > 0 ? { locales: rspressLocales.map((lang) => ({ lang })) } : {},
|
|
129
|
+
...rspressLang != null ? { lang: rspressLang } : {},
|
|
130
|
+
...docsRoot != null ? { root: docsRoot } : {}
|
|
131
|
+
};
|
|
132
|
+
await effectRuntime.runPromise(Effect.gen(function* () {
|
|
133
|
+
const buildContext = yield* (yield* ConfigService).resolve(rspressConfigSubset);
|
|
134
|
+
yield* Effect.logInfo("Generating API documentation...");
|
|
135
|
+
buildResults.length = 0;
|
|
136
|
+
yield* Effect.forEach(buildContext.apiConfigs, (apiConfig) => generateApiDocs({
|
|
137
|
+
...apiConfig,
|
|
138
|
+
suppressExampleErrors: buildContext.suppressExampleErrors
|
|
139
|
+
}, buildContext, fileContextMap).pipe(Effect.tap((result) => {
|
|
140
|
+
buildResults.push(result);
|
|
141
|
+
return Effect.void;
|
|
142
|
+
}), Effect.tap(() => isVerbose ? Effect.logDebug(`Generating docs for ${apiConfig.packageName}`) : Effect.void)), { concurrency: 2 });
|
|
143
|
+
}).pipe(Effect.scoped));
|
|
144
|
+
if (logLevel !== "none") {
|
|
145
|
+
const totalTime = ((performance.now() - buildStartTime) / 1e3).toFixed(2);
|
|
146
|
+
console.log(`✅ API documentation complete (${totalTime}s)`);
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error(`❌ Error generating API documentation: ${error instanceof Error ? error.message : String(error)}`);
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
const updatedConfig = { ..._config };
|
|
153
|
+
if (!updatedConfig.builderConfig) updatedConfig.builderConfig = {};
|
|
154
|
+
if (!updatedConfig.builderConfig.source) updatedConfig.builderConfig.source = {};
|
|
155
|
+
if (rspressLlmsEnabled && resolvedLlmsPlugin.enabled && resolvedLlmsPlugin.scopes) {
|
|
156
|
+
if (!updatedConfig.builderConfig.resolve) updatedConfig.builderConfig.resolve = {};
|
|
157
|
+
const pluginDir = path.dirname(fileURLToPath(import.meta.url));
|
|
158
|
+
const customLlmsViewOptions = path.resolve(pluginDir, "runtime/components/ApiLlmsViewOptions/index.js");
|
|
159
|
+
const pluginRequire = createRequire(import.meta.url);
|
|
160
|
+
const rspressCoreDir = path.dirname(pluginRequire.resolve("@rspress/core/package.json"));
|
|
161
|
+
const originalLlmsViewOptions = path.join(rspressCoreDir, "dist/theme/components/Llms/LlmsViewOptions.js");
|
|
162
|
+
const existingAlias = updatedConfig.builderConfig.resolve.alias;
|
|
163
|
+
updatedConfig.builderConfig.resolve.alias = {
|
|
164
|
+
...typeof existingAlias === "object" && existingAlias !== null ? existingAlias : {},
|
|
165
|
+
[originalLlmsViewOptions]: customLlmsViewOptions
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const existingInclude = updatedConfig.builderConfig.source.include || [];
|
|
169
|
+
if (!existingInclude.includes("rspress-plugin-api-extractor/runtime")) updatedConfig.builderConfig.source.include = [...existingInclude, "rspress-plugin-api-extractor/runtime"];
|
|
170
|
+
if (!updatedConfig.markdown) updatedConfig.markdown = {};
|
|
171
|
+
if (!updatedConfig.markdown.remarkPlugins) updatedConfig.markdown.remarkPlugins = [];
|
|
172
|
+
const remarkTheme = normalizeThemeConfig(options.api?.theme ?? options.apis?.[0]?.theme);
|
|
173
|
+
updatedConfig.markdown.remarkPlugins.push([remarkWithApi, {
|
|
174
|
+
shikiCrossLinker,
|
|
175
|
+
getTransformer: () => TwoslashManager.getInstance().getTransformer(),
|
|
176
|
+
theme: remarkTheme
|
|
177
|
+
}]);
|
|
178
|
+
updatedConfig.markdown.remarkPlugins.push([remarkApiCodeblocks]);
|
|
179
|
+
if (rspressLlmsEnabled && resolvedLlmsPlugin.enabled && resolvedLlmsPlugin.scopes) {
|
|
180
|
+
packageRoutes.clear();
|
|
181
|
+
if (options.api) packageRoutes.set(options.api.packageName, normalizeBaseRoute(options.api.baseRoute ?? "/"));
|
|
182
|
+
else if (options.apis) for (const api of options.apis) packageRoutes.set(api.packageName, normalizeBaseRoute(api.baseRoute ?? `/${unscopedName(api.packageName)}`));
|
|
183
|
+
const scopes = buildResults.map((result) => ({
|
|
184
|
+
name: result.apiName ?? result.packageName,
|
|
185
|
+
packageName: result.packageName,
|
|
186
|
+
packageRoute: packageRoutes.get(result.packageName) ?? result.baseRoute,
|
|
187
|
+
baseRoute: result.baseRoute,
|
|
188
|
+
version: null,
|
|
189
|
+
locale: null,
|
|
190
|
+
llmsTxt: `${packageRoutes.get(result.packageName) ?? result.baseRoute}/llms.txt`,
|
|
191
|
+
llmsFullTxt: `${packageRoutes.get(result.packageName) ?? result.baseRoute}/llms-full.txt`,
|
|
192
|
+
llmsDocsTxt: `${packageRoutes.get(result.packageName) ?? result.baseRoute}/llms-docs.txt`,
|
|
193
|
+
llmsApiTxt: resolvedLlmsPlugin.apiTxt ? `${packageRoutes.get(result.packageName) ?? result.baseRoute}/llms-api.txt` : null
|
|
194
|
+
}));
|
|
195
|
+
if (!updatedConfig.themeConfig) updatedConfig.themeConfig = {};
|
|
196
|
+
updatedConfig.themeConfig.apiExtractorScopes = scopes;
|
|
197
|
+
if (!updatedConfig.globalUIComponents) updatedConfig.globalUIComponents = [];
|
|
198
|
+
const llmsComponentPluginDir = path.dirname(fileURLToPath(import.meta.url));
|
|
199
|
+
const llmsComponentPath = path.resolve(llmsComponentPluginDir, "runtime/components/ApiLlmsPackageActions/index.js");
|
|
200
|
+
updatedConfig.globalUIComponents.push(llmsComponentPath);
|
|
201
|
+
}
|
|
202
|
+
return updatedConfig;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* RSPress plugin for generating API documentation from API Extractor model
|
|
208
|
+
* files. Config helpers are available under `ApiExtractorPlugin.api` (single
|
|
209
|
+
* package → one config for the `api:` option) and `ApiExtractorPlugin.apis`
|
|
210
|
+
* (parent directory → array for the `apis:` option).
|
|
211
|
+
*/
|
|
212
|
+
const ApiExtractorPlugin = Object.assign(ApiExtractorPluginImpl, {
|
|
213
|
+
api: { fromDir },
|
|
214
|
+
apis: { fromDir: fromParentDir }
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
//#endregion
|
|
218
|
+
export { ApiExtractorPlugin };
|