rspress-plugin-api-extractor 0.10.0 → 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/build-program.js +33 -31
- package/build-stages.js +47 -39
- package/errors.js +0 -1
- package/layers/ConfigServiceLive.js +339 -433
- package/layers/HighlighterServiceLive.js +52 -0
- package/layers/OgServiceLive.js +134 -0
- package/layers/TwoslashCacheServiceLive.js +74 -19
- package/layers/TwoslashEnvironmentsLive.js +33 -0
- package/layers/TypeRegistryServiceLive.js +54 -47
- 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 -7
- package/observability/spans.js +3 -1
- package/observability/sync-emitter.js +78 -0
- package/og-resolver.js +46 -287
- package/package.json +2 -3
- package/path-derivation.js +19 -1
- package/plugin.js +48 -73
- package/prettier-formatter.js +4 -10
- package/remark-api-codeblocks.js +10 -18
- package/remark-with-api.js +11 -21
- package/services/HighlighterService.js +30 -0
- package/services/OgService.js +23 -0
- package/services/PluginConfig.js +26 -0
- package/services/TwoslashEnvironments.js +7 -0
- package/shiki-transformer.js +53 -234
- package/twoslash-access.js +48 -0
- package/twoslash-transformer.js +106 -83
- package/vfs-registry.js +1 -31
- package/layers/PathDerivationServiceLive.js +0 -16
- package/services/PathDerivationService.js +0 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TypeReferenceExtractor } from "../../type-reference-extractor.js";
|
|
2
|
-
import { escapeMdxGenerics, formatExampleCode, generateAvailableFrom, generateFrontmatter, prepareExampleCode, prependHiddenImports,
|
|
2
|
+
import { escapeMdxGenerics, formatExampleCode, generateAvailableFrom, generateFrontmatter, prepareExampleCode, prependHiddenImports, stripTwoslashDirectives } from "../helpers.js";
|
|
3
3
|
import { linkProse } from "../prose-linker.js";
|
|
4
|
-
import { ApiItems, Signature, Tsdoc } from "@tsdoctor/model";
|
|
4
|
+
import { ApiItems, Routes, Signature, Tsdoc } from "@tsdoctor/model";
|
|
5
5
|
|
|
6
6
|
//#region src/markdown/page-generators/class-page.ts
|
|
7
7
|
/**
|
|
@@ -60,7 +60,7 @@ var ClassPageGenerator = class {
|
|
|
60
60
|
*
|
|
61
61
|
* @param apiScope - API scope identifier for VFS lookup
|
|
62
62
|
*/
|
|
63
|
-
async generate(apiClass, baseRoute, packageName, singularName, apiScope, apiName, sourceConfig, suppressExampleErrors, llmsPlugin, availableFrom, syntheticBase) {
|
|
63
|
+
async generate(apiClass, baseRoute, packageName, singularName, apiScope, apiName, sourceConfig, suppressExampleErrors, llmsPlugin, availableFrom, syntheticBase, memberAnchors) {
|
|
64
64
|
const shouldSuppressErrors = suppressExampleErrors ?? true;
|
|
65
65
|
const name = apiClass.displayName;
|
|
66
66
|
const summary = Tsdoc.summary(apiClass) || "No description available.";
|
|
@@ -99,7 +99,7 @@ var ClassPageGenerator = class {
|
|
|
99
99
|
content += `## Constructors\n\n`;
|
|
100
100
|
for (const ctor of constructors) {
|
|
101
101
|
const ctorSummary = Tsdoc.summary(ctor);
|
|
102
|
-
const ctorId =
|
|
102
|
+
const ctorId = Routes.memberAnchor("constructor");
|
|
103
103
|
const ctorItem = ctor;
|
|
104
104
|
const params = Tsdoc.params(ctor);
|
|
105
105
|
const hasParameters = params.length > 0;
|
|
@@ -131,15 +131,14 @@ var ClassPageGenerator = class {
|
|
|
131
131
|
const isGetter = m.displayName.startsWith("get ") || m.displayName.startsWith("set ");
|
|
132
132
|
return !isStatic && !isGetter;
|
|
133
133
|
});
|
|
134
|
-
const
|
|
134
|
+
const anchors = memberAnchors ?? ApiItems.memberAnchors(apiClass);
|
|
135
|
+
const anchorFor = (member) => anchors.get(member.canonicalReference?.toString() ?? member.displayName) ?? Routes.memberAnchor(member.displayName);
|
|
135
136
|
const renderProperties = async (title, propList) => {
|
|
136
137
|
if (propList.length === 0) return;
|
|
137
138
|
content += `## ${title}\n\n`;
|
|
138
139
|
for (const prop of propList) {
|
|
139
140
|
const propSummary = Tsdoc.summary(prop);
|
|
140
|
-
const
|
|
141
|
-
const prefix = prefixMap.get(baseName) || "";
|
|
142
|
-
const propId = sanitizeId(prop.displayName, prefix);
|
|
141
|
+
const propId = anchorFor(prop);
|
|
143
142
|
const propItem = prop;
|
|
144
143
|
if (propItem.excerpt?.text) {
|
|
145
144
|
const memberSignature = Signature.format(propItem.excerpt).trim();
|
|
@@ -154,9 +153,7 @@ var ClassPageGenerator = class {
|
|
|
154
153
|
content += `## ${title}\n\n`;
|
|
155
154
|
for (const method of methodList) {
|
|
156
155
|
const methodSummary = Tsdoc.summary(method);
|
|
157
|
-
const
|
|
158
|
-
const prefix = prefixMap.get(baseName) || "";
|
|
159
|
-
const methodId = sanitizeId(method.displayName, prefix);
|
|
156
|
+
const methodId = anchorFor(method);
|
|
160
157
|
const methodItem = method;
|
|
161
158
|
const params = Tsdoc.params(method);
|
|
162
159
|
const hasParameters = params.length > 0;
|
|
@@ -258,26 +255,6 @@ var ClassPageGenerator = class {
|
|
|
258
255
|
};
|
|
259
256
|
}
|
|
260
257
|
/**
|
|
261
|
-
* Detect naming conflicts between class members and return prefixes to apply.
|
|
262
|
-
* Returns a Map of sanitized name to prefix (empty string if no conflict).
|
|
263
|
-
*/
|
|
264
|
-
detectMemberConflicts(staticProps, staticMethods, instanceProps, instanceMethods, getters) {
|
|
265
|
-
const prefixMap = /* @__PURE__ */ new Map();
|
|
266
|
-
const staticPropNames = new Set(staticProps.map((p) => sanitizeId(p.displayName)));
|
|
267
|
-
const staticMethodNames = new Set(staticMethods.map((m) => sanitizeId(m.displayName)));
|
|
268
|
-
const instancePropNames = new Set(instanceProps.map((p) => sanitizeId(p.displayName)));
|
|
269
|
-
const instanceMethodNames = new Set(instanceMethods.map((m) => sanitizeId(m.displayName)));
|
|
270
|
-
const getterNames = new Set(getters.map((g) => sanitizeId(g.displayName)));
|
|
271
|
-
for (const name of staticPropNames) if (staticMethodNames.has(name) || instancePropNames.has(name) || instanceMethodNames.has(name) || getterNames.has(name)) prefixMap.set(name, "static-property");
|
|
272
|
-
for (const name of staticMethodNames) if (instanceMethodNames.has(name) || instancePropNames.has(name) || getterNames.has(name)) prefixMap.set(name, "static");
|
|
273
|
-
else if (staticPropNames.has(name)) prefixMap.set(name, "");
|
|
274
|
-
for (const name of instancePropNames) if (instanceMethodNames.has(name) || getterNames.has(name)) prefixMap.set(name, "property");
|
|
275
|
-
else if (staticMethodNames.has(name)) prefixMap.set(name, "");
|
|
276
|
-
for (const name of instanceMethodNames) if (!prefixMap.has(name)) prefixMap.set(name, "");
|
|
277
|
-
for (const name of getterNames) if (!prefixMap.has(name)) prefixMap.set(name, "");
|
|
278
|
-
return prefixMap;
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
258
|
* Generate a class member signature with full class context
|
|
282
259
|
* Includes hidden imports with cut directive for external type resolution
|
|
283
260
|
* Uses the simplified approach: 3 lines (class opening, member, closing)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { emitFrontmatterBlock } from "../../frontmatter.js";
|
|
2
2
|
|
|
3
3
|
//#region src/markdown/page-generators/index-pages.ts
|
|
4
4
|
/**
|
|
@@ -9,13 +9,11 @@ var MainIndexPageGenerator = class {
|
|
|
9
9
|
* Generate the main API index page
|
|
10
10
|
*/
|
|
11
11
|
generate(packageName, baseRoute, _categoryCounts) {
|
|
12
|
-
const content =
|
|
13
|
-
title: API Reference
|
|
14
|
-
description: Auto-generated API documentation for ${
|
|
15
|
-
overview: true
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`;
|
|
12
|
+
const content = emitFrontmatterBlock({
|
|
13
|
+
title: "API Reference",
|
|
14
|
+
description: `Auto-generated API documentation for ${packageName}`,
|
|
15
|
+
overview: true
|
|
16
|
+
});
|
|
19
17
|
return {
|
|
20
18
|
routePath: `${baseRoute}/index`,
|
|
21
19
|
content
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TypeReferenceExtractor } from "../../type-reference-extractor.js";
|
|
2
|
-
import { escapeMdxGenerics, formatExampleCode, generateAvailableFrom, generateFrontmatter, prepareExampleCode, prependHiddenImports,
|
|
2
|
+
import { escapeMdxGenerics, formatExampleCode, generateAvailableFrom, generateFrontmatter, prepareExampleCode, prependHiddenImports, stripTwoslashDirectives } from "../helpers.js";
|
|
3
3
|
import { linkProse } from "../prose-linker.js";
|
|
4
|
-
import { ApiItems, Signature, Tsdoc } from "@tsdoctor/model";
|
|
4
|
+
import { ApiItems, Routes, Signature, Tsdoc } from "@tsdoctor/model";
|
|
5
5
|
|
|
6
6
|
//#region src/markdown/page-generators/interface-page.ts
|
|
7
7
|
/**
|
|
@@ -98,7 +98,7 @@ var InterfacePageGenerator = class {
|
|
|
98
98
|
content += `## Call Signatures\n\n`;
|
|
99
99
|
for (const callSig of callSignatures) {
|
|
100
100
|
const callSigSummary = Tsdoc.summary(callSig);
|
|
101
|
-
const callSigId =
|
|
101
|
+
const callSigId = Routes.memberAnchor("call-signature");
|
|
102
102
|
const callSigItem = callSig;
|
|
103
103
|
if (callSigItem.excerpt?.text) {
|
|
104
104
|
const memberSignature = Signature.format(callSigItem.excerpt).trim();
|
|
@@ -113,7 +113,7 @@ var InterfacePageGenerator = class {
|
|
|
113
113
|
content += `## Construct Signatures\n\n`;
|
|
114
114
|
for (const constructSig of constructSignatures) {
|
|
115
115
|
const constructSigSummary = Tsdoc.summary(constructSig);
|
|
116
|
-
const constructSigId =
|
|
116
|
+
const constructSigId = Routes.memberAnchor("construct-signature");
|
|
117
117
|
const constructSigItem = constructSig;
|
|
118
118
|
if (constructSigItem.excerpt?.text) {
|
|
119
119
|
const memberSignature = Signature.format(constructSigItem.excerpt).trim();
|
|
@@ -128,7 +128,7 @@ var InterfacePageGenerator = class {
|
|
|
128
128
|
content += `## Index Signature\n\n`;
|
|
129
129
|
for (const indexSig of indexSignatures) {
|
|
130
130
|
const indexSigSummary = Tsdoc.summary(indexSig);
|
|
131
|
-
const indexSigId =
|
|
131
|
+
const indexSigId = Routes.memberAnchor("index-signature");
|
|
132
132
|
const indexSigItem = indexSig;
|
|
133
133
|
if (indexSigItem.excerpt?.text) {
|
|
134
134
|
const memberSignature = Signature.format(indexSigItem.excerpt).trim();
|
|
@@ -143,7 +143,7 @@ var InterfacePageGenerator = class {
|
|
|
143
143
|
content += `## Properties\n\n`;
|
|
144
144
|
for (const prop of properties) {
|
|
145
145
|
const propSummary = Tsdoc.summary(prop);
|
|
146
|
-
const propId =
|
|
146
|
+
const propId = Routes.memberAnchor(prop.displayName);
|
|
147
147
|
const propItem = prop;
|
|
148
148
|
if (propItem.excerpt?.text) {
|
|
149
149
|
const memberSignature = Signature.format(propItem.excerpt).trim();
|
|
@@ -158,7 +158,7 @@ var InterfacePageGenerator = class {
|
|
|
158
158
|
content += `## Methods\n\n`;
|
|
159
159
|
for (const method of methods) {
|
|
160
160
|
const methodSummary = Tsdoc.summary(method);
|
|
161
|
-
const methodId =
|
|
161
|
+
const methodId = Routes.memberAnchor(method.displayName);
|
|
162
162
|
const methodItem = method;
|
|
163
163
|
if (methodItem.excerpt?.text) {
|
|
164
164
|
const memberSignature = Signature.format(methodItem.excerpt).trim();
|
package/markdown/shiki-utils.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import { PluginEvent } from "../observability/events.js";
|
|
2
|
+
import { emitSync, syncBuildId } from "../observability/sync-emitter.js";
|
|
2
3
|
|
|
3
4
|
//#region src/markdown/shiki-utils.ts
|
|
4
|
-
/** Module-level emitter injected by plugin.ts at startup. */
|
|
5
|
-
let emitEvent = () => {};
|
|
6
|
-
let currentBuildId = "";
|
|
7
|
-
function setShikiUtilsEventEmitter(fn, buildId = "") {
|
|
8
|
-
emitEvent = fn;
|
|
9
|
-
currentBuildId = buildId;
|
|
10
|
-
}
|
|
11
5
|
/**
|
|
12
6
|
* Default Shiki theme configuration
|
|
13
7
|
*/
|
|
@@ -48,8 +42,8 @@ async function generateShikiHast(code, highlighter, transformers, enableTwoslash
|
|
|
48
42
|
if (enableTwoslash) options.meta = { __raw: "twoslash" };
|
|
49
43
|
return await highlighter.codeToHast(code, options);
|
|
50
44
|
} catch (error) {
|
|
51
|
-
|
|
52
|
-
ctx: { buildId:
|
|
45
|
+
emitSync(PluginEvent.ShikiError({
|
|
46
|
+
ctx: { buildId: syncBuildId() },
|
|
53
47
|
file: "unknown",
|
|
54
48
|
reason: String(error),
|
|
55
49
|
level: "warn"
|
|
@@ -57,6 +51,67 @@ async function generateShikiHast(code, highlighter, transformers, enableTwoslash
|
|
|
57
51
|
return null;
|
|
58
52
|
}
|
|
59
53
|
}
|
|
54
|
+
/* v8 ignore stop -- everything below is pure and covered by __test__/markdown/shiki-themes.test.ts */
|
|
55
|
+
/**
|
|
56
|
+
* Normalize a theme option into the `{ light, dark }` pair the highlighter and
|
|
57
|
+
* the remark plugins both expect.
|
|
58
|
+
*
|
|
59
|
+
* @remarks
|
|
60
|
+
* Accepts a single theme name applied to both modes, an explicit pair, or a
|
|
61
|
+
* raw theme object. Lives beside {@link DEFAULT_SHIKI_THEMES} because it falls
|
|
62
|
+
* back to it; it was previously duplicated byte-for-byte in `plugin.ts` and
|
|
63
|
+
* `ConfigServiceLive.ts`.
|
|
64
|
+
*/
|
|
65
|
+
function normalizeThemeConfig(theme) {
|
|
66
|
+
if (!theme) return { ...DEFAULT_SHIKI_THEMES };
|
|
67
|
+
if (typeof theme === "string") return {
|
|
68
|
+
light: theme,
|
|
69
|
+
dark: theme
|
|
70
|
+
};
|
|
71
|
+
if ("light" in theme && "dark" in theme && typeof theme.light === "string" && typeof theme.dark === "string") return {
|
|
72
|
+
light: theme.light,
|
|
73
|
+
dark: theme.dark
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
light: theme,
|
|
77
|
+
dark: theme
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Every theme the build's single highlighter must load: one normalized pair
|
|
82
|
+
* per documented API, plus the two defaults.
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* Takes the RAW option configs rather than resolved ones, because the
|
|
86
|
+
* highlighter is now acquired when its layer builds — before `resolve()` has
|
|
87
|
+
* run. That is sound: a resolved config's `theme` is exactly
|
|
88
|
+
* `normalizeThemeConfig(api.theme)` and no version-level override exists, so
|
|
89
|
+
* the two inputs cannot disagree.
|
|
90
|
+
*
|
|
91
|
+
* Named theme strings are deduplicated; object themes are not (they have no
|
|
92
|
+
* identity to compare), which matches the previous behaviour and is harmless —
|
|
93
|
+
* Shiki keys a loaded theme by its `name`.
|
|
94
|
+
*/
|
|
95
|
+
function collectShikiThemes(apis) {
|
|
96
|
+
const named = /* @__PURE__ */ new Set();
|
|
97
|
+
const objects = [];
|
|
98
|
+
for (const api of apis) {
|
|
99
|
+
const theme = normalizeThemeConfig(api.theme);
|
|
100
|
+
for (const input of [theme.light, theme.dark]) if (typeof input === "string") named.add(input);
|
|
101
|
+
else objects.push(input);
|
|
102
|
+
}
|
|
103
|
+
if (typeof DEFAULT_SHIKI_THEMES.light === "string") named.add(DEFAULT_SHIKI_THEMES.light);
|
|
104
|
+
if (typeof DEFAULT_SHIKI_THEMES.dark === "string") named.add(DEFAULT_SHIKI_THEMES.dark);
|
|
105
|
+
return [...named, ...objects];
|
|
106
|
+
}
|
|
107
|
+
/** Languages the highlighter loads. Every code block the plugin renders is one of these. */
|
|
108
|
+
const SHIKI_LANGS = [
|
|
109
|
+
"typescript",
|
|
110
|
+
"javascript",
|
|
111
|
+
"json",
|
|
112
|
+
"bash",
|
|
113
|
+
"sh"
|
|
114
|
+
];
|
|
60
115
|
|
|
61
116
|
//#endregion
|
|
62
|
-
export { DEFAULT_SHIKI_THEMES, generateShikiHast,
|
|
117
|
+
export { DEFAULT_SHIKI_THEMES, SHIKI_LANGS, collectShikiThemes, generateShikiHast, normalizeThemeConfig };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BuildId } from "../BuildEnv.js";
|
|
1
2
|
import { LEVEL_RANK, levelOf } from "./events.js";
|
|
2
3
|
import { Context, Effect, Layer, Option } from "effect";
|
|
3
4
|
|
|
@@ -16,9 +17,34 @@ function makeShape(sinks) {
|
|
|
16
17
|
function makeEventBusLayer(sinks) {
|
|
17
18
|
return Layer.succeed(EventBus, makeShape(sinks));
|
|
18
19
|
}
|
|
19
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Emit when a bus is in context; silently no-op otherwise.
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* Fills `ctx.buildId` from the {@link BuildId} Reference when the caller left
|
|
25
|
+
* it empty, which is why no emit site passes one. Before this, 24 sites wrote
|
|
26
|
+
* `ctx: { buildId: "" }` — 22 in `ConfigServiceLive`, where the real value sat
|
|
27
|
+
* three scopes up and was simply not reached, and every site in
|
|
28
|
+
* `TypeRegistryServiceLive`, where the layer is module-level and there is no
|
|
29
|
+
* build to name. The second group is why a Reference is the fix and a
|
|
30
|
+
* find-and-replace is not: a Reference reaches code that no parameter can.
|
|
31
|
+
*
|
|
32
|
+
* A caller that sets a non-empty `buildId` keeps it, so a test can still emit
|
|
33
|
+
* with an explicit id.
|
|
34
|
+
*/
|
|
20
35
|
function emit(event) {
|
|
21
|
-
return Effect.
|
|
36
|
+
return Effect.gen(function* () {
|
|
37
|
+
const maybe = yield* Effect.serviceOption(EventBus);
|
|
38
|
+
if (Option.isNone(maybe)) return;
|
|
39
|
+
const filled = (event.ctx.buildId ?? "") === "" ? {
|
|
40
|
+
...event,
|
|
41
|
+
ctx: {
|
|
42
|
+
...event.ctx,
|
|
43
|
+
buildId: yield* BuildId
|
|
44
|
+
}
|
|
45
|
+
} : event;
|
|
46
|
+
yield* maybe.value.emit(filled);
|
|
47
|
+
});
|
|
22
48
|
}
|
|
23
49
|
/**
|
|
24
50
|
* Returns true when a bus is in context and has at least one sink admitted at
|
|
@@ -27,10 +53,6 @@ function emit(event) {
|
|
|
27
53
|
function wantsLevel(level) {
|
|
28
54
|
return Effect.serviceOption(EventBus).pipe(Effect.flatMap((maybe) => Option.isSome(maybe) ? maybe.value.wantsLevel(level) : Effect.succeed(false)));
|
|
29
55
|
}
|
|
30
|
-
/** Bind a runtime so non-Effect (sync island) callbacks can emit. */
|
|
31
|
-
function makeRuntimeEmitter(runtime) {
|
|
32
|
-
return (event) => runtime.runSync(emit(event));
|
|
33
|
-
}
|
|
34
56
|
|
|
35
57
|
//#endregion
|
|
36
|
-
export { EventBus, emit, makeEventBusLayer,
|
|
58
|
+
export { EventBus, emit, makeEventBusLayer, wantsLevel };
|
package/observability/spans.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Thresholds } from "../BuildEnv.js";
|
|
1
2
|
import { PluginEvent } from "./events.js";
|
|
2
3
|
import { emit } from "./EventBus.js";
|
|
3
4
|
import { Effect } from "effect";
|
|
@@ -24,8 +25,9 @@ const PHASE_THRESHOLD_KEY = {
|
|
|
24
25
|
* R type is unchanged: emit() is serviceOption-based (R = never), so adding
|
|
25
26
|
* the span/events adds zero requirements to the caller.
|
|
26
27
|
*/
|
|
27
|
-
function withPhase(phase, ctx, effect
|
|
28
|
+
function withPhase(phase, ctx, effect) {
|
|
28
29
|
return Effect.gen(function* () {
|
|
30
|
+
const thresholds = yield* Thresholds;
|
|
29
31
|
yield* emit(PluginEvent.PhaseStarted({
|
|
30
32
|
ctx,
|
|
31
33
|
level: "debug",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { BuildId, Thresholds } from "../BuildEnv.js";
|
|
2
|
+
import { emit } from "./EventBus.js";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/observability/sync-emitter.ts
|
|
6
|
+
/**
|
|
7
|
+
* The one bridge from synchronous, fiber-less code to the EventBus.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Seven modules run outside any Effect fiber — remark visitors, Shiki's
|
|
11
|
+
* `preprocess` hook, Prettier callbacks, the page-generation stages — and each
|
|
12
|
+
* carried its own byte-identical copy of this seam: a module-level
|
|
13
|
+
* `emitEvent`, a module-level `currentBuildId`, and a `setXEventEmitter(fn,
|
|
14
|
+
* buildId)` for `plugin.ts` to call. Two of them had already grown a third
|
|
15
|
+
* parameter for `slowCodeBlockMs`, which is how a duplicated seam decays: the
|
|
16
|
+
* copies stop being identical one caller at a time.
|
|
17
|
+
*
|
|
18
|
+
* The seam itself is forced. The duplication was not, and neither was the
|
|
19
|
+
* threading: every value those setters carried is now a `Context.Reference`
|
|
20
|
+
* read from the runtime, so the signature is one runtime and nothing else.
|
|
21
|
+
*
|
|
22
|
+
* **The runtime handed here must be synchronously buildable.** `runSync`
|
|
23
|
+
* builds the runtime's layer before running anything, so a runtime whose layer
|
|
24
|
+
* opens a database fails with `AsyncFiberError` at the first emit — from a
|
|
25
|
+
* remark plugin, during RSPress's render pass, invisible to every unit test.
|
|
26
|
+
* `plugin.ts` builds a small observability-only runtime for exactly this
|
|
27
|
+
* reason.
|
|
28
|
+
*
|
|
29
|
+
* @packageDocumentation
|
|
30
|
+
*/
|
|
31
|
+
const NOOP = {
|
|
32
|
+
emit: () => {},
|
|
33
|
+
buildId: "",
|
|
34
|
+
slowCodeBlockMs: Number.POSITIVE_INFINITY
|
|
35
|
+
};
|
|
36
|
+
let current = NOOP;
|
|
37
|
+
/**
|
|
38
|
+
* Bind the sync islands to a runtime.
|
|
39
|
+
*
|
|
40
|
+
* @remarks
|
|
41
|
+
* Call once, immediately after constructing the observability runtime. The
|
|
42
|
+
* References are read here rather than per emit: an emit happens per code
|
|
43
|
+
* block on a large site, and these values are fixed for the build.
|
|
44
|
+
*/
|
|
45
|
+
function installSyncEmitter(runtime) {
|
|
46
|
+
const env = runtime.runSync(Effect.gen(function* () {
|
|
47
|
+
return {
|
|
48
|
+
buildId: yield* BuildId,
|
|
49
|
+
slowCodeBlockMs: (yield* Thresholds).slowCodeBlock
|
|
50
|
+
};
|
|
51
|
+
}));
|
|
52
|
+
current = {
|
|
53
|
+
emit: (event) => runtime.runSync(emit(event)),
|
|
54
|
+
buildId: env.buildId,
|
|
55
|
+
slowCodeBlockMs: env.slowCodeBlockMs
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Emit an event from synchronous code. A no-op when nothing is installed. */
|
|
59
|
+
function emitSync(event) {
|
|
60
|
+
current.emit(event);
|
|
61
|
+
}
|
|
62
|
+
/** The current build's id, for a sync site assembling an `EventContext`. */
|
|
63
|
+
function syncBuildId() {
|
|
64
|
+
return current.buildId;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The slow-code-block threshold, for the two remark plugins that time blocks.
|
|
68
|
+
*
|
|
69
|
+
* @remarks
|
|
70
|
+
* The only piece of configuration a sync island needs beyond the build id, and
|
|
71
|
+
* the reason the old seams had begun growing divergent signatures.
|
|
72
|
+
*/
|
|
73
|
+
function syncSlowCodeBlockMs() {
|
|
74
|
+
return current.slowCodeBlockMs;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
export { emitSync, installSyncEmitter, syncBuildId, syncSlowCodeBlockMs };
|