rspress-plugin-api-extractor 0.9.2 → 0.10.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/README.md +2 -1
- package/build-program.js +2 -1
- package/build-stages.js +1 -1
- package/index.d.ts +22 -14
- package/layers/ConfigServiceLive.js +59 -16
- package/layers/ObservabilityLive.js +26 -7
- package/layers/TwoslashCacheServiceLive.js +53 -0
- package/layers/build-metrics.js +32 -5
- package/markdown/helpers.js +1 -1
- package/observability/EventBus.js +0 -2
- 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 +1 -1
- package/package.json +3 -3
- package/plugin.js +43 -6
- package/remark-api-codeblocks.js +29 -3
- package/remark-with-api.js +13 -6
- package/schemas/config.js +11 -7
- package/services/TwoslashCacheService.js +15 -0
- package/shiki-transformer.js +2 -22
- package/twoslash-cache.js +174 -0
- package/twoslash-patterns.js +1 -1
- package/twoslash-timing-wrapper.js +23 -0
- package/twoslash-transformer.js +51 -10
- package/runtime/components/MarkdownText/index.js +0 -34
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rspress-plugin-api-extractor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "RSPress plugin for generating API documentation from TypeScript API Extractor models",
|
|
6
6
|
"keywords": [
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@effected/glob": "^0.4.0",
|
|
43
43
|
"@effected/jsonc": "^0.8.0",
|
|
44
44
|
"@effected/markdown": "^0.7.0",
|
|
45
|
-
"@effected/npm": "^0.12.
|
|
45
|
+
"@effected/npm": "^0.12.1",
|
|
46
46
|
"@effected/package-json": "^0.12.0",
|
|
47
47
|
"@effected/semver": "^0.5.0",
|
|
48
48
|
"@effected/store": "^0.5.0",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@shikijs/twoslash": "^4.4.3",
|
|
55
55
|
"@tsdoctor/bundle": "0.2.0",
|
|
56
56
|
"@tsdoctor/model": "0.2.2",
|
|
57
|
-
"@tsdoctor/registry": "0.2.
|
|
57
|
+
"@tsdoctor/registry": "0.2.1",
|
|
58
58
|
"@tsdoctor/snapshot": "0.1.1",
|
|
59
59
|
"@typescript/vfs": "^1.6.4",
|
|
60
60
|
"clsx": "^2.1.1",
|
package/plugin.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { PluginEvent } from "./observability/events.js";
|
|
2
2
|
import { emit, makeRuntimeEmitter } from "./observability/EventBus.js";
|
|
3
|
+
import { codeBlockReport } from "./observability/metric-report.js";
|
|
3
4
|
import { runHeartbeat } from "./observability/heartbeat.js";
|
|
4
5
|
import { writeIssuesJson } from "./observability/sinks/issues-sink.js";
|
|
6
|
+
import { writeRenderPhaseJson } from "./observability/sinks/render-sink.js";
|
|
5
7
|
import { buildEventBus, logBuildSummary, makeSummaryLoggerLayer } from "./layers/ObservabilityLive.js";
|
|
6
8
|
import { setPrettierEventEmitter } from "./prettier-formatter.js";
|
|
7
9
|
import { setOgResolverEventEmitter } from "./og-resolver.js";
|
|
@@ -17,8 +19,10 @@ import { resolveObservability } from "./schemas/observability.js";
|
|
|
17
19
|
import { PluginOptions } from "./schemas/config.js";
|
|
18
20
|
import "./schemas/index.js";
|
|
19
21
|
import { ConfigService } from "./services/ConfigService.js";
|
|
22
|
+
import { TwoslashCacheService } from "./services/TwoslashCacheService.js";
|
|
20
23
|
import { ConfigServiceLive } from "./layers/ConfigServiceLive.js";
|
|
21
24
|
import { PathDerivationServiceLive } from "./layers/PathDerivationServiceLive.js";
|
|
25
|
+
import { TwoslashCacheServiceLive } from "./layers/TwoslashCacheServiceLive.js";
|
|
22
26
|
import { TypeRegistryServiceLive } from "./layers/TypeRegistryServiceLive.js";
|
|
23
27
|
import { remarkApiCodeblocks, setRemarkApiCodeblocksEventEmitter } from "./remark-api-codeblocks.js";
|
|
24
28
|
import { remarkWithApi, setRemarkWithApiEventEmitter } from "./remark-with-api.js";
|
|
@@ -83,10 +87,10 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
83
87
|
cwd: process.cwd(),
|
|
84
88
|
buildId
|
|
85
89
|
});
|
|
86
|
-
const { layer: eventBusLayer, trace: traceSink, issues: issuesSink } = buildEventBus(obs);
|
|
90
|
+
const { layer: eventBusLayer, trace: traceSink, issues: issuesSink, render: renderSink, metrics: metricStore } = buildEventBus(obs);
|
|
87
91
|
const dbPath = path.resolve(process.cwd(), ".api-docs", "snapshot", "api-docs.db");
|
|
88
92
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
89
|
-
const BaseLayer = Layer.mergeAll(PathDerivationServiceLive, eventBusLayer, TypeRegistryServiceLive, NodeFileSystem.layer, SnapshotServiceLive(dbPath), makeSummaryLoggerLayer(obs.logLevel));
|
|
93
|
+
const BaseLayer = Layer.mergeAll(PathDerivationServiceLive, eventBusLayer, metricStore.layer, TypeRegistryServiceLive, NodeFileSystem.layer, SnapshotServiceLive(dbPath), TwoslashCacheServiceLive, makeSummaryLoggerLayer(obs.logLevel));
|
|
90
94
|
const EffectAppLayer = Layer.provideMerge(ConfigServiceLive(options, shikiCrossLinker, buildId, obs.thresholds), BaseLayer);
|
|
91
95
|
const effectRuntime = ManagedRuntime.make(EffectAppLayer);
|
|
92
96
|
const emitSync = makeRuntimeEmitter(effectRuntime);
|
|
@@ -95,8 +99,13 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
95
99
|
setPrettierEventEmitter(emitSync, buildId);
|
|
96
100
|
setOgResolverEventEmitter(emitSync, buildId);
|
|
97
101
|
setRemarkWithApiEventEmitter(emitSync, buildId, obs.thresholds.slowCodeBlock);
|
|
98
|
-
setRemarkApiCodeblocksEventEmitter(emitSync, buildId);
|
|
102
|
+
setRemarkApiCodeblocksEventEmitter(emitSync, buildId, obs.thresholds.slowCodeBlock);
|
|
99
103
|
setBuildStagesEventEmitter(emitSync, buildId);
|
|
104
|
+
/**
|
|
105
|
+
* The build's Twoslash result cache, captured in `config()` and persisted in
|
|
106
|
+
* `afterBuild` — the render phase that populates it runs in between.
|
|
107
|
+
*/
|
|
108
|
+
let twoslashCacheHandle = null;
|
|
100
109
|
const fileContextMap = /* @__PURE__ */ new Map();
|
|
101
110
|
let docsRoot;
|
|
102
111
|
let isFirstBuild = true;
|
|
@@ -110,13 +119,37 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
110
119
|
async beforeBuild(_config, _isProd) {},
|
|
111
120
|
async afterBuild(_config, isProd) {
|
|
112
121
|
if (isFirstBuild && !isInert) {
|
|
113
|
-
|
|
122
|
+
const renderSamples = renderSink.snapshot();
|
|
123
|
+
const report = await effectRuntime.runPromise(codeBlockReport);
|
|
124
|
+
if (twoslashCacheHandle) {
|
|
125
|
+
const { cache, envHash } = twoslashCacheHandle;
|
|
126
|
+
const stats = cache.stats();
|
|
127
|
+
await effectRuntime.runPromise(Effect.gen(function* () {
|
|
128
|
+
if (stats.dirty) yield* (yield* TwoslashCacheService).save(envHash, cache.entries());
|
|
129
|
+
yield* emit(PluginEvent.TwoslashCacheSaved({
|
|
130
|
+
ctx: { buildId },
|
|
131
|
+
level: "info",
|
|
132
|
+
envHash,
|
|
133
|
+
hits: stats.hits,
|
|
134
|
+
misses: stats.misses,
|
|
135
|
+
entries: stats.entries,
|
|
136
|
+
persisted: stats.dirty
|
|
137
|
+
}));
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
await effectRuntime.runPromise(logBuildSummary(obs.thresholds.slowCodeBlock, report));
|
|
114
141
|
if (isProd) await effectRuntime.runPromise(Effect.gen(function* () {
|
|
115
142
|
const packageName = yield* readSitePackageName;
|
|
143
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
116
144
|
yield* writeIssuesJson(issuesSink.snapshot(), {
|
|
117
145
|
cwd: process.cwd(),
|
|
118
146
|
packageName,
|
|
119
|
-
generatedAt
|
|
147
|
+
generatedAt
|
|
148
|
+
});
|
|
149
|
+
yield* writeRenderPhaseJson(report, renderSamples, {
|
|
150
|
+
cwd: process.cwd(),
|
|
151
|
+
packageName,
|
|
152
|
+
generatedAt
|
|
120
153
|
});
|
|
121
154
|
}));
|
|
122
155
|
if (rspressLlmsEnabled && resolvedLlmsPlugin.enabled) {
|
|
@@ -205,6 +238,10 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
205
238
|
buildId
|
|
206
239
|
}));
|
|
207
240
|
const buildContext = yield* (yield* ConfigService).resolve(rspressConfigSubset);
|
|
241
|
+
twoslashCacheHandle = {
|
|
242
|
+
cache: buildContext.twoslashCache,
|
|
243
|
+
envHash: buildContext.twoslashEnvHash
|
|
244
|
+
};
|
|
208
245
|
buildResults.length = 0;
|
|
209
246
|
yield* Ref.set(phaseRef, "generate");
|
|
210
247
|
yield* Effect.forEach(buildContext.apiConfigs, (apiConfig) => generateApiDocs({
|
|
@@ -271,7 +308,7 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
271
308
|
const remarkTheme = normalizeThemeConfig(options.api?.theme ?? options.apis?.[0]?.theme);
|
|
272
309
|
updatedConfig.markdown.remarkPlugins.push([remarkWithApi, {
|
|
273
310
|
shikiCrossLinker,
|
|
274
|
-
getTransformer: () => TwoslashManager.getInstance().getTransformer(),
|
|
311
|
+
getTransformer: (apiScope) => TwoslashManager.getInstance().getTransformer(apiScope),
|
|
275
312
|
theme: remarkTheme
|
|
276
313
|
}]);
|
|
277
314
|
updatedConfig.markdown.remarkPlugins.push([remarkApiCodeblocks]);
|
package/remark-api-codeblocks.js
CHANGED
|
@@ -2,15 +2,18 @@ import { PluginEvent } from "./observability/events.js";
|
|
|
2
2
|
import { TwoslashManager } from "./twoslash-transformer.js";
|
|
3
3
|
import { VfsRegistry } from "./vfs-registry.js";
|
|
4
4
|
import { generateShikiHast } from "./markdown/shiki-utils.js";
|
|
5
|
+
import { createTwoslashTimingWrapper } from "./twoslash-timing-wrapper.js";
|
|
5
6
|
import { visit } from "unist-util-visit";
|
|
6
7
|
|
|
7
8
|
//#region src/remark-api-codeblocks.ts
|
|
8
9
|
/** Module-level emitter injected by plugin.ts at startup. */
|
|
9
10
|
let emitEvent = () => {};
|
|
10
11
|
let currentBuildId = "";
|
|
11
|
-
|
|
12
|
+
let currentSlowCodeBlockMs = Number.POSITIVE_INFINITY;
|
|
13
|
+
function setRemarkApiCodeblocksEventEmitter(fn, buildId = "", slowCodeBlockMs = Number.POSITIVE_INFINITY) {
|
|
12
14
|
emitEvent = fn;
|
|
13
15
|
currentBuildId = buildId;
|
|
16
|
+
currentSlowCodeBlockMs = slowCodeBlockMs;
|
|
14
17
|
}
|
|
15
18
|
/**
|
|
16
19
|
* Create an MDX JSX attribute value expression with proper estree AST.
|
|
@@ -123,14 +126,37 @@ const remarkApiCodeblocks = () => {
|
|
|
123
126
|
return;
|
|
124
127
|
}
|
|
125
128
|
const transformers = [];
|
|
126
|
-
|
|
129
|
+
let twoslashMs = 0;
|
|
130
|
+
if (node.name === "ApiExample" && vfsConfig.twoslashTransformer) transformers.push(createTwoslashTimingWrapper(vfsConfig.twoslashTransformer, (duration) => {
|
|
131
|
+
twoslashMs += duration;
|
|
132
|
+
}));
|
|
127
133
|
else if (node.name === "ApiMember" && vfsConfig.hideCutTransformer) transformers.push(vfsConfig.hideCutTransformer);
|
|
128
134
|
else if (node.name === "ApiSignature") {
|
|
129
135
|
if (vfsConfig.hideCutLinesTransformer) transformers.push(vfsConfig.hideCutLinesTransformer);
|
|
130
136
|
}
|
|
131
137
|
const isExample = node.name === "ApiExample" && !!vfsConfig.twoslashTransformer;
|
|
132
|
-
|
|
138
|
+
const shikiStart = performance.now();
|
|
139
|
+
const hastPromise = generateShikiHast(source, vfsConfig.highlighter, transformers.length > 0 ? transformers : void 0, isExample, vfsConfig.theme);
|
|
140
|
+
const renderMs = performance.now() - shikiStart;
|
|
141
|
+
let hast = await hastPromise;
|
|
142
|
+
const postStart = performance.now();
|
|
133
143
|
if (hast && vfsConfig.crossLinker) hast = vfsConfig.crossLinker.transformHast(hast, apiScopeValue);
|
|
144
|
+
const totalBlockTime = renderMs + (performance.now() - postStart);
|
|
145
|
+
emitEvent(PluginEvent.CodeBlockProcessed({
|
|
146
|
+
ctx: {
|
|
147
|
+
buildId: currentBuildId,
|
|
148
|
+
apiScope: apiScopeValue,
|
|
149
|
+
file: currentFilePath
|
|
150
|
+
},
|
|
151
|
+
lang: "typescript",
|
|
152
|
+
component: node.name,
|
|
153
|
+
twoslash: isExample,
|
|
154
|
+
twoslashMs,
|
|
155
|
+
shikiMs: Math.max(0, renderMs - twoslashMs),
|
|
156
|
+
totalMs: totalBlockTime,
|
|
157
|
+
slow: totalBlockTime > currentSlowCodeBlockMs,
|
|
158
|
+
level: "debug"
|
|
159
|
+
}));
|
|
134
160
|
const hastBase64 = hast ? Buffer.from(JSON.stringify(hast), "utf-8").toString("base64") : "";
|
|
135
161
|
node.attributes.push({
|
|
136
162
|
type: "mdxJsxAttribute",
|
package/remark-with-api.js
CHANGED
|
@@ -3,6 +3,7 @@ import { formatCode } from "./prettier-formatter.js";
|
|
|
3
3
|
import { stripTwoslashDirectives } from "./markdown/helpers.js";
|
|
4
4
|
import { TwoslashManager } from "./twoslash-transformer.js";
|
|
5
5
|
import { DEFAULT_SHIKI_THEMES } from "./markdown/shiki-utils.js";
|
|
6
|
+
import { createTwoslashTimingWrapper } from "./twoslash-timing-wrapper.js";
|
|
6
7
|
import { codeToHast, hastToHtml } from "shiki";
|
|
7
8
|
import { visit } from "unist-util-visit";
|
|
8
9
|
|
|
@@ -78,9 +79,13 @@ const remarkWithApi = (options) => {
|
|
|
78
79
|
const promise = (async () => {
|
|
79
80
|
const blockStart = performance.now();
|
|
80
81
|
const rawCode = node.value;
|
|
82
|
+
const apiScope = currentFilePath ? inferApiScope(currentFilePath) : void 0;
|
|
81
83
|
const code = (await formatCode(rawCode, lang)).code;
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
+
let twoslashMs = 0;
|
|
85
|
+
const twoslashTransformer = getTransformer(apiScope);
|
|
86
|
+
const transformers = twoslashTransformer ? [createTwoslashTimingWrapper(twoslashTransformer, (duration) => {
|
|
87
|
+
twoslashMs += duration;
|
|
88
|
+
})] : [];
|
|
84
89
|
const shikiStart = performance.now();
|
|
85
90
|
let hast = await codeToHast(code, {
|
|
86
91
|
lang,
|
|
@@ -93,19 +98,21 @@ const remarkWithApi = (options) => {
|
|
|
93
98
|
cssVariablePrefix: "--api-shiki-",
|
|
94
99
|
transformers
|
|
95
100
|
});
|
|
96
|
-
const
|
|
101
|
+
const renderMs = performance.now() - shikiStart;
|
|
97
102
|
if (apiScope) hast = shikiCrossLinker.transformHast(hast, apiScope);
|
|
98
|
-
const shikiTime = performance.now() - shikiStart;
|
|
99
103
|
const totalBlockTime = performance.now() - blockStart;
|
|
100
104
|
const isSlow = totalBlockTime > currentSlowCodeBlockMs;
|
|
101
105
|
emitEvent(PluginEvent.CodeBlockProcessed({
|
|
102
106
|
ctx: {
|
|
103
107
|
buildId: currentBuildId,
|
|
108
|
+
...apiScope != null ? { apiScope } : {},
|
|
104
109
|
...currentFilePath != null ? { file: currentFilePath } : {}
|
|
105
110
|
},
|
|
106
111
|
lang,
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
component: "with-api",
|
|
113
|
+
twoslash: twoslashTransformer != null,
|
|
114
|
+
twoslashMs,
|
|
115
|
+
shikiMs: Math.max(0, renderMs - twoslashMs),
|
|
109
116
|
totalMs: totalBlockTime,
|
|
110
117
|
slow: isSlow,
|
|
111
118
|
level: "debug"
|
package/schemas/config.js
CHANGED
|
@@ -273,15 +273,19 @@ const MultiApiConfig = Schema.Struct({
|
|
|
273
273
|
* Path to a `tsconfig.json` for Twoslash.
|
|
274
274
|
*
|
|
275
275
|
* @remarks
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* configured tsconfigs are equivalent, or set the intended one on the
|
|
281
|
-
* first API only.
|
|
276
|
+
* This API's code blocks are type-checked under this config. APIs that
|
|
277
|
+
* declare the same config share one TypeScript environment; the file set
|
|
278
|
+
* is shared across all documented APIs either way, so a type owned by
|
|
279
|
+
* another documented package still resolves.
|
|
282
280
|
*/
|
|
283
281
|
tsconfig: Schema.optional(ModelInput),
|
|
284
|
-
/**
|
|
282
|
+
/**
|
|
283
|
+
* TypeScript compiler options for Twoslash, applying to this API only.
|
|
284
|
+
*
|
|
285
|
+
* @remarks
|
|
286
|
+
* Merged on top of the defaults and of this API's `tsconfig`, so declaring
|
|
287
|
+
* a single option overrides just that one.
|
|
288
|
+
*/
|
|
285
289
|
compilerOptions: Schema.optional(Schema.Unknown)
|
|
286
290
|
});
|
|
287
291
|
/**
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Context } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/services/TwoslashCacheService.ts
|
|
4
|
+
/**
|
|
5
|
+
* Persistence for the Twoslash result cache.
|
|
6
|
+
*
|
|
7
|
+
* Split from the synchronous cache object (`twoslash-cache.ts`) because
|
|
8
|
+
* `TwoslashTypesCache.read`/`write` are called from inside Shiki's `preprocess`
|
|
9
|
+
* hook and cannot await: the service loads once before the render phase and
|
|
10
|
+
* saves once after it, while every lookup in between is a synchronous map hit.
|
|
11
|
+
*/
|
|
12
|
+
var TwoslashCacheService = class extends Context.Service()("rspress-plugin-api-extractor/TwoslashCacheService") {};
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { TwoslashCacheService };
|
package/shiki-transformer.js
CHANGED
|
@@ -35,13 +35,9 @@
|
|
|
35
35
|
* const crossLinker = new ShikiCrossLinker();
|
|
36
36
|
* crossLinker.reinitialize(routes, kinds, "my-api");
|
|
37
37
|
* crossLinker.setApiScope("my-api");
|
|
38
|
-
* const transformer = crossLinker.createTransformer();
|
|
39
38
|
*
|
|
40
|
-
* //
|
|
41
|
-
* const
|
|
42
|
-
* lang: "typescript",
|
|
43
|
-
* transformers: [transformer]
|
|
44
|
-
* });
|
|
39
|
+
* // Cross-link the finalized HAST, after Shiki and Twoslash have run
|
|
40
|
+
* const linked = crossLinker.transformHast(hast, "my-api");
|
|
45
41
|
* ```
|
|
46
42
|
*
|
|
47
43
|
* @see the `@tsdoctor/model` CrossLinker for the markdown equivalent
|
|
@@ -142,22 +138,6 @@ var ShikiCrossLinker = class {
|
|
|
142
138
|
return this.classMembersMapByScope.get(scope) || /* @__PURE__ */ new Map();
|
|
143
139
|
}
|
|
144
140
|
/**
|
|
145
|
-
* Create a Shiki transformer that adds cross-links to type references in code blocks.
|
|
146
|
-
*
|
|
147
|
-
* **DEPRECATED:** This method now returns a no-op transformer. Cross-linking has been
|
|
148
|
-
* moved to post-processing via {@link transformHast} to avoid interfering with Twoslash
|
|
149
|
-
* popup positioning. The Twoslash transformer calculates popup positions based on the
|
|
150
|
-
* original span structure, and modifying spans during the Shiki pipeline caused popups
|
|
151
|
-
* to appear offset from their intended positions.
|
|
152
|
-
*
|
|
153
|
-
* @param _apiScope - Unused, kept for API compatibility
|
|
154
|
-
* @returns A no-op Shiki transformer
|
|
155
|
-
* @deprecated Use {@link transformHast} after Shiki processing completes instead
|
|
156
|
-
*/
|
|
157
|
-
createTransformer(_apiScope) {
|
|
158
|
-
return { name: "api-docs-cross-linker" };
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
141
|
* Transform a finalized HAST tree to add cross-links to type references.
|
|
162
142
|
*
|
|
163
143
|
* This method should be called AFTER Shiki (including Twoslash) has fully processed
|