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.
Files changed (54) hide show
  1. package/BuildEnv.js +58 -0
  2. package/build-program.js +34 -33
  3. package/build-stages.js +48 -42
  4. package/config-helpers.js +7 -7
  5. package/errors.js +1 -6
  6. package/index.d.ts +84 -86
  7. package/layers/AppLayer.js +67 -0
  8. package/layers/api-results.js +83 -0
  9. package/layers/build-metrics.js +1 -1
  10. package/layers/config-resolution.js +407 -0
  11. package/layers/external-types.js +74 -0
  12. package/layers/{ObservabilityLive.js → observability.js} +3 -3
  13. package/layers/type-environment.js +109 -0
  14. package/layers/xdg.js +44 -0
  15. package/markdown/helpers.js +9 -55
  16. package/markdown/page-generators/class-page.js +8 -31
  17. package/markdown/page-generators/index-pages.js +6 -8
  18. package/markdown/page-generators/interface-page.js +7 -7
  19. package/markdown/shiki-utils.js +65 -10
  20. package/model-loader.js +3 -3
  21. package/observability/EventBus.js +29 -7
  22. package/observability/heartbeat.js +1 -1
  23. package/observability/sinks/metrics-sink.js +1 -1
  24. package/observability/sinks/trace-sink.js +4 -4
  25. package/observability/spans.js +3 -1
  26. package/observability/sync-emitter.js +78 -0
  27. package/og-resolver.js +74 -284
  28. package/package.json +3 -4
  29. package/path-derivation.js +19 -1
  30. package/plugin.js +63 -91
  31. package/prettier-formatter.js +5 -11
  32. package/remark-api-codeblocks.js +11 -19
  33. package/remark-with-api.js +11 -21
  34. package/schemas/config.js +0 -2
  35. package/services/ConfigService.js +37 -2
  36. package/services/HighlighterService.js +75 -0
  37. package/services/OgService.js +190 -0
  38. package/services/PluginConfig.js +26 -0
  39. package/services/TwoslashCacheService.js +128 -2
  40. package/services/TwoslashEnvironments.js +35 -0
  41. package/services/TypeRegistryService.js +178 -2
  42. package/shiki-transformer.js +53 -234
  43. package/sync-node-fs.js +6 -6
  44. package/tsconfig-parser.js +77 -95
  45. package/twoslash-access.js +48 -0
  46. package/twoslash-transformer.js +106 -83
  47. package/vfs-registry.js +1 -31
  48. package/layers/ConfigServiceLive.js +0 -600
  49. package/layers/PathDerivationServiceLive.js +0 -16
  50. package/layers/TwoslashCacheServiceLive.js +0 -53
  51. package/layers/TypeRegistryServiceLive.js +0 -155
  52. package/markdown/index.js +0 -11
  53. package/schemas/index.js +0 -6
  54. 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 };
@@ -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 `ObservabilityLive.ts`
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/ObservabilityLive.ts
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 { BuildMetrics, buildEventBus, logBuildSummary, makeSummaryLoggerLayer };
120
+ export { buildEventBus, logBuildSummary, makeSummaryLoggerLayer };