rspress-plugin-api-extractor 0.11.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 (46) hide show
  1. package/BuildEnv.js +3 -3
  2. package/build-program.js +2 -3
  3. package/build-stages.js +3 -5
  4. package/config-helpers.js +7 -7
  5. package/errors.js +1 -5
  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/{ConfigServiceLive.js → config-resolution.js} +76 -175
  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 +1 -1
  15. package/markdown/shiki-utils.js +1 -1
  16. package/model-loader.js +3 -3
  17. package/observability/EventBus.js +2 -2
  18. package/observability/heartbeat.js +1 -1
  19. package/observability/sinks/metrics-sink.js +1 -1
  20. package/observability/sinks/trace-sink.js +4 -4
  21. package/og-resolver.js +32 -1
  22. package/package.json +2 -2
  23. package/plugin.js +30 -33
  24. package/prettier-formatter.js +1 -1
  25. package/remark-api-codeblocks.js +1 -1
  26. package/remark-with-api.js +1 -1
  27. package/schemas/config.js +0 -2
  28. package/services/ConfigService.js +37 -2
  29. package/services/HighlighterService.js +48 -3
  30. package/services/OgService.js +169 -2
  31. package/services/PluginConfig.js +1 -1
  32. package/services/TwoslashCacheService.js +128 -2
  33. package/services/TwoslashEnvironments.js +30 -2
  34. package/services/TypeRegistryService.js +178 -2
  35. package/shiki-transformer.js +1 -1
  36. package/sync-node-fs.js +6 -6
  37. package/tsconfig-parser.js +77 -95
  38. package/twoslash-access.js +1 -1
  39. package/twoslash-transformer.js +1 -1
  40. package/layers/HighlighterServiceLive.js +0 -52
  41. package/layers/OgServiceLive.js +0 -134
  42. package/layers/TwoslashCacheServiceLive.js +0 -108
  43. package/layers/TwoslashEnvironmentsLive.js +0 -33
  44. package/layers/TypeRegistryServiceLive.js +0 -162
  45. package/markdown/index.js +0 -11
  46. package/schemas/index.js +0 -6
@@ -1,32 +1,28 @@
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";
6
+ import { deriveSiteUrl } from "../og-resolver.js";
7
7
  import { withPhase } from "../observability/spans.js";
8
- import { TwoslashEnvironments } from "../services/TwoslashEnvironments.js";
9
- import { resolveTypeScriptConfig } from "../typescript-config.js";
8
+ import { normalizeThemeConfig } from "../markdown/shiki-utils.js";
10
9
  import { apiScopeOf, deriveOutputPaths, normalizeBaseRoute, unscopedName } from "../path-derivation.js";
11
10
  import { classifyApiConfig, extractAutoDetectedPackages, isVersionConfig, mergeLlmsPluginConfig, validateExternalPackages } from "../config-utils.js";
12
11
  import { ApiExtractedPackage } from "../api-extracted-package.js";
13
12
  import { CategoryResolver } from "../category-resolver.js";
14
13
  import { ConfigValidationError } from "../errors.js";
15
- import { normalizeThemeConfig } from "../markdown/shiki-utils.js";
16
14
  import { loadApiModel, loadPackageJson, loadVersionModel } from "../model-loader.js";
17
15
  import { DEFAULT_CATEGORIES } from "../schemas/config.js";
18
- import "../schemas/index.js";
19
- import { ConfigService } from "../services/ConfigService.js";
20
16
  import { PluginConfig } from "../services/PluginConfig.js";
21
- import { TwoslashCacheService } from "../services/TwoslashCacheService.js";
22
17
  import { TypeRegistryService } from "../services/TypeRegistryService.js";
23
- import { twoslashEnvHash } from "../twoslash-cache.js";
18
+ import { emitVfsPayloadEvents, mergeApiResult } from "./api-results.js";
19
+ import { mergeExternalTypes } from "./external-types.js";
20
+ import { registerTypeEnvironments, resolveTsConfigTyped } from "./type-environment.js";
24
21
  import path from "node:path";
22
+ import { Effect, Metric } from "effect";
25
23
  import { hashContent } from "@tsdoctor/snapshot";
26
- import { Effect, Layer, Metric } from "effect";
27
- import ts from "typescript";
28
24
 
29
- //#region src/layers/ConfigServiceLive.ts
25
+ //#region src/layers/config-resolution.ts
30
26
  /**
31
27
  * Prepend import statements for external type references to the VFS declaration files.
32
28
  * Returns per-entry payloads for event emission (heavy content/importRefs gated on wantTrace).
@@ -129,8 +125,12 @@ function validateOptions(options, rspressConfig) {
129
125
  * would build a second `ConfigService` with its own captured `TypeRegistry`.
130
126
  * The options come from {@link PluginConfig} now, so there is nothing to pass
131
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
132
  */
133
- const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
133
+ const makeConfigService = Effect.gen(function* () {
134
134
  const typeRegistry = yield* TypeRegistryService;
135
135
  const options = yield* PluginConfig;
136
136
  return { resolve: (rspressConfig) => Effect.gen(function* () {
@@ -146,11 +146,18 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
146
146
  const rspressLang = rspressConfig.lang;
147
147
  const docsRoot = rspressConfig.root;
148
148
  const rspressRoot = docsRoot || process.cwd();
149
+ const siteUrl = deriveSiteUrl(rspressConfig.siteOrigin, rspressConfig.base);
149
150
  const categoryResolver = new CategoryResolver();
150
151
  const pluginDefaults = categoryResolver.mergeCategories(DEFAULT_CATEGORIES, options.defaultCategories);
151
152
  const apiConfigs = [];
152
153
  const combinedVfs = /* @__PURE__ */ new Map();
153
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
+ };
154
161
  let firstApiTsconfig;
155
162
  let firstApiCompilerOptions;
156
163
  /**
@@ -176,14 +183,28 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
176
183
  */
177
184
  const processSimpleApi = (api, model, outputDir, fullRoute, wantTrace) => Effect.gen(function* () {
178
185
  const { apiPackage, source: loaderSource } = yield* withModelLoadEvents(loadApiModel(model));
179
- return yield* Effect.promise(async () => {
186
+ {
180
187
  const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories);
181
188
  const resolvedSource = categoryResolver.resolveSourceConfig(api.source, loaderSource);
182
189
  const resolvedLlms = mergeLlmsPluginConfig(options.llmsPlugin, api.llmsPlugin);
183
- const packageJson = api.packageJson ? await loadPackageJson(api.packageJson) : void 0;
184
- validateExternalPackages(api.externalPackages, packageJson);
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
+ });
185
206
  const externalPackages = api.externalPackages || extractAutoDetectedPackages(packageJson, api.autoDetectDependencies);
186
- if (externalPackages && externalPackages.length > 0) Effect.runSync(Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length));
207
+ if (externalPackages && externalPackages.length > 0) yield* Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length);
187
208
  const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
188
209
  const vfsPayloads = prependImportsToVfs(vfs, apiPackage, api.packageName, wantTrace);
189
210
  const resolvedOgImage = api.ogImage ?? options.ogImage;
@@ -202,14 +223,14 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
202
223
  ...resolvedSource != null ? { source: resolvedSource } : {},
203
224
  ...packageJson != null ? { packageJson } : {},
204
225
  ...resolvedLlms != null ? { llmsPlugin: resolvedLlms } : {},
205
- ...options.siteUrl != null ? { siteUrl: options.siteUrl } : {},
226
+ ...siteUrl != null ? { siteUrl } : {},
206
227
  ...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
207
228
  docsDir: path.dirname(outputDir),
208
229
  ...docsRoot != null ? { docsRoot } : {},
209
230
  ...resolvedTheme != null ? { theme: resolvedTheme } : {}
210
231
  }
211
232
  };
212
- });
233
+ }
213
234
  });
214
235
  yield* withPhase("modelLoad", { buildId }, Effect.gen(function* () {
215
236
  if (options.api) {
@@ -238,16 +259,30 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
238
259
  };
239
260
  const versionConfig = isVersionConfig(versionValue) ? versionValue : { model: versionValue };
240
261
  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));
262
+ {
263
+ yield* Metric.update(BuildMetrics.apiVersionsLoaded, 1);
243
264
  const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories, versionCategories);
244
265
  const resolvedSource = categoryResolver.resolveSourceConfig(api.source, versionSource);
245
266
  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);
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
+ });
248
283
  const autoDetectOptions = versionAutoDetectDependencies || api.autoDetectDependencies;
249
284
  const externalPackages = versionExternalPackages || api.externalPackages || extractAutoDetectedPackages(packageJson, autoDetectOptions);
250
- if (externalPackages && externalPackages.length > 0) Effect.runSync(Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length));
285
+ if (externalPackages && externalPackages.length > 0) yield* Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length);
251
286
  const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
252
287
  const vfsPayloads = prependImportsToVfs(vfs, apiPackage, api.packageName, wantTrace);
253
288
  const resolvedOgImage = versionOgImage ?? api.ogImage ?? options.ogImage;
@@ -268,41 +303,18 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
268
303
  ...resolvedSource != null ? { source: resolvedSource } : {},
269
304
  ...packageJson != null ? { packageJson } : {},
270
305
  ...resolvedLlms != null ? { llmsPlugin: resolvedLlms } : {},
271
- ...options.siteUrl != null ? { siteUrl: options.siteUrl } : {},
306
+ ...siteUrl != null ? { siteUrl } : {},
272
307
  ...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
273
308
  docsDir: path.dirname(outputDir),
274
309
  ...docsRoot != null ? { docsRoot } : {},
275
310
  ...resolvedTheme != null ? { theme: resolvedTheme } : {}
276
311
  }
277
312
  };
278
- });
313
+ }
279
314
  }), { concurrency: "unbounded" });
280
315
  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
- }
316
+ mergeApiResult(acc, result);
317
+ yield* emitVfsPayloadEvents(api.packageName, result.vfsPayloads, wantTrace);
306
318
  }
307
319
  } else if (api.model) {
308
320
  const dp = deriveOutputPaths({
@@ -317,31 +329,8 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
317
329
  })[0];
318
330
  if (dp) {
319
331
  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
- }
332
+ mergeApiResult(acc, result);
333
+ yield* emitVfsPayloadEvents(api.packageName, result.vfsPayloads, wantTrace);
345
334
  }
346
335
  }
347
336
  } else if (options.apis) {
@@ -367,35 +356,10 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
367
356
  })[0];
368
357
  if (!dp) return [];
369
358
  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
- }
359
+ yield* emitVfsPayloadEvents(api.packageName, result.vfsPayloads, wantTrace);
392
360
  return [result];
393
361
  }), { 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
- }
362
+ for (const results of multiResults) for (const result of results) mergeApiResult(acc, result);
399
363
  }
400
364
  }));
401
365
  const loadMs = performance.now() - loadStart;
@@ -413,86 +377,23 @@ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
413
377
  if (firstApiTsconfig != null) globalTsConfig.tsconfig = firstApiTsconfig;
414
378
  if (firstApiCompilerOptions != null) globalTsConfig.compilerOptions = firstApiCompilerOptions;
415
379
  }
416
- const resolvedCompilerOptions = yield* Effect.promise(() => resolveTypeScriptConfig(projectRoot, globalTsConfig));
380
+ const resolvedCompilerOptions = yield* resolveTsConfigTyped(projectRoot, globalTsConfig);
417
381
  yield* emit(PluginEvent.TsCacheCreated({
418
382
  ctx: {},
419
383
  level: "debug",
420
384
  compilerOptions: `target=${resolvedCompilerOptions.target}, module=${resolvedCompilerOptions.module}, lib=[${resolvedCompilerOptions.lib?.join(", ") ?? ""}]`,
421
385
  durationMs: 0
422
386
  }));
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
387
+ yield* mergeExternalTypes(typeRegistry, combinedVfs, apiConfigs, allExternalPackages);
388
+ yield* registerTypeEnvironments({
389
+ combinedVfs,
390
+ resolvedCompilerOptions,
391
+ scopeTsConfigs,
392
+ projectRoot
467
393
  });
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
394
  return apiConfigs;
494
395
  }) };
495
- }));
396
+ });
496
397
  /** The raw TypeScript config an API declares, or undefined when it declares none. */
497
398
  function rawTsConfig(api) {
498
399
  if (api.tsconfig == null && api.compilerOptions == null) return void 0;
@@ -503,4 +404,4 @@ function rawTsConfig(api) {
503
404
  }
504
405
 
505
406
  //#endregion
506
- export { ConfigServiceLive };
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 };
@@ -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
@@ -60,7 +60,7 @@ async function generateShikiHast(code, highlighter, transformers, enableTwoslash
60
60
  * Accepts a single theme name applied to both modes, an explicit pair, or a
61
61
  * raw theme object. Lives beside {@link DEFAULT_SHIKI_THEMES} because it falls
62
62
  * back to it; it was previously duplicated byte-for-byte in `plugin.ts` and
63
- * `ConfigServiceLive.ts`.
63
+ * `ConfigService.layer.ts`.
64
64
  */
65
65
  function normalizeThemeConfig(theme) {
66
66
  if (!theme) return { ...DEFAULT_SHIKI_THEMES };
package/model-loader.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isLoadedModel, isVersionConfig } from "./config-utils.js";
2
- import fs from "node:fs";
2
+ import fsSync from "node:fs";
3
3
  import path from "node:path";
4
4
  import { Effect } from "effect";
5
5
  import { Model } from "@tsdoctor/model";
@@ -10,8 +10,8 @@ import { Model } from "@tsdoctor/model";
10
10
  */
11
11
  async function loadPackageJsonFromPath(pkgPath) {
12
12
  const resolvedPath = path.resolve(pkgPath.toString());
13
- if (!fs.existsSync(resolvedPath)) throw new Error(`Package.json file not found: ${resolvedPath}`);
14
- const content = fs.readFileSync(resolvedPath, "utf-8");
13
+ if (!fsSync.existsSync(resolvedPath)) throw new Error(`Package.json file not found: ${resolvedPath}`);
14
+ const content = fsSync.readFileSync(resolvedPath, "utf-8");
15
15
  try {
16
16
  return JSON.parse(content);
17
17
  } catch (error) {
@@ -23,9 +23,9 @@ function makeEventBusLayer(sinks) {
23
23
  * @remarks
24
24
  * Fills `ctx.buildId` from the {@link BuildId} Reference when the caller left
25
25
  * it empty, which is why no emit site passes one. Before this, 24 sites wrote
26
- * `ctx: { buildId: "" }` — 22 in `ConfigServiceLive`, where the real value sat
26
+ * `ctx: { buildId: "" }` — 22 in `ConfigService.layer`, where the real value sat
27
27
  * three scopes up and was simply not reached, and every site in
28
- * `TypeRegistryServiceLive`, where the layer is module-level and there is no
28
+ * `TypeRegistryService.layer`, where the layer is module-level and there is no
29
29
  * build to name. The second group is why a Reference is the fix and a
30
30
  * find-and-replace is not: a Reference reaches code that no parameter can.
31
31
  *
@@ -1,6 +1,6 @@
1
+ import { BuildMetrics } from "../layers/build-metrics.js";
1
2
  import { PluginEvent } from "./events.js";
2
3
  import { emit } from "./EventBus.js";
3
- import { BuildMetrics } from "../layers/build-metrics.js";
4
4
  import { Duration, Effect, Metric, Ref } from "effect";
5
5
 
6
6
  //#region src/observability/heartbeat.ts
@@ -18,7 +18,7 @@ import { Metric } from "effect";
18
18
  * such as a file path, belongs in a sample-shaped sink instead.
19
19
  *
20
20
  * Intentionally NOT derived here: `externalPackagesTotal` and `apiVersionsLoaded`
21
- * remain inline increments in `ConfigServiceLive`. `externalPackagesTotal` counts
21
+ * remain inline increments in `ConfigService.layer`. `externalPackagesTotal` counts
22
22
  * CONFIGURED packages via `incrementBy(length)`; the only candidate event,
23
23
  * `TypeRegistryEvent{BatchComplete}`, carries an unstructured `detail` string and
24
24
  * a `loaded` (SUCCEEDED) count — different semantics, so deriving it here would