rspress-plugin-api-extractor 0.10.0 → 0.12.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/build-program.js +34 -33
- package/build-stages.js +48 -42
- package/config-helpers.js +7 -7
- package/errors.js +1 -6
- package/index.d.ts +84 -86
- package/layers/AppLayer.js +67 -0
- package/layers/api-results.js +83 -0
- package/layers/build-metrics.js +1 -1
- package/layers/config-resolution.js +407 -0
- package/layers/external-types.js +74 -0
- package/layers/{ObservabilityLive.js → observability.js} +3 -3
- package/layers/type-environment.js +109 -0
- 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/model-loader.js +3 -3
- package/observability/EventBus.js +29 -7
- package/observability/heartbeat.js +1 -1
- package/observability/sinks/metrics-sink.js +1 -1
- package/observability/sinks/trace-sink.js +4 -4
- package/observability/spans.js +3 -1
- package/observability/sync-emitter.js +78 -0
- package/og-resolver.js +74 -284
- package/package.json +3 -4
- package/path-derivation.js +19 -1
- package/plugin.js +63 -91
- package/prettier-formatter.js +5 -11
- package/remark-api-codeblocks.js +11 -19
- package/remark-with-api.js +11 -21
- package/schemas/config.js +0 -2
- package/services/ConfigService.js +37 -2
- package/services/HighlighterService.js +75 -0
- package/services/OgService.js +190 -0
- package/services/PluginConfig.js +26 -0
- package/services/TwoslashCacheService.js +128 -2
- package/services/TwoslashEnvironments.js +35 -0
- package/services/TypeRegistryService.js +178 -2
- package/shiki-transformer.js +53 -234
- package/sync-node-fs.js +6 -6
- package/tsconfig-parser.js +77 -95
- package/twoslash-access.js +48 -0
- package/twoslash-transformer.js +106 -83
- package/vfs-registry.js +1 -31
- package/layers/ConfigServiceLive.js +0 -600
- package/layers/PathDerivationServiceLive.js +0 -16
- package/layers/TwoslashCacheServiceLive.js +0 -53
- package/layers/TypeRegistryServiceLive.js +0 -155
- package/markdown/index.js +0 -11
- package/schemas/index.js +0 -6
- package/services/PathDerivationService.js +0 -7
package/plugin.js
CHANGED
|
@@ -1,39 +1,33 @@
|
|
|
1
1
|
import { PluginEvent } from "./observability/events.js";
|
|
2
|
-
import { emit
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { buildEventBus, logBuildSummary, makeSummaryLoggerLayer } from "./layers/ObservabilityLive.js";
|
|
8
|
-
import { setPrettierEventEmitter } from "./prettier-formatter.js";
|
|
9
|
-
import { setOgResolverEventEmitter } from "./og-resolver.js";
|
|
10
|
-
import { setBuildStagesEventEmitter } from "./build-stages.js";
|
|
11
|
-
import { TwoslashManager, setEventEmitter } from "./twoslash-transformer.js";
|
|
2
|
+
import { emit } from "./observability/EventBus.js";
|
|
3
|
+
import { emitSync, installSyncEmitter } from "./observability/sync-emitter.js";
|
|
4
|
+
import { normalizeThemeConfig } from "./markdown/shiki-utils.js";
|
|
5
|
+
import { clearTypeRoutes } from "./twoslash-transformer.js";
|
|
6
|
+
import { TwoslashEnvironments } from "./services/TwoslashEnvironments.js";
|
|
12
7
|
import { VfsRegistry } from "./vfs-registry.js";
|
|
13
8
|
import { generateApiDocs } from "./build-program.js";
|
|
14
9
|
import { deriveOutputPaths, normalizeBaseRoute, unscopedName } from "./path-derivation.js";
|
|
15
10
|
import { fromDir, fromParentDir } from "./config-helpers.js";
|
|
16
11
|
import { classifyApiConfig, mergeLlmsPluginConfig } from "./config-utils.js";
|
|
17
|
-
import { DEFAULT_SHIKI_THEMES, setShikiUtilsEventEmitter } from "./markdown/shiki-utils.js";
|
|
18
12
|
import { resolveObservability } from "./schemas/observability.js";
|
|
19
13
|
import { PluginOptions } from "./schemas/config.js";
|
|
20
|
-
import "./schemas/index.js";
|
|
21
|
-
import { ConfigService } from "./services/ConfigService.js";
|
|
22
14
|
import { TwoslashCacheService } from "./services/TwoslashCacheService.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
15
|
+
import { ConfigService } from "./services/ConfigService.js";
|
|
16
|
+
import { codeBlockReport } from "./observability/metric-report.js";
|
|
17
|
+
import { runHeartbeat } from "./observability/heartbeat.js";
|
|
18
|
+
import { writeIssuesJson } from "./observability/sinks/issues-sink.js";
|
|
19
|
+
import { writeRenderPhaseJson } from "./observability/sinks/render-sink.js";
|
|
20
|
+
import { buildEventBus, logBuildSummary } from "./layers/observability.js";
|
|
21
|
+
import { makeAppLayers } from "./layers/AppLayer.js";
|
|
22
|
+
import { clearTwoslashAccess, installTwoslashAccess, twoslashTransformerFor } from "./twoslash-access.js";
|
|
23
|
+
import { remarkApiCodeblocks } from "./remark-api-codeblocks.js";
|
|
24
|
+
import { remarkWithApi } from "./remark-with-api.js";
|
|
30
25
|
import { createRequire } from "node:module";
|
|
31
|
-
import
|
|
26
|
+
import fsSync from "node:fs";
|
|
27
|
+
import os from "node:os";
|
|
32
28
|
import path from "node:path";
|
|
33
29
|
import { fileURLToPath } from "node:url";
|
|
34
|
-
import {
|
|
35
|
-
import { SnapshotServiceLive } from "@tsdoctor/snapshot";
|
|
36
|
-
import { Effect, FileSystem, Layer, ManagedRuntime, Ref, Schema } from "effect";
|
|
30
|
+
import { Effect, FileSystem, ManagedRuntime, Option, Ref, Schema } from "effect";
|
|
37
31
|
|
|
38
32
|
//#region src/plugin.ts
|
|
39
33
|
/* v8 ignore start -- RSPress plugin adapter, requires RSPress runtime */
|
|
@@ -53,30 +47,11 @@ const readSitePackageName = Effect.gen(function* () {
|
|
|
53
47
|
return "unknown";
|
|
54
48
|
});
|
|
55
49
|
/**
|
|
56
|
-
* Normalize theme configuration from user input to a consistent format.
|
|
57
|
-
*/
|
|
58
|
-
function normalizeThemeConfig(theme) {
|
|
59
|
-
if (!theme) return { ...DEFAULT_SHIKI_THEMES };
|
|
60
|
-
if (typeof theme === "string") return {
|
|
61
|
-
light: theme,
|
|
62
|
-
dark: theme
|
|
63
|
-
};
|
|
64
|
-
if ("light" in theme && "dark" in theme && typeof theme.light === "string" && typeof theme.dark === "string") return {
|
|
65
|
-
light: theme.light,
|
|
66
|
-
dark: theme.dark
|
|
67
|
-
};
|
|
68
|
-
return {
|
|
69
|
-
light: theme,
|
|
70
|
-
dark: theme
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
50
|
* RSPress plugin for generating API documentation from API Extractor model files
|
|
75
51
|
*/
|
|
76
52
|
function ApiExtractorPluginImpl(rawOptions) {
|
|
77
53
|
const options = Schema.decodeUnknownSync(PluginOptions)(rawOptions);
|
|
78
54
|
const isInert = classifyApiConfig(options) === "disabled";
|
|
79
|
-
const shikiCrossLinker = new ShikiCrossLinker();
|
|
80
55
|
const envLogLevel = process.env.LOG_LEVEL?.toLowerCase();
|
|
81
56
|
const buildId = `${process.pid}-${performance.now().toString(36)}`;
|
|
82
57
|
const { resolved: obs, deprecations } = resolveObservability({
|
|
@@ -89,23 +64,19 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
89
64
|
});
|
|
90
65
|
const { layer: eventBusLayer, trace: traceSink, issues: issuesSink, render: renderSink, metrics: metricStore } = buildEventBus(obs);
|
|
91
66
|
const dbPath = path.resolve(process.cwd(), ".api-docs", "snapshot", "api-docs.db");
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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;
|
|
67
|
+
fsSync.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
68
|
+
const appLayers = makeAppLayers({
|
|
69
|
+
options,
|
|
70
|
+
obs,
|
|
71
|
+
buildId,
|
|
72
|
+
dbPath,
|
|
73
|
+
pageConcurrency: os.cpus().length,
|
|
74
|
+
eventBus: eventBusLayer,
|
|
75
|
+
metrics: metricStore
|
|
76
|
+
});
|
|
77
|
+
const effectRuntime = ManagedRuntime.make(appLayers.app);
|
|
78
|
+
const emitterRuntime = ManagedRuntime.make(appLayers.emitter);
|
|
79
|
+
installSyncEmitter(emitterRuntime);
|
|
109
80
|
const fileContextMap = /* @__PURE__ */ new Map();
|
|
110
81
|
let docsRoot;
|
|
111
82
|
let isFirstBuild = true;
|
|
@@ -121,22 +92,20 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
121
92
|
if (isFirstBuild && !isInert) {
|
|
122
93
|
const renderSamples = renderSink.snapshot();
|
|
123
94
|
const report = await effectRuntime.runPromise(codeBlockReport);
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
persisted: stats.dirty
|
|
137
|
-
}));
|
|
95
|
+
await effectRuntime.runPromise(Effect.gen(function* () {
|
|
96
|
+
const saved = yield* (yield* TwoslashCacheService).persist();
|
|
97
|
+
if (Option.isNone(saved)) return;
|
|
98
|
+
const stats = saved.value;
|
|
99
|
+
yield* emit(PluginEvent.TwoslashCacheSaved({
|
|
100
|
+
ctx: { buildId },
|
|
101
|
+
level: "info",
|
|
102
|
+
envHash: stats.envHash,
|
|
103
|
+
hits: stats.hits,
|
|
104
|
+
misses: stats.misses,
|
|
105
|
+
entries: stats.entries,
|
|
106
|
+
persisted: stats.dirty
|
|
138
107
|
}));
|
|
139
|
-
}
|
|
108
|
+
}));
|
|
140
109
|
await effectRuntime.runPromise(logBuildSummary(obs.thresholds.slowCodeBlock, report));
|
|
141
110
|
if (isProd) await effectRuntime.runPromise(Effect.gen(function* () {
|
|
142
111
|
const packageName = yield* readSitePackageName;
|
|
@@ -165,7 +134,10 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
165
134
|
isFirstBuild = false;
|
|
166
135
|
}
|
|
167
136
|
if (traceSink) traceSink.flush();
|
|
168
|
-
if (isProd)
|
|
137
|
+
if (isProd) {
|
|
138
|
+
await effectRuntime.dispose();
|
|
139
|
+
await emitterRuntime.dispose();
|
|
140
|
+
}
|
|
169
141
|
},
|
|
170
142
|
async config(_config, _utils, isProd) {
|
|
171
143
|
const buildStartTime = performance.now();
|
|
@@ -174,6 +146,8 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
174
146
|
const rspressLocales = _config.locales?.map((l) => l.lang) ?? [];
|
|
175
147
|
const rspressLang = _config.lang;
|
|
176
148
|
const rspressMultiVersion = _config.multiVersion;
|
|
149
|
+
const rspressSiteOrigin = _config.siteOrigin;
|
|
150
|
+
const rspressBase = _config.base;
|
|
177
151
|
rspressLlmsEnabled = Boolean(_config.llms);
|
|
178
152
|
rspressOutDir = _config.outDir ?? "dist";
|
|
179
153
|
if (options.api) {
|
|
@@ -190,7 +164,7 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
190
164
|
versions,
|
|
191
165
|
defaultVersion: rspressMultiVersion?.default
|
|
192
166
|
});
|
|
193
|
-
for (const dp of derivedPaths)
|
|
167
|
+
for (const dp of derivedPaths) fsSync.mkdirSync(dp.outputDir, { recursive: true });
|
|
194
168
|
} else if (options.apis) for (const api of options.apis) {
|
|
195
169
|
const baseRoute = normalizeBaseRoute(api.baseRoute ?? `/${unscopedName(api.packageName)}`);
|
|
196
170
|
const derivedPaths = deriveOutputPaths({
|
|
@@ -203,9 +177,11 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
203
177
|
versions: [],
|
|
204
178
|
defaultVersion: void 0
|
|
205
179
|
});
|
|
206
|
-
for (const dp of derivedPaths)
|
|
180
|
+
for (const dp of derivedPaths) fsSync.mkdirSync(dp.outputDir, { recursive: true });
|
|
207
181
|
}
|
|
208
182
|
VfsRegistry.clear();
|
|
183
|
+
clearTwoslashAccess();
|
|
184
|
+
clearTypeRoutes();
|
|
209
185
|
fileContextMap.clear();
|
|
210
186
|
issuesSink.reset();
|
|
211
187
|
for (const dep of deprecations) emitSync(PluginEvent.DeprecatedConfigUsed({
|
|
@@ -219,7 +195,9 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
219
195
|
...rspressMultiVersion != null ? { multiVersion: rspressMultiVersion } : {},
|
|
220
196
|
...rspressLocales.length > 0 ? { locales: rspressLocales.map((lang) => ({ lang })) } : {},
|
|
221
197
|
...rspressLang != null ? { lang: rspressLang } : {},
|
|
222
|
-
...docsRoot != null ? { root: docsRoot } : {}
|
|
198
|
+
...docsRoot != null ? { root: docsRoot } : {},
|
|
199
|
+
...rspressSiteOrigin != null ? { siteOrigin: rspressSiteOrigin } : {},
|
|
200
|
+
...rspressBase != null ? { base: rspressBase } : {}
|
|
223
201
|
};
|
|
224
202
|
await effectRuntime.runPromise(Effect.gen(function* () {
|
|
225
203
|
const apiCount = options.api ? 1 : options.apis?.length ?? 0;
|
|
@@ -237,17 +215,11 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
237
215
|
apisTotal: apiCount,
|
|
238
216
|
buildId
|
|
239
217
|
}));
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
cache: buildContext.twoslashCache,
|
|
243
|
-
envHash: buildContext.twoslashEnvHash
|
|
244
|
-
};
|
|
218
|
+
installTwoslashAccess(yield* TwoslashEnvironments);
|
|
219
|
+
const apiConfigs = yield* (yield* ConfigService).resolve(rspressConfigSubset);
|
|
245
220
|
buildResults.length = 0;
|
|
246
221
|
yield* Ref.set(phaseRef, "generate");
|
|
247
|
-
yield* Effect.forEach(
|
|
248
|
-
...apiConfig,
|
|
249
|
-
suppressExampleErrors: buildContext.suppressExampleErrors
|
|
250
|
-
}, buildContext, fileContextMap).pipe(Effect.tap((result) => {
|
|
222
|
+
yield* Effect.forEach(apiConfigs, (apiConfig) => generateApiDocs(apiConfig, fileContextMap).pipe(Effect.tap((result) => {
|
|
251
223
|
buildResults.push(result);
|
|
252
224
|
return emit(PluginEvent.ApiDocsCompleted({
|
|
253
225
|
ctx: { buildId },
|
|
@@ -305,10 +277,10 @@ function ApiExtractorPluginImpl(rawOptions) {
|
|
|
305
277
|
if (!existingInclude.includes("rspress-plugin-api-extractor/runtime")) updatedConfig.builderConfig.source.include = [...existingInclude, "rspress-plugin-api-extractor/runtime"];
|
|
306
278
|
if (!updatedConfig.markdown) updatedConfig.markdown = {};
|
|
307
279
|
if (!updatedConfig.markdown.remarkPlugins) updatedConfig.markdown.remarkPlugins = [];
|
|
308
|
-
const
|
|
280
|
+
const firstApiTheme = options.api?.theme ?? options.apis?.[0]?.theme;
|
|
281
|
+
const remarkTheme = normalizeThemeConfig(firstApiTheme);
|
|
309
282
|
updatedConfig.markdown.remarkPlugins.push([remarkWithApi, {
|
|
310
|
-
|
|
311
|
-
getTransformer: (apiScope) => TwoslashManager.getInstance().getTransformer(apiScope),
|
|
283
|
+
getTransformer: (apiScope) => twoslashTransformerFor(apiScope),
|
|
312
284
|
theme: remarkTheme
|
|
313
285
|
}]);
|
|
314
286
|
updatedConfig.markdown.remarkPlugins.push([remarkApiCodeblocks]);
|
package/prettier-formatter.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import { PluginEvent } from "./observability/events.js";
|
|
2
1
|
import { addLogicalBlankLines } from "./code-post-processor.js";
|
|
2
|
+
import { PluginEvent } from "./observability/events.js";
|
|
3
|
+
import { emitSync, syncBuildId } from "./observability/sync-emitter.js";
|
|
3
4
|
import { format } from "prettier";
|
|
4
5
|
|
|
5
6
|
//#region src/prettier-formatter.ts
|
|
6
7
|
/* 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
|
-
}
|
|
14
8
|
/**
|
|
15
9
|
* Map code fence languages to Prettier parsers
|
|
16
10
|
*/
|
|
@@ -65,8 +59,8 @@ async function formatCode(code, language) {
|
|
|
65
59
|
} catch (error) {
|
|
66
60
|
const formatTime = performance.now() - start;
|
|
67
61
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
68
|
-
|
|
69
|
-
ctx: { buildId:
|
|
62
|
+
emitSync(PluginEvent.PrettierError({
|
|
63
|
+
ctx: { buildId: syncBuildId() },
|
|
70
64
|
file: "unknown",
|
|
71
65
|
reason: errorMsg,
|
|
72
66
|
level: "warn"
|
|
@@ -81,4 +75,4 @@ async function formatCode(code, language) {
|
|
|
81
75
|
}
|
|
82
76
|
|
|
83
77
|
//#endregion
|
|
84
|
-
export { formatCode
|
|
78
|
+
export { formatCode };
|
package/remark-api-codeblocks.js
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
import { PluginEvent } from "./observability/events.js";
|
|
2
|
-
import {
|
|
3
|
-
import { VfsRegistry } from "./vfs-registry.js";
|
|
2
|
+
import { emitSync, syncBuildId, syncSlowCodeBlockMs } from "./observability/sync-emitter.js";
|
|
4
3
|
import { generateShikiHast } from "./markdown/shiki-utils.js";
|
|
4
|
+
import { VfsRegistry } from "./vfs-registry.js";
|
|
5
|
+
import { setTwoslashFile } from "./twoslash-access.js";
|
|
5
6
|
import { createTwoslashTimingWrapper } from "./twoslash-timing-wrapper.js";
|
|
6
7
|
import { visit } from "unist-util-visit";
|
|
7
8
|
|
|
8
9
|
//#region src/remark-api-codeblocks.ts
|
|
9
|
-
/** Module-level emitter injected by plugin.ts at startup. */
|
|
10
|
-
let emitEvent = () => {};
|
|
11
|
-
let currentBuildId = "";
|
|
12
|
-
let currentSlowCodeBlockMs = Number.POSITIVE_INFINITY;
|
|
13
|
-
function setRemarkApiCodeblocksEventEmitter(fn, buildId = "", slowCodeBlockMs = Number.POSITIVE_INFINITY) {
|
|
14
|
-
emitEvent = fn;
|
|
15
|
-
currentBuildId = buildId;
|
|
16
|
-
currentSlowCodeBlockMs = slowCodeBlockMs;
|
|
17
|
-
}
|
|
18
10
|
/**
|
|
19
11
|
* Create an MDX JSX attribute value expression with proper estree AST.
|
|
20
12
|
* This ensures the value is properly serialized as a JavaScript string literal.
|
|
@@ -94,7 +86,7 @@ const remarkApiCodeblocks = () => {
|
|
|
94
86
|
const promises = [];
|
|
95
87
|
const isSsgMd = import.meta.env?.SSG_MD || process.env.RSBUILD_ENVIRONMENT === "node_md" || process.env.BUILD_TARGET === "node_md";
|
|
96
88
|
const currentFilePath = file.path || "unknown";
|
|
97
|
-
if (file.path)
|
|
89
|
+
if (file.path) setTwoslashFile(file.path);
|
|
98
90
|
const jsxComponentNames = /* @__PURE__ */ new Set([
|
|
99
91
|
"ApiSignature",
|
|
100
92
|
"ApiMember",
|
|
@@ -112,9 +104,9 @@ const remarkApiCodeblocks = () => {
|
|
|
112
104
|
}
|
|
113
105
|
const vfsConfig = VfsRegistry.get(apiScopeValue);
|
|
114
106
|
if (!vfsConfig) {
|
|
115
|
-
|
|
107
|
+
emitSync(PluginEvent.ConfigCascadeWarning({
|
|
116
108
|
ctx: {
|
|
117
|
-
buildId:
|
|
109
|
+
buildId: syncBuildId(),
|
|
118
110
|
file: currentFilePath
|
|
119
111
|
},
|
|
120
112
|
field: "vfs",
|
|
@@ -140,11 +132,11 @@ const remarkApiCodeblocks = () => {
|
|
|
140
132
|
const renderMs = performance.now() - shikiStart;
|
|
141
133
|
let hast = await hastPromise;
|
|
142
134
|
const postStart = performance.now();
|
|
143
|
-
if (hast && vfsConfig.crossLinker) hast = vfsConfig.crossLinker.transformHast(hast
|
|
135
|
+
if (hast && vfsConfig.crossLinker) hast = vfsConfig.crossLinker.transformHast(hast);
|
|
144
136
|
const totalBlockTime = renderMs + (performance.now() - postStart);
|
|
145
|
-
|
|
137
|
+
emitSync(PluginEvent.CodeBlockProcessed({
|
|
146
138
|
ctx: {
|
|
147
|
-
buildId:
|
|
139
|
+
buildId: syncBuildId(),
|
|
148
140
|
apiScope: apiScopeValue,
|
|
149
141
|
file: currentFilePath
|
|
150
142
|
},
|
|
@@ -154,7 +146,7 @@ const remarkApiCodeblocks = () => {
|
|
|
154
146
|
twoslashMs,
|
|
155
147
|
shikiMs: Math.max(0, renderMs - twoslashMs),
|
|
156
148
|
totalMs: totalBlockTime,
|
|
157
|
-
slow: totalBlockTime >
|
|
149
|
+
slow: totalBlockTime > syncSlowCodeBlockMs(),
|
|
158
150
|
level: "debug"
|
|
159
151
|
}));
|
|
160
152
|
const hastBase64 = hast ? Buffer.from(JSON.stringify(hast), "utf-8").toString("base64") : "";
|
|
@@ -172,4 +164,4 @@ const remarkApiCodeblocks = () => {
|
|
|
172
164
|
};
|
|
173
165
|
|
|
174
166
|
//#endregion
|
|
175
|
-
export { remarkApiCodeblocks
|
|
167
|
+
export { remarkApiCodeblocks };
|
package/remark-with-api.js
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
import { PluginEvent } from "./observability/events.js";
|
|
2
|
+
import { emitSync, syncBuildId, syncSlowCodeBlockMs } from "./observability/sync-emitter.js";
|
|
2
3
|
import { formatCode } from "./prettier-formatter.js";
|
|
3
4
|
import { stripTwoslashDirectives } from "./markdown/helpers.js";
|
|
4
|
-
import { TwoslashManager } from "./twoslash-transformer.js";
|
|
5
5
|
import { DEFAULT_SHIKI_THEMES } from "./markdown/shiki-utils.js";
|
|
6
|
+
import { VfsRegistry } from "./vfs-registry.js";
|
|
7
|
+
import { setTwoslashFile } from "./twoslash-access.js";
|
|
6
8
|
import { createTwoslashTimingWrapper } from "./twoslash-timing-wrapper.js";
|
|
7
9
|
import { codeToHast, hastToHtml } from "shiki";
|
|
8
10
|
import { visit } from "unist-util-visit";
|
|
9
11
|
|
|
10
12
|
//#region src/remark-with-api.ts
|
|
11
|
-
/** Module-level emitter injected by plugin.ts at startup. */
|
|
12
|
-
let emitEvent = () => {};
|
|
13
|
-
let currentBuildId = "";
|
|
14
|
-
let currentSlowCodeBlockMs = 500;
|
|
15
|
-
function setRemarkWithApiEventEmitter(fn, buildId = "", slowCodeBlockMs = 500) {
|
|
16
|
-
emitEvent = fn;
|
|
17
|
-
currentBuildId = buildId;
|
|
18
|
-
currentSlowCodeBlockMs = slowCodeBlockMs;
|
|
19
|
-
}
|
|
20
13
|
/**
|
|
21
14
|
* Supported languages for with-api code blocks
|
|
22
15
|
* Based on GitHub Linguist standard aliases:
|
|
@@ -59,18 +52,14 @@ function inferApiScope(filePath) {
|
|
|
59
52
|
* 5. Renders to ApiExample component with pre-rendered Shiki HAST
|
|
60
53
|
*/
|
|
61
54
|
const remarkWithApi = (options) => {
|
|
62
|
-
const {
|
|
55
|
+
const { getTransformer, theme } = options;
|
|
63
56
|
const resolvedTheme = theme ?? DEFAULT_SHIKI_THEMES;
|
|
64
57
|
return async function remarkTransformer(tree, file) {
|
|
65
58
|
const promises = [];
|
|
66
59
|
let needsApiExampleImport = false;
|
|
67
60
|
const isSsgMd = import.meta.env?.SSG_MD || process.env.RSBUILD_ENVIRONMENT === "node_md" || process.env.BUILD_TARGET === "node_md";
|
|
68
61
|
const currentFilePath = file.path;
|
|
69
|
-
if (currentFilePath)
|
|
70
|
-
const apiScope = inferApiScope(currentFilePath);
|
|
71
|
-
if (apiScope) shikiCrossLinker.setApiScope(apiScope);
|
|
72
|
-
TwoslashManager.getInstance().setCurrentFile(currentFilePath);
|
|
73
|
-
}
|
|
62
|
+
if (currentFilePath) setTwoslashFile(currentFilePath);
|
|
74
63
|
visit(tree, "code", (node, index, parent) => {
|
|
75
64
|
const hasWithApi = node.meta?.includes("with-api");
|
|
76
65
|
const lang = node.lang || "typescript";
|
|
@@ -99,12 +88,13 @@ const remarkWithApi = (options) => {
|
|
|
99
88
|
transformers
|
|
100
89
|
});
|
|
101
90
|
const renderMs = performance.now() - shikiStart;
|
|
102
|
-
|
|
91
|
+
const scopeLinker = apiScope ? VfsRegistry.get(apiScope)?.crossLinker : void 0;
|
|
92
|
+
if (scopeLinker) hast = scopeLinker.transformHast(hast);
|
|
103
93
|
const totalBlockTime = performance.now() - blockStart;
|
|
104
|
-
const isSlow = totalBlockTime >
|
|
105
|
-
|
|
94
|
+
const isSlow = totalBlockTime > syncSlowCodeBlockMs();
|
|
95
|
+
emitSync(PluginEvent.CodeBlockProcessed({
|
|
106
96
|
ctx: {
|
|
107
|
-
buildId:
|
|
97
|
+
buildId: syncBuildId(),
|
|
108
98
|
...apiScope != null ? { apiScope } : {},
|
|
109
99
|
...currentFilePath != null ? { file: currentFilePath } : {}
|
|
110
100
|
},
|
|
@@ -191,4 +181,4 @@ const remarkWithApi = (options) => {
|
|
|
191
181
|
};
|
|
192
182
|
|
|
193
183
|
//#endregion
|
|
194
|
-
export { remarkWithApi
|
|
184
|
+
export { remarkWithApi };
|
package/schemas/config.js
CHANGED
|
@@ -304,8 +304,6 @@ const PluginOptions = Schema.Struct({
|
|
|
304
304
|
api: Schema.optional(Schema.NullOr(SingleApiConfig)),
|
|
305
305
|
/** Multi-API portal configuration (mutually exclusive with `api`). `null` or `[]` disables generation. */
|
|
306
306
|
apis: Schema.optional(Schema.NullOr(Schema.mutable(Schema.Array(MultiApiConfig)))),
|
|
307
|
-
/** Canonical site URL used for Open Graph absolute URLs. */
|
|
308
|
-
siteUrl: Schema.optional(Schema.String),
|
|
309
307
|
/** Global Open Graph image configuration (overridden per-API). */
|
|
310
308
|
ogImage: Schema.optional(OpenGraphImageConfig),
|
|
311
309
|
/** Override the default category definitions for all APIs. */
|
|
@@ -1,7 +1,42 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { makeConfigService } from "../layers/config-resolution.js";
|
|
2
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
3
|
|
|
3
4
|
//#region src/services/ConfigService.ts
|
|
4
|
-
var ConfigService = class extends Context.Service()("rspress-plugin-api-extractor/ConfigService") {
|
|
5
|
+
var ConfigService = class ConfigService extends Context.Service()("rspress-plugin-api-extractor/ConfigService") {
|
|
6
|
+
/**
|
|
7
|
+
* Config resolution over the plugin options and the RSPress config.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* A plain `const`, not a factory. It used to take the plugin options as an
|
|
11
|
+
* argument, which made it a layer-RETURNING function: layers memoize by
|
|
12
|
+
* reference, so a second call would build a second `ConfigService` with its
|
|
13
|
+
* own captured `TypeRegistry`. The options come from {@link PluginConfig}
|
|
14
|
+
* now, so there is nothing to pass and "call it twice" is a type error
|
|
15
|
+
* rather than a test case.
|
|
16
|
+
*
|
|
17
|
+
* `Effect.suspend` because {@link makeConfigService} is imported from a
|
|
18
|
+
* module this one is also imported BY: a static initializer runs while the
|
|
19
|
+
* module body is still evaluating, so reading the binding eagerly can throw
|
|
20
|
+
* at import time with a completely clean typecheck.
|
|
21
|
+
*/
|
|
22
|
+
static layer = Layer.effect(this, Effect.suspend(() => makeConfigService));
|
|
23
|
+
/**
|
|
24
|
+
* An in-memory double whose unstubbed member dies naming itself.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* **No default `resolve`.** Returning an empty array by default would be a
|
|
28
|
+
* silent "this site documents nothing" — the exact state an inert plugin
|
|
29
|
+
* produces — so a test that forgot to stub it would assert against a build
|
|
30
|
+
* that generated no pages and pass. Stub it explicitly, or provide
|
|
31
|
+
* {@link ConfigService.layer} over real inputs.
|
|
32
|
+
*/
|
|
33
|
+
static makeTest = (overrides = {}) => ({ resolve: overrides.resolve ?? (() => unstubbed("resolve")) });
|
|
34
|
+
/** {@link ConfigService.makeTest} behind a `Layer`. */
|
|
35
|
+
static layerTest = (overrides = {}) => Layer.succeed(ConfigService, ConfigService.makeTest(overrides));
|
|
36
|
+
};
|
|
37
|
+
const unstubbed = (member) => {
|
|
38
|
+
throw new Error(`ConfigService.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
39
|
+
};
|
|
5
40
|
|
|
6
41
|
//#endregion
|
|
7
42
|
export { ConfigService };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { PluginEvent } from "../observability/events.js";
|
|
2
|
+
import { emit } from "../observability/EventBus.js";
|
|
3
|
+
import { SHIKI_LANGS } from "../markdown/shiki-utils.js";
|
|
4
|
+
import { Context, Effect, Layer } from "effect";
|
|
5
|
+
import { createHighlighter } from "shiki";
|
|
6
|
+
|
|
7
|
+
//#region src/services/HighlighterService.ts
|
|
8
|
+
/**
|
|
9
|
+
* The build's single Shiki highlighter.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* A highlighter owns a WASM oniguruma instance and every loaded grammar and
|
|
13
|
+
* theme, and it has a `dispose()` nobody was calling: `ConfigService.layer`
|
|
14
|
+
* created one per `resolve()`, so a dev-mode HMR session leaked one per
|
|
15
|
+
* rebuild. The test run reported it as
|
|
16
|
+
* `[Shiki] 10 instances have been created` — a console leak, not a failure.
|
|
17
|
+
*
|
|
18
|
+
* The fix is NOT to scope it to `resolve()`. `VfsRegistry` hands this
|
|
19
|
+
* highlighter to the remark plugins, which RSPress invokes during its render
|
|
20
|
+
* pass — after `config()` has returned and after any `resolve()`-scoped scope
|
|
21
|
+
* would have closed. The lifetime that matches is the `ManagedRuntime`'s:
|
|
22
|
+
* acquired when the layer builds, released by `effectRuntime.dispose()` in
|
|
23
|
+
* `afterBuild` on production builds, and deliberately kept alive in dev so HMR
|
|
24
|
+
* rebuilds reuse it.
|
|
25
|
+
*
|
|
26
|
+
* Getting that wrong is silent — a disposed highlighter does not throw, the
|
|
27
|
+
* code blocks just render as unhighlighted `<pre>`.
|
|
28
|
+
*
|
|
29
|
+
* @packageDocumentation
|
|
30
|
+
*/
|
|
31
|
+
var HighlighterService = class extends Context.Service()("rspress-plugin-api-extractor/HighlighterService") {
|
|
32
|
+
/**
|
|
33
|
+
* Acquire the build's highlighter, and release it when the runtime is
|
|
34
|
+
* disposed.
|
|
35
|
+
*
|
|
36
|
+
* @remarks
|
|
37
|
+
* `Layer.effect` over `Effect.acquireRelease` is the v4 scoped-constructor
|
|
38
|
+
* idiom (`Layer.scoped` is gone; `Layer.effect` strips `Scope` from `R`).
|
|
39
|
+
* Because the layer sits in the `ManagedRuntime`'s stack, the highlighter is
|
|
40
|
+
* created on the runtime's first use and `dispose()`d by
|
|
41
|
+
* `effectRuntime.dispose()` — which `plugin.ts` calls on production builds
|
|
42
|
+
* only, so a dev HMR session keeps one highlighter across rebuilds instead of
|
|
43
|
+
* leaking one per rebuild.
|
|
44
|
+
*
|
|
45
|
+
* **Bind the result to a `const`.** This is a layer FACTORY: each call mints a
|
|
46
|
+
* fresh layer reference, and layers memoize by reference, so calling it twice
|
|
47
|
+
* in one graph acquires two highlighters — the exact leak this layer exists to
|
|
48
|
+
* fix.
|
|
49
|
+
*
|
|
50
|
+
* The themes are passed in rather than read from a resolved build context
|
|
51
|
+
* because the layer builds before `ConfigService.resolve()` runs. Passing them
|
|
52
|
+
* as an argument rather than through a `Context.Reference` is deliberate: a
|
|
53
|
+
* Reference carries a default, so forgetting to wire it would silently load
|
|
54
|
+
* only the default themes and render every custom-themed block wrong. A
|
|
55
|
+
* missing argument is a type error.
|
|
56
|
+
*/
|
|
57
|
+
static layer = (themes) => Layer.effect(this, make(themes));
|
|
58
|
+
};
|
|
59
|
+
const make = (themes) => Effect.gen(function* () {
|
|
60
|
+
const startedMs = performance.now();
|
|
61
|
+
const highlighter = yield* Effect.acquireRelease(Effect.promise(() => createHighlighter({
|
|
62
|
+
themes: [...themes],
|
|
63
|
+
langs: [...SHIKI_LANGS]
|
|
64
|
+
})), (instance) => Effect.sync(() => instance.dispose()));
|
|
65
|
+
yield* emit(PluginEvent.PhaseCompleted({
|
|
66
|
+
ctx: {},
|
|
67
|
+
level: "debug",
|
|
68
|
+
phase: "shikiInit",
|
|
69
|
+
durationMs: Math.round(performance.now() - startedMs)
|
|
70
|
+
}));
|
|
71
|
+
return { highlighter };
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
export { HighlighterService };
|