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