rspress-plugin-api-extractor 0.11.0 → 0.13.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 (49) hide show
  1. package/BuildEnv.js +3 -3
  2. package/README.md +1 -0
  3. package/build-program.js +14 -5
  4. package/build-stages.js +88 -49
  5. package/config-helpers.js +7 -7
  6. package/errors.js +1 -5
  7. package/index.d.ts +1 -86
  8. package/layers/AppLayer.js +67 -0
  9. package/layers/api-results.js +83 -0
  10. package/layers/build-metrics.js +1 -1
  11. package/layers/{ConfigServiceLive.js → config-resolution.js} +115 -175
  12. package/layers/external-types.js +74 -0
  13. package/layers/{ObservabilityLive.js → observability.js} +3 -3
  14. package/layers/type-environment.js +109 -0
  15. package/layers/xdg.js +1 -1
  16. package/markdown/helpers.js +8 -33
  17. package/markdown/shiki-utils.js +1 -1
  18. package/model-loader.js +3 -3
  19. package/observability/EventBus.js +2 -2
  20. package/observability/heartbeat.js +1 -1
  21. package/observability/sinks/metrics-sink.js +1 -1
  22. package/observability/sinks/trace-sink.js +4 -4
  23. package/package.json +5 -4
  24. package/plugin.js +30 -33
  25. package/prettier-formatter.js +1 -1
  26. package/remark-api-codeblocks.js +1 -1
  27. package/remark-with-api.js +1 -1
  28. package/schemas/config.js +1 -3
  29. package/services/ConfigService.js +37 -2
  30. package/services/HighlighterService.js +48 -3
  31. package/services/OgService.js +154 -2
  32. package/services/PluginConfig.js +1 -1
  33. package/services/TwoslashCacheService.js +128 -2
  34. package/services/TwoslashEnvironments.js +30 -2
  35. package/services/TypeRegistryService.js +178 -2
  36. package/shiki-transformer.js +1 -1
  37. package/sync-node-fs.js +6 -6
  38. package/tsconfig-parser.js +77 -95
  39. package/twoslash-access.js +1 -1
  40. package/twoslash-transformer.js +1 -1
  41. package/layers/HighlighterServiceLive.js +0 -52
  42. package/layers/OgServiceLive.js +0 -134
  43. package/layers/TwoslashCacheServiceLive.js +0 -108
  44. package/layers/TwoslashEnvironmentsLive.js +0 -33
  45. package/layers/TypeRegistryServiceLive.js +0 -162
  46. package/markdown/index.js +0 -11
  47. package/og-resolver.js +0 -64
  48. package/schemas/index.js +0 -6
  49. package/schemas/opengraph.js +0 -56
@@ -1,32 +1,29 @@
1
1
  import { BuildId } from "../BuildEnv.js";
2
+ import { BuildMetrics } from "./build-metrics.js";
2
3
  import { PluginEvent } from "../observability/events.js";
3
4
  import { emit, wantsLevel } from "../observability/EventBus.js";
4
- import { BuildMetrics } from "./build-metrics.js";
5
- import "./ObservabilityLive.js";
6
5
  import { TypeReferenceExtractor } from "../type-reference-extractor.js";
7
6
  import { withPhase } from "../observability/spans.js";
8
- import { TwoslashEnvironments } from "../services/TwoslashEnvironments.js";
9
- import { resolveTypeScriptConfig } from "../typescript-config.js";
7
+ import { normalizeThemeConfig } from "../markdown/shiki-utils.js";
10
8
  import { apiScopeOf, deriveOutputPaths, normalizeBaseRoute, unscopedName } from "../path-derivation.js";
11
9
  import { classifyApiConfig, extractAutoDetectedPackages, isVersionConfig, mergeLlmsPluginConfig, validateExternalPackages } from "../config-utils.js";
12
10
  import { ApiExtractedPackage } from "../api-extracted-package.js";
13
11
  import { CategoryResolver } from "../category-resolver.js";
14
12
  import { ConfigValidationError } from "../errors.js";
15
- import { normalizeThemeConfig } from "../markdown/shiki-utils.js";
16
13
  import { loadApiModel, loadPackageJson, loadVersionModel } from "../model-loader.js";
17
14
  import { DEFAULT_CATEGORIES } from "../schemas/config.js";
18
- import "../schemas/index.js";
19
- import { ConfigService } from "../services/ConfigService.js";
20
15
  import { PluginConfig } from "../services/PluginConfig.js";
21
- import { TwoslashCacheService } from "../services/TwoslashCacheService.js";
22
16
  import { TypeRegistryService } from "../services/TypeRegistryService.js";
23
- import { twoslashEnvHash } from "../twoslash-cache.js";
17
+ import { emitVfsPayloadEvents, mergeApiResult } from "./api-results.js";
18
+ import { mergeExternalTypes } from "./external-types.js";
19
+ import { registerTypeEnvironments, resolveTsConfigTyped } from "./type-environment.js";
24
20
  import path from "node:path";
21
+ import { Effect, Metric } from "effect";
22
+ import { deriveSiteUrl } from "@tsdoctor/seo";
25
23
  import { hashContent } from "@tsdoctor/snapshot";
26
- import { Effect, Layer, Metric } from "effect";
27
- import ts from "typescript";
24
+ import { PackageManifest } from "@effected/package-json";
28
25
 
29
- //#region src/layers/ConfigServiceLive.ts
26
+ //#region src/layers/config-resolution.ts
30
27
  /**
31
28
  * Prepend import statements for external type references to the VFS declaration files.
32
29
  * Returns per-entry payloads for event emission (heavy content/importRefs gated on wantTrace).
@@ -65,6 +62,40 @@ function prependImportsToVfs(vfs, apiPackage, packageName, wantTrace) {
65
62
  return payloads;
66
63
  }
67
64
  /**
65
+ * Decode a loaded package.json into a typed {@link PackageManifest}, degrading
66
+ * to `undefined` when it does not satisfy the codec.
67
+ *
68
+ * @remarks
69
+ * `PackageManifest` is presence-lenient but shape-strict — the private
70
+ * workspace-root shape decodes fine, but one malformed field (a `version` of
71
+ * `"1.0"`, an `author` that is neither a string nor an object) fails the whole
72
+ * decode. That is the right strictness for the SEO layer, which needs real
73
+ * `Person` / `Repository` values rather than the discovery tier's raw unions,
74
+ * but it must never fail a docs build: the failure is surfaced as a
75
+ * `ConfigValidationWarning` (which reaches `issues.json`) and the manifest is
76
+ * simply absent. The same posture as the OG image path in `build-stages.ts`.
77
+ */
78
+ function decodeManifest(packageJson, buildId, packageName) {
79
+ return Effect.gen(function* () {
80
+ if (packageJson == null) return void 0;
81
+ const decoded = yield* Effect.result(PackageManifest.decode(packageJson));
82
+ if (decoded._tag === "Failure") {
83
+ yield* emit(PluginEvent.ConfigValidationWarning({
84
+ ctx: {
85
+ buildId,
86
+ packageName
87
+ },
88
+ field: "packageJson",
89
+ value: packageName,
90
+ reason: decoded.failure.message,
91
+ level: "warn"
92
+ }));
93
+ return;
94
+ }
95
+ return decoded.success;
96
+ });
97
+ }
98
+ /**
68
99
  * Validate plugin options and return an Effect that fails with ConfigValidationError.
69
100
  */
70
101
  function validateOptions(options, rspressConfig) {
@@ -129,8 +160,12 @@ function validateOptions(options, rspressConfig) {
129
160
  * would build a second `ConfigService` with its own captured `TypeRegistry`.
130
161
  * The options come from {@link PluginConfig} now, so there is nothing to pass
131
162
  * and nothing to call twice.
163
+ *
164
+ * The `Layer` around this lives on the service, as `ConfigService.layer`. The
165
+ * implementation stays here rather than in the service module because it is
166
+ * the bulk of config resolution; the service module declares the contract.
132
167
  */
133
- const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
168
+ const makeConfigService = Effect.gen(function* () {
134
169
  const typeRegistry = yield* TypeRegistryService;
135
170
  const options = yield* PluginConfig;
136
171
  return { resolve: (rspressConfig) => Effect.gen(function* () {
@@ -146,11 +181,18 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
146
181
  const rspressLang = rspressConfig.lang;
147
182
  const docsRoot = rspressConfig.root;
148
183
  const rspressRoot = docsRoot || process.cwd();
184
+ const siteUrl = deriveSiteUrl(rspressConfig.siteOrigin, rspressConfig.base);
149
185
  const categoryResolver = new CategoryResolver();
150
186
  const pluginDefaults = categoryResolver.mergeCategories(DEFAULT_CATEGORIES, options.defaultCategories);
151
187
  const apiConfigs = [];
152
188
  const combinedVfs = /* @__PURE__ */ new Map();
153
189
  const allExternalPackages = [];
190
+ /** The three above, as one value the merge helper can take. */
191
+ const acc = {
192
+ apiConfigs,
193
+ combinedVfs,
194
+ allExternalPackages
195
+ };
154
196
  let firstApiTsconfig;
155
197
  let firstApiCompilerOptions;
156
198
  /**
@@ -176,14 +218,29 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
176
218
  */
177
219
  const processSimpleApi = (api, model, outputDir, fullRoute, wantTrace) => Effect.gen(function* () {
178
220
  const { apiPackage, source: loaderSource } = yield* withModelLoadEvents(loadApiModel(model));
179
- return yield* Effect.promise(async () => {
221
+ {
180
222
  const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories);
181
223
  const resolvedSource = categoryResolver.resolveSourceConfig(api.source, loaderSource);
182
224
  const resolvedLlms = mergeLlmsPluginConfig(options.llmsPlugin, api.llmsPlugin);
183
- const packageJson = api.packageJson ? await loadPackageJson(api.packageJson) : void 0;
184
- validateExternalPackages(api.externalPackages, packageJson);
225
+ const packageJson = api.packageJson ? yield* Effect.tryPromise({
226
+ try: () => loadPackageJson(api.packageJson),
227
+ catch: (cause) => new ConfigValidationError({
228
+ field: "packageJson",
229
+ reason: cause instanceof Error ? cause.message : String(cause),
230
+ cause
231
+ })
232
+ }) : void 0;
233
+ const manifest = yield* decodeManifest(packageJson, buildId, api.packageName);
234
+ yield* Effect.try({
235
+ try: () => validateExternalPackages(api.externalPackages, packageJson),
236
+ catch: (cause) => new ConfigValidationError({
237
+ field: "externalPackages",
238
+ reason: cause instanceof Error ? cause.message : String(cause),
239
+ cause
240
+ })
241
+ });
185
242
  const externalPackages = api.externalPackages || extractAutoDetectedPackages(packageJson, api.autoDetectDependencies);
186
- if (externalPackages && externalPackages.length > 0) Effect.runSync(Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length));
243
+ if (externalPackages && externalPackages.length > 0) yield* Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length);
187
244
  const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
188
245
  const vfsPayloads = prependImportsToVfs(vfs, apiPackage, api.packageName, wantTrace);
189
246
  const resolvedOgImage = api.ogImage ?? options.ogImage;
@@ -201,15 +258,16 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
201
258
  categories: resolvedCategories,
202
259
  ...resolvedSource != null ? { source: resolvedSource } : {},
203
260
  ...packageJson != null ? { packageJson } : {},
261
+ ...manifest != null ? { manifest } : {},
204
262
  ...resolvedLlms != null ? { llmsPlugin: resolvedLlms } : {},
205
- ...options.siteUrl != null ? { siteUrl: options.siteUrl } : {},
263
+ ...siteUrl != null ? { siteUrl } : {},
206
264
  ...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
207
265
  docsDir: path.dirname(outputDir),
208
266
  ...docsRoot != null ? { docsRoot } : {},
209
267
  ...resolvedTheme != null ? { theme: resolvedTheme } : {}
210
268
  }
211
269
  };
212
- });
270
+ }
213
271
  });
214
272
  yield* withPhase("modelLoad", { buildId }, Effect.gen(function* () {
215
273
  if (options.api) {
@@ -238,16 +296,31 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
238
296
  };
239
297
  const versionConfig = isVersionConfig(versionValue) ? versionValue : { model: versionValue };
240
298
  const { apiPackage, packageJson: versionPackageJson, categories: versionCategories, source: versionSource, externalPackages: versionExternalPackages, autoDetectDependencies: versionAutoDetectDependencies, llmsPlugin: versionLlms, ogImage: versionOgImage } = yield* withModelLoadEvents(loadVersionModel(versionConfig));
241
- return yield* Effect.promise(async () => {
242
- Effect.runSync(Metric.update(BuildMetrics.apiVersionsLoaded, 1));
299
+ {
300
+ yield* Metric.update(BuildMetrics.apiVersionsLoaded, 1);
243
301
  const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories, versionCategories);
244
302
  const resolvedSource = categoryResolver.resolveSourceConfig(api.source, versionSource);
245
303
  const resolvedLlms = mergeLlmsPluginConfig(options.llmsPlugin, api.llmsPlugin, versionLlms);
246
- const packageJson = versionPackageJson || (api.packageJson ? await loadPackageJson(api.packageJson) : void 0);
247
- validateExternalPackages(versionExternalPackages || api.externalPackages, packageJson);
304
+ const packageJson = versionPackageJson || (api.packageJson ? yield* Effect.tryPromise({
305
+ try: () => loadPackageJson(api.packageJson),
306
+ catch: (cause) => new ConfigValidationError({
307
+ field: "packageJson",
308
+ reason: cause instanceof Error ? cause.message : String(cause),
309
+ cause
310
+ })
311
+ }) : void 0);
312
+ const manifest = yield* decodeManifest(packageJson, buildId, api.packageName);
313
+ yield* Effect.try({
314
+ try: () => validateExternalPackages(versionExternalPackages || api.externalPackages, packageJson),
315
+ catch: (cause) => new ConfigValidationError({
316
+ field: "externalPackages",
317
+ reason: cause instanceof Error ? cause.message : String(cause),
318
+ cause
319
+ })
320
+ });
248
321
  const autoDetectOptions = versionAutoDetectDependencies || api.autoDetectDependencies;
249
322
  const externalPackages = versionExternalPackages || api.externalPackages || extractAutoDetectedPackages(packageJson, autoDetectOptions);
250
- if (externalPackages && externalPackages.length > 0) Effect.runSync(Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length));
323
+ if (externalPackages && externalPackages.length > 0) yield* Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length);
251
324
  const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
252
325
  const vfsPayloads = prependImportsToVfs(vfs, apiPackage, api.packageName, wantTrace);
253
326
  const resolvedOgImage = versionOgImage ?? api.ogImage ?? options.ogImage;
@@ -267,42 +340,20 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
267
340
  categories: resolvedCategories,
268
341
  ...resolvedSource != null ? { source: resolvedSource } : {},
269
342
  ...packageJson != null ? { packageJson } : {},
343
+ ...manifest != null ? { manifest } : {},
270
344
  ...resolvedLlms != null ? { llmsPlugin: resolvedLlms } : {},
271
- ...options.siteUrl != null ? { siteUrl: options.siteUrl } : {},
345
+ ...siteUrl != null ? { siteUrl } : {},
272
346
  ...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
273
347
  docsDir: path.dirname(outputDir),
274
348
  ...docsRoot != null ? { docsRoot } : {},
275
349
  ...resolvedTheme != null ? { theme: resolvedTheme } : {}
276
350
  }
277
351
  };
278
- });
352
+ }
279
353
  }), { concurrency: "unbounded" });
280
354
  for (const result of versionResults) {
281
- for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
282
- if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
283
- if (result.config) apiConfigs.push(result.config);
284
- for (const payload of result.vfsPayloads) {
285
- yield* emit(PluginEvent.VfsGenerated({
286
- ctx: {
287
- packageName: api.packageName,
288
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
289
- },
290
- level: "debug",
291
- file: payload.file,
292
- declCount: payload.declCount,
293
- contentHash: payload.contentHash,
294
- ...wantTrace && payload.content ? { content: payload.content } : {}
295
- }));
296
- if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
297
- ctx: {
298
- packageName: api.packageName,
299
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
300
- },
301
- level: "debug",
302
- file: payload.file,
303
- imports: wantTrace ? payload.importRefs : []
304
- }));
305
- }
355
+ mergeApiResult(acc, result);
356
+ yield* emitVfsPayloadEvents(api.packageName, result.vfsPayloads, wantTrace);
306
357
  }
307
358
  } else if (api.model) {
308
359
  const dp = deriveOutputPaths({
@@ -317,31 +368,8 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
317
368
  })[0];
318
369
  if (dp) {
319
370
  const result = yield* processSimpleApi(api, api.model, dp.outputDir, dp.routeBase, wantTrace);
320
- for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
321
- if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
322
- apiConfigs.push(result.config);
323
- for (const payload of result.vfsPayloads) {
324
- yield* emit(PluginEvent.VfsGenerated({
325
- ctx: {
326
- packageName: api.packageName,
327
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
328
- },
329
- level: "debug",
330
- file: payload.file,
331
- declCount: payload.declCount,
332
- contentHash: payload.contentHash,
333
- ...wantTrace && payload.content ? { content: payload.content } : {}
334
- }));
335
- if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
336
- ctx: {
337
- packageName: api.packageName,
338
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
339
- },
340
- level: "debug",
341
- file: payload.file,
342
- imports: wantTrace ? payload.importRefs : []
343
- }));
344
- }
371
+ mergeApiResult(acc, result);
372
+ yield* emitVfsPayloadEvents(api.packageName, result.vfsPayloads, wantTrace);
345
373
  }
346
374
  }
347
375
  } else if (options.apis) {
@@ -367,35 +395,10 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
367
395
  })[0];
368
396
  if (!dp) return [];
369
397
  const result = yield* processSimpleApi(api, api.model, dp.outputDir, dp.routeBase, wantTrace);
370
- for (const payload of result.vfsPayloads) {
371
- yield* emit(PluginEvent.VfsGenerated({
372
- ctx: {
373
- packageName: api.packageName,
374
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
375
- },
376
- level: "debug",
377
- file: payload.file,
378
- declCount: payload.declCount,
379
- contentHash: payload.contentHash,
380
- ...wantTrace && payload.content ? { content: payload.content } : {}
381
- }));
382
- if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
383
- ctx: {
384
- packageName: api.packageName,
385
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
386
- },
387
- level: "debug",
388
- file: payload.file,
389
- imports: wantTrace ? payload.importRefs : []
390
- }));
391
- }
398
+ yield* emitVfsPayloadEvents(api.packageName, result.vfsPayloads, wantTrace);
392
399
  return [result];
393
400
  }), { concurrency: "unbounded" });
394
- for (const results of multiResults) for (const result of results) {
395
- for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
396
- if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
397
- apiConfigs.push(result.config);
398
- }
401
+ for (const results of multiResults) for (const result of results) mergeApiResult(acc, result);
399
402
  }
400
403
  }));
401
404
  const loadMs = performance.now() - loadStart;
@@ -413,86 +416,23 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
413
416
  if (firstApiTsconfig != null) globalTsConfig.tsconfig = firstApiTsconfig;
414
417
  if (firstApiCompilerOptions != null) globalTsConfig.compilerOptions = firstApiCompilerOptions;
415
418
  }
416
- const resolvedCompilerOptions = yield* Effect.promise(() => resolveTypeScriptConfig(projectRoot, globalTsConfig));
419
+ const resolvedCompilerOptions = yield* resolveTsConfigTyped(projectRoot, globalTsConfig);
417
420
  yield* emit(PluginEvent.TsCacheCreated({
418
421
  ctx: {},
419
422
  level: "debug",
420
423
  compilerOptions: `target=${resolvedCompilerOptions.target}, module=${resolvedCompilerOptions.module}, lib=[${resolvedCompilerOptions.lib?.join(", ") ?? ""}]`,
421
424
  durationMs: 0
422
425
  }));
423
- const documentedPackageNames = new Set(apiConfigs.map((config) => config.packageName));
424
- const externalPackagesToLoad = allExternalPackages.filter((pkg) => !documentedPackageNames.has(pkg.name));
425
- const typeLoadResult = yield* Effect.result(Effect.gen(function* () {
426
- if (externalPackagesToLoad.length > 0) {
427
- const resolvedPackages = yield* typeRegistry.resolveVersions(externalPackagesToLoad);
428
- const droppedCount = externalPackagesToLoad.length - resolvedPackages.length;
429
- if (droppedCount > 0) yield* emit(PluginEvent.ExternalPackageSkipped({
430
- ctx: {},
431
- level: "debug",
432
- reason: `${droppedCount} unresolvable package(s) (unpublished or workspace-only)`
433
- }));
434
- if (resolvedPackages.length > 0) {
435
- const result = yield* typeRegistry.loadPackages(resolvedPackages);
436
- for (const [filePath, content] of result.vfs.entries()) combinedVfs.set(filePath, content);
437
- yield* emit(PluginEvent.VfsMerged({
438
- ctx: {},
439
- level: "debug",
440
- totalFiles: result.vfs.size,
441
- packages: resolvedPackages.map((p) => p.name)
442
- }));
443
- }
444
- }
445
- }));
446
- if (typeLoadResult._tag === "Failure") yield* emit(PluginEvent.ConfigCascadeWarning({
447
- ctx: {},
448
- level: "warn",
449
- field: "externalTypes",
450
- chosen: "empty VFS",
451
- ignored: [typeLoadResult.failure.message ?? String(typeLoadResult.failure)]
452
- }));
453
- const twoslashEnv = twoslashEnvHash(combinedVfs, `typescript@${ts.version}`);
454
- const twoslashCache = yield* (yield* TwoslashCacheService).open(twoslashEnv);
455
- yield* emit(PluginEvent.TwoslashCacheLoaded({
456
- ctx: {},
457
- level: "debug",
458
- envHash: twoslashEnv,
459
- entries: twoslashCache.entries().size
460
- }));
461
- const twoslashStartMs = performance.now();
462
- const environments = yield* TwoslashEnvironments;
463
- environments.registerEnvironment({
464
- vfs: combinedVfs,
465
- compilerOptions: resolvedCompilerOptions,
466
- typesCache: twoslashCache
426
+ yield* mergeExternalTypes(typeRegistry, combinedVfs, apiConfigs, allExternalPackages);
427
+ yield* registerTypeEnvironments({
428
+ combinedVfs,
429
+ resolvedCompilerOptions,
430
+ scopeTsConfigs,
431
+ projectRoot
467
432
  });
468
- const resolvedByRawConfig = /* @__PURE__ */ new Map();
469
- for (const [apiScope, rawConfig] of scopeTsConfigs) {
470
- if (rawConfig === void 0) {
471
- environments.registerScope(apiScope, resolvedCompilerOptions);
472
- continue;
473
- }
474
- const rawKey = JSON.stringify([String(rawConfig.tsconfig ?? ""), rawConfig.compilerOptions ?? null]);
475
- let scopeOptions = resolvedByRawConfig.get(rawKey);
476
- if (scopeOptions === void 0) {
477
- scopeOptions = yield* Effect.promise(() => resolveTypeScriptConfig(projectRoot, rawConfig));
478
- resolvedByRawConfig.set(rawKey, scopeOptions);
479
- }
480
- environments.registerEnvironment({
481
- vfs: combinedVfs,
482
- compilerOptions: scopeOptions,
483
- typesCache: twoslashCache
484
- });
485
- environments.registerScope(apiScope, scopeOptions);
486
- }
487
- yield* emit(PluginEvent.TwoslashInitialized({
488
- ctx: {},
489
- level: "debug",
490
- durationMs: Math.round(performance.now() - twoslashStartMs),
491
- vfsFileCount: combinedVfs.size
492
- }));
493
433
  return apiConfigs;
494
434
  }) };
495
- }));
435
+ });
496
436
  /** The raw TypeScript config an API declares, or undefined when it declares none. */
497
437
  function rawTsConfig(api) {
498
438
  if (api.tsconfig == null && api.compilerOptions == null) return void 0;
@@ -503,4 +443,4 @@ function rawTsConfig(api) {
503
443
  }
504
444
 
505
445
  //#endregion
506
- export { ConfigServiceLive };
446
+ 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 };
@@ -0,0 +1,109 @@
1
+ import { PluginEvent } from "../observability/events.js";
2
+ import { emit } from "../observability/EventBus.js";
3
+ import { resolveTypeScriptConfig } from "../typescript-config.js";
4
+ import { TwoslashEnvironments } from "../services/TwoslashEnvironments.js";
5
+ import { ConfigValidationError } from "../errors.js";
6
+ import { twoslashEnvHash } from "../twoslash-cache.js";
7
+ import { TwoslashCacheService } from "../services/TwoslashCacheService.js";
8
+ import { Effect } from "effect";
9
+ import ts from "typescript";
10
+
11
+ //#region src/layers/type-environment.ts
12
+ /**
13
+ * Building the build's Twoslash type-checking environments.
14
+ *
15
+ * @remarks
16
+ * The last phase of config resolution, and the only one that has to run after
17
+ * every API has contributed to the VFS: the result cache is keyed on the type
18
+ * environment, so it can only be opened once the VFS is final, and no code
19
+ * block may be rendered before it is.
20
+ *
21
+ * @packageDocumentation
22
+ */
23
+ /**
24
+ * Resolve a TypeScript configuration, failing typed on a malformed one.
25
+ *
26
+ * @remarks
27
+ * `resolveTypeScriptConfig` throws a `TsConfigParseError` for a missing file, a
28
+ * syntax error or a semantically invalid config. Both call sites used to run it
29
+ * inside `Effect.promise`, so all three became untyped defects: the build died
30
+ * with an unhandled rejection naming a file the user could fix, and wrote no
31
+ * `issues.json` entry for it.
32
+ *
33
+ * A malformed tsconfig is a user misconfiguration, so it is fatal and TYPED —
34
+ * not degraded. Falling back to default compiler options would type-check every
35
+ * example against a configuration the user did not ask for and silently render
36
+ * wrong hovers, which is the failure shape this subsystem suffers from most
37
+ * (see the `lib`-spelling defect in `type-loading-vfs.md`).
38
+ */
39
+ const resolveTsConfigTyped = (projectRoot, config) => Effect.tryPromise({
40
+ try: () => resolveTypeScriptConfig(projectRoot, config),
41
+ catch: (cause) => new ConfigValidationError({
42
+ field: "tsconfig",
43
+ reason: cause instanceof Error ? cause.message : String(cause),
44
+ cause
45
+ })
46
+ });
47
+ /**
48
+ * Open the Twoslash result cache and register one environment per distinct
49
+ * compiler configuration.
50
+ *
51
+ * @remarks
52
+ * **The build-wide options are registered FIRST, deliberately.** An unknown
53
+ * scope falls back to the first environment registered, and a `with-api` fence
54
+ * can appear on a page outside any documented package's route — checking it
55
+ * under the build-wide configuration beats not checking it at all.
56
+ *
57
+ * Resolution is memoised twice over. This function memoises by RAW config, so
58
+ * N APIs sharing a tsconfig read it from disk once; `registerEnvironment` then
59
+ * dedupes by a fingerprint of the ENCODED options, so APIs that spell the same
60
+ * configuration differently still share one TypeScript environment. The second
61
+ * of those is load-bearing: when the two fingerprints drifted apart once, every
62
+ * scope lookup missed, per-scope type-checking silently degraded to build-wide,
63
+ * and a 994-test suite stayed green through it.
64
+ */
65
+ const registerTypeEnvironments = (input) => Effect.gen(function* () {
66
+ const twoslashEnv = twoslashEnvHash(input.combinedVfs, `typescript@${ts.version}`);
67
+ const twoslashCache = yield* (yield* TwoslashCacheService).open(twoslashEnv);
68
+ yield* emit(PluginEvent.TwoslashCacheLoaded({
69
+ ctx: {},
70
+ level: "debug",
71
+ envHash: twoslashEnv,
72
+ entries: twoslashCache.entries().size
73
+ }));
74
+ const twoslashStartMs = performance.now();
75
+ const environments = yield* TwoslashEnvironments;
76
+ environments.registerEnvironment({
77
+ vfs: input.combinedVfs,
78
+ compilerOptions: input.resolvedCompilerOptions,
79
+ typesCache: twoslashCache
80
+ });
81
+ const resolvedByRawConfig = /* @__PURE__ */ new Map();
82
+ for (const [apiScope, rawConfig] of input.scopeTsConfigs) {
83
+ if (rawConfig === void 0) {
84
+ environments.registerScope(apiScope, input.resolvedCompilerOptions);
85
+ continue;
86
+ }
87
+ const rawKey = JSON.stringify([String(rawConfig.tsconfig ?? ""), rawConfig.compilerOptions ?? null]);
88
+ let scopeOptions = resolvedByRawConfig.get(rawKey);
89
+ if (scopeOptions === void 0) {
90
+ scopeOptions = yield* resolveTsConfigTyped(input.projectRoot, rawConfig);
91
+ resolvedByRawConfig.set(rawKey, scopeOptions);
92
+ }
93
+ environments.registerEnvironment({
94
+ vfs: input.combinedVfs,
95
+ compilerOptions: scopeOptions,
96
+ typesCache: twoslashCache
97
+ });
98
+ environments.registerScope(apiScope, scopeOptions);
99
+ }
100
+ yield* emit(PluginEvent.TwoslashInitialized({
101
+ ctx: {},
102
+ level: "debug",
103
+ durationMs: Math.round(performance.now() - twoslashStartMs),
104
+ vfsFileCount: input.combinedVfs.size
105
+ }));
106
+ });
107
+
108
+ //#endregion
109
+ export { registerTypeEnvironments, resolveTsConfigTyped };
package/layers/xdg.js CHANGED
@@ -1,5 +1,5 @@
1
- import { NodeFileSystem } from "@effect/platform-node";
2
1
  import { Layer, Path } from "effect";
2
+ import { NodeFileSystem } from "@effect/platform-node";
3
3
  import { AppDirs, Xdg } from "@effected/xdg";
4
4
 
5
5
  //#region src/layers/xdg.ts