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.
Files changed (109) hide show
  1. package/README.md +2 -2
  2. package/api-extracted-package.js +426 -0
  3. package/build-program.js +120 -0
  4. package/build-stages.js +664 -0
  5. package/category-resolver.js +50 -0
  6. package/code-post-processor.js +38 -0
  7. package/config-helpers.js +106 -0
  8. package/config-utils.js +258 -0
  9. package/content-hash.js +79 -0
  10. package/errors.js +29 -0
  11. package/formatter.js +69 -0
  12. package/hide-cut-transformer.js +96 -0
  13. package/index.d.ts +901 -831
  14. package/index.js +4 -6128
  15. package/layers/ConfigServiceLive.js +377 -0
  16. package/layers/ObservabilityLive.js +136 -0
  17. package/layers/PathDerivationServiceLive.js +16 -0
  18. package/layers/SnapshotServiceLive.js +94 -0
  19. package/layers/TypeRegistryServiceLive.js +46 -0
  20. package/llms-processing.js +270 -0
  21. package/llms-program.js +262 -0
  22. package/loader.js +186 -0
  23. package/markdown/cross-linker.js +156 -0
  24. package/markdown/helpers.js +364 -0
  25. package/markdown/index.js +11 -0
  26. package/markdown/page-generators/class-page.js +357 -0
  27. package/markdown/page-generators/enum-page.js +152 -0
  28. package/markdown/page-generators/function-page.js +127 -0
  29. package/markdown/page-generators/index-pages.js +27 -0
  30. package/markdown/page-generators/interface-page.js +307 -0
  31. package/markdown/page-generators/namespace-page.js +280 -0
  32. package/markdown/page-generators/type-alias-page.js +110 -0
  33. package/markdown/page-generators/variable-page.js +110 -0
  34. package/markdown/shiki-utils.js +48 -0
  35. package/migrations/001_create_snapshots.js +25 -0
  36. package/model-loader.js +95 -0
  37. package/multi-entry-resolver.js +70 -0
  38. package/og-resolver.js +271 -0
  39. package/package.json +64 -73
  40. package/path-derivation.js +48 -0
  41. package/plugin.js +218 -0
  42. package/prettier-formatter.js +73 -0
  43. package/public/tsconfig/rspress.json +44 -0
  44. package/remark-api-codeblocks.js +130 -0
  45. package/remark-with-api.js +172 -0
  46. package/route-collisions.js +52 -0
  47. package/runtime/components/ApiExample/index.js +29 -25
  48. package/runtime/components/ApiLlmsPackageActions/index.js +252 -323
  49. package/runtime/components/ApiLlmsViewOptions/index.js +269 -340
  50. package/runtime/components/ApiMember/index.js +49 -47
  51. package/runtime/components/ApiSignature/index.js +32 -28
  52. package/runtime/components/EnumMembersTable/{index_module.css → index.css} +18 -19
  53. package/runtime/components/EnumMembersTable/index.js +36 -67
  54. package/runtime/components/EnumMembersTable/index.module.js +9 -6
  55. package/runtime/components/ExampleBlock/{index_module.css → index.css} +2 -3
  56. package/runtime/components/ExampleBlock/index.js +23 -28
  57. package/runtime/components/ExampleBlock/index.module.js +8 -5
  58. package/runtime/components/MarkdownContent/index.js +26 -18
  59. package/runtime/components/MarkdownText/index.js +28 -22
  60. package/runtime/components/MemberSignature/{index_module.css → index.css} +5 -6
  61. package/runtime/components/MemberSignature/index.js +46 -46
  62. package/runtime/components/MemberSignature/index.module.js +9 -6
  63. package/runtime/components/ParametersTable/{index_module.css → index.css} +19 -20
  64. package/runtime/components/ParametersTable/index.js +36 -67
  65. package/runtime/components/ParametersTable/index.module.js +9 -6
  66. package/runtime/components/SignatureBlock/{index_module.css → index.css} +5 -6
  67. package/runtime/components/SignatureBlock/index.js +30 -29
  68. package/runtime/components/SignatureBlock/index.module.js +9 -6
  69. package/runtime/components/SignatureCode/{index_module.css → index.css} +9 -10
  70. package/runtime/components/SignatureCode/index.js +40 -32
  71. package/runtime/components/SignatureCode/index.module.js +9 -6
  72. package/runtime/components/SignatureToolbar/{index_module.css → index.css} +18 -20
  73. package/runtime/components/SignatureToolbar/index.js +48 -51
  74. package/runtime/components/SignatureToolbar/index.module.js +13 -10
  75. package/runtime/components/buttons/ButtonGroup.js +13 -6
  76. package/runtime/components/buttons/CopyCodeButton.js +40 -38
  77. package/runtime/components/buttons/WrapSignatureButton.js +20 -16
  78. package/runtime/components/buttons/{index_module.css → index.css} +5 -6
  79. package/runtime/components/buttons/index.module.js +8 -5
  80. package/runtime/components/icons/CheckIcon/index.js +20 -17
  81. package/runtime/components/icons/CopyIcon/index.js +20 -17
  82. package/runtime/components/icons/UnwrapIcon/index.js +22 -18
  83. package/runtime/components/icons/WrapIcon/index.js +20 -17
  84. package/runtime/components/shared/_twoslash.css +3 -10
  85. package/runtime/components/shared/variables.css +0 -3
  86. package/runtime/hooks/useWrapToggle.js +32 -9
  87. package/runtime/index.d.ts +513 -173
  88. package/runtime/index.js +11 -9
  89. package/runtime/utils/decode-hast.js +31 -16
  90. package/runtime/utils/hast-renderer.js +21 -7
  91. package/schemas/config.js +199 -0
  92. package/schemas/index.js +5 -0
  93. package/schemas/opengraph.js +26 -0
  94. package/schemas/performance.js +19 -0
  95. package/serve.js +133 -0
  96. package/services/ConfigService.js +7 -0
  97. package/services/PathDerivationService.js +7 -0
  98. package/services/SnapshotService.js +7 -0
  99. package/services/TypeRegistryService.js +7 -0
  100. package/shiki-transformer.js +758 -0
  101. package/tsconfig-parser.js +127 -0
  102. package/tsdoc-metadata.json +11 -11
  103. package/twoslash-patterns.js +87 -0
  104. package/twoslash-transformer.js +316 -0
  105. package/type-reference-extractor.js +201 -0
  106. package/typescript-config.js +168 -0
  107. package/vfs-registry.js +121 -0
  108. package/0~llms-program.js +0 -344
  109. package/runtime/components/ApiLlmsPackageActions/index.module.js +0 -2
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
- "name": "rspress-plugin-api-extractor",
3
- "version": "0.1.2",
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
- },
32
- "files": [
33
- "!rspress-plugin-api-extractor.api.json",
34
- "!tsconfig.json",
35
- "!tsdoc.json",
36
- "0~llms-program.js",
37
- "LICENSE",
38
- "README.md",
39
- "index.d.ts",
40
- "index.js",
41
- "package.json",
42
- "runtime",
43
- "tsdoc-metadata.json"
44
- ],
45
- "dependencies": {
46
- "@effect/platform": "^0.96.1",
47
- "@effect/platform-node": "^0.106.0",
48
- "@effect/sql": "^0.51.1",
49
- "@effect/sql-sqlite-node": "^0.52.0",
50
- "@microsoft/api-extractor-model": "^7.33.8",
51
- "@shikijs/twoslash": "^4.1.0",
52
- "api-extractor-llms": "0.1.0",
53
- "clsx": "^2.1.1",
54
- "effect": "^3.21.2",
55
- "gray-matter": "^4.0.3",
56
- "hast-util-to-jsx-runtime": "^2.3.6",
57
- "image-size": "^2.0.2",
58
- "mdast-util-from-markdown": "^2.0.3",
59
- "mdast-util-to-hast": "^13.2.1",
60
- "prettier": "^3.8.3",
61
- "react-markdown": "^10.1.0",
62
- "semver-effect": "^0.2.1",
63
- "shiki": "^4.1.0",
64
- "type-registry-effect": "^0.2.3",
65
- "typescript": "^6.0.3"
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 };