rspress-plugin-api-extractor 0.2.2 → 0.3.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.
Files changed (43) hide show
  1. package/api-extracted-package.js +2 -1
  2. package/build-program.js +20 -12
  3. package/build-stages.js +123 -29
  4. package/config-utils.js +36 -7
  5. package/index.d.ts +329 -202
  6. package/layers/ConfigServiceLive.js +300 -136
  7. package/layers/ObservabilityLive.js +49 -85
  8. package/layers/TypeRegistryServiceLive.js +122 -21
  9. package/layers/build-metrics.js +61 -0
  10. package/llms-program.js +29 -7
  11. package/loader.js +16 -2
  12. package/markdown/shiki-utils.js +16 -2
  13. package/observability/EventBus.js +38 -0
  14. package/observability/events.js +17 -0
  15. package/observability/sinks/console-sink.js +63 -0
  16. package/observability/sinks/metrics-sink.js +68 -0
  17. package/observability/sinks/trace-sink.js +38 -0
  18. package/observability/spans.js +57 -0
  19. package/og-resolver.js +37 -5
  20. package/package.json +4 -4
  21. package/plugin.js +73 -19
  22. package/prettier-formatter.js +15 -4
  23. package/remark-api-codeblocks.js +22 -3
  24. package/remark-with-api.js +27 -14
  25. package/runtime/components/ApiExample/index.js +5 -5
  26. package/runtime/components/ApiMember/index.js +5 -7
  27. package/runtime/components/ApiSignature/index.js +4 -6
  28. package/runtime/components/EnumMembersTable/index.js +5 -0
  29. package/runtime/components/ExampleBlock/index.js +5 -3
  30. package/runtime/components/MemberSignature/index.js +4 -2
  31. package/runtime/components/ParametersTable/index.js +5 -0
  32. package/runtime/components/SignatureBlock/index.js +4 -2
  33. package/runtime/components/shared/variables.css +0 -15
  34. package/runtime/index.d.ts +65 -399
  35. package/runtime/index.js +1 -5
  36. package/runtime/utils/hast-renderer.js +1 -0
  37. package/schemas/config.js +105 -4
  38. package/schemas/index.js +2 -1
  39. package/schemas/observability.js +62 -0
  40. package/schemas/opengraph.js +30 -0
  41. package/schemas/performance.js +1 -1
  42. package/serve.js +13 -0
  43. package/twoslash-transformer.js +93 -8
@@ -0,0 +1,63 @@
1
+ import { LEVEL_RANK, levelOf } from "../events.js";
2
+
3
+ //#region src/observability/sinks/console-sink.ts
4
+ function formatTime(date) {
5
+ return date.toTimeString().slice(0, 8);
6
+ }
7
+ /**
8
+ * One-line human-readable summary of an event. Uses a switch on `_tag` (not
9
+ * `PluginEvent.$match`, whose Effect signature requires EXHAUSTIVE cases with
10
+ * no `_` wildcard). TypeScript narrows `event` to the variant in each case.
11
+ * Extend per _tag as needed; the `default` arm renders the bare tag.
12
+ */
13
+ function render(event) {
14
+ switch (event._tag) {
15
+ case "BuildStarted": return `Generating API documentation (${event.apiCount} API${event.apiCount === 1 ? "" : "s"})…`;
16
+ case "PhaseStarted": return `→ ${event.phase}`;
17
+ case "PhaseCompleted": return `✓ ${event.phase} (${event.durationMs}ms)`;
18
+ case "BuildCompleted": return `API documentation complete (${(event.durationMs / 1e3).toFixed(2)}s)`;
19
+ case "BuildFailed": return `Error in ${event.phase}: ${event.error}`;
20
+ case "SlowOperation": return `slow ${event.operation}: ${event.durationMs}ms (>${event.threshold}ms)`;
21
+ case "ConfigCascadeWarning": return `${event.field}: using '${event.chosen}', ignoring ${event.ignored.join(", ")}`;
22
+ case "ConfigValidationWarning": return `${event.field}: rejected '${event.value}'${event.reason ? ` — ${event.reason}` : ""}`;
23
+ case "DeprecatedConfigUsed": return `option '${event.key}' is deprecated; use ${event.replacement}`;
24
+ case "ModelLoaded": return `loaded model: ${event.itemCount} items, ${event.entryPoints} entry point(s) (${event.durationMs}ms)`;
25
+ case "ConfigResolved": return `resolved ${event.baseRoute}: ${event.categoryCount} categories, ${event.externalCount} external`;
26
+ case "TwoslashDiagnostic": return `Twoslash TS${event.code} in ${event.file}:${event.line}:${event.col}: ${event.message}`;
27
+ case "TwoslashCheckFailed": return `Twoslash check failed (TS${event.code}) in ${event.file}; ${event.fsMapKeys.length} VFS files`;
28
+ case "PageGenerated": return `page ${event.category}/${event.item} (${event.durationMs}ms)`;
29
+ case "FileDecision": return `${event.status}: ${event.file}`;
30
+ case "ItemSkipped": return `skipped ${event.kind} "${event.item}": ${event.reason}`;
31
+ case "ShikiError": return `Shiki error in ${event.file}: ${event.reason}`;
32
+ case "PrettierError": return `Prettier error in ${event.file}: ${event.reason}`;
33
+ case "LlmsPackageFilesGenerated": return `llms files: ${event.dir} (${event.files.length})`;
34
+ case "TypeRegistryEvent": return event.kind === "BatchComplete" ? event.detail : `${event.kind} ${event.ctx.packageName ?? ""} ${event.detail}`.trim();
35
+ default: return event._tag;
36
+ }
37
+ }
38
+ function makeConsoleSink(logLevel, opts = {}) {
39
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
40
+ const json = opts.json ?? false;
41
+ const minLevel = logLevel === "none" ? "error" : logLevel;
42
+ const threshold = logLevel === "none" ? -1 : LEVEL_RANK[minLevel];
43
+ return {
44
+ minLevel,
45
+ capturesPayload: json,
46
+ handle: (event) => {
47
+ if (LEVEL_RANK[levelOf(event)] > threshold) return;
48
+ if (json) {
49
+ console.log(JSON.stringify({
50
+ timestamp: now().getTime(),
51
+ ...event
52
+ }));
53
+ return;
54
+ }
55
+ const level = levelOf(event);
56
+ const prefix = level === "error" ? "🔴 " : level === "warn" ? "⚠️ " : "";
57
+ console.log(`[${formatTime(now())}] ${prefix}${render(event)}`);
58
+ }
59
+ };
60
+ }
61
+
62
+ //#endregion
63
+ export { makeConsoleSink };
@@ -0,0 +1,68 @@
1
+ import { BuildMetrics } from "../../layers/build-metrics.js";
2
+ import { Effect, Metric } from "effect";
3
+
4
+ //#region src/observability/sinks/metrics-sink.ts
5
+ /**
6
+ * Event-driven metrics sink.
7
+ *
8
+ * Translates each `PluginEvent` to the corresponding `BuildMetrics` counter or
9
+ * histogram update via `Effect.runSync`. The fan-out from `EventBus.emit` is
10
+ * synchronous, so by the time the emitting fiber resumes the metrics are already
11
+ * recorded — counts are exact when `logBuildSummary` reads them in `afterBuild`.
12
+ *
13
+ * Unmapped event tags hit the `default` branch and are silently ignored.
14
+ *
15
+ * Intentionally NOT derived here: `externalPackagesTotal` and `apiVersionsLoaded`
16
+ * remain inline increments in `ConfigServiceLive`. `externalPackagesTotal` counts
17
+ * CONFIGURED packages via `incrementBy(length)`; the only candidate event,
18
+ * `TypeRegistryEvent{BatchComplete}`, carries an unstructured `detail` string and
19
+ * a `loaded` (SUCCEEDED) count — different semantics, so deriving it here would
20
+ * change what the metric means. `apiVersionsLoaded` has no corresponding event.
21
+ */
22
+ function makeMetricsSink() {
23
+ return {
24
+ minLevel: "trace",
25
+ handle(event) {
26
+ switch (event._tag) {
27
+ case "FileDecision":
28
+ Effect.runSync(Metric.increment(BuildMetrics.filesTotal));
29
+ if (event.status === "new") Effect.runSync(Metric.increment(BuildMetrics.filesNew));
30
+ else if (event.status === "modified") Effect.runSync(Metric.increment(BuildMetrics.filesModified));
31
+ else Effect.runSync(Metric.increment(BuildMetrics.filesUnchanged));
32
+ break;
33
+ case "PageGenerated":
34
+ Effect.runSync(Metric.increment(BuildMetrics.pagesGenerated));
35
+ break;
36
+ case "TwoslashDiagnostic":
37
+ Effect.runSync(Metric.increment(BuildMetrics.twoslashDiagnostics));
38
+ Effect.runSync(Metric.increment(BuildMetrics.twoslashErrors));
39
+ break;
40
+ case "PrettierError":
41
+ Effect.runSync(Metric.increment(BuildMetrics.prettierErrors));
42
+ break;
43
+ case "CodeBlockProcessed":
44
+ Effect.runSync(Metric.increment(BuildMetrics.codeblockTotal));
45
+ Effect.runSync(Metric.update(BuildMetrics.codeblockDuration, event.totalMs));
46
+ if (event.shikiMs > 0) Effect.runSync(Metric.update(BuildMetrics.codeblockShikiDuration, event.shikiMs));
47
+ if (event.slow) Effect.runSync(Metric.increment(BuildMetrics.codeblockSlow));
48
+ break;
49
+ case "VfsGenerated":
50
+ Effect.runSync(Metric.increment(BuildMetrics.vfsFiles));
51
+ break;
52
+ case "ImportsPrepended":
53
+ Effect.runSync(Metric.increment(BuildMetrics.importsPrepended));
54
+ break;
55
+ case "PhaseCompleted":
56
+ Effect.runSync(Metric.update(BuildMetrics.phaseDuration, event.durationMs));
57
+ break;
58
+ case "DefaultApplied":
59
+ Effect.runSync(Metric.increment(BuildMetrics.configDefaultsApplied));
60
+ break;
61
+ default: break;
62
+ }
63
+ }
64
+ };
65
+ }
66
+
67
+ //#endregion
68
+ export { makeMetricsSink };
@@ -0,0 +1,38 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ //#region src/observability/sinks/trace-sink.ts
5
+ function openTracePath(p) {
6
+ fs.mkdirSync(path.dirname(p), { recursive: true });
7
+ fs.writeFileSync(p, "");
8
+ }
9
+ /**
10
+ * Create a JSONL trace sink.
11
+ *
12
+ * When `initialPath` is provided the file is opened eagerly at construction
13
+ * (existing behaviour for an explicit `trace: "/some/path"` config).
14
+ * When omitted the sink starts in deferred mode — events are silently dropped
15
+ * until `setPath` is called. This lets `plugin.ts` create the sink before
16
+ * the RSPress `outDir` is known and then bind the real path in the `config()`
17
+ * hook once `_config.outDir` is available.
18
+ */
19
+ function makeTraceSink(initialPath) {
20
+ let currentPath = initialPath ?? null;
21
+ if (currentPath) openTracePath(currentPath);
22
+ return {
23
+ minLevel: "trace",
24
+ capturesPayload: true,
25
+ handle: (event) => {
26
+ if (!currentPath) return;
27
+ fs.appendFileSync(currentPath, `${JSON.stringify(event)}\n`);
28
+ },
29
+ flush: () => {},
30
+ setPath: (p) => {
31
+ currentPath = p;
32
+ openTracePath(p);
33
+ }
34
+ };
35
+ }
36
+
37
+ //#endregion
38
+ export { makeTraceSink };
@@ -0,0 +1,57 @@
1
+ import { PluginEvent } from "./events.js";
2
+ import { emit } from "./EventBus.js";
3
+ import { Effect } from "effect";
4
+
5
+ //#region src/observability/spans.ts
6
+ /**
7
+ * Maps phase names to threshold keys in ResolvedObservability["thresholds"].
8
+ * Used by withPhase to determine the breach threshold for a given phase.
9
+ */
10
+ const PHASE_THRESHOLD_KEY = {
11
+ modelLoad: "slowApiLoad",
12
+ resolve: "slowApiLoad",
13
+ generate: "slowPageGeneration",
14
+ write: "slowFileOperation",
15
+ cleanup: "slowDbOperation"
16
+ };
17
+ /**
18
+ * Wrap an Effect in a phase span.
19
+ *
20
+ * Emits PhaseStarted before the effect runs, measures wall-clock duration,
21
+ * emits PhaseCompleted after, and emits SlowOperation when the duration
22
+ * exceeds the threshold mapped for this phase (defaulting to slowApiLoad).
23
+ *
24
+ * R type is unchanged: emit() is serviceOption-based (R = never), so adding
25
+ * the span/events adds zero requirements to the caller.
26
+ */
27
+ function withPhase(phase, ctx, effect, thresholds) {
28
+ return Effect.gen(function* () {
29
+ yield* emit(PluginEvent.PhaseStarted({
30
+ ctx,
31
+ level: "debug",
32
+ phase
33
+ }));
34
+ const start = performance.now();
35
+ const result = yield* Effect.withSpan(`phase.${phase}`)(effect);
36
+ const elapsed = performance.now() - start;
37
+ const durationMs = Math.round(elapsed);
38
+ yield* emit(PluginEvent.PhaseCompleted({
39
+ ctx,
40
+ level: "debug",
41
+ phase,
42
+ durationMs
43
+ }));
44
+ const threshold = thresholds[PHASE_THRESHOLD_KEY[phase] ?? "slowApiLoad"];
45
+ if (elapsed >= threshold) yield* emit(PluginEvent.SlowOperation({
46
+ ctx,
47
+ level: "warn",
48
+ operation: `phase.${phase}`,
49
+ durationMs,
50
+ threshold
51
+ }));
52
+ return result;
53
+ });
54
+ }
55
+
56
+ //#endregion
57
+ export { withPhase };
package/og-resolver.js CHANGED
@@ -1,8 +1,16 @@
1
+ import { PluginEvent } from "./observability/events.js";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { imageSizeFromFile } from "image-size/fromFile";
4
5
 
5
6
  //#region src/og-resolver.ts
7
+ /** Module-level emitter injected by plugin.ts at startup. */
8
+ let emitEvent = () => {};
9
+ let currentBuildId = "";
10
+ function setOgResolverEventEmitter(fn, buildId = "") {
11
+ emitEvent = fn;
12
+ currentBuildId = buildId;
13
+ }
6
14
  /**
7
15
  * MIME type mappings for common image formats.
8
16
  * Used to determine the `og:image:type` meta tag value.
@@ -122,12 +130,24 @@ var OpenGraphResolver = class {
122
130
  const { url, secureUrl, type, width, height, alt } = metadata;
123
131
  const resolvedUrl = this.resolveUrl(url);
124
132
  if (!resolvedUrl) {
125
- console.warn(`[og-resolver] Invalid ogImage URL format: "${url}"`);
133
+ emitEvent(PluginEvent.ConfigValidationWarning({
134
+ ctx: { buildId: currentBuildId },
135
+ field: "ogImage.url",
136
+ value: url,
137
+ reason: "invalid URL format",
138
+ level: "warn"
139
+ }));
126
140
  return;
127
141
  }
128
142
  let resolvedSecureUrl;
129
143
  if (secureUrl) if (secureUrl.startsWith("https://")) resolvedSecureUrl = secureUrl;
130
- else console.warn(`[og-resolver] ogImage secureUrl must be an absolute HTTPS URL: "${secureUrl}"`);
144
+ else emitEvent(PluginEvent.ConfigValidationWarning({
145
+ ctx: { buildId: currentBuildId },
146
+ field: "ogImage.secureUrl",
147
+ value: secureUrl,
148
+ reason: "secureUrl must be absolute HTTPS",
149
+ level: "warn"
150
+ }));
131
151
  return {
132
152
  url: resolvedUrl,
133
153
  secureUrl: resolvedSecureUrl,
@@ -154,7 +174,13 @@ var OpenGraphResolver = class {
154
174
  async resolveFromString(imageUrl, packageName, apiName) {
155
175
  const resolvedUrl = this.resolveUrl(imageUrl);
156
176
  if (!resolvedUrl) {
157
- console.warn(`[og-resolver] Invalid ogImage format: "${imageUrl}" (must be absolute URL or path starting with /)`);
177
+ emitEvent(PluginEvent.ConfigValidationWarning({
178
+ ctx: { buildId: currentBuildId },
179
+ field: "ogImage",
180
+ value: imageUrl,
181
+ reason: "invalid URL format",
182
+ level: "warn"
183
+ }));
158
184
  return;
159
185
  }
160
186
  const localPath = this.findLocalImage(imageUrl);
@@ -207,7 +233,13 @@ var OpenGraphResolver = class {
207
233
  ...mimeType != null ? { type: mimeType } : {}
208
234
  };
209
235
  } catch (error) {
210
- console.warn(`[og-resolver] Failed to read image dimensions from ${filePath}:`, error.message);
236
+ emitEvent(PluginEvent.ConfigValidationWarning({
237
+ ctx: { buildId: currentBuildId },
238
+ field: "ogImage",
239
+ value: filePath,
240
+ reason: error.message ?? String(error),
241
+ level: "warn"
242
+ }));
211
243
  return;
212
244
  }
213
245
  }
@@ -268,4 +300,4 @@ var OpenGraphResolver = class {
268
300
  };
269
301
 
270
302
  //#endregion
271
- export { OpenGraphResolver };
303
+ export { OpenGraphResolver, setOgResolverEventEmitter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rspress-plugin-api-extractor",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "RSPress plugin for generating API documentation from TypeScript API Extractor models",
6
6
  "keywords": [
@@ -32,7 +32,7 @@
32
32
  "./package.json": "./package.json"
33
33
  },
34
34
  "dependencies": {
35
- "@effect/platform": "^0.96.1",
35
+ "@effect/platform": "^0.96.2",
36
36
  "@effect/platform-node": "^0.107.0",
37
37
  "@effect/sql": "^0.51.1",
38
38
  "@effect/sql-sqlite-node": "^0.52.0",
@@ -40,7 +40,7 @@
40
40
  "@shikijs/twoslash": "^4.2.0",
41
41
  "api-extractor-llms": "0.1.0",
42
42
  "clsx": "^2.1.1",
43
- "effect": "^3.21.3",
43
+ "effect": "^3.21.4",
44
44
  "gray-matter": "^4.0.3",
45
45
  "hast-util-to-jsx-runtime": "^2.3.6",
46
46
  "image-size": "^2.0.2",
@@ -51,7 +51,7 @@
51
51
  "react-markdown": "^10.1.0",
52
52
  "semver-effect": "^0.2.1",
53
53
  "shiki": "^4.2.0",
54
- "type-registry-effect": "^0.2.3",
54
+ "type-registry-effect": "^1.0.0",
55
55
  "typescript": "^6.0.3",
56
56
  "unist-util-visit": "^5.1.0"
57
57
  },
package/plugin.js CHANGED
@@ -1,11 +1,17 @@
1
- import { PluginLoggerLayer, logBuildSummary } from "./layers/ObservabilityLive.js";
2
- import { TwoslashManager } from "./twoslash-transformer.js";
1
+ import { PluginEvent } from "./observability/events.js";
2
+ import { emit, makeRuntimeEmitter } from "./observability/EventBus.js";
3
+ import { buildEventBus, logBuildSummary, makeSummaryLoggerLayer } from "./layers/ObservabilityLive.js";
4
+ import { setLoaderEventEmitter } from "./loader.js";
5
+ import { setPrettierEventEmitter } from "./prettier-formatter.js";
6
+ import { setOgResolverEventEmitter } from "./og-resolver.js";
7
+ import { TwoslashManager, setEventEmitter } from "./twoslash-transformer.js";
3
8
  import { VfsRegistry } from "./vfs-registry.js";
4
9
  import { generateApiDocs } from "./build-program.js";
5
10
  import { deriveOutputPaths, normalizeBaseRoute, unscopedName } from "./path-derivation.js";
6
11
  import { fromDir, fromParentDir } from "./config-helpers.js";
7
12
  import { mergeLlmsPluginConfig } from "./config-utils.js";
8
- import { DEFAULT_SHIKI_THEMES } from "./markdown/shiki-utils.js";
13
+ import { DEFAULT_SHIKI_THEMES, setShikiUtilsEventEmitter } from "./markdown/shiki-utils.js";
14
+ import { resolveObservability } from "./schemas/observability.js";
9
15
  import { PluginOptions } from "./schemas/config.js";
10
16
  import "./schemas/index.js";
11
17
  import { ConfigService } from "./services/ConfigService.js";
@@ -13,8 +19,8 @@ import { ConfigServiceLive } from "./layers/ConfigServiceLive.js";
13
19
  import { PathDerivationServiceLive } from "./layers/PathDerivationServiceLive.js";
14
20
  import { SnapshotServiceLive } from "./layers/SnapshotServiceLive.js";
15
21
  import { TypeRegistryServiceLive } from "./layers/TypeRegistryServiceLive.js";
16
- import { remarkApiCodeblocks } from "./remark-api-codeblocks.js";
17
- import { remarkWithApi } from "./remark-with-api.js";
22
+ import { remarkApiCodeblocks, setRemarkApiCodeblocksEventEmitter } from "./remark-api-codeblocks.js";
23
+ import { remarkWithApi, setRemarkWithApiEventEmitter } from "./remark-with-api.js";
18
24
  import { ShikiCrossLinker } from "./shiki-transformer.js";
19
25
  import { createRequire } from "node:module";
20
26
  import fs from "node:fs";
@@ -49,13 +55,32 @@ function normalizeThemeConfig(theme) {
49
55
  function ApiExtractorPluginImpl(rawOptions) {
50
56
  const options = Schema.decodeUnknownSync(PluginOptions)(rawOptions);
51
57
  const shikiCrossLinker = new ShikiCrossLinker();
52
- const logLevel = process.env.LOG_LEVEL?.toLowerCase() || options.logLevel || "info";
58
+ const envLogLevel = process.env.LOG_LEVEL?.toLowerCase();
59
+ const buildId = `${process.pid}-${performance.now().toString(36)}`;
60
+ const rspressOutDirGuess = path.resolve(process.cwd(), "dist");
61
+ const { resolved: obs, deprecations } = resolveObservability({
62
+ ...options.observability ? { observability: options.observability } : {},
63
+ ...options.logLevel ? { logLevel: options.logLevel } : {},
64
+ ...options.performance ? { performance: { ...options.performance.thresholds !== void 0 ? { thresholds: options.performance.thresholds } : {} } } : {},
65
+ ...envLogLevel ? { envLogLevel } : {},
66
+ outDir: rspressOutDirGuess,
67
+ buildId
68
+ });
69
+ const traceIsDefault = options.observability?.trace === true;
70
+ const { layer: eventBusLayer, trace: traceSink } = buildEventBus(obs, traceIsDefault);
53
71
  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);
72
+ const BaseLayer = Layer.mergeAll(PathDerivationServiceLive, eventBusLayer, TypeRegistryServiceLive, NodeFileSystem.layer, SnapshotServiceLive(dbPath), makeSummaryLoggerLayer(obs.logLevel));
73
+ const EffectAppLayer = Layer.provideMerge(ConfigServiceLive(options, shikiCrossLinker, buildId, obs.thresholds), BaseLayer);
56
74
  const effectRuntime = ManagedRuntime.make(EffectAppLayer);
75
+ const emitSync = makeRuntimeEmitter(effectRuntime);
76
+ setEventEmitter(emitSync, buildId);
77
+ setLoaderEventEmitter(emitSync, buildId);
78
+ setShikiUtilsEventEmitter(emitSync, buildId);
79
+ setPrettierEventEmitter(emitSync, buildId);
80
+ setOgResolverEventEmitter(emitSync, buildId);
81
+ setRemarkWithApiEventEmitter(emitSync, buildId, obs.thresholds.slowCodeBlock);
82
+ setRemarkApiCodeblocksEventEmitter(emitSync, buildId);
57
83
  const fileContextMap = /* @__PURE__ */ new Map();
58
- const isVerbose = logLevel === "verbose" || logLevel === "debug";
59
84
  let docsRoot;
60
85
  let isFirstBuild = true;
61
86
  let rspressLlmsEnabled = false;
@@ -68,18 +93,20 @@ function ApiExtractorPluginImpl(rawOptions) {
68
93
  async beforeBuild(_config, _isProd) {},
69
94
  async afterBuild(_config, isProd) {
70
95
  if (isFirstBuild) {
71
- await effectRuntime.runPromise(logBuildSummary);
96
+ await effectRuntime.runPromise(logBuildSummary(obs.thresholds.slowCodeBlock));
72
97
  if (rspressLlmsEnabled && resolvedLlmsPlugin.enabled) {
73
98
  const { processLlmsFiles } = await import("./llms-program.js");
74
99
  await effectRuntime.runPromise(processLlmsFiles({
75
100
  outDir: path.resolve(process.cwd(), rspressOutDir),
76
101
  buildResults,
77
102
  llmsPlugin: resolvedLlmsPlugin,
78
- packageRoutes
103
+ packageRoutes,
104
+ buildId
79
105
  }));
80
106
  }
81
107
  isFirstBuild = false;
82
108
  }
109
+ if (traceSink) traceSink.flush();
83
110
  if (isProd) await effectRuntime.dispose();
84
111
  },
85
112
  async config(_config) {
@@ -91,6 +118,10 @@ function ApiExtractorPluginImpl(rawOptions) {
91
118
  const rspressMultiVersion = _config.multiVersion;
92
119
  rspressLlmsEnabled = Boolean(_config.llms);
93
120
  rspressOutDir = _config.outDir ?? "dist";
121
+ if (traceIsDefault && traceSink) {
122
+ const realTracePath = path.resolve(process.cwd(), rspressOutDir, ".api-extractor", `trace-${buildId}.jsonl`);
123
+ traceSink.setPath(realTracePath);
124
+ }
94
125
  if (options.api) {
95
126
  const api = options.api;
96
127
  const baseRoute = normalizeBaseRoute(api.baseRoute ?? "/");
@@ -121,7 +152,12 @@ function ApiExtractorPluginImpl(rawOptions) {
121
152
  }
122
153
  VfsRegistry.clear();
123
154
  fileContextMap.clear();
124
- if (isVerbose) console.log("🚀 RSPress API Extractor Plugin");
155
+ for (const dep of deprecations) emitSync(PluginEvent.DeprecatedConfigUsed({
156
+ ctx: { buildId },
157
+ level: "warn",
158
+ key: dep.key,
159
+ replacement: dep.replacement
160
+ }));
125
161
  try {
126
162
  const rspressConfigSubset = {
127
163
  ...rspressMultiVersion != null ? { multiVersion: rspressMultiVersion } : {},
@@ -130,8 +166,14 @@ function ApiExtractorPluginImpl(rawOptions) {
130
166
  ...docsRoot != null ? { root: docsRoot } : {}
131
167
  };
132
168
  await effectRuntime.runPromise(Effect.gen(function* () {
169
+ const apiCount = options.api ? 1 : options.apis?.length ?? 0;
170
+ yield* emit(PluginEvent.BuildStarted({
171
+ ctx: { buildId },
172
+ level: "info",
173
+ mode: "prod",
174
+ apiCount
175
+ }));
133
176
  const buildContext = yield* (yield* ConfigService).resolve(rspressConfigSubset);
134
- yield* Effect.logInfo("Generating API documentation...");
135
177
  buildResults.length = 0;
136
178
  yield* Effect.forEach(buildContext.apiConfigs, (apiConfig) => generateApiDocs({
137
179
  ...apiConfig,
@@ -139,14 +181,24 @@ function ApiExtractorPluginImpl(rawOptions) {
139
181
  }, buildContext, fileContextMap).pipe(Effect.tap((result) => {
140
182
  buildResults.push(result);
141
183
  return Effect.void;
142
- }), Effect.tap(() => isVerbose ? Effect.logDebug(`Generating docs for ${apiConfig.packageName}`) : Effect.void)), { concurrency: 2 });
184
+ })), { concurrency: 2 });
185
+ const totalMs = performance.now() - buildStartTime;
186
+ yield* emit(PluginEvent.BuildCompleted({
187
+ ctx: { buildId },
188
+ level: "info",
189
+ durationMs: totalMs,
190
+ totals: {}
191
+ }));
143
192
  }).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
193
  } catch (error) {
149
- console.error(`❌ Error generating API documentation: ${error instanceof Error ? error.message : String(error)}`);
194
+ const message = error instanceof Error ? error.message : String(error);
195
+ emitSync(PluginEvent.BuildFailed({
196
+ ctx: { buildId },
197
+ level: "error",
198
+ phase: "generate",
199
+ error: message
200
+ }));
201
+ if (traceSink) traceSink.flush();
150
202
  throw error;
151
203
  }
152
204
  const updatedConfig = { ..._config };
@@ -208,6 +260,8 @@ function ApiExtractorPluginImpl(rawOptions) {
208
260
  * files. Config helpers are available under `ApiExtractorPlugin.api` (single
209
261
  * package → one config for the `api:` option) and `ApiExtractorPlugin.apis`
210
262
  * (parent directory → array for the `apis:` option).
263
+ *
264
+ * @public
211
265
  */
212
266
  const ApiExtractorPlugin = Object.assign(ApiExtractorPluginImpl, {
213
267
  api: { fromDir },
@@ -1,10 +1,16 @@
1
- import { BuildMetrics } from "./layers/ObservabilityLive.js";
1
+ import { PluginEvent } from "./observability/events.js";
2
2
  import { addLogicalBlankLines } from "./code-post-processor.js";
3
- import { Effect, Metric } from "effect";
4
3
  import { format } from "prettier";
5
4
 
6
5
  //#region src/prettier-formatter.ts
7
6
  /* v8 ignore start -- Prettier integration wrapper, tested via page generator integration tests */
7
+ /** Module-level emitter injected by plugin.ts at startup. */
8
+ let emitEvent = () => {};
9
+ let currentBuildId = "";
10
+ function setPrettierEventEmitter(fn, buildId = "") {
11
+ emitEvent = fn;
12
+ currentBuildId = buildId;
13
+ }
8
14
  /**
9
15
  * Map code fence languages to Prettier parsers
10
16
  */
@@ -59,7 +65,12 @@ async function formatCode(code, language) {
59
65
  } catch (error) {
60
66
  const formatTime = performance.now() - start;
61
67
  const errorMsg = error instanceof Error ? error.message : String(error);
62
- Effect.runSync(Metric.increment(BuildMetrics.prettierErrors));
68
+ emitEvent(PluginEvent.PrettierError({
69
+ ctx: { buildId: currentBuildId },
70
+ file: "unknown",
71
+ reason: errorMsg,
72
+ level: "warn"
73
+ }));
63
74
  return {
64
75
  code,
65
76
  success: false,
@@ -70,4 +81,4 @@ async function formatCode(code, language) {
70
81
  }
71
82
 
72
83
  //#endregion
73
- export { formatCode };
84
+ export { formatCode, setPrettierEventEmitter };
@@ -1,8 +1,17 @@
1
+ import { PluginEvent } from "./observability/events.js";
2
+ import { TwoslashManager } from "./twoslash-transformer.js";
1
3
  import { VfsRegistry } from "./vfs-registry.js";
2
4
  import { generateShikiHast } from "./markdown/shiki-utils.js";
3
5
  import { visit } from "unist-util-visit";
4
6
 
5
7
  //#region src/remark-api-codeblocks.ts
8
+ /** Module-level emitter injected by plugin.ts at startup. */
9
+ let emitEvent = () => {};
10
+ let currentBuildId = "";
11
+ function setRemarkApiCodeblocksEventEmitter(fn, buildId = "") {
12
+ emitEvent = fn;
13
+ currentBuildId = buildId;
14
+ }
6
15
  /**
7
16
  * Create an MDX JSX attribute value expression with proper estree AST.
8
17
  * This ensures the value is properly serialized as a JavaScript string literal.
@@ -82,7 +91,8 @@ const remarkApiCodeblocks = () => {
82
91
  const promises = [];
83
92
  const isSsgMd = import.meta.env?.SSG_MD || process.env.RSBUILD_ENVIRONMENT === "node_md" || process.env.BUILD_TARGET === "node_md";
84
93
  const currentFilePath = file.path || "unknown";
85
- const jsxComponentNames = new Set([
94
+ if (file.path) TwoslashManager.getInstance().setCurrentFile(file.path);
95
+ const jsxComponentNames = /* @__PURE__ */ new Set([
86
96
  "ApiSignature",
87
97
  "ApiMember",
88
98
  "ApiExample"
@@ -99,7 +109,16 @@ const remarkApiCodeblocks = () => {
99
109
  }
100
110
  const vfsConfig = VfsRegistry.get(apiScopeValue);
101
111
  if (!vfsConfig) {
102
- console.warn(`[remark-api-codeblocks] No VFS config found for scope "${apiScopeValue}" in ${currentFilePath}`);
112
+ emitEvent(PluginEvent.ConfigCascadeWarning({
113
+ ctx: {
114
+ buildId: currentBuildId,
115
+ file: currentFilePath
116
+ },
117
+ field: "vfs",
118
+ chosen: apiScopeValue,
119
+ ignored: [],
120
+ level: "warn"
121
+ }));
103
122
  removeJsxAttrs(node, ["source", "apiScope"]);
104
123
  return;
105
124
  }
@@ -127,4 +146,4 @@ const remarkApiCodeblocks = () => {
127
146
  };
128
147
 
129
148
  //#endregion
130
- export { remarkApiCodeblocks };
149
+ export { remarkApiCodeblocks, setRemarkApiCodeblocksEventEmitter };