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 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 resolvedItems) {
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(resolvedItems, categories);
76
- const namespaceMembers = ApiParser.extractNamespaceMembers(resolvedItems);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rspress-plugin-api-extractor",
3
- "version": "0.3.8",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "description": "RSPress plugin for generating API documentation from TypeScript API Extractor models",
6
6
  "keywords": [
@@ -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 };