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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { BuildId, PageConcurrency, SuppressExampleErrors, Thresholds } from "../BuildEnv.js";
|
|
2
|
+
import { OgService } from "../services/OgService.js";
|
|
3
|
+
import { collectShikiThemes } from "../markdown/shiki-utils.js";
|
|
4
|
+
import { HighlighterService } from "../services/HighlighterService.js";
|
|
5
|
+
import { TwoslashEnvironments } from "../services/TwoslashEnvironments.js";
|
|
6
|
+
import { PluginConfig } from "../services/PluginConfig.js";
|
|
7
|
+
import { PlatformLive } from "./xdg.js";
|
|
8
|
+
import { TypeRegistryService } from "../services/TypeRegistryService.js";
|
|
9
|
+
import { TwoslashCacheService } from "../services/TwoslashCacheService.js";
|
|
10
|
+
import { ConfigService } from "../services/ConfigService.js";
|
|
11
|
+
import { makeSummaryLoggerLayer } from "./observability.js";
|
|
12
|
+
import { Layer } from "effect";
|
|
13
|
+
import { SnapshotService } from "@tsdoctor/snapshot";
|
|
14
|
+
import { NodeFileSystem } from "@effect/platform-node";
|
|
15
|
+
|
|
16
|
+
//#region src/layers/AppLayer.ts
|
|
17
|
+
/**
|
|
18
|
+
* The build's layer stack, tiered.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* `plugin.ts` used to merge eleven layers side by side in one `Layer.mergeAll`
|
|
22
|
+
* — `NodeFileSystem.layer` (platform) next to `TypeRegistryService.layer`
|
|
23
|
+
* (domain) next to `makeSummaryLoggerLayer` (observability) — with one of them
|
|
24
|
+
* carrying a local `Layer.provide` because a flat merge could not feed it its
|
|
25
|
+
* dependencies. Reading that told you what the build contains but not what
|
|
26
|
+
* depends on what.
|
|
27
|
+
*
|
|
28
|
+
* The tiers below are ordered by what they may reach: platform knows nothing
|
|
29
|
+
* about this plugin, core services know the platform, and build-scoped services
|
|
30
|
+
* know both.
|
|
31
|
+
*
|
|
32
|
+
* @packageDocumentation
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Build both stacks for one build.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* **A layer factory: call it once and bind the result to a `const`.** Layers
|
|
39
|
+
* memoize by reference, so a second call mints a second stack — a second Shiki
|
|
40
|
+
* highlighter, a second snapshot database, a second metric registry.
|
|
41
|
+
*/
|
|
42
|
+
function makeAppLayers(input) {
|
|
43
|
+
/**
|
|
44
|
+
* Per-build configuration, provided to BOTH stacks. Sharing these values is
|
|
45
|
+
* what lets a sync island and an Effect program agree on the build id and
|
|
46
|
+
* the slow-block threshold without either being handed them.
|
|
47
|
+
*/
|
|
48
|
+
const BuildEnvLayer = Layer.mergeAll(Layer.succeed(BuildId, input.buildId), Layer.succeed(Thresholds, input.obs.thresholds), Layer.succeed(PageConcurrency, input.pageConcurrency), Layer.succeed(SuppressExampleErrors, input.options.errors?.example !== "show"));
|
|
49
|
+
/**
|
|
50
|
+
* Sinks, metrics and the logger gate. Synchronously buildable, which is what
|
|
51
|
+
* lets the emitter stack below reuse it wholesale.
|
|
52
|
+
*/
|
|
53
|
+
const ObservabilityLayer = Layer.mergeAll(input.eventBus, input.metrics.layer, makeSummaryLoggerLayer(input.obs.logLevel));
|
|
54
|
+
/** Services that own a resource and need only the platform to build. */
|
|
55
|
+
const CoreLayer = Layer.mergeAll(TypeRegistryService.layer, TwoslashCacheService.layer, SnapshotService.layer(input.dbPath), Layer.provide(OgService.layer, PlatformLive));
|
|
56
|
+
/** Bound to a `const`: this is a factory, and a second call acquires a second highlighter. */
|
|
57
|
+
const HighlighterLive = HighlighterService.layer(collectShikiThemes(input.options.api ? [input.options.api] : input.options.apis ?? []));
|
|
58
|
+
/** Services scoped to this build's configuration. */
|
|
59
|
+
const BuildLayer = Layer.mergeAll(Layer.succeed(PluginConfig, input.options), HighlighterLive, TwoslashEnvironments.layer, BuildEnvLayer);
|
|
60
|
+
return {
|
|
61
|
+
app: Layer.provideMerge(ConfigService.layer, Layer.mergeAll(BuildLayer, CoreLayer, ObservabilityLayer, NodeFileSystem.layer)),
|
|
62
|
+
emitter: Layer.mergeAll(ObservabilityLayer, BuildEnvLayer)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
export { makeAppLayers };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { PluginEvent } from "../observability/events.js";
|
|
2
|
+
import { emit } from "../observability/EventBus.js";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/layers/api-results.ts
|
|
6
|
+
/**
|
|
7
|
+
* Accumulating one API's resolution result into the build-wide totals.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* `ConfigService.layer` resolves APIs down three paths — versioned, single
|
|
11
|
+
* non-versioned, and multi-API — and each one ended with a near-identical
|
|
12
|
+
* ~35-line block that merged the same three accumulators and then emitted the
|
|
13
|
+
* same two events per VFS entry. Three copies of one algorithm is three places
|
|
14
|
+
* for it to drift, and the third copy had ALREADY drifted: it emits its events
|
|
15
|
+
* inside the per-API effect and merges afterwards, rather than doing both in
|
|
16
|
+
* one pass.
|
|
17
|
+
*
|
|
18
|
+
* Splitting the block in two is what makes all three paths expressible. The
|
|
19
|
+
* merge is pure and the emission is effectful, and the multi-API path needs
|
|
20
|
+
* them at different moments; a single combined helper would have fitted two
|
|
21
|
+
* paths and forced the third to keep its own copy.
|
|
22
|
+
*
|
|
23
|
+
* @packageDocumentation
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Merge one result into the build-wide accumulators.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* Mutates `acc` rather than returning a new one. The accumulators are three
|
|
30
|
+
* `const` collections in a long generator that appends to them from several
|
|
31
|
+
* branches, and threading a replacement through every branch would be a larger
|
|
32
|
+
* change than this task is buying.
|
|
33
|
+
*
|
|
34
|
+
* **The VFS is a single flat namespace shared by every documented API.** A
|
|
35
|
+
* later entry silently overwrites an earlier one at the same path, which is
|
|
36
|
+
* load-bearing rather than accidental: cross-package type references resolve
|
|
37
|
+
* only because every package's declarations live in one environment (see
|
|
38
|
+
* `type-loading-vfs.md`).
|
|
39
|
+
*/
|
|
40
|
+
function mergeApiResult(acc, result) {
|
|
41
|
+
for (const [filepath, content] of result.vfs.entries()) acc.combinedVfs.set(filepath, content);
|
|
42
|
+
if (result.externalPackages.length > 0) acc.allExternalPackages.push(...result.externalPackages);
|
|
43
|
+
if (result.config) acc.apiConfigs.push(result.config);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Emit the per-entry VFS events for one API's payloads.
|
|
47
|
+
*
|
|
48
|
+
* @remarks
|
|
49
|
+
* `ImportsPrepended` fires only when imports were actually prepended, so an
|
|
50
|
+
* entry that needed none produces one event rather than two.
|
|
51
|
+
*
|
|
52
|
+
* `wantTrace` gates the two heavy fields — the full declaration text and the
|
|
53
|
+
* resolved import refs. Both are only ever read by the JSONL trace sink, and
|
|
54
|
+
* carrying them unconditionally would put every generated declaration file
|
|
55
|
+
* through the event bus on every build.
|
|
56
|
+
*/
|
|
57
|
+
function emitVfsPayloadEvents(packageName, payloads, wantTrace) {
|
|
58
|
+
return Effect.gen(function* () {
|
|
59
|
+
for (const payload of payloads) {
|
|
60
|
+
const ctx = {
|
|
61
|
+
packageName,
|
|
62
|
+
...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
|
|
63
|
+
};
|
|
64
|
+
yield* emit(PluginEvent.VfsGenerated({
|
|
65
|
+
ctx,
|
|
66
|
+
level: "debug",
|
|
67
|
+
file: payload.file,
|
|
68
|
+
declCount: payload.declCount,
|
|
69
|
+
contentHash: payload.contentHash,
|
|
70
|
+
...wantTrace && payload.content ? { content: payload.content } : {}
|
|
71
|
+
}));
|
|
72
|
+
if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
|
|
73
|
+
ctx,
|
|
74
|
+
level: "debug",
|
|
75
|
+
file: payload.file,
|
|
76
|
+
imports: wantTrace ? payload.importRefs : []
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
//#endregion
|
|
83
|
+
export { emitVfsPayloadEvents, mergeApiResult };
|
package/layers/build-metrics.js
CHANGED
|
@@ -18,7 +18,7 @@ function makeMetricStore() {
|
|
|
18
18
|
* build still gets its own counters — see {@link MetricRegistryLive}.
|
|
19
19
|
*
|
|
20
20
|
* Extracted into its own module so that `metrics-sink.ts` can import it
|
|
21
|
-
* without creating a circular dependency through `
|
|
21
|
+
* without creating a circular dependency through `observability.ts`
|
|
22
22
|
* (which itself imports `metrics-sink.ts`).
|
|
23
23
|
*/
|
|
24
24
|
const BuildMetrics = {
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { BuildId } from "../BuildEnv.js";
|
|
2
|
+
import { BuildMetrics } from "./build-metrics.js";
|
|
3
|
+
import { PluginEvent } from "../observability/events.js";
|
|
4
|
+
import { emit, wantsLevel } from "../observability/EventBus.js";
|
|
5
|
+
import { TypeReferenceExtractor } from "../type-reference-extractor.js";
|
|
6
|
+
import { deriveSiteUrl } from "../og-resolver.js";
|
|
7
|
+
import { withPhase } from "../observability/spans.js";
|
|
8
|
+
import { normalizeThemeConfig } from "../markdown/shiki-utils.js";
|
|
9
|
+
import { apiScopeOf, deriveOutputPaths, normalizeBaseRoute, unscopedName } from "../path-derivation.js";
|
|
10
|
+
import { classifyApiConfig, extractAutoDetectedPackages, isVersionConfig, mergeLlmsPluginConfig, validateExternalPackages } from "../config-utils.js";
|
|
11
|
+
import { ApiExtractedPackage } from "../api-extracted-package.js";
|
|
12
|
+
import { CategoryResolver } from "../category-resolver.js";
|
|
13
|
+
import { ConfigValidationError } from "../errors.js";
|
|
14
|
+
import { loadApiModel, loadPackageJson, loadVersionModel } from "../model-loader.js";
|
|
15
|
+
import { DEFAULT_CATEGORIES } from "../schemas/config.js";
|
|
16
|
+
import { PluginConfig } from "../services/PluginConfig.js";
|
|
17
|
+
import { TypeRegistryService } from "../services/TypeRegistryService.js";
|
|
18
|
+
import { emitVfsPayloadEvents, mergeApiResult } from "./api-results.js";
|
|
19
|
+
import { mergeExternalTypes } from "./external-types.js";
|
|
20
|
+
import { registerTypeEnvironments, resolveTsConfigTyped } from "./type-environment.js";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { Effect, Metric } from "effect";
|
|
23
|
+
import { hashContent } from "@tsdoctor/snapshot";
|
|
24
|
+
|
|
25
|
+
//#region src/layers/config-resolution.ts
|
|
26
|
+
/**
|
|
27
|
+
* Prepend import statements for external type references to the VFS declaration files.
|
|
28
|
+
* Returns per-entry payloads for event emission (heavy content/importRefs gated on wantTrace).
|
|
29
|
+
*/
|
|
30
|
+
function prependImportsToVfs(vfs, apiPackage, packageName, wantTrace) {
|
|
31
|
+
const extractor = new TypeReferenceExtractor(apiPackage, packageName);
|
|
32
|
+
const payloads = [];
|
|
33
|
+
for (const entryPoint of apiPackage.entryPoints) {
|
|
34
|
+
const entryEp = entryPoint;
|
|
35
|
+
const imports = extractor.extractImportsForEntryPoint(entryEp);
|
|
36
|
+
const importStatements = TypeReferenceExtractor.formatImports(imports);
|
|
37
|
+
const entryName = entryEp.displayName || "";
|
|
38
|
+
const file = `node_modules/${packageName}/${entryName ? `${entryName}.d.ts` : "index.d.ts"}`;
|
|
39
|
+
const hasImports = importStatements.length > 0;
|
|
40
|
+
if (hasImports) {
|
|
41
|
+
const existing = vfs.get(file);
|
|
42
|
+
if (existing) vfs.set(file, `${importStatements.join("\n")}\n\n${existing}`);
|
|
43
|
+
}
|
|
44
|
+
const content = vfs.get(file) ?? "";
|
|
45
|
+
const declCount = entryEp.members.length;
|
|
46
|
+
const contentHash = hashContent(content);
|
|
47
|
+
const importRefs = wantTrace && hasImports ? imports.map((i) => ({
|
|
48
|
+
from: i.packageName,
|
|
49
|
+
symbols: [...i.symbols]
|
|
50
|
+
})) : [];
|
|
51
|
+
payloads.push({
|
|
52
|
+
file,
|
|
53
|
+
entryPoint: entryName,
|
|
54
|
+
declCount,
|
|
55
|
+
contentHash,
|
|
56
|
+
content: wantTrace ? content : "",
|
|
57
|
+
hasImports,
|
|
58
|
+
importRefs
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return payloads;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Validate plugin options and return an Effect that fails with ConfigValidationError.
|
|
65
|
+
*/
|
|
66
|
+
function validateOptions(options, rspressConfig) {
|
|
67
|
+
return Effect.gen(function* () {
|
|
68
|
+
const api = options.api ?? void 0;
|
|
69
|
+
const apis = options.apis != null && options.apis.length > 0 ? options.apis : void 0;
|
|
70
|
+
const { multiVersion } = rspressConfig;
|
|
71
|
+
if (api && apis) return yield* new ConfigValidationError({
|
|
72
|
+
field: "api/apis",
|
|
73
|
+
reason: "Cannot provide both 'api' and 'apis'. Use 'api' for single-package sites or 'apis' for multi-package portals."
|
|
74
|
+
});
|
|
75
|
+
if (!api && !apis) {
|
|
76
|
+
if (classifyApiConfig(options) === "missing") return yield* new ConfigValidationError({
|
|
77
|
+
field: "api/apis",
|
|
78
|
+
reason: "Must provide either 'api' or 'apis'."
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (apis) {
|
|
83
|
+
if (multiVersion) return yield* new ConfigValidationError({
|
|
84
|
+
field: "apis",
|
|
85
|
+
reason: "multiVersion is not supported with 'apis' (multi-API mode). Use 'api' (single-API mode) for versioned documentation."
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (api) {
|
|
90
|
+
if (multiVersion) {
|
|
91
|
+
if (!api.versions) return yield* new ConfigValidationError({
|
|
92
|
+
field: "api.versions",
|
|
93
|
+
reason: "'versions' is required when multiVersion is active."
|
|
94
|
+
});
|
|
95
|
+
const pluginKeys = new Set(Object.keys(api.versions));
|
|
96
|
+
const rspressKeys = new Set(multiVersion.versions);
|
|
97
|
+
if (pluginKeys.size !== rspressKeys.size || ![...pluginKeys].every((k) => rspressKeys.has(k))) return yield* new ConfigValidationError({
|
|
98
|
+
field: "api.versions",
|
|
99
|
+
reason: `api.versions keys [${[...pluginKeys].join(", ")}] must exactly match multiVersion.versions [${[...rspressKeys].join(", ")}].`
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
if (api.versions) yield* emit(PluginEvent.ConfigCascadeWarning({
|
|
103
|
+
ctx: {},
|
|
104
|
+
level: "warn",
|
|
105
|
+
field: "versions",
|
|
106
|
+
chosen: "(none — multiVersion not configured)",
|
|
107
|
+
ignored: ["api.versions"]
|
|
108
|
+
}));
|
|
109
|
+
if (!api.model) return yield* new ConfigValidationError({
|
|
110
|
+
field: "api.model",
|
|
111
|
+
reason: "'model' is required when multiVersion is not active."
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Resolve plugin options + RSPress config into the API configs the pipeline
|
|
119
|
+
* runs over.
|
|
120
|
+
*
|
|
121
|
+
* @remarks
|
|
122
|
+
* A module-level `const`, not a factory. It took the plugin options as an
|
|
123
|
+
* argument and was called inline at the merge site, which made it a
|
|
124
|
+
* layer-returning function: layers memoize by reference, so a second call
|
|
125
|
+
* would build a second `ConfigService` with its own captured `TypeRegistry`.
|
|
126
|
+
* The options come from {@link PluginConfig} now, so there is nothing to pass
|
|
127
|
+
* and nothing to call twice.
|
|
128
|
+
*
|
|
129
|
+
* The `Layer` around this lives on the service, as `ConfigService.layer`. The
|
|
130
|
+
* implementation stays here rather than in the service module because it is
|
|
131
|
+
* the bulk of config resolution; the service module declares the contract.
|
|
132
|
+
*/
|
|
133
|
+
const makeConfigService = Effect.gen(function* () {
|
|
134
|
+
const typeRegistry = yield* TypeRegistryService;
|
|
135
|
+
const options = yield* PluginConfig;
|
|
136
|
+
return { resolve: (rspressConfig) => Effect.gen(function* () {
|
|
137
|
+
const buildId = yield* BuildId;
|
|
138
|
+
const loadStart = performance.now();
|
|
139
|
+
const wantTrace = yield* wantsLevel("trace");
|
|
140
|
+
yield* validateOptions(options, { ...rspressConfig.multiVersion ? { multiVersion: {
|
|
141
|
+
default: rspressConfig.multiVersion.default,
|
|
142
|
+
versions: [...rspressConfig.multiVersion.versions]
|
|
143
|
+
} } : {} });
|
|
144
|
+
const rspressMultiVersion = rspressConfig.multiVersion;
|
|
145
|
+
const rspressLocales = rspressConfig.locales?.map((l) => l.lang) ?? [];
|
|
146
|
+
const rspressLang = rspressConfig.lang;
|
|
147
|
+
const docsRoot = rspressConfig.root;
|
|
148
|
+
const rspressRoot = docsRoot || process.cwd();
|
|
149
|
+
const siteUrl = deriveSiteUrl(rspressConfig.siteOrigin, rspressConfig.base);
|
|
150
|
+
const categoryResolver = new CategoryResolver();
|
|
151
|
+
const pluginDefaults = categoryResolver.mergeCategories(DEFAULT_CATEGORIES, options.defaultCategories);
|
|
152
|
+
const apiConfigs = [];
|
|
153
|
+
const combinedVfs = /* @__PURE__ */ new Map();
|
|
154
|
+
const allExternalPackages = [];
|
|
155
|
+
/** The three above, as one value the merge helper can take. */
|
|
156
|
+
const acc = {
|
|
157
|
+
apiConfigs,
|
|
158
|
+
combinedVfs,
|
|
159
|
+
allExternalPackages
|
|
160
|
+
};
|
|
161
|
+
let firstApiTsconfig;
|
|
162
|
+
let firstApiCompilerOptions;
|
|
163
|
+
/**
|
|
164
|
+
* Raw TypeScript config per API scope. Each documented package is
|
|
165
|
+
* type-checked under its OWN configuration; the build no longer picks
|
|
166
|
+
* one and applies it to everything.
|
|
167
|
+
*/
|
|
168
|
+
const scopeTsConfigs = /* @__PURE__ */ new Map();
|
|
169
|
+
/**
|
|
170
|
+
* Emit a typed ModelLoadFailed event for a failed model load, then
|
|
171
|
+
* convert the typed failure to a defect — a missing or unparsable
|
|
172
|
+
* model remains fatal to the build, exactly as before, but the
|
|
173
|
+
* event now rides the error channel instead of a sync-island seam.
|
|
174
|
+
*/
|
|
175
|
+
const withModelLoadEvents = (self) => self.pipe(Effect.tapError((error) => emit(PluginEvent.ModelLoadFailed({
|
|
176
|
+
ctx: { buildId },
|
|
177
|
+
level: "error",
|
|
178
|
+
modelPath: "modelPath" in error ? error.modelPath : "<loader function>",
|
|
179
|
+
reason: error.message
|
|
180
|
+
}))), Effect.orDie);
|
|
181
|
+
/**
|
|
182
|
+
* Helper to process a single API model (shared by single and multi modes).
|
|
183
|
+
*/
|
|
184
|
+
const processSimpleApi = (api, model, outputDir, fullRoute, wantTrace) => Effect.gen(function* () {
|
|
185
|
+
const { apiPackage, source: loaderSource } = yield* withModelLoadEvents(loadApiModel(model));
|
|
186
|
+
{
|
|
187
|
+
const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories);
|
|
188
|
+
const resolvedSource = categoryResolver.resolveSourceConfig(api.source, loaderSource);
|
|
189
|
+
const resolvedLlms = mergeLlmsPluginConfig(options.llmsPlugin, api.llmsPlugin);
|
|
190
|
+
const packageJson = api.packageJson ? yield* Effect.tryPromise({
|
|
191
|
+
try: () => loadPackageJson(api.packageJson),
|
|
192
|
+
catch: (cause) => new ConfigValidationError({
|
|
193
|
+
field: "packageJson",
|
|
194
|
+
reason: cause instanceof Error ? cause.message : String(cause),
|
|
195
|
+
cause
|
|
196
|
+
})
|
|
197
|
+
}) : void 0;
|
|
198
|
+
yield* Effect.try({
|
|
199
|
+
try: () => validateExternalPackages(api.externalPackages, packageJson),
|
|
200
|
+
catch: (cause) => new ConfigValidationError({
|
|
201
|
+
field: "externalPackages",
|
|
202
|
+
reason: cause instanceof Error ? cause.message : String(cause),
|
|
203
|
+
cause
|
|
204
|
+
})
|
|
205
|
+
});
|
|
206
|
+
const externalPackages = api.externalPackages || extractAutoDetectedPackages(packageJson, api.autoDetectDependencies);
|
|
207
|
+
if (externalPackages && externalPackages.length > 0) yield* Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length);
|
|
208
|
+
const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
|
|
209
|
+
const vfsPayloads = prependImportsToVfs(vfs, apiPackage, api.packageName, wantTrace);
|
|
210
|
+
const resolvedOgImage = api.ogImage ?? options.ogImage;
|
|
211
|
+
const resolvedTheme = normalizeThemeConfig(api.theme);
|
|
212
|
+
return {
|
|
213
|
+
vfs,
|
|
214
|
+
vfsPayloads,
|
|
215
|
+
externalPackages: externalPackages || [],
|
|
216
|
+
config: {
|
|
217
|
+
apiPackage,
|
|
218
|
+
packageName: api.packageName,
|
|
219
|
+
...api.name != null ? { apiName: api.name } : {},
|
|
220
|
+
outputDir,
|
|
221
|
+
baseRoute: fullRoute,
|
|
222
|
+
categories: resolvedCategories,
|
|
223
|
+
...resolvedSource != null ? { source: resolvedSource } : {},
|
|
224
|
+
...packageJson != null ? { packageJson } : {},
|
|
225
|
+
...resolvedLlms != null ? { llmsPlugin: resolvedLlms } : {},
|
|
226
|
+
...siteUrl != null ? { siteUrl } : {},
|
|
227
|
+
...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
|
|
228
|
+
docsDir: path.dirname(outputDir),
|
|
229
|
+
...docsRoot != null ? { docsRoot } : {},
|
|
230
|
+
...resolvedTheme != null ? { theme: resolvedTheme } : {}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
yield* withPhase("modelLoad", { buildId }, Effect.gen(function* () {
|
|
236
|
+
if (options.api) {
|
|
237
|
+
const api = options.api;
|
|
238
|
+
const baseRoute = normalizeBaseRoute(api.baseRoute ?? "/");
|
|
239
|
+
firstApiTsconfig = api.tsconfig;
|
|
240
|
+
firstApiCompilerOptions = api.compilerOptions;
|
|
241
|
+
scopeTsConfigs.set(apiScopeOf(baseRoute, api.packageName), rawTsConfig(api));
|
|
242
|
+
if (rspressMultiVersion && api.versions) {
|
|
243
|
+
const versionResults = yield* Effect.forEach(Object.entries(api.versions), ([version, versionValue]) => Effect.gen(function* () {
|
|
244
|
+
const versionDp = deriveOutputPaths({
|
|
245
|
+
mode: "single",
|
|
246
|
+
docsRoot: rspressRoot,
|
|
247
|
+
baseRoute,
|
|
248
|
+
apiFolder: api.apiFolder ?? "api",
|
|
249
|
+
locales: rspressLocales,
|
|
250
|
+
defaultLang: rspressLang,
|
|
251
|
+
versions: [version],
|
|
252
|
+
defaultVersion: rspressMultiVersion?.default
|
|
253
|
+
})[0];
|
|
254
|
+
if (!versionDp) return {
|
|
255
|
+
vfs: /* @__PURE__ */ new Map(),
|
|
256
|
+
vfsPayloads: [],
|
|
257
|
+
externalPackages: [],
|
|
258
|
+
config: null
|
|
259
|
+
};
|
|
260
|
+
const versionConfig = isVersionConfig(versionValue) ? versionValue : { model: versionValue };
|
|
261
|
+
const { apiPackage, packageJson: versionPackageJson, categories: versionCategories, source: versionSource, externalPackages: versionExternalPackages, autoDetectDependencies: versionAutoDetectDependencies, llmsPlugin: versionLlms, ogImage: versionOgImage } = yield* withModelLoadEvents(loadVersionModel(versionConfig));
|
|
262
|
+
{
|
|
263
|
+
yield* Metric.update(BuildMetrics.apiVersionsLoaded, 1);
|
|
264
|
+
const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories, versionCategories);
|
|
265
|
+
const resolvedSource = categoryResolver.resolveSourceConfig(api.source, versionSource);
|
|
266
|
+
const resolvedLlms = mergeLlmsPluginConfig(options.llmsPlugin, api.llmsPlugin, versionLlms);
|
|
267
|
+
const packageJson = versionPackageJson || (api.packageJson ? yield* Effect.tryPromise({
|
|
268
|
+
try: () => loadPackageJson(api.packageJson),
|
|
269
|
+
catch: (cause) => new ConfigValidationError({
|
|
270
|
+
field: "packageJson",
|
|
271
|
+
reason: cause instanceof Error ? cause.message : String(cause),
|
|
272
|
+
cause
|
|
273
|
+
})
|
|
274
|
+
}) : void 0);
|
|
275
|
+
yield* Effect.try({
|
|
276
|
+
try: () => validateExternalPackages(versionExternalPackages || api.externalPackages, packageJson),
|
|
277
|
+
catch: (cause) => new ConfigValidationError({
|
|
278
|
+
field: "externalPackages",
|
|
279
|
+
reason: cause instanceof Error ? cause.message : String(cause),
|
|
280
|
+
cause
|
|
281
|
+
})
|
|
282
|
+
});
|
|
283
|
+
const autoDetectOptions = versionAutoDetectDependencies || api.autoDetectDependencies;
|
|
284
|
+
const externalPackages = versionExternalPackages || api.externalPackages || extractAutoDetectedPackages(packageJson, autoDetectOptions);
|
|
285
|
+
if (externalPackages && externalPackages.length > 0) yield* Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length);
|
|
286
|
+
const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
|
|
287
|
+
const vfsPayloads = prependImportsToVfs(vfs, apiPackage, api.packageName, wantTrace);
|
|
288
|
+
const resolvedOgImage = versionOgImage ?? api.ogImage ?? options.ogImage;
|
|
289
|
+
const resolvedTheme = normalizeThemeConfig(api.theme);
|
|
290
|
+
const outputDir = versionDp.outputDir;
|
|
291
|
+
const fullRoute = versionDp.routeBase;
|
|
292
|
+
return {
|
|
293
|
+
vfs,
|
|
294
|
+
vfsPayloads,
|
|
295
|
+
externalPackages: externalPackages || [],
|
|
296
|
+
config: {
|
|
297
|
+
apiPackage,
|
|
298
|
+
packageName: `${api.packageName} (${version})`,
|
|
299
|
+
...api.name != null ? { apiName: api.name } : {},
|
|
300
|
+
outputDir,
|
|
301
|
+
baseRoute: fullRoute,
|
|
302
|
+
categories: resolvedCategories,
|
|
303
|
+
...resolvedSource != null ? { source: resolvedSource } : {},
|
|
304
|
+
...packageJson != null ? { packageJson } : {},
|
|
305
|
+
...resolvedLlms != null ? { llmsPlugin: resolvedLlms } : {},
|
|
306
|
+
...siteUrl != null ? { siteUrl } : {},
|
|
307
|
+
...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
|
|
308
|
+
docsDir: path.dirname(outputDir),
|
|
309
|
+
...docsRoot != null ? { docsRoot } : {},
|
|
310
|
+
...resolvedTheme != null ? { theme: resolvedTheme } : {}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}), { concurrency: "unbounded" });
|
|
315
|
+
for (const result of versionResults) {
|
|
316
|
+
mergeApiResult(acc, result);
|
|
317
|
+
yield* emitVfsPayloadEvents(api.packageName, result.vfsPayloads, wantTrace);
|
|
318
|
+
}
|
|
319
|
+
} else if (api.model) {
|
|
320
|
+
const dp = deriveOutputPaths({
|
|
321
|
+
mode: "single",
|
|
322
|
+
docsRoot: rspressRoot,
|
|
323
|
+
baseRoute,
|
|
324
|
+
apiFolder: api.apiFolder ?? "api",
|
|
325
|
+
locales: rspressLocales,
|
|
326
|
+
defaultLang: rspressLang,
|
|
327
|
+
versions: [],
|
|
328
|
+
defaultVersion: void 0
|
|
329
|
+
})[0];
|
|
330
|
+
if (dp) {
|
|
331
|
+
const result = yield* processSimpleApi(api, api.model, dp.outputDir, dp.routeBase, wantTrace);
|
|
332
|
+
mergeApiResult(acc, result);
|
|
333
|
+
yield* emitVfsPayloadEvents(api.packageName, result.vfsPayloads, wantTrace);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else if (options.apis) {
|
|
337
|
+
const apisWithTsconfig = options.apis.filter((a) => a.tsconfig);
|
|
338
|
+
if (apisWithTsconfig.length > 0) firstApiTsconfig = apisWithTsconfig[0].tsconfig;
|
|
339
|
+
const apisWithCompilerOptions = options.apis.filter((a) => a.compilerOptions);
|
|
340
|
+
if (apisWithCompilerOptions.length > 0) firstApiCompilerOptions = apisWithCompilerOptions[0].compilerOptions;
|
|
341
|
+
for (const a of options.apis) {
|
|
342
|
+
const scopeRoute = normalizeBaseRoute(a.baseRoute ?? `/${unscopedName(a.packageName)}`);
|
|
343
|
+
scopeTsConfigs.set(apiScopeOf(scopeRoute, a.packageName), rawTsConfig(a));
|
|
344
|
+
}
|
|
345
|
+
const multiResults = yield* Effect.forEach(options.apis, (api) => Effect.gen(function* () {
|
|
346
|
+
const apiBaseRoute = normalizeBaseRoute(api.baseRoute ?? `/${unscopedName(api.packageName)}`);
|
|
347
|
+
const dp = deriveOutputPaths({
|
|
348
|
+
mode: "multi",
|
|
349
|
+
docsRoot: rspressRoot,
|
|
350
|
+
baseRoute: apiBaseRoute,
|
|
351
|
+
apiFolder: api.apiFolder ?? "api",
|
|
352
|
+
locales: rspressLocales,
|
|
353
|
+
defaultLang: rspressLang,
|
|
354
|
+
versions: [],
|
|
355
|
+
defaultVersion: void 0
|
|
356
|
+
})[0];
|
|
357
|
+
if (!dp) return [];
|
|
358
|
+
const result = yield* processSimpleApi(api, api.model, dp.outputDir, dp.routeBase, wantTrace);
|
|
359
|
+
yield* emitVfsPayloadEvents(api.packageName, result.vfsPayloads, wantTrace);
|
|
360
|
+
return [result];
|
|
361
|
+
}), { concurrency: "unbounded" });
|
|
362
|
+
for (const results of multiResults) for (const result of results) mergeApiResult(acc, result);
|
|
363
|
+
}
|
|
364
|
+
}));
|
|
365
|
+
const loadMs = performance.now() - loadStart;
|
|
366
|
+
yield* emit(PluginEvent.ModelLoaded({
|
|
367
|
+
ctx: {},
|
|
368
|
+
level: "debug",
|
|
369
|
+
entryPoints: apiConfigs.length,
|
|
370
|
+
itemCount: apiConfigs.reduce((sum, cfg) => sum + cfg.apiPackage.entryPoints.reduce((s, ep) => s + ep.members.length, 0), 0),
|
|
371
|
+
durationMs: Math.round(loadMs)
|
|
372
|
+
}));
|
|
373
|
+
const projectRoot = process.cwd();
|
|
374
|
+
let globalTsConfig;
|
|
375
|
+
if (firstApiTsconfig || firstApiCompilerOptions) {
|
|
376
|
+
globalTsConfig = {};
|
|
377
|
+
if (firstApiTsconfig != null) globalTsConfig.tsconfig = firstApiTsconfig;
|
|
378
|
+
if (firstApiCompilerOptions != null) globalTsConfig.compilerOptions = firstApiCompilerOptions;
|
|
379
|
+
}
|
|
380
|
+
const resolvedCompilerOptions = yield* resolveTsConfigTyped(projectRoot, globalTsConfig);
|
|
381
|
+
yield* emit(PluginEvent.TsCacheCreated({
|
|
382
|
+
ctx: {},
|
|
383
|
+
level: "debug",
|
|
384
|
+
compilerOptions: `target=${resolvedCompilerOptions.target}, module=${resolvedCompilerOptions.module}, lib=[${resolvedCompilerOptions.lib?.join(", ") ?? ""}]`,
|
|
385
|
+
durationMs: 0
|
|
386
|
+
}));
|
|
387
|
+
yield* mergeExternalTypes(typeRegistry, combinedVfs, apiConfigs, allExternalPackages);
|
|
388
|
+
yield* registerTypeEnvironments({
|
|
389
|
+
combinedVfs,
|
|
390
|
+
resolvedCompilerOptions,
|
|
391
|
+
scopeTsConfigs,
|
|
392
|
+
projectRoot
|
|
393
|
+
});
|
|
394
|
+
return apiConfigs;
|
|
395
|
+
}) };
|
|
396
|
+
});
|
|
397
|
+
/** The raw TypeScript config an API declares, or undefined when it declares none. */
|
|
398
|
+
function rawTsConfig(api) {
|
|
399
|
+
if (api.tsconfig == null && api.compilerOptions == null) return void 0;
|
|
400
|
+
const cfg = {};
|
|
401
|
+
if (api.tsconfig != null) cfg.tsconfig = api.tsconfig;
|
|
402
|
+
if (api.compilerOptions != null) cfg.compilerOptions = api.compilerOptions;
|
|
403
|
+
return cfg;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
//#endregion
|
|
407
|
+
export { makeConfigService };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { PluginEvent } from "../observability/events.js";
|
|
2
|
+
import { emit } from "../observability/EventBus.js";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/layers/external-types.ts
|
|
6
|
+
/**
|
|
7
|
+
* Merging external package declarations into the build's VFS.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The one phase of config resolution that **degrades rather than fails**.
|
|
11
|
+
* External types are an enhancement: without them, code blocks render without
|
|
12
|
+
* Twoslash enrichment, which is a worse page rather than a broken build. The
|
|
13
|
+
* caller therefore never sees this fail — the failure is reported as a warning
|
|
14
|
+
* and the VFS is left as it was.
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Fetch external package declarations and merge them into `combinedVfs`.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* **First-party packages are excluded, and that exclusion is load-bearing.**
|
|
23
|
+
* The packages being documented are already served from their api.json-derived
|
|
24
|
+
* virtual VFS, which is authoritative. Their published version may not exist
|
|
25
|
+
* yet (an optimistic next version), and if it did, fetching it would clobber
|
|
26
|
+
* the generated declarations with the previous release's — silently
|
|
27
|
+
* documenting the wrong API.
|
|
28
|
+
*
|
|
29
|
+
* Versions are resolved to exact published ones first: the CDN behind
|
|
30
|
+
* `loadPackages` 404s on a range or an unpublished package, so a spec that
|
|
31
|
+
* cannot be resolved is dropped with a debug event rather than failing the
|
|
32
|
+
* batch it is in.
|
|
33
|
+
*
|
|
34
|
+
* Mutates `combinedVfs` in place, matching the other resolution phases.
|
|
35
|
+
*
|
|
36
|
+
* The registry arrives as an argument rather than being pulled from context.
|
|
37
|
+
* `ConfigService.layer` resolves it ONCE at layer construction, and yielding
|
|
38
|
+
* the tag here instead would move it into `resolve`'s per-call requirement
|
|
39
|
+
* channel — a different resolution point, and a widened public signature, for
|
|
40
|
+
* a dependency that does not vary per call.
|
|
41
|
+
*/
|
|
42
|
+
const mergeExternalTypes = (typeRegistry, combinedVfs, apiConfigs, allExternalPackages) => Effect.gen(function* () {
|
|
43
|
+
const documentedPackageNames = new Set(apiConfigs.map((config) => config.packageName));
|
|
44
|
+
const externalPackagesToLoad = allExternalPackages.filter((pkg) => !documentedPackageNames.has(pkg.name));
|
|
45
|
+
const typeLoadResult = yield* Effect.result(Effect.gen(function* () {
|
|
46
|
+
if (externalPackagesToLoad.length === 0) return;
|
|
47
|
+
const resolvedPackages = yield* typeRegistry.resolveVersions(externalPackagesToLoad);
|
|
48
|
+
const droppedCount = externalPackagesToLoad.length - resolvedPackages.length;
|
|
49
|
+
if (droppedCount > 0) yield* emit(PluginEvent.ExternalPackageSkipped({
|
|
50
|
+
ctx: {},
|
|
51
|
+
level: "debug",
|
|
52
|
+
reason: `${droppedCount} unresolvable package(s) (unpublished or workspace-only)`
|
|
53
|
+
}));
|
|
54
|
+
if (resolvedPackages.length === 0) return;
|
|
55
|
+
const result = yield* typeRegistry.loadPackages(resolvedPackages);
|
|
56
|
+
for (const [filePath, content] of result.vfs.entries()) combinedVfs.set(filePath, content);
|
|
57
|
+
yield* emit(PluginEvent.VfsMerged({
|
|
58
|
+
ctx: {},
|
|
59
|
+
level: "debug",
|
|
60
|
+
totalFiles: result.vfs.size,
|
|
61
|
+
packages: resolvedPackages.map((p) => p.name)
|
|
62
|
+
}));
|
|
63
|
+
}));
|
|
64
|
+
if (typeLoadResult._tag === "Failure") yield* emit(PluginEvent.ConfigCascadeWarning({
|
|
65
|
+
ctx: {},
|
|
66
|
+
level: "warn",
|
|
67
|
+
field: "externalTypes",
|
|
68
|
+
chosen: "empty VFS",
|
|
69
|
+
ignored: [typeLoadResult.failure.message ?? String(typeLoadResult.failure)]
|
|
70
|
+
}));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
export { mergeExternalTypes };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { BuildMetrics, makeMetricStore } from "./build-metrics.js";
|
|
1
2
|
import { makeEventBusLayer } from "../observability/EventBus.js";
|
|
2
3
|
import { formatCodeBlockReport, seriesFor } from "../observability/metric-report.js";
|
|
3
|
-
import { BuildMetrics, makeMetricStore } from "./build-metrics.js";
|
|
4
4
|
import { makeConsoleSink } from "../observability/sinks/console-sink.js";
|
|
5
5
|
import { makeIssuesSink } from "../observability/sinks/issues-sink.js";
|
|
6
6
|
import { makeMetricsSink } from "../observability/sinks/metrics-sink.js";
|
|
@@ -8,7 +8,7 @@ import { makeRenderSink } from "../observability/sinks/render-sink.js";
|
|
|
8
8
|
import { makeTraceSink } from "../observability/sinks/trace-sink.js";
|
|
9
9
|
import { Effect, Layer, Logger, Metric, References } from "effect";
|
|
10
10
|
|
|
11
|
-
//#region src/layers/
|
|
11
|
+
//#region src/layers/observability.ts
|
|
12
12
|
function formatTime(date) {
|
|
13
13
|
return date.toTimeString().slice(0, 8);
|
|
14
14
|
}
|
|
@@ -117,4 +117,4 @@ const logBuildSummary = (slowCodeBlockMs, renderPhase) => Effect.gen(function* (
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
//#endregion
|
|
120
|
-
export {
|
|
120
|
+
export { buildEventBus, logBuildSummary, makeSummaryLoggerLayer };
|