rspress-plugin-api-extractor 0.9.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/BuildEnv.js +58 -0
  2. package/README.md +2 -1
  3. package/build-program.js +33 -30
  4. package/build-stages.js +47 -39
  5. package/errors.js +0 -1
  6. package/index.d.ts +22 -14
  7. package/layers/ConfigServiceLive.js +349 -400
  8. package/layers/HighlighterServiceLive.js +52 -0
  9. package/layers/ObservabilityLive.js +26 -7
  10. package/layers/OgServiceLive.js +134 -0
  11. package/layers/TwoslashCacheServiceLive.js +108 -0
  12. package/layers/TwoslashEnvironmentsLive.js +33 -0
  13. package/layers/TypeRegistryServiceLive.js +54 -47
  14. package/layers/build-metrics.js +32 -5
  15. package/layers/xdg.js +44 -0
  16. package/markdown/helpers.js +9 -55
  17. package/markdown/page-generators/class-page.js +8 -31
  18. package/markdown/page-generators/index-pages.js +6 -8
  19. package/markdown/page-generators/interface-page.js +7 -7
  20. package/markdown/shiki-utils.js +65 -10
  21. package/observability/EventBus.js +29 -9
  22. package/observability/heartbeat.js +1 -1
  23. package/observability/metric-report.js +124 -0
  24. package/observability/sinks/console-sink.js +6 -0
  25. package/observability/sinks/metrics-sink.js +64 -21
  26. package/observability/sinks/render-sink.js +86 -0
  27. package/observability/sinks/trace-sink.js +10 -17
  28. package/observability/spans.js +4 -2
  29. package/observability/sync-emitter.js +78 -0
  30. package/og-resolver.js +46 -287
  31. package/package.json +4 -5
  32. package/path-derivation.js +19 -1
  33. package/plugin.js +64 -52
  34. package/prettier-formatter.js +4 -10
  35. package/remark-api-codeblocks.js +33 -15
  36. package/remark-with-api.js +24 -27
  37. package/schemas/config.js +11 -7
  38. package/services/HighlighterService.js +30 -0
  39. package/services/OgService.js +23 -0
  40. package/services/PluginConfig.js +26 -0
  41. package/services/TwoslashCacheService.js +15 -0
  42. package/services/TwoslashEnvironments.js +7 -0
  43. package/shiki-transformer.js +55 -256
  44. package/twoslash-access.js +48 -0
  45. package/twoslash-cache.js +174 -0
  46. package/twoslash-patterns.js +1 -1
  47. package/twoslash-timing-wrapper.js +23 -0
  48. package/twoslash-transformer.js +153 -89
  49. package/vfs-registry.js +1 -31
  50. package/layers/PathDerivationServiceLive.js +0 -16
  51. package/runtime/components/MarkdownText/index.js +0 -34
  52. package/services/PathDerivationService.js +0 -7
package/og-resolver.js CHANGED
@@ -1,19 +1,6 @@
1
- import { PluginEvent } from "./observability/events.js";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { imageSizeFromFile } from "image-size/fromFile";
5
-
6
1
  //#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
- }
14
2
  /**
15
- * MIME type mappings for common image formats.
16
- * Used to determine the `og:image:type` meta tag value.
3
+ * MIME type mappings for common image formats, used for `og:image:type`.
17
4
  */
18
5
  const IMAGE_MIME_TYPES = {
19
6
  jpg: "image/jpeg",
@@ -24,282 +11,54 @@ const IMAGE_MIME_TYPES = {
24
11
  svg: "image/svg+xml"
25
12
  };
26
13
  /**
27
- * Resolves Open Graph image configurations into fully-qualified metadata.
14
+ * The `og:image:type` value for a detected image format, or `undefined` for a
15
+ * format with no mapping.
16
+ */
17
+ function imageMimeType(type) {
18
+ if (type == null) return void 0;
19
+ return IMAGE_MIME_TYPES[type.toLowerCase()];
20
+ }
21
+ /**
22
+ * Turn a configured image URL into an absolute one.
28
23
  *
29
- * This class handles the conversion of flexible OG image configuration formats
30
- * (strings or metadata objects) into complete `OpenGraphImageMetadata` objects
31
- * with resolved URLs and auto-detected dimensions for local images.
24
+ * @returns The absolute URL, or `undefined` when the input is neither an
25
+ * absolute `http(s)` URL nor a site-root-relative path. A bare relative path
26
+ * is deliberately rejected rather than guessed at there is no base to
27
+ * resolve it against that would not silently produce a broken link.
28
+ */
29
+ function resolveOgUrl(siteUrl, url) {
30
+ if (url.startsWith("http://") || url.startsWith("https://")) return url;
31
+ if (url.startsWith("/")) return `${siteUrl}${url}`;
32
+ }
33
+ /** Descriptive alt text for a package's (or one API's) OG image. */
34
+ function ogAltText(packageName, apiName) {
35
+ return apiName ? `${apiName} - ${packageName} API Documentation` : `${packageName} API Documentation`;
36
+ }
37
+ /**
38
+ * Assemble the complete Open Graph metadata for one documentation page.
32
39
  *
33
40
  * @remarks
34
- * The resolver supports two input formats:
35
- * - **String format**: A URL or path that will be resolved and optionally enhanced
36
- * with auto-detected dimensions if pointing to a local file.
37
- * - **Object format**: Detailed metadata with explicit properties that will be
38
- * validated and URL-resolved.
39
- *
40
- * @example Basic usage with a relative path
41
- * ```typescript
42
- * const resolver = new OpenGraphResolver({
43
- * siteUrl: "https://example.com",
44
- * docsRoot: "/path/to/docs"
45
- * });
46
- *
47
- * const metadata = await resolver.resolve(
48
- * "/images/og-api.png",
49
- * "my-package",
50
- * "MyClass"
51
- * );
52
- * // Result: { url: "https://example.com/images/og-api.png", width: 1200, height: 630, ... }
53
- * ```
54
- *
55
- * @example Using detailed configuration
56
- * ```typescript
57
- * const metadata = await resolver.resolve(
58
- * {
59
- * url: "/images/og.png",
60
- * alt: "Custom alt text",
61
- * width: 1200,
62
- * height: 630
63
- * },
64
- * "my-package"
65
- * );
66
- * ```
41
+ * Was `OpenGraphResolver.createPageMetadata`. It never touched the resolver's
42
+ * instance state, so it is a free function now rather than a static on a class
43
+ * that no longer exists.
67
44
  */
68
- var OpenGraphResolver = class {
69
- siteUrl;
70
- docsRoot;
71
- /**
72
- * Creates a new OpenGraphResolver instance.
73
- *
74
- * @param options - Configuration options for the resolver.
75
- * `siteUrl`: Base URL for the website (e.g., "https://example.com"),
76
- * used to construct absolute URLs from relative paths.
77
- * `docsRoot`: Optional root directory for documentation files;
78
- * when provided, enables auto-detection of image dimensions for local files
79
- * by looking in the `public` subdirectory.
80
- */
81
- constructor(options) {
82
- this.siteUrl = options.siteUrl;
83
- if (options.docsRoot != null) this.docsRoot = options.docsRoot;
84
- }
85
- /**
86
- * Resolves an Open Graph image configuration into complete metadata.
87
- *
88
- * Handles both string URLs/paths and detailed metadata objects, converting them
89
- * into fully-qualified `OpenGraphImageMetadata` with absolute URLs.
90
- *
91
- * @param config - The OG image configuration to resolve. Can be:
92
- * - A string URL (absolute or relative path starting with `/`)
93
- * - An `OpenGraphImageMetadata` object with explicit properties
94
- * - `undefined` to indicate no OG image
95
- * @param packageName - The package name for generating default alt text
96
- * @param apiName - Optional API name for more descriptive alt text
97
- * @returns Resolved metadata with absolute URLs, or `undefined` if:
98
- * - `config` is `undefined`
99
- * - The URL format is invalid
100
- *
101
- * @example Resolve a relative path
102
- * ```typescript
103
- * const metadata = await resolver.resolve("/images/og.png", "my-lib");
104
- * // Returns: { url: "https://example.com/images/og.png", alt: "my-lib API Documentation", ... }
105
- * ```
106
- *
107
- * @example Resolve an absolute URL
108
- * ```typescript
109
- * const metadata = await resolver.resolve("https://cdn.example.com/og.png", "my-lib");
110
- * // Returns: { url: "https://cdn.example.com/og.png", alt: "my-lib API Documentation" }
111
- * ```
112
- */
113
- async resolve(config, packageName, apiName) {
114
- if (!config) return;
115
- if (typeof config === "object") return this.resolveFromMetadata(config, packageName, apiName);
116
- return this.resolveFromString(config, packageName, apiName);
117
- }
118
- /**
119
- * Resolves a metadata object configuration into complete OG image metadata.
120
- *
121
- * Validates and resolves URLs in the provided metadata object, ensuring all
122
- * URLs are absolute and properly formatted.
123
- *
124
- * @param metadata - The metadata object containing OG image properties
125
- * @param packageName - Package name for default alt text generation
126
- * @param apiName - Optional API name for more descriptive alt text
127
- * @returns Resolved metadata with absolute URLs, or `undefined` if URL is invalid
128
- */
129
- async resolveFromMetadata(metadata, packageName, apiName) {
130
- const { url, secureUrl, type, width, height, alt } = metadata;
131
- const resolvedUrl = this.resolveUrl(url);
132
- if (!resolvedUrl) {
133
- emitEvent(PluginEvent.ConfigValidationWarning({
134
- ctx: { buildId: currentBuildId },
135
- field: "ogImage.url",
136
- value: url,
137
- reason: "invalid URL format",
138
- level: "warn"
139
- }));
140
- return;
141
- }
142
- let resolvedSecureUrl;
143
- if (secureUrl) {
144
- if (secureUrl.startsWith("https://")) resolvedSecureUrl = secureUrl;
145
- else emitEvent(PluginEvent.ConfigValidationWarning({
146
- ctx: { buildId: currentBuildId },
147
- field: "ogImage.secureUrl",
148
- value: secureUrl,
149
- reason: "secureUrl must be absolute HTTPS",
150
- level: "warn"
151
- }));
152
- }
153
- return {
154
- url: resolvedUrl,
155
- secureUrl: resolvedSecureUrl,
156
- type,
157
- width,
158
- height,
159
- alt: alt ?? this.generateAltText(packageName, apiName)
160
- };
161
- }
162
- /**
163
- * Resolves a string URL/path into complete OG image metadata.
164
- *
165
- * For relative paths pointing to local files, this method will attempt to:
166
- * 1. Locate the file in the docs `public` directory
167
- * 2. Read the image dimensions using `image-size`
168
- * 3. Determine the MIME type from the file extension
169
- *
170
- * @param imageUrl - The image URL or path to resolve
171
- * @param packageName - Package name for alt text generation
172
- * @param apiName - Optional API name for more descriptive alt text
173
- * @returns Resolved metadata with auto-detected dimensions for local files,
174
- * or `undefined` if the URL format is invalid
175
- */
176
- async resolveFromString(imageUrl, packageName, apiName) {
177
- const resolvedUrl = this.resolveUrl(imageUrl);
178
- if (!resolvedUrl) {
179
- emitEvent(PluginEvent.ConfigValidationWarning({
180
- ctx: { buildId: currentBuildId },
181
- field: "ogImage",
182
- value: imageUrl,
183
- reason: "invalid URL format",
184
- level: "warn"
185
- }));
186
- return;
187
- }
188
- const localPath = this.findLocalImage(imageUrl);
189
- const dimensions = localPath ? await this.readImageDimensions(localPath) : void 0;
190
- return {
191
- url: resolvedUrl,
192
- type: dimensions?.type,
193
- width: dimensions?.width,
194
- height: dimensions?.height,
195
- alt: this.generateAltText(packageName, apiName)
196
- };
197
- }
198
- /**
199
- * Resolves a URL string to an absolute URL.
200
- *
201
- * @param url - The URL to resolve (absolute URL or relative path)
202
- * @returns The absolute URL, or `undefined` if the format is invalid
203
- */
204
- resolveUrl(url) {
205
- if (url.startsWith("http://") || url.startsWith("https://")) return url;
206
- if (url.startsWith("/")) return `${this.siteUrl}${url}`;
207
- }
208
- /**
209
- * Attempts to find a local image file in the docs public directory.
210
- *
211
- * @param imagePath - The relative image path (starting with `/`)
212
- * @returns The absolute file path if found, or `undefined` if not found
213
- * or if `docsRoot` is not configured
214
- */
215
- findLocalImage(imagePath) {
216
- if (!this.docsRoot || !imagePath.startsWith("/")) return;
217
- const publicPath = path.join(this.docsRoot, "public", imagePath);
218
- if (fs.existsSync(publicPath)) return publicPath;
219
- }
220
- /**
221
- * Reads image dimensions and type from a local file.
222
- *
223
- * @param filePath - Absolute path to the image file
224
- * @returns Object containing width, height, and MIME type if successful,
225
- * or `undefined` if the file cannot be read or analyzed
226
- */
227
- async readImageDimensions(filePath) {
228
- try {
229
- const dimensions = await imageSizeFromFile(filePath);
230
- let mimeType;
231
- if (dimensions.type) mimeType = IMAGE_MIME_TYPES[dimensions.type.toLowerCase()];
232
- return {
233
- width: dimensions.width,
234
- height: dimensions.height,
235
- ...mimeType != null ? { type: mimeType } : {}
236
- };
237
- } catch (error) {
238
- emitEvent(PluginEvent.ConfigValidationWarning({
239
- ctx: { buildId: currentBuildId },
240
- field: "ogImage",
241
- value: filePath,
242
- reason: error.message ?? String(error),
243
- level: "warn"
244
- }));
245
- return;
246
- }
247
- }
248
- /**
249
- * Generates descriptive alt text for the OG image.
250
- *
251
- * @param packageName - The package name
252
- * @param apiName - Optional API name for more specific text
253
- * @returns Generated alt text string
254
- */
255
- generateAltText(packageName, apiName) {
256
- if (apiName) return `${apiName} - ${packageName} API Documentation`;
257
- return `${packageName} API Documentation`;
258
- }
259
- /**
260
- * Creates complete Open Graph metadata for an API documentation page.
261
- *
262
- * This static factory method builds a complete `OpenGraphMetadata` object
263
- * suitable for inclusion in page frontmatter, combining resolved image
264
- * metadata with article-specific information.
265
- *
266
- * @param options - Configuration for the OG metadata.
267
- * Includes `siteUrl`, `pageRoute`, `description`, `publishedTime`,
268
- * `modifiedTime`, `section`, `packageName`, and optional `ogImage`.
269
- * @returns Complete Open Graph metadata object
270
- *
271
- * @example
272
- * ```typescript
273
- * const ogMetadata = OpenGraphResolver.createPageMetadata({
274
- * siteUrl: "https://example.com",
275
- * pageRoute: "/api/classes/MyClass",
276
- * description: "MyClass provides...",
277
- * publishedTime: "2024-01-15T10:00:00Z",
278
- * modifiedTime: "2024-01-20T15:30:00Z",
279
- * section: "Classes",
280
- * packageName: "my-library",
281
- * ogImage: resolvedImageMetadata
282
- * });
283
- * ```
284
- */
285
- static createPageMetadata(options) {
286
- return {
287
- siteUrl: options.siteUrl,
288
- pageRoute: options.pageRoute,
289
- description: options.description,
290
- publishedTime: options.publishedTime,
291
- modifiedTime: options.modifiedTime,
292
- section: options.section,
293
- tags: [
294
- "TypeScript",
295
- "API",
296
- options.packageName
297
- ],
298
- ...options.ogImage != null ? { ogImage: options.ogImage } : {},
299
- ogType: "article"
300
- };
301
- }
302
- };
45
+ function createPageMetadata(options) {
46
+ return {
47
+ siteUrl: options.siteUrl,
48
+ pageRoute: options.pageRoute,
49
+ description: options.description,
50
+ publishedTime: options.publishedTime,
51
+ modifiedTime: options.modifiedTime,
52
+ section: options.section,
53
+ tags: [
54
+ "TypeScript",
55
+ "API",
56
+ options.packageName
57
+ ],
58
+ ...options.ogImage != null ? { ogImage: options.ogImage } : {},
59
+ ogType: "article"
60
+ };
61
+ }
303
62
 
304
63
  //#endregion
305
- export { OpenGraphResolver, setOgResolverEventEmitter };
64
+ export { createPageMetadata, imageMimeType, ogAltText, resolveOgUrl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rspress-plugin-api-extractor",
3
- "version": "0.9.2",
3
+ "version": "0.11.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.0",
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",
@@ -53,8 +53,8 @@
53
53
  "@microsoft/api-extractor-model": "^7.33.11",
54
54
  "@shikijs/twoslash": "^4.4.3",
55
55
  "@tsdoctor/bundle": "0.2.0",
56
- "@tsdoctor/model": "0.2.2",
57
- "@tsdoctor/registry": "0.2.0",
56
+ "@tsdoctor/model": "0.3.0",
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",
@@ -62,7 +62,6 @@
62
62
  "hast-util-to-jsx-runtime": "^2.3.6",
63
63
  "image-size": "^2.0.2",
64
64
  "ioredis": "^5.7.0",
65
- "mdast-util-from-markdown": "^2.0.3",
66
65
  "mdast-util-to-hast": "^13.2.1",
67
66
  "open": "^11.0.0",
68
67
  "prettier": "^3.9.6",
@@ -43,6 +43,24 @@ function deriveOutputPaths(input) {
43
43
  }
44
44
  return results;
45
45
  }
46
+ /**
47
+ * The API scope key derived from a base route.
48
+ *
49
+ * @remarks
50
+ * Load-bearing and previously duplicated. Config resolution registers each
51
+ * API's Twoslash environment under this key and the build program looks it up
52
+ * by the same key; if the two derivations disagree, every lookup misses and
53
+ * `getTransformer` falls back to the build-wide environment. Per-scope
54
+ * type-checking degrades to build-wide with no error and nothing visibly
55
+ * wrong in the output — the failure mode is silent, which is why one
56
+ * definition matters more here than the duplication was costing.
57
+ *
58
+ * Falls back to the package name so a single-API site mounted at `/` still
59
+ * gets a non-empty scope.
60
+ */
61
+ function apiScopeOf(baseRoute, packageName) {
62
+ return baseRoute.replace(/^\//, "").split("/")[0] || packageName;
63
+ }
46
64
 
47
65
  //#endregion
48
- export { deriveOutputPaths, normalizeBaseRoute, unscopedName };
66
+ export { apiScopeOf, deriveOutputPaths, normalizeBaseRoute, unscopedName };
package/plugin.js CHANGED
@@ -1,35 +1,44 @@
1
+ import { BuildId, PageConcurrency, SuppressExampleErrors, Thresholds } from "./BuildEnv.js";
1
2
  import { PluginEvent } from "./observability/events.js";
2
- import { emit, makeRuntimeEmitter } from "./observability/EventBus.js";
3
+ import { emit } from "./observability/EventBus.js";
4
+ import { codeBlockReport } from "./observability/metric-report.js";
3
5
  import { runHeartbeat } from "./observability/heartbeat.js";
4
6
  import { writeIssuesJson } from "./observability/sinks/issues-sink.js";
7
+ import { writeRenderPhaseJson } from "./observability/sinks/render-sink.js";
5
8
  import { buildEventBus, logBuildSummary, makeSummaryLoggerLayer } from "./layers/ObservabilityLive.js";
6
- import { setPrettierEventEmitter } from "./prettier-formatter.js";
7
- import { setOgResolverEventEmitter } from "./og-resolver.js";
8
- import { setBuildStagesEventEmitter } from "./build-stages.js";
9
- import { TwoslashManager, setEventEmitter } from "./twoslash-transformer.js";
9
+ import { emitSync, installSyncEmitter } from "./observability/sync-emitter.js";
10
+ import { TwoslashEnvironments } from "./services/TwoslashEnvironments.js";
11
+ import { clearTypeRoutes } from "./twoslash-transformer.js";
10
12
  import { VfsRegistry } from "./vfs-registry.js";
11
13
  import { generateApiDocs } from "./build-program.js";
12
14
  import { deriveOutputPaths, normalizeBaseRoute, unscopedName } from "./path-derivation.js";
13
15
  import { fromDir, fromParentDir } from "./config-helpers.js";
14
16
  import { classifyApiConfig, mergeLlmsPluginConfig } from "./config-utils.js";
15
- import { DEFAULT_SHIKI_THEMES, setShikiUtilsEventEmitter } from "./markdown/shiki-utils.js";
17
+ import { collectShikiThemes, normalizeThemeConfig } from "./markdown/shiki-utils.js";
16
18
  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 { PluginConfig } from "./services/PluginConfig.js";
23
+ import { TwoslashCacheService } from "./services/TwoslashCacheService.js";
20
24
  import { ConfigServiceLive } from "./layers/ConfigServiceLive.js";
21
- import { PathDerivationServiceLive } from "./layers/PathDerivationServiceLive.js";
25
+ import { HighlighterServiceLive } from "./layers/HighlighterServiceLive.js";
26
+ import { OgServiceLive } from "./layers/OgServiceLive.js";
27
+ import { PlatformLive } from "./layers/xdg.js";
28
+ import { TwoslashCacheServiceLive } from "./layers/TwoslashCacheServiceLive.js";
29
+ import { TwoslashEnvironmentsLive } from "./layers/TwoslashEnvironmentsLive.js";
22
30
  import { TypeRegistryServiceLive } from "./layers/TypeRegistryServiceLive.js";
23
- import { remarkApiCodeblocks, setRemarkApiCodeblocksEventEmitter } from "./remark-api-codeblocks.js";
24
- import { remarkWithApi, setRemarkWithApiEventEmitter } from "./remark-with-api.js";
25
- import { ShikiCrossLinker } from "./shiki-transformer.js";
31
+ import { clearTwoslashAccess, installTwoslashAccess, twoslashTransformerFor } from "./twoslash-access.js";
32
+ import { remarkApiCodeblocks } from "./remark-api-codeblocks.js";
33
+ import { remarkWithApi } from "./remark-with-api.js";
26
34
  import { createRequire } from "node:module";
27
35
  import fs from "node:fs";
36
+ import os from "node:os";
28
37
  import path from "node:path";
29
38
  import { fileURLToPath } from "node:url";
30
39
  import { NodeFileSystem } from "@effect/platform-node";
31
40
  import { SnapshotServiceLive } from "@tsdoctor/snapshot";
32
- import { Effect, FileSystem, Layer, ManagedRuntime, Ref, Schema } from "effect";
41
+ import { Effect, FileSystem, Layer, ManagedRuntime, Option, Ref, Schema } from "effect";
33
42
 
34
43
  //#region src/plugin.ts
35
44
  /* v8 ignore start -- RSPress plugin adapter, requires RSPress runtime */
@@ -49,30 +58,11 @@ const readSitePackageName = Effect.gen(function* () {
49
58
  return "unknown";
50
59
  });
51
60
  /**
52
- * Normalize theme configuration from user input to a consistent format.
53
- */
54
- function normalizeThemeConfig(theme) {
55
- if (!theme) return { ...DEFAULT_SHIKI_THEMES };
56
- if (typeof theme === "string") return {
57
- light: theme,
58
- dark: theme
59
- };
60
- if ("light" in theme && "dark" in theme && typeof theme.light === "string" && typeof theme.dark === "string") return {
61
- light: theme.light,
62
- dark: theme.dark
63
- };
64
- return {
65
- light: theme,
66
- dark: theme
67
- };
68
- }
69
- /**
70
61
  * RSPress plugin for generating API documentation from API Extractor model files
71
62
  */
72
63
  function ApiExtractorPluginImpl(rawOptions) {
73
64
  const options = Schema.decodeUnknownSync(PluginOptions)(rawOptions);
74
65
  const isInert = classifyApiConfig(options) === "disabled";
75
- const shikiCrossLinker = new ShikiCrossLinker();
76
66
  const envLogLevel = process.env.LOG_LEVEL?.toLowerCase();
77
67
  const buildId = `${process.pid}-${performance.now().toString(36)}`;
78
68
  const { resolved: obs, deprecations } = resolveObservability({
@@ -83,20 +73,17 @@ function ApiExtractorPluginImpl(rawOptions) {
83
73
  cwd: process.cwd(),
84
74
  buildId
85
75
  });
86
- const { layer: eventBusLayer, trace: traceSink, issues: issuesSink } = buildEventBus(obs);
76
+ const { layer: eventBusLayer, trace: traceSink, issues: issuesSink, render: renderSink, metrics: metricStore } = buildEventBus(obs);
87
77
  const dbPath = path.resolve(process.cwd(), ".api-docs", "snapshot", "api-docs.db");
88
78
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
89
- const BaseLayer = Layer.mergeAll(PathDerivationServiceLive, eventBusLayer, TypeRegistryServiceLive, NodeFileSystem.layer, SnapshotServiceLive(dbPath), makeSummaryLoggerLayer(obs.logLevel));
90
- const EffectAppLayer = Layer.provideMerge(ConfigServiceLive(options, shikiCrossLinker, buildId, obs.thresholds), BaseLayer);
79
+ const BuildEnvLayer = Layer.mergeAll(Layer.succeed(BuildId, buildId), Layer.succeed(Thresholds, obs.thresholds), Layer.succeed(PageConcurrency, os.cpus().length), Layer.succeed(SuppressExampleErrors, options.errors?.example !== "show"));
80
+ const PluginConfigLive = Layer.succeed(PluginConfig, options);
81
+ const HighlighterLive = HighlighterServiceLive(collectShikiThemes(options.api ? [options.api] : options.apis ?? []));
82
+ const BaseLayer = Layer.mergeAll(eventBusLayer, PluginConfigLive, HighlighterLive, TwoslashEnvironmentsLive, Layer.provide(OgServiceLive, PlatformLive), BuildEnvLayer, metricStore.layer, TypeRegistryServiceLive, NodeFileSystem.layer, SnapshotServiceLive(dbPath), TwoslashCacheServiceLive, makeSummaryLoggerLayer(obs.logLevel));
83
+ const EffectAppLayer = Layer.provideMerge(ConfigServiceLive, BaseLayer);
91
84
  const effectRuntime = ManagedRuntime.make(EffectAppLayer);
92
- const emitSync = makeRuntimeEmitter(effectRuntime);
93
- setEventEmitter(emitSync, buildId);
94
- setShikiUtilsEventEmitter(emitSync, buildId);
95
- setPrettierEventEmitter(emitSync, buildId);
96
- setOgResolverEventEmitter(emitSync, buildId);
97
- setRemarkWithApiEventEmitter(emitSync, buildId, obs.thresholds.slowCodeBlock);
98
- setRemarkApiCodeblocksEventEmitter(emitSync, buildId);
99
- setBuildStagesEventEmitter(emitSync, buildId);
85
+ const emitterRuntime = ManagedRuntime.make(Layer.mergeAll(eventBusLayer, metricStore.layer, makeSummaryLoggerLayer(obs.logLevel), BuildEnvLayer));
86
+ installSyncEmitter(emitterRuntime);
100
87
  const fileContextMap = /* @__PURE__ */ new Map();
101
88
  let docsRoot;
102
89
  let isFirstBuild = true;
@@ -110,13 +97,35 @@ function ApiExtractorPluginImpl(rawOptions) {
110
97
  async beforeBuild(_config, _isProd) {},
111
98
  async afterBuild(_config, isProd) {
112
99
  if (isFirstBuild && !isInert) {
113
- await effectRuntime.runPromise(logBuildSummary(obs.thresholds.slowCodeBlock));
100
+ const renderSamples = renderSink.snapshot();
101
+ const report = await effectRuntime.runPromise(codeBlockReport);
102
+ await effectRuntime.runPromise(Effect.gen(function* () {
103
+ const saved = yield* (yield* TwoslashCacheService).persist();
104
+ if (Option.isNone(saved)) return;
105
+ const stats = saved.value;
106
+ yield* emit(PluginEvent.TwoslashCacheSaved({
107
+ ctx: { buildId },
108
+ level: "info",
109
+ envHash: stats.envHash,
110
+ hits: stats.hits,
111
+ misses: stats.misses,
112
+ entries: stats.entries,
113
+ persisted: stats.dirty
114
+ }));
115
+ }));
116
+ await effectRuntime.runPromise(logBuildSummary(obs.thresholds.slowCodeBlock, report));
114
117
  if (isProd) await effectRuntime.runPromise(Effect.gen(function* () {
115
118
  const packageName = yield* readSitePackageName;
119
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
116
120
  yield* writeIssuesJson(issuesSink.snapshot(), {
117
121
  cwd: process.cwd(),
118
122
  packageName,
119
- generatedAt: (/* @__PURE__ */ new Date()).toISOString()
123
+ generatedAt
124
+ });
125
+ yield* writeRenderPhaseJson(report, renderSamples, {
126
+ cwd: process.cwd(),
127
+ packageName,
128
+ generatedAt
120
129
  });
121
130
  }));
122
131
  if (rspressLlmsEnabled && resolvedLlmsPlugin.enabled) {
@@ -132,7 +141,10 @@ function ApiExtractorPluginImpl(rawOptions) {
132
141
  isFirstBuild = false;
133
142
  }
134
143
  if (traceSink) traceSink.flush();
135
- if (isProd) await effectRuntime.dispose();
144
+ if (isProd) {
145
+ await effectRuntime.dispose();
146
+ await emitterRuntime.dispose();
147
+ }
136
148
  },
137
149
  async config(_config, _utils, isProd) {
138
150
  const buildStartTime = performance.now();
@@ -173,6 +185,8 @@ function ApiExtractorPluginImpl(rawOptions) {
173
185
  for (const dp of derivedPaths) fs.mkdirSync(dp.outputDir, { recursive: true });
174
186
  }
175
187
  VfsRegistry.clear();
188
+ clearTwoslashAccess();
189
+ clearTypeRoutes();
176
190
  fileContextMap.clear();
177
191
  issuesSink.reset();
178
192
  for (const dep of deprecations) emitSync(PluginEvent.DeprecatedConfigUsed({
@@ -204,13 +218,11 @@ function ApiExtractorPluginImpl(rawOptions) {
204
218
  apisTotal: apiCount,
205
219
  buildId
206
220
  }));
207
- const buildContext = yield* (yield* ConfigService).resolve(rspressConfigSubset);
221
+ installTwoslashAccess(yield* TwoslashEnvironments);
222
+ const apiConfigs = yield* (yield* ConfigService).resolve(rspressConfigSubset);
208
223
  buildResults.length = 0;
209
224
  yield* Ref.set(phaseRef, "generate");
210
- yield* Effect.forEach(buildContext.apiConfigs, (apiConfig) => generateApiDocs({
211
- ...apiConfig,
212
- suppressExampleErrors: buildContext.suppressExampleErrors
213
- }, buildContext, fileContextMap).pipe(Effect.tap((result) => {
225
+ yield* Effect.forEach(apiConfigs, (apiConfig) => generateApiDocs(apiConfig, fileContextMap).pipe(Effect.tap((result) => {
214
226
  buildResults.push(result);
215
227
  return emit(PluginEvent.ApiDocsCompleted({
216
228
  ctx: { buildId },
@@ -268,10 +280,10 @@ function ApiExtractorPluginImpl(rawOptions) {
268
280
  if (!existingInclude.includes("rspress-plugin-api-extractor/runtime")) updatedConfig.builderConfig.source.include = [...existingInclude, "rspress-plugin-api-extractor/runtime"];
269
281
  if (!updatedConfig.markdown) updatedConfig.markdown = {};
270
282
  if (!updatedConfig.markdown.remarkPlugins) updatedConfig.markdown.remarkPlugins = [];
271
- const remarkTheme = normalizeThemeConfig(options.api?.theme ?? options.apis?.[0]?.theme);
283
+ const firstApiTheme = options.api?.theme ?? options.apis?.[0]?.theme;
284
+ const remarkTheme = normalizeThemeConfig(firstApiTheme);
272
285
  updatedConfig.markdown.remarkPlugins.push([remarkWithApi, {
273
- shikiCrossLinker,
274
- getTransformer: () => TwoslashManager.getInstance().getTransformer(),
286
+ getTransformer: (apiScope) => twoslashTransformerFor(apiScope),
275
287
  theme: remarkTheme
276
288
  }]);
277
289
  updatedConfig.markdown.remarkPlugins.push([remarkApiCodeblocks]);
@@ -1,16 +1,10 @@
1
1
  import { PluginEvent } from "./observability/events.js";
2
2
  import { addLogicalBlankLines } from "./code-post-processor.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
- emitEvent(PluginEvent.PrettierError({
69
- ctx: { buildId: currentBuildId },
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, setPrettierEventEmitter };
78
+ export { formatCode };