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
@@ -1,102 +1,61 @@
1
- import { Effect, Layer, LogLevel, Logger, Metric, MetricBoundaries } from "effect";
1
+ import { makeEventBusLayer } from "../observability/EventBus.js";
2
+ import { makeConsoleSink } from "../observability/sinks/console-sink.js";
3
+ import { BuildMetrics } from "./build-metrics.js";
4
+ import { makeMetricsSink } from "../observability/sinks/metrics-sink.js";
5
+ import { makeTraceSink } from "../observability/sinks/trace-sink.js";
6
+ import { Effect, Layer, LogLevel, Logger, Metric } from "effect";
2
7
 
3
8
  //#region src/layers/ObservabilityLive.ts
4
- /**
5
- * All build metrics as named counters/histograms.
6
- *
7
- * Note: Effect Metrics use a process-wide registry. In tests, counters
8
- * accumulate across test cases within the same process. Test assertions
9
- * should use loose matching (toContain) rather than exact count checks.
10
- */
11
- const BuildMetrics = {
12
- filesTotal: Metric.counter("files.total"),
13
- filesNew: Metric.counter("files.new"),
14
- filesModified: Metric.counter("files.modified"),
15
- filesUnchanged: Metric.counter("files.unchanged"),
16
- codeblockDuration: Metric.histogram("codeblock.duration", MetricBoundaries.fromIterable([
17
- 10,
18
- 25,
19
- 50,
20
- 100,
21
- 200,
22
- 500,
23
- 1e3
24
- ])),
25
- codeblockShikiDuration: Metric.histogram("codeblock.shiki.duration", MetricBoundaries.fromIterable([
26
- 5,
27
- 10,
28
- 25,
29
- 50,
30
- 100,
31
- 250
32
- ])),
33
- codeblockTotal: Metric.counter("codeblock.total"),
34
- codeblockSlow: Metric.counter("codeblock.slow"),
35
- twoslashErrors: Metric.counter("twoslash.errors"),
36
- prettierErrors: Metric.counter("prettier.errors"),
37
- pagesGenerated: Metric.counter("pages.generated"),
38
- apiVersionsLoaded: Metric.counter("api.versions.loaded"),
39
- externalPackagesTotal: Metric.counter("external.packages.total")
40
- };
41
- /**
42
- * Format a Date as HH:MM:SS for console output.
43
- */
44
9
  function formatTime(date) {
45
10
  return date.toTimeString().slice(0, 8);
46
11
  }
47
12
  /**
48
- * Extract annotations from HashMap to a plain object.
49
- */
50
- function annotationsToObject(annotations) {
51
- const obj = {};
52
- for (const [key, value] of annotations) obj[key] = value;
53
- return obj;
54
- }
55
- /**
56
- * Create a custom plugin logger for the given mode.
57
- * Uses a closure to capture debugMode — no mutable module state.
13
+ * A slim Effect Logger layer that gates the residual `Effect.log*` calls in
14
+ * `build-program.ts` and `logBuildSummary` at the configured level.
15
+ *
16
+ * Level mapping: none→None, error→Error, warn→Warning, info→Info,
17
+ * debug/trace→Debug. Format: `[HH:MM:SS] <prefix><message>` with
18
+ * `⚠️ ` / `🔴 ` prefixes for Warning / Error to match the EventBus
19
+ * console-sink style.
58
20
  */
59
- function makePluginLogger(debugMode) {
60
- return Logger.make(({ logLevel, message, date, annotations }) => {
61
- if (debugMode) {
62
- const entry = {
63
- timestamp: date.getTime(),
64
- level: logLevel.label.toLowerCase(),
65
- message: typeof message === "string" ? message : String(message),
66
- ...annotationsToObject(annotations)
67
- };
68
- console.log(JSON.stringify(entry));
69
- } else {
70
- const time = formatTime(date);
71
- const msg = typeof message === "string" ? message : String(message);
72
- const prefix = logLevel._tag === "Warning" ? "⚠️ " : logLevel._tag === "Error" ? "🔴 " : "";
73
- console.log(`[${time}] ${prefix}${msg}`);
74
- }
21
+ function makeSummaryLoggerLayer(logLevel) {
22
+ const effectLevel = logLevel === "none" ? LogLevel.None : logLevel === "error" ? LogLevel.Error : logLevel === "warn" ? LogLevel.Warning : logLevel === "info" ? LogLevel.Info : LogLevel.Debug;
23
+ const pluginLogger = Logger.make(({ logLevel: lvl, message, date }) => {
24
+ const time = formatTime(date);
25
+ const msg = typeof message === "string" ? message : String(message);
26
+ const prefix = lvl._tag === "Warning" ? "⚠️ " : lvl._tag === "Error" ? "🔴 " : "";
27
+ console.log(`[${time}] ${prefix}${msg}`);
75
28
  });
29
+ return Layer.mergeAll(Logger.replace(Logger.defaultLogger, pluginLogger), Logger.minimumLogLevel(effectLevel));
76
30
  }
77
31
  /**
78
- * Create the complete observability layer for the plugin.
79
- * Replaces the default Effect logger with a custom one and sets minimum log level.
32
+ * Compose the console + metrics (+ optional trace) sinks into an EventBus layer.
33
+ *
34
+ * When `traceIsDefault` is true the trace path was derived from the guessed
35
+ * outDir at factory time. In that case we create the sink in deferred mode
36
+ * (no `initialPath`) so no stray empty file is written to the guessed path;
37
+ * `plugin.ts` must call `trace.setPath(realPath)` in the `config()` hook once
38
+ * the real RSPress `outDir` is known.
80
39
  *
81
- * @param logLevel - Plugin log level from options
40
+ * When `traceIsDefault` is false the caller supplied an explicit path string,
41
+ * so we open the file eagerly (existing behaviour).
82
42
  */
83
- function PluginLoggerLayer(logLevel = "info") {
84
- const pluginLogger = makePluginLogger(logLevel === "debug");
85
- const effectLogLevel = {
86
- debug: LogLevel.Debug,
87
- verbose: LogLevel.Debug,
88
- info: LogLevel.Info,
89
- warn: LogLevel.Warning,
90
- error: LogLevel.Error,
91
- none: LogLevel.None
92
- }[logLevel];
93
- return Layer.mergeAll(Logger.replace(Logger.defaultLogger, pluginLogger), Logger.minimumLogLevel(effectLogLevel));
43
+ function buildEventBus(obs, traceIsDefault = false) {
44
+ const sinks = [makeConsoleSink(obs.logLevel, { json: obs.json }), makeMetricsSink()];
45
+ const trace = obs.tracePath ? makeTraceSink(traceIsDefault ? void 0 : obs.tracePath) : null;
46
+ if (trace) sinks.push(trace);
47
+ return {
48
+ layer: makeEventBusLayer(sinks),
49
+ trace
50
+ };
94
51
  }
95
52
  /**
96
53
  * Log a build summary by reading all metric snapshots.
54
+ * Accepts the configured slow-codeblock threshold so the warning message
55
+ * interpolates the actual threshold rather than a hard-coded 100ms.
97
56
  * Replaces the 4 separate logSummary() calls in afterBuild.
98
57
  */
99
- const logBuildSummary = Effect.gen(function* () {
58
+ const logBuildSummary = (slowCodeBlockMs) => Effect.gen(function* () {
100
59
  const filesTotal = yield* Metric.value(BuildMetrics.filesTotal);
101
60
  const filesNew = yield* Metric.value(BuildMetrics.filesNew);
102
61
  const filesModified = yield* Metric.value(BuildMetrics.filesModified);
@@ -105,6 +64,9 @@ const logBuildSummary = Effect.gen(function* () {
105
64
  const prettierErrors = yield* Metric.value(BuildMetrics.prettierErrors);
106
65
  const codeblockTotal = yield* Metric.value(BuildMetrics.codeblockTotal);
107
66
  const codeblockSlow = yield* Metric.value(BuildMetrics.codeblockSlow);
67
+ const pagesGenerated = yield* Metric.value(BuildMetrics.pagesGenerated);
68
+ const externalPackages = yield* Metric.value(BuildMetrics.externalPackagesTotal);
69
+ const phaseDurationSnapshot = yield* Metric.value(BuildMetrics.phaseDuration);
108
70
  const total = filesTotal.count;
109
71
  const newCount = filesNew.count;
110
72
  const modified = filesModified.count;
@@ -122,15 +84,17 @@ const logBuildSummary = Effect.gen(function* () {
122
84
  if (unchanged > 0) parts.push(`${unchanged} unchanged`);
123
85
  yield* Effect.log(`📝 ${total} files (${parts.join(", ")})`);
124
86
  }
125
- if (blocks > 0 && slowBlocks > 0) yield* Effect.logWarning(`⚠️ Code block performance: ${slowBlocks} of ${blocks} blocks were slow (>100ms)`);
87
+ if (pagesGenerated.count > 0) yield* Effect.log(`🧩 ${pagesGenerated.count} pages, ${externalPackages.count} external package(s)`);
88
+ if (phaseDurationSnapshot.count > 0) yield* Effect.log(`⏱ ${phaseDurationSnapshot.count} phase(s) timed`);
89
+ if (blocks > 0 && slowBlocks > 0) yield* Effect.logWarning(`Code block performance: ${slowBlocks} of ${blocks} blocks were slow (>${slowCodeBlockMs}ms)`);
126
90
  const totalErrors = tsErrors + prErrors;
127
91
  if (totalErrors > 0) {
128
92
  const errorParts = [];
129
93
  if (tsErrors > 0) errorParts.push(`${tsErrors} Twoslash`);
130
94
  if (prErrors > 0) errorParts.push(`${prErrors} Prettier`);
131
- yield* Effect.logWarning(`🔴 ${totalErrors} error(s) in code blocks (${errorParts.join(", ")})`);
95
+ yield* Effect.logWarning(`${totalErrors} error(s) in code blocks (${errorParts.join(", ")})`);
132
96
  }
133
97
  });
134
98
 
135
99
  //#endregion
136
- export { BuildMetrics, PluginLoggerLayer, logBuildSummary };
100
+ export { buildEventBus, logBuildSummary, makeSummaryLoggerLayer };
@@ -1,20 +1,134 @@
1
+ import { PluginEvent } from "../observability/events.js";
2
+ import { emit } from "../observability/EventBus.js";
3
+ import { resolveExternalPackageVersions } from "../config-utils.js";
1
4
  import { TypeRegistryError } from "../errors.js";
2
5
  import { TypeRegistryService } from "../services/TypeRegistryService.js";
3
6
  import { Effect, Layer } from "effect";
4
- import { PackageSpec, TypeRegistry } from "type-registry-effect";
5
- import { NodeLayer, createTypeScriptCache } from "type-registry-effect/node";
7
+ import { PackageSpec, RegistryEvent, TypeRegistry, TypeRegistryObserver } from "type-registry-effect";
8
+ import { NodeLayer } from "type-registry-effect/node";
6
9
 
7
10
  //#region src/layers/TypeRegistryServiceLive.ts
8
11
  /**
9
- * TypeRegistryServiceLive: uses type-registry-effect Effect programs directly.
12
+ * Forward type-registry-effect's typed `RegistryEvent`s to the plugin's Effect
13
+ * logger. Since v1 the library emits no logs of its own — observers are the only
14
+ * diagnostic surface — so this restores the build output and routes it through
15
+ * the plugin's configured log level/format (a single source, no duplication).
10
16
  *
11
- * The NodeLayer from type-registry-effect provides CacheService, PackageFetcher,
12
- * and TypeResolver with Node.js platform implementations (FileSystem, HttpClient).
17
+ * The summary (`BatchComplete`) and failures are surfaced at info/warning;
18
+ * per-package detail stays at debug so a normal build is quiet.
19
+ */
20
+ const RegistryObserverLayer = Layer.succeed(TypeRegistryObserver, { emit: (event) => RegistryEvent.$match(event, {
21
+ VersionResolved: ({ package: pkg, requested, resolved }) => emit(PluginEvent.TypeRegistryEvent({
22
+ ctx: {
23
+ buildId: "",
24
+ packageName: pkg
25
+ },
26
+ level: "debug",
27
+ kind: "VersionResolved",
28
+ detail: `${requested} -> ${resolved}`
29
+ })),
30
+ VersionResolveFailed: ({ package: pkg, requested, reason }) => emit(PluginEvent.TypeRegistryEvent({
31
+ ctx: {
32
+ buildId: "",
33
+ packageName: pkg
34
+ },
35
+ level: "debug",
36
+ kind: "VersionResolveFailed",
37
+ detail: `${requested}: ${reason}`
38
+ })),
39
+ CacheHit: ({ package: pkg, version }) => emit(PluginEvent.TypeRegistryEvent({
40
+ ctx: {
41
+ buildId: "",
42
+ packageName: pkg,
43
+ version
44
+ },
45
+ level: "debug",
46
+ kind: "CacheHit",
47
+ detail: ""
48
+ })),
49
+ CacheStale: ({ package: pkg, version }) => emit(PluginEvent.TypeRegistryEvent({
50
+ ctx: {
51
+ buildId: "",
52
+ packageName: pkg,
53
+ version
54
+ },
55
+ level: "debug",
56
+ kind: "CacheStale",
57
+ detail: ""
58
+ })),
59
+ CacheMiss: ({ package: pkg, version }) => emit(PluginEvent.TypeRegistryEvent({
60
+ ctx: {
61
+ buildId: "",
62
+ packageName: pkg,
63
+ version
64
+ },
65
+ level: "debug",
66
+ kind: "CacheMiss",
67
+ detail: ""
68
+ })),
69
+ FetchStart: ({ package: pkg, version }) => emit(PluginEvent.TypeRegistryEvent({
70
+ ctx: {
71
+ buildId: "",
72
+ packageName: pkg,
73
+ version
74
+ },
75
+ level: "debug",
76
+ kind: "FetchStart",
77
+ detail: ""
78
+ })),
79
+ FetchFailed: ({ url, status, bodySnippet }) => emit(PluginEvent.TypeRegistryEvent({
80
+ ctx: { buildId: "" },
81
+ level: "debug",
82
+ kind: "FetchFailed",
83
+ detail: `HTTP ${status}: ${url}${bodySnippet ? ` — ${bodySnippet}` : ""}`
84
+ })),
85
+ PackageLoaded: ({ package: pkg, version, files, source }) => emit(PluginEvent.TypeRegistryEvent({
86
+ ctx: {
87
+ buildId: "",
88
+ packageName: pkg,
89
+ version
90
+ },
91
+ level: "debug",
92
+ kind: "PackageLoaded",
93
+ detail: `${files} files, ${source}`
94
+ })),
95
+ PackageLoadFailed: ({ package: pkg, version, kind, message }) => emit(PluginEvent.TypeRegistryEvent({
96
+ ctx: {
97
+ buildId: "",
98
+ packageName: pkg,
99
+ version
100
+ },
101
+ level: "warn",
102
+ kind: "PackageLoadFailed",
103
+ detail: `[${kind}] ${message}`
104
+ })),
105
+ BatchStart: ({ total }) => emit(PluginEvent.TypeRegistryEvent({
106
+ ctx: { buildId: "" },
107
+ level: "debug",
108
+ kind: "BatchStart",
109
+ detail: `${total} package(s)`
110
+ })),
111
+ BatchComplete: ({ loaded, total, totalFiles, durationMs }) => emit(PluginEvent.TypeRegistryEvent({
112
+ ctx: { buildId: "" },
113
+ level: "info",
114
+ kind: "BatchComplete",
115
+ detail: `${loaded}/${total} packages, ${totalFiles} files, ${durationMs}ms`
116
+ }))
117
+ }) });
118
+ /**
119
+ * type-registry-effect runtime: the Node platform layer plus the observer that
120
+ * forwards registry events to the plugin logger.
13
121
  *
14
- * Built-in metrics (packagesLoaded, packagesFailed, cacheHits, etc.) are
15
- * automatically tracked by the upstream library.
122
+ * `NodeLayer` provides CacheService, PackageFetcher, and TypeResolver with
123
+ * Node.js platform implementations (FileSystem, HttpClient). Built-in metrics
124
+ * (packagesLoaded, packagesFailed, cacheHits, etc.) are tracked by the library.
125
+ */
126
+ const RegistryLayer = Layer.merge(NodeLayer, RegistryObserverLayer);
127
+ /**
128
+ * TypeRegistryServiceLive: uses type-registry-effect Effect programs directly.
16
129
  */
17
130
  const TypeRegistryServiceLive = Layer.succeed(TypeRegistryService, {
131
+ resolveVersions: (packages) => resolveExternalPackageVersions(packages, (pkg) => TypeRegistry.resolveVersion(pkg.name, pkg.version)).pipe(Effect.provide(RegistryLayer)),
18
132
  loadPackages: (packages) => Effect.gen(function* () {
19
133
  if (packages.length === 0) return { vfs: /* @__PURE__ */ new Map() };
20
134
  const specs = packages.map((pkg) => new PackageSpec({
@@ -26,20 +140,7 @@ const TypeRegistryServiceLive = Layer.succeed(TypeRegistryService, {
26
140
  version: packages.map((p) => p.version).join(", "),
27
141
  reason: error.message ?? String(error)
28
142
  })))) };
29
- }).pipe(Effect.provide(NodeLayer)),
30
- createTypeScriptCache: (packages, compilerOptions) => Effect.tryPromise({
31
- try: () => {
32
- return createTypeScriptCache(packages.map((pkg) => new PackageSpec({
33
- name: pkg.name,
34
- version: pkg.version
35
- })), compilerOptions);
36
- },
37
- catch: (error) => new TypeRegistryError({
38
- packageName: packages.map((p) => p.name).join(", "),
39
- version: "",
40
- reason: error instanceof Error ? error.message : String(error)
41
- })
42
- })
143
+ }).pipe(Effect.provide(RegistryLayer))
43
144
  });
44
145
 
45
146
  //#endregion
@@ -0,0 +1,61 @@
1
+ import { Metric, MetricBoundaries } from "effect";
2
+
3
+ //#region src/layers/build-metrics.ts
4
+ /**
5
+ * All build metrics as named counters/histograms.
6
+ *
7
+ * Note: Effect Metrics use a process-wide registry. In tests, counters
8
+ * accumulate across test cases within the same process. Test assertions
9
+ * should use loose matching (toContain) rather than exact count checks.
10
+ *
11
+ * Extracted into its own module so that `metrics-sink.ts` can import it
12
+ * without creating a circular dependency through `ObservabilityLive.ts`
13
+ * (which itself imports `metrics-sink.ts`).
14
+ */
15
+ const BuildMetrics = {
16
+ filesTotal: Metric.counter("files.total"),
17
+ filesNew: Metric.counter("files.new"),
18
+ filesModified: Metric.counter("files.modified"),
19
+ filesUnchanged: Metric.counter("files.unchanged"),
20
+ codeblockDuration: Metric.histogram("codeblock.duration", MetricBoundaries.fromIterable([
21
+ 10,
22
+ 25,
23
+ 50,
24
+ 100,
25
+ 200,
26
+ 500,
27
+ 1e3
28
+ ])),
29
+ codeblockShikiDuration: Metric.histogram("codeblock.shiki.duration", MetricBoundaries.fromIterable([
30
+ 5,
31
+ 10,
32
+ 25,
33
+ 50,
34
+ 100,
35
+ 250
36
+ ])),
37
+ codeblockTotal: Metric.counter("codeblock.total"),
38
+ codeblockSlow: Metric.counter("codeblock.slow"),
39
+ twoslashErrors: Metric.counter("twoslash.errors"),
40
+ prettierErrors: Metric.counter("prettier.errors"),
41
+ pagesGenerated: Metric.counter("pages.generated"),
42
+ apiVersionsLoaded: Metric.counter("api.versions.loaded"),
43
+ externalPackagesTotal: Metric.counter("external.packages.total"),
44
+ phaseDuration: Metric.histogram("phase.duration", MetricBoundaries.fromIterable([
45
+ 50,
46
+ 100,
47
+ 250,
48
+ 500,
49
+ 1e3,
50
+ 2500,
51
+ 5e3,
52
+ 1e4
53
+ ])),
54
+ vfsFiles: Metric.counter("vfs.files"),
55
+ importsPrepended: Metric.counter("imports.prepended"),
56
+ twoslashDiagnostics: Metric.counter("twoslash.diagnostics"),
57
+ configDefaultsApplied: Metric.counter("config.defaults.applied")
58
+ };
59
+
60
+ //#endregion
61
+ export { BuildMetrics };
package/llms-program.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { PluginEvent } from "./observability/events.js";
2
+ import { emit } from "./observability/EventBus.js";
1
3
  import { filterLlmsFullTxt, filterLlmsTxt, generatePackageLlmsFullTxt, generatePackageLlmsTxt, generateStructuredLlmsTxt, parseLlmsTxtLine } from "./llms-processing.js";
2
4
  import path from "node:path";
3
5
  import { Effect } from "effect";
@@ -173,19 +175,24 @@ function collectApiPageContent(globalLlmsFullContent, result) {
173
175
  function processLlmsFiles(input) {
174
176
  return Effect.gen(function* () {
175
177
  const fs = yield* FileSystem.FileSystem;
176
- const { outDir, buildResults, llmsPlugin, packageRoutes } = input;
178
+ const { outDir, buildResults, llmsPlugin, packageRoutes, buildId } = input;
179
+ const ctx = { buildId };
177
180
  if (buildResults.length === 0) return;
178
181
  const apiRoutes = buildApiRoutes(buildResults);
179
- yield* Effect.logDebug(`Built ${apiRoutes.size} API routes for LLMs filtering`);
182
+ yield* emit(PluginEvent.LlmsRoutesBuilt({
183
+ ctx,
184
+ level: "debug",
185
+ count: apiRoutes.size
186
+ }));
180
187
  if (apiRoutes.size === 0) return;
181
188
  const prefixes = discoverPrefixes(buildResults);
182
- yield* Effect.forEach([...prefixes], (prefix) => processPrefix(fs, outDir, prefix, buildResults, apiRoutes, llmsPlugin, packageRoutes), { concurrency: "unbounded" });
189
+ yield* Effect.forEach([...prefixes], (prefix) => processPrefix(fs, outDir, prefix, buildResults, apiRoutes, llmsPlugin, packageRoutes, ctx), { concurrency: "unbounded" });
183
190
  });
184
191
  }
185
192
  /**
186
193
  * Process global and per-package LLMs files for a single prefix.
187
194
  */
188
- function processPrefix(fs, outDir, prefix, buildResults, apiRoutes, llmsPlugin, packageRoutes) {
195
+ function processPrefix(fs, outDir, prefix, buildResults, apiRoutes, llmsPlugin, packageRoutes, ctx) {
189
196
  return Effect.gen(function* () {
190
197
  const prefixDir = prefix ? path.join(outDir, prefix) : outDir;
191
198
  const llmsTxtPath = path.join(prefixDir, "llms.txt");
@@ -216,19 +223,25 @@ function processPrefix(fs, outDir, prefix, buildResults, apiRoutes, llmsPlugin,
216
223
  }
217
224
  if (llmsPlugin.scopes) {
218
225
  const prefixResults = prefix === "" ? [...buildResults] : buildResults.filter((r) => r.baseRoute.startsWith(`/${prefix}/`));
219
- yield* Effect.forEach(prefixResults, (result) => generatePerPackageFiles(fs, outDir, result, llmsTxtContent, llmsFullTxtContent, apiRoutes, llmsPlugin, packageRoutes.get(result.packageName) ?? result.baseRoute), { concurrency: "unbounded" });
226
+ yield* Effect.forEach(prefixResults, (result) => generatePerPackageFiles(fs, outDir, result, llmsTxtContent, llmsFullTxtContent, apiRoutes, llmsPlugin, packageRoutes.get(result.packageName) ?? result.baseRoute, ctx), { concurrency: "unbounded" });
220
227
  }
228
+ yield* emit(PluginEvent.LlmsPrefixProcessed({
229
+ ctx,
230
+ level: "debug",
231
+ prefix
232
+ }));
221
233
  });
222
234
  }
223
235
  /**
224
236
  * Generate per-package LLMs files (llms.txt, llms-full.txt, llms-docs.txt, llms-api.txt).
225
237
  */
226
- function generatePerPackageFiles(fs, outDir, result, globalLlmsTxtContent, globalLlmsFullContent, apiRoutes, llmsPlugin, packageRoute) {
238
+ function generatePerPackageFiles(fs, outDir, result, globalLlmsTxtContent, globalLlmsFullContent, apiRoutes, llmsPlugin, packageRoute, ctx) {
227
239
  return Effect.gen(function* () {
228
240
  const pkgRouteSegment = packageRoute.replace(/^\//, "");
229
241
  const packageLlmsDir = pkgRouteSegment ? path.join(outDir, pkgRouteSegment) : outDir;
230
242
  yield* fs.makeDirectory(packageLlmsDir, { recursive: true }).pipe(Effect.orDie);
231
243
  const displayName = result.apiName ?? result.packageName;
244
+ const writtenFiles = [];
232
245
  const apiEntries = collectApiEntries(globalLlmsTxtContent, result);
233
246
  const guideEntries = collectGuideEntries(globalLlmsTxtContent, apiRoutes, packageRoute);
234
247
  const packageLlmsTxt = generatePackageLlmsTxt({
@@ -238,6 +251,7 @@ function generatePerPackageFiles(fs, outDir, result, globalLlmsTxtContent, globa
238
251
  apiPages: apiEntries
239
252
  });
240
253
  yield* fs.writeFileString(path.join(packageLlmsDir, "llms.txt"), packageLlmsTxt).pipe(Effect.orDie);
254
+ writtenFiles.push("llms.txt");
241
255
  const apiPageContent = collectApiPageContent(globalLlmsFullContent, result);
242
256
  const guideRouteUrls = new Set(guideEntries.map((e) => e.url));
243
257
  const guidePageContent = globalLlmsFullContent ? extractSections(globalLlmsFullContent, (url) => guideRouteUrls.has(url)) : [];
@@ -245,16 +259,24 @@ function generatePerPackageFiles(fs, outDir, result, globalLlmsTxtContent, globa
245
259
  if (fullPageContent.length > 0) {
246
260
  const packageLlmsFullTxt = generatePackageLlmsFullTxt(fullPageContent);
247
261
  yield* fs.writeFileString(path.join(packageLlmsDir, "llms-full.txt"), packageLlmsFullTxt).pipe(Effect.orDie);
262
+ writtenFiles.push("llms-full.txt");
248
263
  }
249
264
  if (llmsPlugin.apiTxt && apiPageContent.length > 0) {
250
265
  const apiTxtContent = generatePackageLlmsFullTxt(apiPageContent);
251
266
  yield* fs.writeFileString(path.join(packageLlmsDir, "llms-api.txt"), apiTxtContent).pipe(Effect.orDie);
267
+ writtenFiles.push("llms-api.txt");
252
268
  }
253
269
  if (guidePageContent.length > 0) {
254
270
  const docsTxtContent = generatePackageLlmsFullTxt(guidePageContent);
255
271
  yield* fs.writeFileString(path.join(packageLlmsDir, "llms-docs.txt"), docsTxtContent).pipe(Effect.orDie);
272
+ writtenFiles.push("llms-docs.txt");
256
273
  }
257
- yield* Effect.logDebug(`Generated LLMs files for ${displayName} in ${packageLlmsDir}`);
274
+ yield* emit(PluginEvent.LlmsPackageFilesGenerated({
275
+ ctx,
276
+ level: "debug",
277
+ dir: packageLlmsDir,
278
+ files: writtenFiles
279
+ }));
258
280
  });
259
281
  }
260
282
 
package/loader.js CHANGED
@@ -1,7 +1,15 @@
1
+ import { PluginEvent } from "./observability/events.js";
1
2
  import { ApiDocumentedItem, ApiItemKind } from "@microsoft/api-extractor-model";
2
3
  import { extractPlainText, getDeprecation, getExamples, getParams, getReleaseTag, getReturns, getSummary, hasModifierTag } from "api-extractor-llms";
3
4
 
4
5
  //#region src/loader.ts
6
+ /** Module-level emitter injected by plugin.ts at startup. */
7
+ let emitEvent = () => {};
8
+ let currentBuildId = "";
9
+ function setLoaderEventEmitter(fn, buildId = "") {
10
+ emitEvent = fn;
11
+ currentBuildId = buildId;
12
+ }
5
13
  /**
6
14
  * Parser for extracting and analyzing information from API Extractor models and TSDoc comments
7
15
  */
@@ -53,7 +61,13 @@ var ApiParser = class ApiParser {
53
61
  break;
54
62
  }
55
63
  }
56
- if (!categorized && typeof process !== "undefined" && !process.env.VITEST) console.warn(`⚠️ API item "${member.displayName}" (kind: ${member.kind}) not categorized`);
64
+ if (!categorized && typeof process !== "undefined" && !process.env.VITEST) emitEvent(PluginEvent.ItemSkipped({
65
+ ctx: { buildId: currentBuildId },
66
+ item: member.displayName,
67
+ kind: String(member.kind),
68
+ reason: "uncategorized",
69
+ level: "warn"
70
+ }));
57
71
  }
58
72
  return items;
59
73
  }
@@ -183,4 +197,4 @@ var ApiParser = class ApiParser {
183
197
  };
184
198
 
185
199
  //#endregion
186
- export { ApiParser };
200
+ export { ApiParser, setLoaderEventEmitter };
@@ -1,4 +1,13 @@
1
+ import { PluginEvent } from "../observability/events.js";
2
+
1
3
  //#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
+ }
2
11
  /**
3
12
  * Default Shiki theme configuration
4
13
  */
@@ -39,10 +48,15 @@ async function generateShikiHast(code, highlighter, transformers, enableTwoslash
39
48
  if (enableTwoslash) options.meta = { __raw: "twoslash" };
40
49
  return await highlighter.codeToHast(code, options);
41
50
  } catch (error) {
42
- console.error("Failed to generate Shiki HAST:", error);
51
+ emitEvent(PluginEvent.ShikiError({
52
+ ctx: { buildId: currentBuildId },
53
+ file: "unknown",
54
+ reason: String(error),
55
+ level: "warn"
56
+ }));
43
57
  return null;
44
58
  }
45
59
  }
46
60
 
47
61
  //#endregion
48
- export { DEFAULT_SHIKI_THEMES, generateShikiHast };
62
+ export { DEFAULT_SHIKI_THEMES, generateShikiHast, setShikiUtilsEventEmitter };
@@ -0,0 +1,38 @@
1
+ import { LEVEL_RANK, levelOf } from "./events.js";
2
+ import { Context, Effect, Layer, Option } from "effect";
3
+
4
+ //#region src/observability/EventBus.ts
5
+ var EventBus = class extends Context.Tag("rspress-plugin-api-extractor/EventBus")() {};
6
+ function makeShape(sinks) {
7
+ const maxAdmitted = sinks.filter((s) => s.capturesPayload === true).reduce((max, s) => Math.max(max, LEVEL_RANK[s.minLevel]), -1);
8
+ return {
9
+ emit: (event) => Effect.sync(() => {
10
+ const rank = LEVEL_RANK[levelOf(event)];
11
+ for (const sink of sinks) if (rank <= LEVEL_RANK[sink.minLevel]) sink.handle(event);
12
+ }),
13
+ wantsLevel: (level) => Effect.succeed(LEVEL_RANK[level] <= maxAdmitted)
14
+ };
15
+ }
16
+ function makeEventBusLayer(sinks) {
17
+ return Layer.succeed(EventBus, makeShape(sinks));
18
+ }
19
+ /** No sinks: every emit is a no-op, wantsLevel always false. */
20
+ const EventBusNoop = makeEventBusLayer([]);
21
+ /** Emit when a bus is in context; silently no-op otherwise. */
22
+ function emit(event) {
23
+ return Effect.serviceOption(EventBus).pipe(Effect.flatMap((maybe) => Option.isSome(maybe) ? maybe.value.emit(event) : Effect.void));
24
+ }
25
+ /**
26
+ * Returns true when a bus is in context and has at least one sink admitted at
27
+ * `level`; false otherwise. R = never, safe to use anywhere emit is used.
28
+ */
29
+ function wantsLevel(level) {
30
+ return Effect.serviceOption(EventBus).pipe(Effect.flatMap((maybe) => Option.isSome(maybe) ? maybe.value.wantsLevel(level) : Effect.succeed(false)));
31
+ }
32
+ /** Bind a runtime so non-Effect (sync island) callbacks can emit. */
33
+ function makeRuntimeEmitter(runtime) {
34
+ return (event) => runtime.runSync(emit(event));
35
+ }
36
+
37
+ //#endregion
38
+ export { emit, makeEventBusLayer, makeRuntimeEmitter, wantsLevel };
@@ -0,0 +1,17 @@
1
+ import { Data } from "effect";
2
+
3
+ //#region src/observability/events.ts
4
+ const LEVEL_RANK = {
5
+ error: 0,
6
+ warn: 1,
7
+ info: 2,
8
+ debug: 3,
9
+ trace: 4
10
+ };
11
+ const PluginEvent = Data.taggedEnum();
12
+ function levelOf(event) {
13
+ return event.level;
14
+ }
15
+
16
+ //#endregion
17
+ export { LEVEL_RANK, PluginEvent, levelOf };