rspress-plugin-api-extractor 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BuildEnv.js +58 -0
- package/README.md +2 -1
- package/build-program.js +33 -30
- package/build-stages.js +47 -39
- package/errors.js +0 -1
- package/index.d.ts +22 -14
- package/layers/ConfigServiceLive.js +349 -400
- package/layers/HighlighterServiceLive.js +52 -0
- package/layers/ObservabilityLive.js +26 -7
- package/layers/OgServiceLive.js +134 -0
- package/layers/TwoslashCacheServiceLive.js +108 -0
- package/layers/TwoslashEnvironmentsLive.js +33 -0
- package/layers/TypeRegistryServiceLive.js +54 -47
- package/layers/build-metrics.js +32 -5
- package/layers/xdg.js +44 -0
- package/markdown/helpers.js +9 -55
- package/markdown/page-generators/class-page.js +8 -31
- package/markdown/page-generators/index-pages.js +6 -8
- package/markdown/page-generators/interface-page.js +7 -7
- package/markdown/shiki-utils.js +65 -10
- package/observability/EventBus.js +29 -9
- package/observability/heartbeat.js +1 -1
- package/observability/metric-report.js +124 -0
- package/observability/sinks/console-sink.js +6 -0
- package/observability/sinks/metrics-sink.js +64 -21
- package/observability/sinks/render-sink.js +86 -0
- package/observability/sinks/trace-sink.js +10 -17
- package/observability/spans.js +4 -2
- package/observability/sync-emitter.js +78 -0
- package/og-resolver.js +46 -287
- package/package.json +4 -5
- package/path-derivation.js +19 -1
- package/plugin.js +64 -52
- package/prettier-formatter.js +4 -10
- package/remark-api-codeblocks.js +33 -15
- package/remark-with-api.js +24 -27
- package/schemas/config.js +11 -7
- package/services/HighlighterService.js +30 -0
- package/services/OgService.js +23 -0
- package/services/PluginConfig.js +26 -0
- package/services/TwoslashCacheService.js +15 -0
- package/services/TwoslashEnvironments.js +7 -0
- package/shiki-transformer.js +55 -256
- package/twoslash-access.js +48 -0
- package/twoslash-cache.js +174 -0
- package/twoslash-patterns.js +1 -1
- package/twoslash-timing-wrapper.js +23 -0
- package/twoslash-transformer.js +153 -89
- package/vfs-registry.js +1 -31
- package/layers/PathDerivationServiceLive.js +0 -16
- package/runtime/components/MarkdownText/index.js +0 -34
- package/services/PathDerivationService.js +0 -7
package/remark-api-codeblocks.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import { PluginEvent } from "./observability/events.js";
|
|
2
|
-
import {
|
|
2
|
+
import { emitSync, syncBuildId, syncSlowCodeBlockMs } from "./observability/sync-emitter.js";
|
|
3
3
|
import { VfsRegistry } from "./vfs-registry.js";
|
|
4
4
|
import { generateShikiHast } from "./markdown/shiki-utils.js";
|
|
5
|
+
import { setTwoslashFile } from "./twoslash-access.js";
|
|
6
|
+
import { createTwoslashTimingWrapper } from "./twoslash-timing-wrapper.js";
|
|
5
7
|
import { visit } from "unist-util-visit";
|
|
6
8
|
|
|
7
9
|
//#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
|
-
}
|
|
15
10
|
/**
|
|
16
11
|
* Create an MDX JSX attribute value expression with proper estree AST.
|
|
17
12
|
* This ensures the value is properly serialized as a JavaScript string literal.
|
|
@@ -91,7 +86,7 @@ const remarkApiCodeblocks = () => {
|
|
|
91
86
|
const promises = [];
|
|
92
87
|
const isSsgMd = import.meta.env?.SSG_MD || process.env.RSBUILD_ENVIRONMENT === "node_md" || process.env.BUILD_TARGET === "node_md";
|
|
93
88
|
const currentFilePath = file.path || "unknown";
|
|
94
|
-
if (file.path)
|
|
89
|
+
if (file.path) setTwoslashFile(file.path);
|
|
95
90
|
const jsxComponentNames = /* @__PURE__ */ new Set([
|
|
96
91
|
"ApiSignature",
|
|
97
92
|
"ApiMember",
|
|
@@ -109,9 +104,9 @@ const remarkApiCodeblocks = () => {
|
|
|
109
104
|
}
|
|
110
105
|
const vfsConfig = VfsRegistry.get(apiScopeValue);
|
|
111
106
|
if (!vfsConfig) {
|
|
112
|
-
|
|
107
|
+
emitSync(PluginEvent.ConfigCascadeWarning({
|
|
113
108
|
ctx: {
|
|
114
|
-
buildId:
|
|
109
|
+
buildId: syncBuildId(),
|
|
115
110
|
file: currentFilePath
|
|
116
111
|
},
|
|
117
112
|
field: "vfs",
|
|
@@ -123,14 +118,37 @@ const remarkApiCodeblocks = () => {
|
|
|
123
118
|
return;
|
|
124
119
|
}
|
|
125
120
|
const transformers = [];
|
|
126
|
-
|
|
121
|
+
let twoslashMs = 0;
|
|
122
|
+
if (node.name === "ApiExample" && vfsConfig.twoslashTransformer) transformers.push(createTwoslashTimingWrapper(vfsConfig.twoslashTransformer, (duration) => {
|
|
123
|
+
twoslashMs += duration;
|
|
124
|
+
}));
|
|
127
125
|
else if (node.name === "ApiMember" && vfsConfig.hideCutTransformer) transformers.push(vfsConfig.hideCutTransformer);
|
|
128
126
|
else if (node.name === "ApiSignature") {
|
|
129
127
|
if (vfsConfig.hideCutLinesTransformer) transformers.push(vfsConfig.hideCutLinesTransformer);
|
|
130
128
|
}
|
|
131
129
|
const isExample = node.name === "ApiExample" && !!vfsConfig.twoslashTransformer;
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
const shikiStart = performance.now();
|
|
131
|
+
const hastPromise = generateShikiHast(source, vfsConfig.highlighter, transformers.length > 0 ? transformers : void 0, isExample, vfsConfig.theme);
|
|
132
|
+
const renderMs = performance.now() - shikiStart;
|
|
133
|
+
let hast = await hastPromise;
|
|
134
|
+
const postStart = performance.now();
|
|
135
|
+
if (hast && vfsConfig.crossLinker) hast = vfsConfig.crossLinker.transformHast(hast);
|
|
136
|
+
const totalBlockTime = renderMs + (performance.now() - postStart);
|
|
137
|
+
emitSync(PluginEvent.CodeBlockProcessed({
|
|
138
|
+
ctx: {
|
|
139
|
+
buildId: syncBuildId(),
|
|
140
|
+
apiScope: apiScopeValue,
|
|
141
|
+
file: currentFilePath
|
|
142
|
+
},
|
|
143
|
+
lang: "typescript",
|
|
144
|
+
component: node.name,
|
|
145
|
+
twoslash: isExample,
|
|
146
|
+
twoslashMs,
|
|
147
|
+
shikiMs: Math.max(0, renderMs - twoslashMs),
|
|
148
|
+
totalMs: totalBlockTime,
|
|
149
|
+
slow: totalBlockTime > syncSlowCodeBlockMs(),
|
|
150
|
+
level: "debug"
|
|
151
|
+
}));
|
|
134
152
|
const hastBase64 = hast ? Buffer.from(JSON.stringify(hast), "utf-8").toString("base64") : "";
|
|
135
153
|
node.attributes.push({
|
|
136
154
|
type: "mdxJsxAttribute",
|
|
@@ -146,4 +164,4 @@ const remarkApiCodeblocks = () => {
|
|
|
146
164
|
};
|
|
147
165
|
|
|
148
166
|
//#endregion
|
|
149
|
-
export { remarkApiCodeblocks
|
|
167
|
+
export { remarkApiCodeblocks };
|
package/remark-with-api.js
CHANGED
|
@@ -1,21 +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 {
|
|
5
|
+
import { VfsRegistry } from "./vfs-registry.js";
|
|
5
6
|
import { DEFAULT_SHIKI_THEMES } from "./markdown/shiki-utils.js";
|
|
7
|
+
import { setTwoslashFile } from "./twoslash-access.js";
|
|
8
|
+
import { createTwoslashTimingWrapper } from "./twoslash-timing-wrapper.js";
|
|
6
9
|
import { codeToHast, hastToHtml } from "shiki";
|
|
7
10
|
import { visit } from "unist-util-visit";
|
|
8
11
|
|
|
9
12
|
//#region src/remark-with-api.ts
|
|
10
|
-
/** Module-level emitter injected by plugin.ts at startup. */
|
|
11
|
-
let emitEvent = () => {};
|
|
12
|
-
let currentBuildId = "";
|
|
13
|
-
let currentSlowCodeBlockMs = 500;
|
|
14
|
-
function setRemarkWithApiEventEmitter(fn, buildId = "", slowCodeBlockMs = 500) {
|
|
15
|
-
emitEvent = fn;
|
|
16
|
-
currentBuildId = buildId;
|
|
17
|
-
currentSlowCodeBlockMs = slowCodeBlockMs;
|
|
18
|
-
}
|
|
19
13
|
/**
|
|
20
14
|
* Supported languages for with-api code blocks
|
|
21
15
|
* Based on GitHub Linguist standard aliases:
|
|
@@ -58,18 +52,14 @@ function inferApiScope(filePath) {
|
|
|
58
52
|
* 5. Renders to ApiExample component with pre-rendered Shiki HAST
|
|
59
53
|
*/
|
|
60
54
|
const remarkWithApi = (options) => {
|
|
61
|
-
const {
|
|
55
|
+
const { getTransformer, theme } = options;
|
|
62
56
|
const resolvedTheme = theme ?? DEFAULT_SHIKI_THEMES;
|
|
63
57
|
return async function remarkTransformer(tree, file) {
|
|
64
58
|
const promises = [];
|
|
65
59
|
let needsApiExampleImport = false;
|
|
66
60
|
const isSsgMd = import.meta.env?.SSG_MD || process.env.RSBUILD_ENVIRONMENT === "node_md" || process.env.BUILD_TARGET === "node_md";
|
|
67
61
|
const currentFilePath = file.path;
|
|
68
|
-
if (currentFilePath)
|
|
69
|
-
const apiScope = inferApiScope(currentFilePath);
|
|
70
|
-
if (apiScope) shikiCrossLinker.setApiScope(apiScope);
|
|
71
|
-
TwoslashManager.getInstance().setCurrentFile(currentFilePath);
|
|
72
|
-
}
|
|
62
|
+
if (currentFilePath) setTwoslashFile(currentFilePath);
|
|
73
63
|
visit(tree, "code", (node, index, parent) => {
|
|
74
64
|
const hasWithApi = node.meta?.includes("with-api");
|
|
75
65
|
const lang = node.lang || "typescript";
|
|
@@ -78,9 +68,13 @@ const remarkWithApi = (options) => {
|
|
|
78
68
|
const promise = (async () => {
|
|
79
69
|
const blockStart = performance.now();
|
|
80
70
|
const rawCode = node.value;
|
|
71
|
+
const apiScope = currentFilePath ? inferApiScope(currentFilePath) : void 0;
|
|
81
72
|
const code = (await formatCode(rawCode, lang)).code;
|
|
82
|
-
|
|
83
|
-
const
|
|
73
|
+
let twoslashMs = 0;
|
|
74
|
+
const twoslashTransformer = getTransformer(apiScope);
|
|
75
|
+
const transformers = twoslashTransformer ? [createTwoslashTimingWrapper(twoslashTransformer, (duration) => {
|
|
76
|
+
twoslashMs += duration;
|
|
77
|
+
})] : [];
|
|
84
78
|
const shikiStart = performance.now();
|
|
85
79
|
let hast = await codeToHast(code, {
|
|
86
80
|
lang,
|
|
@@ -93,19 +87,22 @@ const remarkWithApi = (options) => {
|
|
|
93
87
|
cssVariablePrefix: "--api-shiki-",
|
|
94
88
|
transformers
|
|
95
89
|
});
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
const renderMs = performance.now() - shikiStart;
|
|
91
|
+
const scopeLinker = apiScope ? VfsRegistry.get(apiScope)?.crossLinker : void 0;
|
|
92
|
+
if (scopeLinker) hast = scopeLinker.transformHast(hast);
|
|
99
93
|
const totalBlockTime = performance.now() - blockStart;
|
|
100
|
-
const isSlow = totalBlockTime >
|
|
101
|
-
|
|
94
|
+
const isSlow = totalBlockTime > syncSlowCodeBlockMs();
|
|
95
|
+
emitSync(PluginEvent.CodeBlockProcessed({
|
|
102
96
|
ctx: {
|
|
103
|
-
buildId:
|
|
97
|
+
buildId: syncBuildId(),
|
|
98
|
+
...apiScope != null ? { apiScope } : {},
|
|
104
99
|
...currentFilePath != null ? { file: currentFilePath } : {}
|
|
105
100
|
},
|
|
106
101
|
lang,
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
component: "with-api",
|
|
103
|
+
twoslash: twoslashTransformer != null,
|
|
104
|
+
twoslashMs,
|
|
105
|
+
shikiMs: Math.max(0, renderMs - twoslashMs),
|
|
109
106
|
totalMs: totalBlockTime,
|
|
110
107
|
slow: isSlow,
|
|
111
108
|
level: "debug"
|
|
@@ -184,4 +181,4 @@ const remarkWithApi = (options) => {
|
|
|
184
181
|
};
|
|
185
182
|
|
|
186
183
|
//#endregion
|
|
187
|
-
export { remarkWithApi
|
|
184
|
+
export { remarkWithApi };
|
package/schemas/config.js
CHANGED
|
@@ -273,15 +273,19 @@ const MultiApiConfig = Schema.Struct({
|
|
|
273
273
|
* Path to a `tsconfig.json` for Twoslash.
|
|
274
274
|
*
|
|
275
275
|
* @remarks
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* configured tsconfigs are equivalent, or set the intended one on the
|
|
281
|
-
* first API only.
|
|
276
|
+
* This API's code blocks are type-checked under this config. APIs that
|
|
277
|
+
* declare the same config share one TypeScript environment; the file set
|
|
278
|
+
* is shared across all documented APIs either way, so a type owned by
|
|
279
|
+
* another documented package still resolves.
|
|
282
280
|
*/
|
|
283
281
|
tsconfig: Schema.optional(ModelInput),
|
|
284
|
-
/**
|
|
282
|
+
/**
|
|
283
|
+
* TypeScript compiler options for Twoslash, applying to this API only.
|
|
284
|
+
*
|
|
285
|
+
* @remarks
|
|
286
|
+
* Merged on top of the defaults and of this API's `tsconfig`, so declaring
|
|
287
|
+
* a single option overrides just that one.
|
|
288
|
+
*/
|
|
285
289
|
compilerOptions: Schema.optional(Schema.Unknown)
|
|
286
290
|
});
|
|
287
291
|
/**
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Context } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/services/HighlighterService.ts
|
|
4
|
+
/**
|
|
5
|
+
* The build's single Shiki highlighter.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* A highlighter owns a WASM oniguruma instance and every loaded grammar and
|
|
9
|
+
* theme, and it has a `dispose()` nobody was calling: `ConfigServiceLive`
|
|
10
|
+
* created one per `resolve()`, so a dev-mode HMR session leaked one per
|
|
11
|
+
* rebuild. The test run reported it as
|
|
12
|
+
* `[Shiki] 10 instances have been created` — a console leak, not a failure.
|
|
13
|
+
*
|
|
14
|
+
* The fix is NOT to scope it to `resolve()`. `VfsRegistry` hands this
|
|
15
|
+
* highlighter to the remark plugins, which RSPress invokes during its render
|
|
16
|
+
* pass — after `config()` has returned and after any `resolve()`-scoped scope
|
|
17
|
+
* would have closed. The lifetime that matches is the `ManagedRuntime`'s:
|
|
18
|
+
* acquired when the layer builds, released by `effectRuntime.dispose()` in
|
|
19
|
+
* `afterBuild` on production builds, and deliberately kept alive in dev so HMR
|
|
20
|
+
* rebuilds reuse it.
|
|
21
|
+
*
|
|
22
|
+
* Getting that wrong is silent — a disposed highlighter does not throw, the
|
|
23
|
+
* code blocks just render as unhighlighted `<pre>`.
|
|
24
|
+
*
|
|
25
|
+
* @packageDocumentation
|
|
26
|
+
*/
|
|
27
|
+
var HighlighterService = class extends Context.Service()("rspress-plugin-api-extractor/HighlighterService") {};
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { HighlighterService };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Context, Data } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/services/OgService.ts
|
|
4
|
+
const OgImageErrorBase = Data.TaggedError("OgImageError");
|
|
5
|
+
/**
|
|
6
|
+
* A configured OG image that could not be resolved.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* `cause` carries the original failure (an `image-size` parse error, a
|
|
10
|
+
* filesystem error) rather than a stringified copy of it.
|
|
11
|
+
*/
|
|
12
|
+
var OgImageError = class extends OgImageErrorBase {
|
|
13
|
+
get message() {
|
|
14
|
+
if (this.code === "invalid-url") return `Invalid Open Graph image URL in '${this.field}': ${this.value} — expected an absolute http(s) URL or a path starting with '/'`;
|
|
15
|
+
if (this.code === "invalid-secure-url") return `Invalid Open Graph secure URL in '${this.field}': ${this.value} — secureUrl must be an absolute https URL`;
|
|
16
|
+
const cause = this.cause instanceof Error ? this.cause.message : String(this.cause);
|
|
17
|
+
return `Could not read Open Graph image '${this.value}': ${cause}`;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var OgService = class extends Context.Service()("rspress-plugin-api-extractor/OgService") {};
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { OgImageError, OgService };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Context } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/services/PluginConfig.ts
|
|
4
|
+
/**
|
|
5
|
+
* The decoded plugin options, as a service.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* `ConfigServiceLive` used to be a factory taking these as an argument, which
|
|
9
|
+
* made it a layer-returning function — the shape the house rules warn about,
|
|
10
|
+
* since layers memoize by reference and a second call mints a second layer.
|
|
11
|
+
* It was only ever called once, but "only ever called once" is a property of
|
|
12
|
+
* the current call sites, not of the design.
|
|
13
|
+
*
|
|
14
|
+
* A `Context.Service` rather than a `Context.Reference`, deliberately, and for
|
|
15
|
+
* the same reason the Shiki themes are a layer argument: a Reference carries a
|
|
16
|
+
* default, so a wiring mistake would silently resolve to empty options and the
|
|
17
|
+
* build would document nothing while reporting success. There is no sensible
|
|
18
|
+
* default for "which APIs is this site documenting", so forgetting to provide
|
|
19
|
+
* it should be a loud "service not provided", which is what this gives.
|
|
20
|
+
*
|
|
21
|
+
* @packageDocumentation
|
|
22
|
+
*/
|
|
23
|
+
var PluginConfig = class extends Context.Service()("rspress-plugin-api-extractor/PluginConfig") {};
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { PluginConfig };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Context } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/services/TwoslashCacheService.ts
|
|
4
|
+
/**
|
|
5
|
+
* Persistence for the Twoslash result cache.
|
|
6
|
+
*
|
|
7
|
+
* Split from the synchronous cache object (`twoslash-cache.ts`) because
|
|
8
|
+
* `TwoslashTypesCache.read`/`write` are called from inside Shiki's `preprocess`
|
|
9
|
+
* hook and cannot await: the service loads once before the render phase and
|
|
10
|
+
* saves once after it, while every lookup in between is a synchronous map hit.
|
|
11
|
+
*/
|
|
12
|
+
var TwoslashCacheService = class extends Context.Service()("rspress-plugin-api-extractor/TwoslashCacheService") {};
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { TwoslashCacheService };
|