rspress-plugin-api-extractor 0.10.0 → 0.11.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.
@@ -1,7 +1,7 @@
1
1
  import { TypeReferenceExtractor } from "../../type-reference-extractor.js";
2
- import { escapeMdxGenerics, formatExampleCode, generateAvailableFrom, generateFrontmatter, prepareExampleCode, prependHiddenImports, sanitizeId, stripTwoslashDirectives } from "../helpers.js";
2
+ import { escapeMdxGenerics, formatExampleCode, generateAvailableFrom, generateFrontmatter, prepareExampleCode, prependHiddenImports, stripTwoslashDirectives } from "../helpers.js";
3
3
  import { linkProse } from "../prose-linker.js";
4
- import { ApiItems, Signature, Tsdoc } from "@tsdoctor/model";
4
+ import { ApiItems, Routes, Signature, Tsdoc } from "@tsdoctor/model";
5
5
 
6
6
  //#region src/markdown/page-generators/class-page.ts
7
7
  /**
@@ -60,7 +60,7 @@ var ClassPageGenerator = class {
60
60
  *
61
61
  * @param apiScope - API scope identifier for VFS lookup
62
62
  */
63
- async generate(apiClass, baseRoute, packageName, singularName, apiScope, apiName, sourceConfig, suppressExampleErrors, llmsPlugin, availableFrom, syntheticBase) {
63
+ async generate(apiClass, baseRoute, packageName, singularName, apiScope, apiName, sourceConfig, suppressExampleErrors, llmsPlugin, availableFrom, syntheticBase, memberAnchors) {
64
64
  const shouldSuppressErrors = suppressExampleErrors ?? true;
65
65
  const name = apiClass.displayName;
66
66
  const summary = Tsdoc.summary(apiClass) || "No description available.";
@@ -99,7 +99,7 @@ var ClassPageGenerator = class {
99
99
  content += `## Constructors\n\n`;
100
100
  for (const ctor of constructors) {
101
101
  const ctorSummary = Tsdoc.summary(ctor);
102
- const ctorId = sanitizeId("constructor");
102
+ const ctorId = Routes.memberAnchor("constructor");
103
103
  const ctorItem = ctor;
104
104
  const params = Tsdoc.params(ctor);
105
105
  const hasParameters = params.length > 0;
@@ -131,15 +131,14 @@ var ClassPageGenerator = class {
131
131
  const isGetter = m.displayName.startsWith("get ") || m.displayName.startsWith("set ");
132
132
  return !isStatic && !isGetter;
133
133
  });
134
- const prefixMap = this.detectMemberConflicts(staticProperties, grouped.staticMethods, instanceProperties, grouped.instanceMethods, grouped.getters);
134
+ const anchors = memberAnchors ?? ApiItems.memberAnchors(apiClass);
135
+ const anchorFor = (member) => anchors.get(member.canonicalReference?.toString() ?? member.displayName) ?? Routes.memberAnchor(member.displayName);
135
136
  const renderProperties = async (title, propList) => {
136
137
  if (propList.length === 0) return;
137
138
  content += `## ${title}\n\n`;
138
139
  for (const prop of propList) {
139
140
  const propSummary = Tsdoc.summary(prop);
140
- const baseName = sanitizeId(prop.displayName);
141
- const prefix = prefixMap.get(baseName) || "";
142
- const propId = sanitizeId(prop.displayName, prefix);
141
+ const propId = anchorFor(prop);
143
142
  const propItem = prop;
144
143
  if (propItem.excerpt?.text) {
145
144
  const memberSignature = Signature.format(propItem.excerpt).trim();
@@ -154,9 +153,7 @@ var ClassPageGenerator = class {
154
153
  content += `## ${title}\n\n`;
155
154
  for (const method of methodList) {
156
155
  const methodSummary = Tsdoc.summary(method);
157
- const baseName = sanitizeId(method.displayName);
158
- const prefix = prefixMap.get(baseName) || "";
159
- const methodId = sanitizeId(method.displayName, prefix);
156
+ const methodId = anchorFor(method);
160
157
  const methodItem = method;
161
158
  const params = Tsdoc.params(method);
162
159
  const hasParameters = params.length > 0;
@@ -258,26 +255,6 @@ var ClassPageGenerator = class {
258
255
  };
259
256
  }
260
257
  /**
261
- * Detect naming conflicts between class members and return prefixes to apply.
262
- * Returns a Map of sanitized name to prefix (empty string if no conflict).
263
- */
264
- detectMemberConflicts(staticProps, staticMethods, instanceProps, instanceMethods, getters) {
265
- const prefixMap = /* @__PURE__ */ new Map();
266
- const staticPropNames = new Set(staticProps.map((p) => sanitizeId(p.displayName)));
267
- const staticMethodNames = new Set(staticMethods.map((m) => sanitizeId(m.displayName)));
268
- const instancePropNames = new Set(instanceProps.map((p) => sanitizeId(p.displayName)));
269
- const instanceMethodNames = new Set(instanceMethods.map((m) => sanitizeId(m.displayName)));
270
- const getterNames = new Set(getters.map((g) => sanitizeId(g.displayName)));
271
- for (const name of staticPropNames) if (staticMethodNames.has(name) || instancePropNames.has(name) || instanceMethodNames.has(name) || getterNames.has(name)) prefixMap.set(name, "static-property");
272
- for (const name of staticMethodNames) if (instanceMethodNames.has(name) || instancePropNames.has(name) || getterNames.has(name)) prefixMap.set(name, "static");
273
- else if (staticPropNames.has(name)) prefixMap.set(name, "");
274
- for (const name of instancePropNames) if (instanceMethodNames.has(name) || getterNames.has(name)) prefixMap.set(name, "property");
275
- else if (staticMethodNames.has(name)) prefixMap.set(name, "");
276
- for (const name of instanceMethodNames) if (!prefixMap.has(name)) prefixMap.set(name, "");
277
- for (const name of getterNames) if (!prefixMap.has(name)) prefixMap.set(name, "");
278
- return prefixMap;
279
- }
280
- /**
281
258
  * Generate a class member signature with full class context
282
259
  * Includes hidden imports with cut directive for external type resolution
283
260
  * Uses the simplified approach: 3 lines (class opening, member, closing)
@@ -1,4 +1,4 @@
1
- import { escapeYamlString } from "../helpers.js";
1
+ import { emitFrontmatterBlock } from "../../frontmatter.js";
2
2
 
3
3
  //#region src/markdown/page-generators/index-pages.ts
4
4
  /**
@@ -9,13 +9,11 @@ var MainIndexPageGenerator = class {
9
9
  * Generate the main API index page
10
10
  */
11
11
  generate(packageName, baseRoute, _categoryCounts) {
12
- const content = `---
13
- title: API Reference
14
- description: Auto-generated API documentation for ${escapeYamlString(packageName)}
15
- overview: true
16
- ---
17
-
18
- `;
12
+ const content = emitFrontmatterBlock({
13
+ title: "API Reference",
14
+ description: `Auto-generated API documentation for ${packageName}`,
15
+ overview: true
16
+ });
19
17
  return {
20
18
  routePath: `${baseRoute}/index`,
21
19
  content
@@ -1,7 +1,7 @@
1
1
  import { TypeReferenceExtractor } from "../../type-reference-extractor.js";
2
- import { escapeMdxGenerics, formatExampleCode, generateAvailableFrom, generateFrontmatter, prepareExampleCode, prependHiddenImports, sanitizeId, stripTwoslashDirectives } from "../helpers.js";
2
+ import { escapeMdxGenerics, formatExampleCode, generateAvailableFrom, generateFrontmatter, prepareExampleCode, prependHiddenImports, stripTwoslashDirectives } from "../helpers.js";
3
3
  import { linkProse } from "../prose-linker.js";
4
- import { ApiItems, Signature, Tsdoc } from "@tsdoctor/model";
4
+ import { ApiItems, Routes, Signature, Tsdoc } from "@tsdoctor/model";
5
5
 
6
6
  //#region src/markdown/page-generators/interface-page.ts
7
7
  /**
@@ -98,7 +98,7 @@ var InterfacePageGenerator = class {
98
98
  content += `## Call Signatures\n\n`;
99
99
  for (const callSig of callSignatures) {
100
100
  const callSigSummary = Tsdoc.summary(callSig);
101
- const callSigId = sanitizeId("call-signature");
101
+ const callSigId = Routes.memberAnchor("call-signature");
102
102
  const callSigItem = callSig;
103
103
  if (callSigItem.excerpt?.text) {
104
104
  const memberSignature = Signature.format(callSigItem.excerpt).trim();
@@ -113,7 +113,7 @@ var InterfacePageGenerator = class {
113
113
  content += `## Construct Signatures\n\n`;
114
114
  for (const constructSig of constructSignatures) {
115
115
  const constructSigSummary = Tsdoc.summary(constructSig);
116
- const constructSigId = sanitizeId("construct-signature");
116
+ const constructSigId = Routes.memberAnchor("construct-signature");
117
117
  const constructSigItem = constructSig;
118
118
  if (constructSigItem.excerpt?.text) {
119
119
  const memberSignature = Signature.format(constructSigItem.excerpt).trim();
@@ -128,7 +128,7 @@ var InterfacePageGenerator = class {
128
128
  content += `## Index Signature\n\n`;
129
129
  for (const indexSig of indexSignatures) {
130
130
  const indexSigSummary = Tsdoc.summary(indexSig);
131
- const indexSigId = sanitizeId("index-signature");
131
+ const indexSigId = Routes.memberAnchor("index-signature");
132
132
  const indexSigItem = indexSig;
133
133
  if (indexSigItem.excerpt?.text) {
134
134
  const memberSignature = Signature.format(indexSigItem.excerpt).trim();
@@ -143,7 +143,7 @@ var InterfacePageGenerator = class {
143
143
  content += `## Properties\n\n`;
144
144
  for (const prop of properties) {
145
145
  const propSummary = Tsdoc.summary(prop);
146
- const propId = sanitizeId(prop.displayName);
146
+ const propId = Routes.memberAnchor(prop.displayName);
147
147
  const propItem = prop;
148
148
  if (propItem.excerpt?.text) {
149
149
  const memberSignature = Signature.format(propItem.excerpt).trim();
@@ -158,7 +158,7 @@ var InterfacePageGenerator = class {
158
158
  content += `## Methods\n\n`;
159
159
  for (const method of methods) {
160
160
  const methodSummary = Tsdoc.summary(method);
161
- const methodId = sanitizeId(method.displayName);
161
+ const methodId = Routes.memberAnchor(method.displayName);
162
162
  const methodItem = method;
163
163
  if (methodItem.excerpt?.text) {
164
164
  const memberSignature = Signature.format(methodItem.excerpt).trim();
@@ -1,13 +1,7 @@
1
1
  import { PluginEvent } from "../observability/events.js";
2
+ import { emitSync, syncBuildId } from "../observability/sync-emitter.js";
2
3
 
3
4
  //#region src/markdown/shiki-utils.ts
4
- /** Module-level emitter injected by plugin.ts at startup. */
5
- let emitEvent = () => {};
6
- let currentBuildId = "";
7
- function setShikiUtilsEventEmitter(fn, buildId = "") {
8
- emitEvent = fn;
9
- currentBuildId = buildId;
10
- }
11
5
  /**
12
6
  * Default Shiki theme configuration
13
7
  */
@@ -48,8 +42,8 @@ async function generateShikiHast(code, highlighter, transformers, enableTwoslash
48
42
  if (enableTwoslash) options.meta = { __raw: "twoslash" };
49
43
  return await highlighter.codeToHast(code, options);
50
44
  } catch (error) {
51
- emitEvent(PluginEvent.ShikiError({
52
- ctx: { buildId: currentBuildId },
45
+ emitSync(PluginEvent.ShikiError({
46
+ ctx: { buildId: syncBuildId() },
53
47
  file: "unknown",
54
48
  reason: String(error),
55
49
  level: "warn"
@@ -57,6 +51,67 @@ async function generateShikiHast(code, highlighter, transformers, enableTwoslash
57
51
  return null;
58
52
  }
59
53
  }
54
+ /* v8 ignore stop -- everything below is pure and covered by __test__/markdown/shiki-themes.test.ts */
55
+ /**
56
+ * Normalize a theme option into the `{ light, dark }` pair the highlighter and
57
+ * the remark plugins both expect.
58
+ *
59
+ * @remarks
60
+ * Accepts a single theme name applied to both modes, an explicit pair, or a
61
+ * raw theme object. Lives beside {@link DEFAULT_SHIKI_THEMES} because it falls
62
+ * back to it; it was previously duplicated byte-for-byte in `plugin.ts` and
63
+ * `ConfigServiceLive.ts`.
64
+ */
65
+ function normalizeThemeConfig(theme) {
66
+ if (!theme) return { ...DEFAULT_SHIKI_THEMES };
67
+ if (typeof theme === "string") return {
68
+ light: theme,
69
+ dark: theme
70
+ };
71
+ if ("light" in theme && "dark" in theme && typeof theme.light === "string" && typeof theme.dark === "string") return {
72
+ light: theme.light,
73
+ dark: theme.dark
74
+ };
75
+ return {
76
+ light: theme,
77
+ dark: theme
78
+ };
79
+ }
80
+ /**
81
+ * Every theme the build's single highlighter must load: one normalized pair
82
+ * per documented API, plus the two defaults.
83
+ *
84
+ * @remarks
85
+ * Takes the RAW option configs rather than resolved ones, because the
86
+ * highlighter is now acquired when its layer builds — before `resolve()` has
87
+ * run. That is sound: a resolved config's `theme` is exactly
88
+ * `normalizeThemeConfig(api.theme)` and no version-level override exists, so
89
+ * the two inputs cannot disagree.
90
+ *
91
+ * Named theme strings are deduplicated; object themes are not (they have no
92
+ * identity to compare), which matches the previous behaviour and is harmless —
93
+ * Shiki keys a loaded theme by its `name`.
94
+ */
95
+ function collectShikiThemes(apis) {
96
+ const named = /* @__PURE__ */ new Set();
97
+ const objects = [];
98
+ for (const api of apis) {
99
+ const theme = normalizeThemeConfig(api.theme);
100
+ for (const input of [theme.light, theme.dark]) if (typeof input === "string") named.add(input);
101
+ else objects.push(input);
102
+ }
103
+ if (typeof DEFAULT_SHIKI_THEMES.light === "string") named.add(DEFAULT_SHIKI_THEMES.light);
104
+ if (typeof DEFAULT_SHIKI_THEMES.dark === "string") named.add(DEFAULT_SHIKI_THEMES.dark);
105
+ return [...named, ...objects];
106
+ }
107
+ /** Languages the highlighter loads. Every code block the plugin renders is one of these. */
108
+ const SHIKI_LANGS = [
109
+ "typescript",
110
+ "javascript",
111
+ "json",
112
+ "bash",
113
+ "sh"
114
+ ];
60
115
 
61
116
  //#endregion
62
- export { DEFAULT_SHIKI_THEMES, generateShikiHast, setShikiUtilsEventEmitter };
117
+ export { DEFAULT_SHIKI_THEMES, SHIKI_LANGS, collectShikiThemes, generateShikiHast, normalizeThemeConfig };
@@ -1,3 +1,4 @@
1
+ import { BuildId } from "../BuildEnv.js";
1
2
  import { LEVEL_RANK, levelOf } from "./events.js";
2
3
  import { Context, Effect, Layer, Option } from "effect";
3
4
 
@@ -16,9 +17,34 @@ function makeShape(sinks) {
16
17
  function makeEventBusLayer(sinks) {
17
18
  return Layer.succeed(EventBus, makeShape(sinks));
18
19
  }
19
- /** Emit when a bus is in context; silently no-op otherwise. */
20
+ /**
21
+ * Emit when a bus is in context; silently no-op otherwise.
22
+ *
23
+ * @remarks
24
+ * Fills `ctx.buildId` from the {@link BuildId} Reference when the caller left
25
+ * it empty, which is why no emit site passes one. Before this, 24 sites wrote
26
+ * `ctx: { buildId: "" }` — 22 in `ConfigServiceLive`, where the real value sat
27
+ * three scopes up and was simply not reached, and every site in
28
+ * `TypeRegistryServiceLive`, where the layer is module-level and there is no
29
+ * build to name. The second group is why a Reference is the fix and a
30
+ * find-and-replace is not: a Reference reaches code that no parameter can.
31
+ *
32
+ * A caller that sets a non-empty `buildId` keeps it, so a test can still emit
33
+ * with an explicit id.
34
+ */
20
35
  function emit(event) {
21
- return Effect.serviceOption(EventBus).pipe(Effect.flatMap((maybe) => Option.isSome(maybe) ? maybe.value.emit(event) : Effect.void));
36
+ return Effect.gen(function* () {
37
+ const maybe = yield* Effect.serviceOption(EventBus);
38
+ if (Option.isNone(maybe)) return;
39
+ const filled = (event.ctx.buildId ?? "") === "" ? {
40
+ ...event,
41
+ ctx: {
42
+ ...event.ctx,
43
+ buildId: yield* BuildId
44
+ }
45
+ } : event;
46
+ yield* maybe.value.emit(filled);
47
+ });
22
48
  }
23
49
  /**
24
50
  * Returns true when a bus is in context and has at least one sink admitted at
@@ -27,10 +53,6 @@ function emit(event) {
27
53
  function wantsLevel(level) {
28
54
  return Effect.serviceOption(EventBus).pipe(Effect.flatMap((maybe) => Option.isSome(maybe) ? maybe.value.wantsLevel(level) : Effect.succeed(false)));
29
55
  }
30
- /** Bind a runtime so non-Effect (sync island) callbacks can emit. */
31
- function makeRuntimeEmitter(runtime) {
32
- return (event) => runtime.runSync(emit(event));
33
- }
34
56
 
35
57
  //#endregion
36
- export { EventBus, emit, makeEventBusLayer, makeRuntimeEmitter, wantsLevel };
58
+ export { EventBus, emit, makeEventBusLayer, wantsLevel };
@@ -1,3 +1,4 @@
1
+ import { Thresholds } from "../BuildEnv.js";
1
2
  import { PluginEvent } from "./events.js";
2
3
  import { emit } from "./EventBus.js";
3
4
  import { Effect } from "effect";
@@ -24,8 +25,9 @@ const PHASE_THRESHOLD_KEY = {
24
25
  * R type is unchanged: emit() is serviceOption-based (R = never), so adding
25
26
  * the span/events adds zero requirements to the caller.
26
27
  */
27
- function withPhase(phase, ctx, effect, thresholds) {
28
+ function withPhase(phase, ctx, effect) {
28
29
  return Effect.gen(function* () {
30
+ const thresholds = yield* Thresholds;
29
31
  yield* emit(PluginEvent.PhaseStarted({
30
32
  ctx,
31
33
  level: "debug",
@@ -0,0 +1,78 @@
1
+ import { BuildId, Thresholds } from "../BuildEnv.js";
2
+ import { emit } from "./EventBus.js";
3
+ import { Effect } from "effect";
4
+
5
+ //#region src/observability/sync-emitter.ts
6
+ /**
7
+ * The one bridge from synchronous, fiber-less code to the EventBus.
8
+ *
9
+ * @remarks
10
+ * Seven modules run outside any Effect fiber — remark visitors, Shiki's
11
+ * `preprocess` hook, Prettier callbacks, the page-generation stages — and each
12
+ * carried its own byte-identical copy of this seam: a module-level
13
+ * `emitEvent`, a module-level `currentBuildId`, and a `setXEventEmitter(fn,
14
+ * buildId)` for `plugin.ts` to call. Two of them had already grown a third
15
+ * parameter for `slowCodeBlockMs`, which is how a duplicated seam decays: the
16
+ * copies stop being identical one caller at a time.
17
+ *
18
+ * The seam itself is forced. The duplication was not, and neither was the
19
+ * threading: every value those setters carried is now a `Context.Reference`
20
+ * read from the runtime, so the signature is one runtime and nothing else.
21
+ *
22
+ * **The runtime handed here must be synchronously buildable.** `runSync`
23
+ * builds the runtime's layer before running anything, so a runtime whose layer
24
+ * opens a database fails with `AsyncFiberError` at the first emit — from a
25
+ * remark plugin, during RSPress's render pass, invisible to every unit test.
26
+ * `plugin.ts` builds a small observability-only runtime for exactly this
27
+ * reason.
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+ const NOOP = {
32
+ emit: () => {},
33
+ buildId: "",
34
+ slowCodeBlockMs: Number.POSITIVE_INFINITY
35
+ };
36
+ let current = NOOP;
37
+ /**
38
+ * Bind the sync islands to a runtime.
39
+ *
40
+ * @remarks
41
+ * Call once, immediately after constructing the observability runtime. The
42
+ * References are read here rather than per emit: an emit happens per code
43
+ * block on a large site, and these values are fixed for the build.
44
+ */
45
+ function installSyncEmitter(runtime) {
46
+ const env = runtime.runSync(Effect.gen(function* () {
47
+ return {
48
+ buildId: yield* BuildId,
49
+ slowCodeBlockMs: (yield* Thresholds).slowCodeBlock
50
+ };
51
+ }));
52
+ current = {
53
+ emit: (event) => runtime.runSync(emit(event)),
54
+ buildId: env.buildId,
55
+ slowCodeBlockMs: env.slowCodeBlockMs
56
+ };
57
+ }
58
+ /** Emit an event from synchronous code. A no-op when nothing is installed. */
59
+ function emitSync(event) {
60
+ current.emit(event);
61
+ }
62
+ /** The current build's id, for a sync site assembling an `EventContext`. */
63
+ function syncBuildId() {
64
+ return current.buildId;
65
+ }
66
+ /**
67
+ * The slow-code-block threshold, for the two remark plugins that time blocks.
68
+ *
69
+ * @remarks
70
+ * The only piece of configuration a sync island needs beyond the build id, and
71
+ * the reason the old seams had begun growing divergent signatures.
72
+ */
73
+ function syncSlowCodeBlockMs() {
74
+ return current.slowCodeBlockMs;
75
+ }
76
+
77
+ //#endregion
78
+ export { emitSync, installSyncEmitter, syncBuildId, syncSlowCodeBlockMs };