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.
- package/BuildEnv.js +58 -0
- package/README.md +2 -1
- package/build-program.js +33 -30
- package/build-stages.js +47 -39
- package/errors.js +0 -1
- package/index.d.ts +22 -14
- package/layers/ConfigServiceLive.js +349 -400
- package/layers/HighlighterServiceLive.js +52 -0
- package/layers/ObservabilityLive.js +26 -7
- package/layers/OgServiceLive.js +134 -0
- package/layers/TwoslashCacheServiceLive.js +108 -0
- package/layers/TwoslashEnvironmentsLive.js +33 -0
- package/layers/TypeRegistryServiceLive.js +54 -47
- package/layers/build-metrics.js +32 -5
- package/layers/xdg.js +44 -0
- package/markdown/helpers.js +9 -55
- package/markdown/page-generators/class-page.js +8 -31
- package/markdown/page-generators/index-pages.js +6 -8
- package/markdown/page-generators/interface-page.js +7 -7
- package/markdown/shiki-utils.js +65 -10
- package/observability/EventBus.js +29 -9
- package/observability/heartbeat.js +1 -1
- package/observability/metric-report.js +124 -0
- package/observability/sinks/console-sink.js +6 -0
- package/observability/sinks/metrics-sink.js +64 -21
- package/observability/sinks/render-sink.js +86 -0
- package/observability/sinks/trace-sink.js +10 -17
- package/observability/spans.js +4 -2
- package/observability/sync-emitter.js +78 -0
- package/og-resolver.js +46 -287
- package/package.json +4 -5
- package/path-derivation.js +19 -1
- package/plugin.js +64 -52
- package/prettier-formatter.js +4 -10
- package/remark-api-codeblocks.js +33 -15
- package/remark-with-api.js +24 -27
- package/schemas/config.js +11 -7
- package/services/HighlighterService.js +30 -0
- package/services/OgService.js +23 -0
- package/services/PluginConfig.js +26 -0
- package/services/TwoslashCacheService.js +15 -0
- package/services/TwoslashEnvironments.js +7 -0
- package/shiki-transformer.js +55 -256
- package/twoslash-access.js +48 -0
- package/twoslash-cache.js +174 -0
- package/twoslash-patterns.js +1 -1
- package/twoslash-timing-wrapper.js +23 -0
- package/twoslash-transformer.js +153 -89
- package/vfs-registry.js +1 -31
- package/layers/PathDerivationServiceLive.js +0 -16
- package/runtime/components/MarkdownText/index.js +0 -34
- package/services/PathDerivationService.js +0 -7
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { BuildMetrics } from "../../layers/build-metrics.js";
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
49
|
+
update(BuildMetrics.pagesGenerated, 1);
|
|
35
50
|
break;
|
|
36
51
|
case "ApiDocsCompleted":
|
|
37
|
-
|
|
52
|
+
update(BuildMetrics.apisCompleted, 1);
|
|
38
53
|
break;
|
|
39
54
|
case "TwoslashDiagnostic":
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
95
|
+
update(BuildMetrics.vfsFiles, 1);
|
|
54
96
|
break;
|
|
55
97
|
case "ImportsPrepended":
|
|
56
|
-
|
|
98
|
+
update(BuildMetrics.importsPrepended, 1);
|
|
57
99
|
break;
|
|
58
100
|
case "PhaseCompleted":
|
|
59
|
-
|
|
101
|
+
update(BuildMetrics.phaseDuration, event.durationMs);
|
|
102
|
+
both(BuildMetrics.phaseTimeMs, event.durationMs, { phase: event.phase });
|
|
60
103
|
break;
|
|
61
|
-
case "DefaultApplied":
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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(
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
package/observability/spans.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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 };
|