rspress-plugin-api-extractor 0.3.8 → 0.4.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/README.md +1 -0
- package/build-stages.js +19 -5
- package/markdown/page-generators/class-page.js +24 -1
- package/package.json +1 -1
- package/synthetic-bases.js +74 -0
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ The plugin reads your `.api.json` model and writes one MDX page per public API i
|
|
|
49
49
|
- Generates API docs from `.api.json` models for classes, interfaces, functions, type aliases, enums, variables and namespaces.
|
|
50
50
|
- Type-checks code examples and adds Twoslash hover tooltips that show inferred types.
|
|
51
51
|
- Cross-links type references between pages, so a type named in a signature links to its own page.
|
|
52
|
+
- Inlines compiler-generated base declarations (the `Foo_base` pattern from Effect `Schema.Class`, `Data.TaggedError` and mixin factories) in a "Base Class" section on the owning class page instead of documenting them as orphan variables.
|
|
52
53
|
- Drives single-package sites, multi-package portals, RSPress multiVersion and i18n from one plugin.
|
|
53
54
|
- Handles multi-entry-point packages: it deduplicates re-exports and notes which entry points each item is available from.
|
|
54
55
|
- Writes per-package `llms*.txt` files and in-page actions for pointing an assistant at one package's docs.
|
package/build-stages.js
CHANGED
|
@@ -18,6 +18,7 @@ import { resolveEntryPoints } from "./multi-entry-resolver.js";
|
|
|
18
18
|
import { OpenGraphResolver } from "./og-resolver.js";
|
|
19
19
|
import { assertNoRouteCollisions } from "./route-collisions.js";
|
|
20
20
|
import { SnapshotService } from "./services/SnapshotService.js";
|
|
21
|
+
import { BASE_CLASS_ANCHOR, detectSyntheticBases } from "./synthetic-bases.js";
|
|
21
22
|
import path from "node:path";
|
|
22
23
|
import { Effect, Metric, Stream } from "effect";
|
|
23
24
|
import { FileSystem } from "@effect/platform";
|
|
@@ -67,13 +68,15 @@ function sanitizeId(displayName) {
|
|
|
67
68
|
function prepareWorkItems(input) {
|
|
68
69
|
const { apiPackage, categories, baseRoute } = input;
|
|
69
70
|
const resolvedItems = resolveEntryPoints(apiPackage);
|
|
71
|
+
const syntheticBases = detectSyntheticBases(resolvedItems.map((r) => r.item));
|
|
72
|
+
const docItems = syntheticBases.bases.size ? resolvedItems.filter((r) => !syntheticBases.bases.has(r.item)) : resolvedItems;
|
|
70
73
|
const resolvedLookup = /* @__PURE__ */ new Map();
|
|
71
|
-
for (const resolved of
|
|
74
|
+
for (const resolved of docItems) {
|
|
72
75
|
const key = `${resolved.item.displayName}::${resolved.item.kind}`;
|
|
73
76
|
resolvedLookup.set(key, resolved);
|
|
74
77
|
}
|
|
75
|
-
const items = ApiParser.categorizeApiItems(
|
|
76
|
-
const namespaceMembers = ApiParser.extractNamespaceMembers(
|
|
78
|
+
const items = ApiParser.categorizeApiItems(docItems, categories);
|
|
79
|
+
const namespaceMembers = ApiParser.extractNamespaceMembers(docItems);
|
|
77
80
|
const candidates = [];
|
|
78
81
|
for (const [categoryKey, categoryConfig] of Object.entries(categories)) for (const item of items[categoryKey] || []) candidates.push({
|
|
79
82
|
id: `${item.displayName}::${item.kind}`,
|
|
@@ -142,17 +145,28 @@ function prepareWorkItems(input) {
|
|
|
142
145
|
kinds.set(displayName, nsMember.item.kind);
|
|
143
146
|
}
|
|
144
147
|
}
|
|
148
|
+
for (const [baseItem, syntheticBase] of syntheticBases.bases) {
|
|
149
|
+
const baseName = baseItem.displayName;
|
|
150
|
+
if (routes.has(baseName)) continue;
|
|
151
|
+
const owner = syntheticBase.ownerClasses[0];
|
|
152
|
+
const ownerRoute = owner ? routes.get(owner.displayName) : void 0;
|
|
153
|
+
if (!ownerRoute) continue;
|
|
154
|
+
routes.set(baseName, `${ownerRoute}#${BASE_CLASS_ANCHOR}`);
|
|
155
|
+
kinds.set(baseName, baseItem.kind);
|
|
156
|
+
}
|
|
145
157
|
const workItems = [];
|
|
146
158
|
for (const [categoryKey, categoryConfig] of Object.entries(categories)) {
|
|
147
159
|
const categoryItems = items[categoryKey] || [];
|
|
148
160
|
for (const item of categoryItems) {
|
|
149
161
|
const lookupKey = `${item.displayName}::${item.kind}`;
|
|
150
162
|
const resolved = resolvedLookup.get(lookupKey);
|
|
163
|
+
const syntheticBase = syntheticBases.baseByOwner.get(item);
|
|
151
164
|
workItems.push({
|
|
152
165
|
item,
|
|
153
166
|
categoryKey,
|
|
154
167
|
categoryConfig,
|
|
155
|
-
...resolved?.availableFrom != null ? { availableFrom: resolved.availableFrom } : {}
|
|
168
|
+
...resolved?.availableFrom != null ? { availableFrom: resolved.availableFrom } : {},
|
|
169
|
+
...syntheticBase != null ? { syntheticBase } : {}
|
|
156
170
|
});
|
|
157
171
|
}
|
|
158
172
|
}
|
|
@@ -197,7 +211,7 @@ function generateSinglePage(workItem, ctx) {
|
|
|
197
211
|
switch (item.kind) {
|
|
198
212
|
case ApiItemKind.Class: {
|
|
199
213
|
const generator = new ClassPageGenerator();
|
|
200
|
-
page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom));
|
|
214
|
+
page = yield* Effect.promise(() => generator.generate(item, baseRoute, packageName, categoryConfig.singularName, apiScope, apiName, source, suppressExampleErrors, llmsPlugin, workItem.availableFrom, workItem.syntheticBase));
|
|
201
215
|
page = {
|
|
202
216
|
routePath: page.routePath.replace("/class/", `/${categoryConfig.folderName}/`),
|
|
203
217
|
content: page.content
|
|
@@ -62,7 +62,7 @@ var ClassPageGenerator = class {
|
|
|
62
62
|
*
|
|
63
63
|
* @param apiScope - API scope identifier for VFS lookup
|
|
64
64
|
*/
|
|
65
|
-
async generate(apiClass, baseRoute, packageName, singularName, apiScope, apiName, sourceConfig, suppressExampleErrors, llmsPlugin, availableFrom) {
|
|
65
|
+
async generate(apiClass, baseRoute, packageName, singularName, apiScope, apiName, sourceConfig, suppressExampleErrors, llmsPlugin, availableFrom, syntheticBase) {
|
|
66
66
|
const shouldSuppressErrors = suppressExampleErrors ?? true;
|
|
67
67
|
const name = apiClass.displayName;
|
|
68
68
|
const summary = ApiParser.getSummary(apiClass) || "No description available.";
|
|
@@ -95,6 +95,7 @@ var ClassPageGenerator = class {
|
|
|
95
95
|
const skeleton = this.generateClassSkeletonWithTwoslash(apiClass, packageName);
|
|
96
96
|
const displayCode = stripTwoslashDirectives(skeleton);
|
|
97
97
|
content += `<ApiSignature code={${JSON.stringify(displayCode)}} source={${JSON.stringify(skeleton)}} apiScope={${JSON.stringify(apiScope)}} />\n\n`;
|
|
98
|
+
content += this.generateBaseClassSection(apiClass, syntheticBase, packageName, apiScope);
|
|
98
99
|
const constructors = apiClass.members.filter((m) => m.kind === "Constructor");
|
|
99
100
|
if (constructors.length > 0) {
|
|
100
101
|
content += `## Constructors\n\n`;
|
|
@@ -214,6 +215,28 @@ var ClassPageGenerator = class {
|
|
|
214
215
|
};
|
|
215
216
|
}
|
|
216
217
|
/**
|
|
218
|
+
* Render the inline "Base Class" section for a synthetic base declaration
|
|
219
|
+
* (an unexported item referenced by the class's extends clause, e.g. the
|
|
220
|
+
* `Foo_base` variable TypeScript emits for `Schema.Class`-style patterns).
|
|
221
|
+
*
|
|
222
|
+
* The `## Base Class` heading slugs to `BASE_CLASS_ANCHOR` from
|
|
223
|
+
* `synthetic-bases.ts`, which is where the cross-link route for the base
|
|
224
|
+
* name points.
|
|
225
|
+
*/
|
|
226
|
+
generateBaseClassSection(apiClass, syntheticBase, packageName, apiScope) {
|
|
227
|
+
const baseDecl = syntheticBase;
|
|
228
|
+
if (!baseDecl?.excerpt?.text) return "";
|
|
229
|
+
let section = `## Base Class\n\n`;
|
|
230
|
+
section += `\`${apiClass.displayName}\` extends \`${baseDecl.displayName}\`, a compiler-generated declaration that is not exported from \`${packageName}\`.\n\n`;
|
|
231
|
+
const signature = this.typeFormatter.format(baseDecl.excerpt).trim();
|
|
232
|
+
let source = signature;
|
|
233
|
+
const apiPackage = apiClass.getAssociatedPackage?.();
|
|
234
|
+
if (apiPackage) source = prependHiddenImports(signature, new TypeReferenceExtractor(apiPackage, packageName).extractImportsForApiItem(baseDecl));
|
|
235
|
+
const displayCode = stripTwoslashDirectives(source);
|
|
236
|
+
section += `<ApiSignature code={${JSON.stringify(displayCode)}} source={${JSON.stringify(source)}} apiScope={${JSON.stringify(apiScope)}} />\n\n`;
|
|
237
|
+
return section;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
217
240
|
* Group class members by their type (static, instance, getters/setters)
|
|
218
241
|
*/
|
|
219
242
|
groupClassMembers(members) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ApiExportedMixin, ApiItemKind, ExcerptTokenKind } from "@microsoft/api-extractor-model";
|
|
2
|
+
|
|
3
|
+
//#region src/synthetic-bases.ts
|
|
4
|
+
/**
|
|
5
|
+
* Anchor id of the inline "Base Class" section rendered on the owner class
|
|
6
|
+
* page. Must match the slug RSPress derives from the `## Base Class` heading
|
|
7
|
+
* emitted by ClassPageGenerator.
|
|
8
|
+
*/
|
|
9
|
+
const BASE_CLASS_ANCHOR = "base-class";
|
|
10
|
+
const EMPTY_DETECTION = {
|
|
11
|
+
bases: /* @__PURE__ */ new Map(),
|
|
12
|
+
baseByOwner: /* @__PURE__ */ new Map()
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Strip the trailing meaning (`:class`, `:var`, `:function(1)`, ...) from a
|
|
16
|
+
* canonical reference string so the reference token in an extends clause
|
|
17
|
+
* (`example!~Person_base`) matches the declaration's canonical reference
|
|
18
|
+
* (`example!~Person_base:var`).
|
|
19
|
+
*/
|
|
20
|
+
function stripMeaning(canonicalRef) {
|
|
21
|
+
return canonicalRef.replace(/:[a-z]+(\(\d+\))?$/i, "");
|
|
22
|
+
}
|
|
23
|
+
/** True when the item carries ApiExportedMixin and is NOT exported from its entry point. */
|
|
24
|
+
function isUnexported(item) {
|
|
25
|
+
return ApiExportedMixin.isBaseClassOf(item) && !item.isExported;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Detect synthetic base declarations among top-level API items.
|
|
29
|
+
*
|
|
30
|
+
* An item qualifies when it is unexported (hoisted into the model only because
|
|
31
|
+
* something references it) AND at least one class's extends clause references
|
|
32
|
+
* its canonical symbol. Unexported items with no class referencing them
|
|
33
|
+
* (genuine forgotten exports) are left alone, as are extends references whose
|
|
34
|
+
* target is absent from the model.
|
|
35
|
+
*/
|
|
36
|
+
function detectSyntheticBases(items) {
|
|
37
|
+
const unexportedByRef = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const item of items) {
|
|
39
|
+
if (!isUnexported(item)) continue;
|
|
40
|
+
const ref = item.canonicalReference?.toString();
|
|
41
|
+
if (ref) unexportedByRef.set(stripMeaning(ref), item);
|
|
42
|
+
}
|
|
43
|
+
if (unexportedByRef.size === 0) return EMPTY_DETECTION;
|
|
44
|
+
const owners = /* @__PURE__ */ new Map();
|
|
45
|
+
const baseByOwner = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const item of items) {
|
|
47
|
+
if (item.kind !== ApiItemKind.Class) continue;
|
|
48
|
+
const apiClass = item;
|
|
49
|
+
const extendsType = apiClass.extendsType;
|
|
50
|
+
if (!extendsType) continue;
|
|
51
|
+
for (const token of extendsType.excerpt.spannedTokens) {
|
|
52
|
+
if (token.kind !== ExcerptTokenKind.Reference || !token.canonicalReference) continue;
|
|
53
|
+
const base = unexportedByRef.get(stripMeaning(token.canonicalReference.toString()));
|
|
54
|
+
if (!base || base === item || baseByOwner.has(item)) continue;
|
|
55
|
+
baseByOwner.set(item, base);
|
|
56
|
+
const ownerList = owners.get(base);
|
|
57
|
+
if (ownerList) ownerList.push(apiClass);
|
|
58
|
+
else owners.set(base, [apiClass]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (owners.size === 0) return EMPTY_DETECTION;
|
|
62
|
+
const bases = /* @__PURE__ */ new Map();
|
|
63
|
+
for (const [baseItem, ownerClasses] of owners) bases.set(baseItem, {
|
|
64
|
+
baseItem,
|
|
65
|
+
ownerClasses
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
bases,
|
|
69
|
+
baseByOwner
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
export { BASE_CLASS_ANCHOR, detectSyntheticBases };
|