rspress-plugin-api-extractor 0.9.2 → 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.
Files changed (52) hide show
  1. package/BuildEnv.js +58 -0
  2. package/README.md +2 -1
  3. package/build-program.js +33 -30
  4. package/build-stages.js +47 -39
  5. package/errors.js +0 -1
  6. package/index.d.ts +22 -14
  7. package/layers/ConfigServiceLive.js +349 -400
  8. package/layers/HighlighterServiceLive.js +52 -0
  9. package/layers/ObservabilityLive.js +26 -7
  10. package/layers/OgServiceLive.js +134 -0
  11. package/layers/TwoslashCacheServiceLive.js +108 -0
  12. package/layers/TwoslashEnvironmentsLive.js +33 -0
  13. package/layers/TypeRegistryServiceLive.js +54 -47
  14. package/layers/build-metrics.js +32 -5
  15. package/layers/xdg.js +44 -0
  16. package/markdown/helpers.js +9 -55
  17. package/markdown/page-generators/class-page.js +8 -31
  18. package/markdown/page-generators/index-pages.js +6 -8
  19. package/markdown/page-generators/interface-page.js +7 -7
  20. package/markdown/shiki-utils.js +65 -10
  21. package/observability/EventBus.js +29 -9
  22. package/observability/heartbeat.js +1 -1
  23. package/observability/metric-report.js +124 -0
  24. package/observability/sinks/console-sink.js +6 -0
  25. package/observability/sinks/metrics-sink.js +64 -21
  26. package/observability/sinks/render-sink.js +86 -0
  27. package/observability/sinks/trace-sink.js +10 -17
  28. package/observability/spans.js +4 -2
  29. package/observability/sync-emitter.js +78 -0
  30. package/og-resolver.js +46 -287
  31. package/package.json +4 -5
  32. package/path-derivation.js +19 -1
  33. package/plugin.js +64 -52
  34. package/prettier-formatter.js +4 -10
  35. package/remark-api-codeblocks.js +33 -15
  36. package/remark-with-api.js +24 -27
  37. package/schemas/config.js +11 -7
  38. package/services/HighlighterService.js +30 -0
  39. package/services/OgService.js +23 -0
  40. package/services/PluginConfig.js +26 -0
  41. package/services/TwoslashCacheService.js +15 -0
  42. package/services/TwoslashEnvironments.js +7 -0
  43. package/shiki-transformer.js +55 -256
  44. package/twoslash-access.js +48 -0
  45. package/twoslash-cache.js +174 -0
  46. package/twoslash-patterns.js +1 -1
  47. package/twoslash-timing-wrapper.js +23 -0
  48. package/twoslash-transformer.js +153 -89
  49. package/vfs-registry.js +1 -31
  50. package/layers/PathDerivationServiceLive.js +0 -16
  51. package/runtime/components/MarkdownText/index.js +0 -34
  52. package/services/PathDerivationService.js +0 -7
@@ -1,17 +1,22 @@
1
1
  import { BuildMetrics } from "../../layers/build-metrics.js";
2
- import { Effect, Metric } from "effect";
2
+ import { Metric } from "effect";
3
3
 
4
4
  //#region src/observability/sinks/metrics-sink.ts
5
5
  /**
6
6
  * Event-driven metrics sink.
7
7
  *
8
8
  * Translates each `PluginEvent` to the corresponding `BuildMetrics` counter or
9
- * histogram update via `Effect.runSync`. The fan-out from `EventBus.emit` is
9
+ * histogram update against the build's metric registry. The fan-out from `EventBus.emit` is
10
10
  * synchronous, so by the time the emitting fiber resumes the metrics are already
11
11
  * recorded — counts are exact when `logBuildSummary` reads them in `afterBuild`.
12
12
  *
13
13
  * Unmapped event tags hit the `default` branch and are silently ignored.
14
14
  *
15
+ * Events whose breakdown is worth querying are recorded twice — undimensioned
16
+ * for the build-wide totals, and tagged for `metric-report.ts`. The attributes
17
+ * are deliberately bounded (scope, status, phase, TS code); anything unbounded,
18
+ * such as a file path, belongs in a sample-shaped sink instead.
19
+ *
15
20
  * Intentionally NOT derived here: `externalPackagesTotal` and `apiVersionsLoaded`
16
21
  * remain inline increments in `ConfigServiceLive`. `externalPackagesTotal` counts
17
22
  * CONFIGURED packages via `incrementBy(length)`; the only candidate event,
@@ -19,46 +24,84 @@ import { Effect, Metric } from "effect";
19
24
  * a `loaded` (SUCCEEDED) count — different semantics, so deriving it here would
20
25
  * change what the metric means. `apiVersionsLoaded` has no corresponding event.
21
26
  */
22
- function makeMetricsSink() {
27
+ function makeMetricsSink(context) {
28
+ const update = (metric, input) => metric.updateUnsafe(input, context);
29
+ /** Record `metric` twice: undimensioned, and tagged for breakdown queries. */
30
+ const both = (metric, input, attributes) => {
31
+ update(metric, input);
32
+ update(Metric.withAttributes(metric, attributes), input);
33
+ };
34
+ const scopeOf = (event) => event.ctx.apiScope ?? "(unscoped)";
23
35
  return {
24
36
  minLevel: "trace",
25
37
  handle(event) {
26
38
  switch (event._tag) {
27
39
  case "FileDecision":
28
- Effect.runSync(Metric.update(BuildMetrics.filesTotal, 1));
29
- if (event.status === "new") Effect.runSync(Metric.update(BuildMetrics.filesNew, 1));
30
- else if (event.status === "modified") Effect.runSync(Metric.update(BuildMetrics.filesModified, 1));
31
- else Effect.runSync(Metric.update(BuildMetrics.filesUnchanged, 1));
40
+ both(BuildMetrics.filesTotal, 1, {
41
+ scope: scopeOf(event),
42
+ status: event.status
43
+ });
44
+ if (event.status === "new") update(BuildMetrics.filesNew, 1);
45
+ else if (event.status === "modified") update(BuildMetrics.filesModified, 1);
46
+ else update(BuildMetrics.filesUnchanged, 1);
32
47
  break;
33
48
  case "PageGenerated":
34
- Effect.runSync(Metric.update(BuildMetrics.pagesGenerated, 1));
49
+ update(BuildMetrics.pagesGenerated, 1);
35
50
  break;
36
51
  case "ApiDocsCompleted":
37
- Effect.runSync(Metric.update(BuildMetrics.apisCompleted, 1));
52
+ update(BuildMetrics.apisCompleted, 1);
38
53
  break;
39
54
  case "TwoslashDiagnostic":
40
- Effect.runSync(Metric.update(BuildMetrics.twoslashDiagnostics, 1));
41
- Effect.runSync(Metric.update(BuildMetrics.twoslashErrors, 1));
55
+ both(BuildMetrics.twoslashDiagnostics, 1, {
56
+ code: `TS${event.code}`,
57
+ scope: scopeOf(event)
58
+ });
59
+ update(BuildMetrics.twoslashErrors, 1);
42
60
  break;
43
61
  case "PrettierError":
44
- Effect.runSync(Metric.update(BuildMetrics.prettierErrors, 1));
62
+ both(BuildMetrics.prettierErrors, 1, { scope: scopeOf(event) });
63
+ break;
64
+ case "ShikiError":
65
+ both(BuildMetrics.shikiErrors, 1, { scope: scopeOf(event) });
45
66
  break;
46
- case "CodeBlockProcessed":
47
- Effect.runSync(Metric.update(BuildMetrics.codeblockTotal, 1));
48
- Effect.runSync(Metric.update(BuildMetrics.codeblockDuration, event.totalMs));
49
- if (event.shikiMs > 0) Effect.runSync(Metric.update(BuildMetrics.codeblockShikiDuration, event.shikiMs));
50
- if (event.slow) Effect.runSync(Metric.update(BuildMetrics.codeblockSlow, 1));
67
+ case "CodeBlockProcessed": {
68
+ const attrs = {
69
+ scope: event.ctx.apiScope ?? "(unscoped)",
70
+ component: event.component,
71
+ twoslash: String(event.twoslash)
72
+ };
73
+ const tagged = (metric) => Metric.withAttributes(metric, attrs);
74
+ update(BuildMetrics.codeblockTotal, 1);
75
+ update(tagged(BuildMetrics.codeblockTotal), 1);
76
+ update(BuildMetrics.codeblockDuration, event.totalMs);
77
+ update(BuildMetrics.codeblockTimeMs, event.totalMs);
78
+ update(tagged(BuildMetrics.codeblockTimeMs), event.totalMs);
79
+ update(BuildMetrics.codeblockTwoslashMs, event.twoslashMs);
80
+ update(tagged(BuildMetrics.codeblockTwoslashMs), event.twoslashMs);
81
+ update(BuildMetrics.codeblockShikiMs, event.shikiMs);
82
+ update(tagged(BuildMetrics.codeblockShikiMs), event.shikiMs);
83
+ if (event.twoslash) {
84
+ update(BuildMetrics.codeblockTwoslashTotal, 1);
85
+ update(tagged(BuildMetrics.codeblockTwoslashTotal), 1);
86
+ }
87
+ if (event.shikiMs > 0) update(BuildMetrics.codeblockShikiDuration, event.shikiMs);
88
+ if (event.slow) {
89
+ update(BuildMetrics.codeblockSlow, 1);
90
+ update(tagged(BuildMetrics.codeblockSlow), 1);
91
+ }
51
92
  break;
93
+ }
52
94
  case "VfsGenerated":
53
- Effect.runSync(Metric.update(BuildMetrics.vfsFiles, 1));
95
+ update(BuildMetrics.vfsFiles, 1);
54
96
  break;
55
97
  case "ImportsPrepended":
56
- Effect.runSync(Metric.update(BuildMetrics.importsPrepended, 1));
98
+ update(BuildMetrics.importsPrepended, 1);
57
99
  break;
58
100
  case "PhaseCompleted":
59
- Effect.runSync(Metric.update(BuildMetrics.phaseDuration, event.durationMs));
101
+ update(BuildMetrics.phaseDuration, event.durationMs);
102
+ both(BuildMetrics.phaseTimeMs, event.durationMs, { phase: event.phase });
60
103
  break;
61
- case "DefaultApplied": Effect.runSync(Metric.update(BuildMetrics.configDefaultsApplied, 1));
104
+ case "DefaultApplied": update(BuildMetrics.configDefaultsApplied, 1);
62
105
  }
63
106
  }
64
107
  };
@@ -0,0 +1,86 @@
1
+ import path from "node:path";
2
+ import { Effect, FileSystem } from "effect";
3
+
4
+ //#region src/observability/sinks/render-sink.ts
5
+ const UNSCOPED = "(unscoped)";
6
+ /** How many of the slowest blocks the artifact retains. */
7
+ const SLOWEST_LIMIT = 25;
8
+ function makeRenderSink() {
9
+ let firstAt = 0;
10
+ let lastAt = 0;
11
+ const byFile = /* @__PURE__ */ new Map();
12
+ const slowest = [];
13
+ return {
14
+ minLevel: "trace",
15
+ handle(event) {
16
+ if (event._tag !== "CodeBlockProcessed") return;
17
+ const now = performance.now();
18
+ if (firstAt === 0) firstAt = now;
19
+ lastAt = now;
20
+ const sample = {
21
+ apiScope: event.ctx.apiScope ?? UNSCOPED,
22
+ file: event.ctx.file ?? "unknown",
23
+ component: event.component,
24
+ totalMs: event.totalMs,
25
+ twoslashMs: event.twoslashMs,
26
+ shikiMs: event.shikiMs
27
+ };
28
+ const current = byFile.get(sample.file) ?? {
29
+ blocks: 0,
30
+ twoslashBlocks: 0,
31
+ totalMs: 0,
32
+ twoslashMs: 0
33
+ };
34
+ byFile.set(sample.file, {
35
+ blocks: current.blocks + 1,
36
+ twoslashBlocks: current.twoslashBlocks + (event.twoslash ? 1 : 0),
37
+ totalMs: current.totalMs + sample.totalMs,
38
+ twoslashMs: current.twoslashMs + sample.twoslashMs
39
+ });
40
+ if (slowest.length < SLOWEST_LIMIT || sample.totalMs > (slowest.at(-1)?.totalMs ?? 0)) {
41
+ const at = slowest.findIndex((s) => s.totalMs < sample.totalMs);
42
+ slowest.splice(at === -1 ? slowest.length : at, 0, sample);
43
+ if (slowest.length > SLOWEST_LIMIT) slowest.length = SLOWEST_LIMIT;
44
+ }
45
+ },
46
+ snapshot() {
47
+ return {
48
+ wallMs: firstAt === 0 ? 0 : lastAt - firstAt,
49
+ byFile: Object.fromEntries(byFile),
50
+ slowest: [...slowest]
51
+ };
52
+ }
53
+ };
54
+ }
55
+ /**
56
+ * Write the render-phase attribution artifact to
57
+ * `<cwd>/.api-docs/build/render-phase.json`, combining the metric-derived
58
+ * rollups with the sample-shaped per-file and slowest-block data.
59
+ *
60
+ * Writes nothing when no code block was processed, so a build that never
61
+ * reached the render phase does not leave an empty artifact behind.
62
+ */
63
+ function writeRenderPhaseJson(report, samples, opts) {
64
+ return Effect.gen(function* () {
65
+ if (report.overall.blocks === 0) return;
66
+ const fs = yield* FileSystem.FileSystem;
67
+ const dir = path.join(opts.cwd, ".api-docs", "build");
68
+ yield* fs.makeDirectory(dir, { recursive: true }).pipe(Effect.ignore);
69
+ const doc = {
70
+ generatedAt: opts.generatedAt,
71
+ package: opts.packageName,
72
+ target: "prod",
73
+ overall: report.overall,
74
+ wallMs: samples.wallMs,
75
+ byScope: report.byScope,
76
+ byComponent: report.byComponent,
77
+ series: report.series,
78
+ byFile: samples.byFile,
79
+ slowest: samples.slowest
80
+ };
81
+ yield* fs.writeFileString(path.join(dir, "render-phase.json"), `${JSON.stringify(doc, null, 2)}\n`).pipe(Effect.ignore);
82
+ });
83
+ }
84
+
85
+ //#endregion
86
+ export { makeRenderSink, writeRenderPhaseJson };
@@ -7,30 +7,23 @@ function openTracePath(p) {
7
7
  fs.writeFileSync(p, "");
8
8
  }
9
9
  /**
10
- * Create a JSONL trace sink.
10
+ * Create a JSONL trace sink, opening the file eagerly at construction.
11
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.
12
+ * The path is always known up front: `resolveObservability` derives it from
13
+ * `cwd`, which (unlike the RSPress `outDir`) is available at plugin-factory
14
+ * time. The sink previously supported a deferred mode — construct with no path,
15
+ * bind one later via `setPath` for the era when the path depended on
16
+ * `outDir`; nothing has called it since, so it is gone.
18
17
  */
19
- function makeTraceSink(initialPath) {
20
- let currentPath = initialPath ?? null;
21
- if (currentPath) openTracePath(currentPath);
18
+ function makeTraceSink(tracePath) {
19
+ openTracePath(tracePath);
22
20
  return {
23
21
  minLevel: "trace",
24
22
  capturesPayload: true,
25
23
  handle: (event) => {
26
- if (!currentPath) return;
27
- fs.appendFileSync(currentPath, `${JSON.stringify(event)}\n`);
24
+ fs.appendFileSync(tracePath, `${JSON.stringify(event)}\n`);
28
25
  },
29
- flush: () => {},
30
- setPath: (p) => {
31
- currentPath = p;
32
- openTracePath(p);
33
- }
26
+ flush: () => {}
34
27
  };
35
28
  }
36
29
 
@@ -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",
@@ -54,4 +56,4 @@ function withPhase(phase, ctx, effect, thresholds) {
54
56
  }
55
57
 
56
58
  //#endregion
57
- export { PHASE_THRESHOLD_KEY, withPhase };
59
+ export { withPhase };
@@ -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 };