rspress-plugin-api-extractor 0.1.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.
@@ -0,0 +1,344 @@
1
+ import node_path from "node:path";
2
+ import { FileSystem } from "@effect/platform";
3
+ import { Effect } from "effect";
4
+ const LLMS_TXT_LINE_RE = /^-\s+\[([^\]]+)\]\(([^)]+)\)(?::\s*(.+))?$/;
5
+ function parseLlmsTxtLine(line) {
6
+ const trimmed = line.trim();
7
+ if ("" === trimmed) return null;
8
+ const match = LLMS_TXT_LINE_RE.exec(trimmed);
9
+ if (!match) return null;
10
+ const title = match[1];
11
+ const url = match[2];
12
+ const rawDescription = match[3];
13
+ return {
14
+ title,
15
+ url,
16
+ description: rawDescription ? rawDescription.trim() : void 0
17
+ };
18
+ }
19
+ function filterLlmsTxt(content, apiRoutes, pointers) {
20
+ const lines = content.split("\n");
21
+ const filtered = [];
22
+ for (const line of lines){
23
+ const entry = parseLlmsTxtLine(line);
24
+ if (!(entry && apiRoutes.has(entry.url))) filtered.push(line);
25
+ }
26
+ let result = filtered.join("\n");
27
+ if (pointers.length > 0) {
28
+ result += "\n\n";
29
+ for (const pointer of pointers)result += `- For ${pointer.name} API docs, see [${pointer.name} llms.txt](${pointer.llmsTxtUrl})\n`;
30
+ }
31
+ return result;
32
+ }
33
+ function generateStructuredLlmsTxt(content, apiRoutes, packages) {
34
+ const lines = content.split("\n");
35
+ let title = "";
36
+ for (const line of lines)if (line.startsWith("# ")) {
37
+ title = line;
38
+ break;
39
+ }
40
+ const allEntries = [];
41
+ for (const line of lines){
42
+ const entry = parseLlmsTxtLine(line);
43
+ if (entry && !apiRoutes.has(entry.url)) allEntries.push(entry);
44
+ }
45
+ const packageEntries = new Map();
46
+ const others = [];
47
+ for (const entry of allEntries){
48
+ let matched = false;
49
+ for (const pkg of packages){
50
+ const base = pkg.packageRoute.endsWith("/") ? pkg.packageRoute : `${pkg.packageRoute}/`;
51
+ if (entry.url.startsWith(base) || entry.url === pkg.packageRoute) {
52
+ const existing = packageEntries.get(pkg.packageName) ?? [];
53
+ existing.push(entry);
54
+ packageEntries.set(pkg.packageName, existing);
55
+ matched = true;
56
+ break;
57
+ }
58
+ }
59
+ if (!matched) others.push(entry);
60
+ }
61
+ const output = [];
62
+ if (title) {
63
+ output.push(title);
64
+ output.push("");
65
+ }
66
+ if (others.length > 0) {
67
+ output.push("## Others");
68
+ output.push("");
69
+ for (const entry of others)output.push(formatEntry(entry));
70
+ output.push("");
71
+ }
72
+ const packagesWithEntries = packages;
73
+ if (packagesWithEntries.length > 0) {
74
+ output.push("## Packages");
75
+ output.push("");
76
+ for (const pkg of packagesWithEntries){
77
+ const versionSuffix = pkg.version ? ` ${pkg.version}` : "";
78
+ output.push(`### ${pkg.name}${versionSuffix}`);
79
+ output.push("");
80
+ if (pkg.description) {
81
+ output.push(pkg.description);
82
+ output.push("");
83
+ }
84
+ const entries = packageEntries.get(pkg.packageName) ?? [];
85
+ for (const entry of entries)output.push(formatEntry(entry));
86
+ output.push(`- [API Reference](${pkg.llmsApiTxtUrl})`);
87
+ output.push("");
88
+ }
89
+ }
90
+ return output.join("\n");
91
+ }
92
+ function parseSections(content) {
93
+ if ("" === content.trim()) return [];
94
+ const sections = [];
95
+ const frontmatterPattern = /^---\nurl:\s*(.+)\n---$/gm;
96
+ let match = frontmatterPattern.exec(content);
97
+ const boundaries = [];
98
+ while(null !== match){
99
+ boundaries.push({
100
+ url: match[1].trim(),
101
+ start: match.index,
102
+ fmEnd: match.index + match[0].length
103
+ });
104
+ match = frontmatterPattern.exec(content);
105
+ }
106
+ for(let i = 0; i < boundaries.length; i++){
107
+ const boundary = boundaries[i];
108
+ const nextStart = i + 1 < boundaries.length ? boundaries[i + 1].start : content.length;
109
+ const sectionContent = content.slice(boundary.start, nextStart);
110
+ sections.push({
111
+ url: boundary.url,
112
+ raw: sectionContent.trimEnd()
113
+ });
114
+ }
115
+ return sections;
116
+ }
117
+ function filterLlmsFullTxt(content, apiRoutes) {
118
+ if ("" === content.trim()) return "";
119
+ const sections = parseSections(content);
120
+ const kept = sections.filter((section)=>!apiRoutes.has(section.url));
121
+ if (0 === kept.length) return "";
122
+ return kept.map((section)=>section.raw).join("\n\n\n");
123
+ }
124
+ function formatEntry(entry) {
125
+ if (entry.description) return `- [${entry.title}](${entry.url}): ${entry.description}`;
126
+ return `- [${entry.title}](${entry.url})`;
127
+ }
128
+ function generatePackageLlmsTxt(input) {
129
+ const parts = [
130
+ `# ${input.name}`,
131
+ "",
132
+ `> API documentation for the ${input.packageName} package`
133
+ ];
134
+ if (input.guidePages.length > 0) {
135
+ parts.push("");
136
+ parts.push("## Guides");
137
+ parts.push("");
138
+ for (const page of input.guidePages)parts.push(formatEntry(page));
139
+ }
140
+ if (input.apiPages.length > 0) {
141
+ parts.push("");
142
+ parts.push("## API Reference");
143
+ parts.push("");
144
+ for (const page of input.apiPages)parts.push(formatEntry(page));
145
+ }
146
+ parts.push("");
147
+ return parts.join("\n");
148
+ }
149
+ function generatePackageLlmsFullTxt(pages) {
150
+ if (0 === pages.length) return "";
151
+ const sections = [];
152
+ for (const page of pages)sections.push(`---\nurl: ${page.url}\n---\n\n${page.content}`);
153
+ return sections.join("\n\n\n");
154
+ }
155
+ function buildApiRoutes(buildResults) {
156
+ const apiRoutes = new Set();
157
+ for (const result of buildResults){
158
+ const base = result.baseRoute.endsWith("/") ? result.baseRoute : `${result.baseRoute}/`;
159
+ for (const relPath of result.generatedFiles){
160
+ if (!relPath.endsWith(".mdx")) continue;
161
+ const mdPath = relPath.replace(/\.mdx$/, ".md");
162
+ const routeUrl = `${base}${mdPath}`;
163
+ apiRoutes.add(routeUrl);
164
+ }
165
+ }
166
+ return apiRoutes;
167
+ }
168
+ function discoverPrefixes(buildResults) {
169
+ const prefixes = new Set();
170
+ prefixes.add("");
171
+ for (const result of buildResults){
172
+ const segments = result.baseRoute.split("/").filter(Boolean);
173
+ if (segments.length > 1) {
174
+ const prefixSegments = segments.slice(0, -1);
175
+ prefixes.add(prefixSegments.join("/"));
176
+ }
177
+ }
178
+ return prefixes;
179
+ }
180
+ function buildPackagePointers(buildResults, prefix, packageRoutes) {
181
+ const pointers = [];
182
+ for (const result of buildResults){
183
+ if ("" !== prefix && !result.baseRoute.startsWith(`/${prefix}/`)) continue;
184
+ const displayName = result.apiName ?? result.packageName;
185
+ const pkgRoute = packageRoutes.get(result.packageName) ?? result.baseRoute;
186
+ const base = pkgRoute.endsWith("/") ? pkgRoute : `${pkgRoute}/`;
187
+ pointers.push({
188
+ name: displayName,
189
+ llmsTxtUrl: `${base}llms.txt`
190
+ });
191
+ }
192
+ return pointers;
193
+ }
194
+ function collectApiEntries(globalLlmsTxtContent, result) {
195
+ const base = result.baseRoute.endsWith("/") ? result.baseRoute : `${result.baseRoute}/`;
196
+ const entries = [];
197
+ for (const line of globalLlmsTxtContent.split("\n")){
198
+ const entry = parseLlmsTxtLine(line);
199
+ if (entry) {
200
+ if (entry.url.startsWith(base)) entries.push(entry);
201
+ }
202
+ }
203
+ return entries;
204
+ }
205
+ function collectGuideEntries(globalLlmsTxtContent, apiRoutes, packageRoute) {
206
+ const base = packageRoute.endsWith("/") ? packageRoute : `${packageRoute}/`;
207
+ const entries = [];
208
+ for (const line of globalLlmsTxtContent.split("\n")){
209
+ const entry = parseLlmsTxtLine(line);
210
+ if (entry) {
211
+ if ((entry.url === packageRoute || entry.url.startsWith(base)) && !apiRoutes.has(entry.url)) entries.push(entry);
212
+ }
213
+ }
214
+ return entries;
215
+ }
216
+ function extractSections(globalLlmsFullContent, urlPredicate) {
217
+ const pages = [];
218
+ if (!globalLlmsFullContent) return pages;
219
+ const frontmatterPattern = /^---\nurl:\s*(.+)\n---$/gm;
220
+ let match = frontmatterPattern.exec(globalLlmsFullContent);
221
+ const boundaries = [];
222
+ while(null !== match){
223
+ boundaries.push({
224
+ url: match[1].trim(),
225
+ start: match.index,
226
+ fmEnd: match.index + match[0].length
227
+ });
228
+ match = frontmatterPattern.exec(globalLlmsFullContent);
229
+ }
230
+ for(let i = 0; i < boundaries.length; i++){
231
+ const boundary = boundaries[i];
232
+ if (!urlPredicate(boundary.url)) continue;
233
+ const nextStart = i + 1 < boundaries.length ? boundaries[i + 1].start : globalLlmsFullContent.length;
234
+ const content = globalLlmsFullContent.slice(boundary.fmEnd, nextStart).trim();
235
+ pages.push({
236
+ url: boundary.url,
237
+ content
238
+ });
239
+ }
240
+ return pages;
241
+ }
242
+ function collectApiPageContent(globalLlmsFullContent, result) {
243
+ const base = result.baseRoute.endsWith("/") ? result.baseRoute : `${result.baseRoute}/`;
244
+ return extractSections(globalLlmsFullContent, (url)=>url.startsWith(base));
245
+ }
246
+ function processLlmsFiles(input) {
247
+ return Effect.gen(function*() {
248
+ const fs = yield* FileSystem.FileSystem;
249
+ const { outDir, buildResults, llmsPlugin, packageRoutes } = input;
250
+ if (0 === buildResults.length) return;
251
+ const apiRoutes = buildApiRoutes(buildResults);
252
+ yield* Effect.logDebug(`Built ${apiRoutes.size} API routes for LLMs filtering`);
253
+ if (0 === apiRoutes.size) return;
254
+ const prefixes = discoverPrefixes(buildResults);
255
+ yield* Effect.forEach([
256
+ ...prefixes
257
+ ], (prefix)=>processPrefix(fs, outDir, prefix, buildResults, apiRoutes, llmsPlugin, packageRoutes), {
258
+ concurrency: "unbounded"
259
+ });
260
+ });
261
+ }
262
+ function processPrefix(fs, outDir, prefix, buildResults, apiRoutes, llmsPlugin, packageRoutes) {
263
+ return Effect.gen(function*() {
264
+ const prefixDir = prefix ? node_path.join(outDir, prefix) : outDir;
265
+ const llmsTxtPath = node_path.join(prefixDir, "llms.txt");
266
+ const llmsTxtExists = yield* fs.exists(llmsTxtPath).pipe(Effect.orElseSucceed(()=>false));
267
+ if (!llmsTxtExists) return;
268
+ const llmsTxtContent = yield* fs.readFileString(llmsTxtPath).pipe(Effect.orDie);
269
+ const llmsFullTxtPath = node_path.join(prefixDir, "llms-full.txt");
270
+ const llmsFullTxtExists = yield* fs.exists(llmsFullTxtPath).pipe(Effect.orElseSucceed(()=>false));
271
+ const llmsFullTxtContent = llmsFullTxtExists ? yield* fs.readFileString(llmsFullTxtPath).pipe(Effect.orDie) : "";
272
+ if (llmsPlugin.scopes) {
273
+ const packageScopes = buildResults.map((r)=>{
274
+ const pkgRoute = packageRoutes.get(r.packageName) ?? r.baseRoute;
275
+ return {
276
+ name: r.apiName ?? r.packageName,
277
+ packageName: r.packageName,
278
+ version: r.packageVersion,
279
+ description: r.packageDescription,
280
+ packageRoute: pkgRoute,
281
+ llmsApiTxtUrl: `${pkgRoute.endsWith("/") ? pkgRoute : `${pkgRoute}/`}llms-api.txt`
282
+ };
283
+ });
284
+ const structuredLlmsTxt = generateStructuredLlmsTxt(llmsTxtContent, apiRoutes, packageScopes);
285
+ yield* fs.writeFileString(llmsTxtPath, structuredLlmsTxt).pipe(Effect.orDie);
286
+ } else {
287
+ const pointers = buildPackagePointers(buildResults, prefix, packageRoutes);
288
+ const filteredLlmsTxt = filterLlmsTxt(llmsTxtContent, apiRoutes, pointers);
289
+ yield* fs.writeFileString(llmsTxtPath, filteredLlmsTxt).pipe(Effect.orDie);
290
+ }
291
+ if (llmsFullTxtContent) {
292
+ const filteredLlmsFullTxt = filterLlmsFullTxt(llmsFullTxtContent, apiRoutes);
293
+ yield* fs.writeFileString(llmsFullTxtPath, filteredLlmsFullTxt).pipe(Effect.orDie);
294
+ }
295
+ if (llmsPlugin.scopes) {
296
+ const prefixResults = "" === prefix ? [
297
+ ...buildResults
298
+ ] : buildResults.filter((r)=>r.baseRoute.startsWith(`/${prefix}/`));
299
+ yield* Effect.forEach(prefixResults, (result)=>generatePerPackageFiles(fs, outDir, result, llmsTxtContent, llmsFullTxtContent, apiRoutes, llmsPlugin, packageRoutes.get(result.packageName) ?? result.baseRoute), {
300
+ concurrency: "unbounded"
301
+ });
302
+ }
303
+ });
304
+ }
305
+ function generatePerPackageFiles(fs, outDir, result, globalLlmsTxtContent, globalLlmsFullContent, apiRoutes, llmsPlugin, packageRoute) {
306
+ return Effect.gen(function*() {
307
+ const pkgRouteSegment = packageRoute.replace(/^\//, "");
308
+ const packageLlmsDir = pkgRouteSegment ? node_path.join(outDir, pkgRouteSegment) : outDir;
309
+ yield* fs.makeDirectory(packageLlmsDir, {
310
+ recursive: true
311
+ }).pipe(Effect.orDie);
312
+ const displayName = result.apiName ?? result.packageName;
313
+ const apiEntries = collectApiEntries(globalLlmsTxtContent, result);
314
+ const guideEntries = collectGuideEntries(globalLlmsTxtContent, apiRoutes, packageRoute);
315
+ const packageLlmsTxt = generatePackageLlmsTxt({
316
+ name: displayName,
317
+ packageName: result.packageName,
318
+ guidePages: guideEntries,
319
+ apiPages: apiEntries
320
+ });
321
+ yield* fs.writeFileString(node_path.join(packageLlmsDir, "llms.txt"), packageLlmsTxt).pipe(Effect.orDie);
322
+ const apiPageContent = collectApiPageContent(globalLlmsFullContent, result);
323
+ const guideRouteUrls = new Set(guideEntries.map((e)=>e.url));
324
+ const guidePageContent = globalLlmsFullContent ? extractSections(globalLlmsFullContent, (url)=>guideRouteUrls.has(url)) : [];
325
+ const fullPageContent = [
326
+ ...guidePageContent,
327
+ ...apiPageContent
328
+ ];
329
+ if (fullPageContent.length > 0) {
330
+ const packageLlmsFullTxt = generatePackageLlmsFullTxt(fullPageContent);
331
+ yield* fs.writeFileString(node_path.join(packageLlmsDir, "llms-full.txt"), packageLlmsFullTxt).pipe(Effect.orDie);
332
+ }
333
+ if (llmsPlugin.apiTxt && apiPageContent.length > 0) {
334
+ const apiTxtContent = generatePackageLlmsFullTxt(apiPageContent);
335
+ yield* fs.writeFileString(node_path.join(packageLlmsDir, "llms-api.txt"), apiTxtContent).pipe(Effect.orDie);
336
+ }
337
+ if (guidePageContent.length > 0) {
338
+ const docsTxtContent = generatePackageLlmsFullTxt(guidePageContent);
339
+ yield* fs.writeFileString(node_path.join(packageLlmsDir, "llms-docs.txt"), docsTxtContent).pipe(Effect.orDie);
340
+ }
341
+ yield* Effect.logDebug(`Generated LLMs files for ${displayName} in ${packageLlmsDir}`);
342
+ });
343
+ }
344
+ export { processLlmsFiles };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C. Spencer Beggs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # rspress-plugin-api-extractor
2
+
3
+ [![npm](https://img.shields.io/npm/v/rspress-plugin-api-extractor?label=npm&color=cb3837)](https://www.npmjs.com/package/rspress-plugin-api-extractor)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-4caf50.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js %3E%3D24.1.0](https://img.shields.io/badge/Node.js-%3E%3D24.1.0-5fa04e.svg)](https://nodejs.org/)
6
+ [![TypeScript 6.0](https://img.shields.io/badge/TypeScript-6.0-3178c6.svg)](https://www.typescriptlang.org/)
7
+
8
+ An [RSPress](https://rspress.dev/) 2.0 plugin that generates interactive API documentation from [Microsoft API Extractor](https://api-extractor.com/) models. Point it at your `.api.json` files and you get a documentation site: syntax-highlighted signatures, Twoslash hover tooltips, type references that cross-link between pages and copy-paste code examples.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install rspress-plugin-api-extractor
14
+ # or
15
+ pnpm add rspress-plugin-api-extractor
16
+ ```
17
+
18
+ The plugin is a peer of `@rspress/core`, `react` and `react-dom`; install those too if your site does not already have them.
19
+
20
+ ## Quick start
21
+
22
+ ```ts
23
+ // rspress.config.ts
24
+ import { defineConfig } from "@rspress/core";
25
+ import { ApiExtractorPlugin } from "rspress-plugin-api-extractor";
26
+
27
+ export default defineConfig({
28
+ root: "docs",
29
+ plugins: [
30
+ ApiExtractorPlugin({
31
+ api: {
32
+ packageName: "my-library",
33
+ model: "./api/my-library.api.json",
34
+ },
35
+ }),
36
+ ],
37
+ });
38
+ ```
39
+
40
+ ```bash
41
+ npx rspress dev
42
+ # Generates one MDX page per public API item and serves them at http://localhost:3000
43
+ ```
44
+
45
+ The plugin reads your `.api.json` model and writes one MDX page per public API item under your docs root, grouped into category folders (classes, interfaces, functions, type aliases, enums, variables and namespaces) with navigation metadata. To produce the model, pair it with [@savvy-web/rslib-builder](https://github.com/savvy-web/rslib-builder), which emits the `.api.json` as part of your TypeScript build.
46
+
47
+ ## Features
48
+
49
+ - Generates API docs from `.api.json` models for classes, interfaces, functions, type aliases, enums, variables and namespaces.
50
+ - Type-checks code examples and adds Twoslash hover tooltips that show inferred types.
51
+ - Cross-links type references between pages, so a type named in a signature links to its own page.
52
+ - Drives single-package sites, multi-package portals, RSPress multiVersion and i18n from one plugin.
53
+ - Handles multi-entry-point packages: it deduplicates re-exports and notes which entry points each item is available from.
54
+ - Writes per-package `llms*.txt` files and in-page actions for pointing an assistant at one package's docs.
55
+
56
+ ## Documentation
57
+
58
+ - [Getting started](https://github.com/spencerbeggs/rspress-plugin-api-extractor/blob/main/docs/01-getting-started.md) — Install, minimal config, first build.
59
+ - [Configuration](https://github.com/spencerbeggs/rspress-plugin-api-extractor/blob/main/docs/02-configuration.md) — Full plugin-options reference.
60
+ - [Config helpers](https://github.com/spencerbeggs/rspress-plugin-api-extractor/blob/main/docs/03-config-helpers.md) — `fromFolder` and `fromModelsDir` for discovering config from package folders.
61
+ - [Single package](https://github.com/spencerbeggs/rspress-plugin-api-extractor/blob/main/docs/04-single-package.md) — The single-API recipe.
62
+ - [Multi-package](https://github.com/spencerbeggs/rspress-plugin-api-extractor/blob/main/docs/05-multi-package.md) — The multi-API portal recipe.
63
+ - [Versioned](https://github.com/spencerbeggs/rspress-plugin-api-extractor/blob/main/docs/06-versioned.md) — Documenting major versions side by side.
64
+ - [i18n](https://github.com/spencerbeggs/rspress-plugin-api-extractor/blob/main/docs/07-i18n.md) — Internationalized documentation.
65
+ - [Multi-entry points](https://github.com/spencerbeggs/rspress-plugin-api-extractor/blob/main/docs/08-multi-entry-points.md) — Deduplication, "Available from" and route collisions.
66
+ - [LLMs](https://github.com/spencerbeggs/rspress-plugin-api-extractor/blob/main/docs/09-llms.md) — Per-package `llms*.txt` files and assistant actions.
67
+ - [Runtime components](https://github.com/spencerbeggs/rspress-plugin-api-extractor/blob/main/docs/10-runtime-components.md) — The runtime components and live `with-api` code blocks.
68
+ - [Troubleshooting](https://github.com/spencerbeggs/rspress-plugin-api-extractor/blob/main/docs/11-troubleshooting.md) — Route collisions, forgotten exports, Twoslash errors and stale caches.
69
+
70
+ ## License
71
+
72
+ [MIT](LICENSE)