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
@@ -1,600 +0,0 @@
1
- import { PluginEvent } from "../observability/events.js";
2
- import { emit, wantsLevel } from "../observability/EventBus.js";
3
- import { BuildMetrics } from "./build-metrics.js";
4
- import "./ObservabilityLive.js";
5
- import { TypeReferenceExtractor } from "../type-reference-extractor.js";
6
- import { OpenGraphResolver } from "../og-resolver.js";
7
- import { withPhase } from "../observability/spans.js";
8
- import { resolveTypeScriptConfig } from "../typescript-config.js";
9
- import { TwoslashManager } from "../twoslash-transformer.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 { HideCutLinesTransformer, MemberFormatTransformer } from "../hide-cut-transformer.js";
15
- import { DEFAULT_SHIKI_THEMES } from "../markdown/shiki-utils.js";
16
- import { loadApiModel, loadPackageJson, loadVersionModel } from "../model-loader.js";
17
- import { DEFAULT_CATEGORIES } from "../schemas/config.js";
18
- import "../schemas/index.js";
19
- import { ConfigService } from "../services/ConfigService.js";
20
- import { PathDerivationService } from "../services/PathDerivationService.js";
21
- import { TwoslashCacheService } from "../services/TwoslashCacheService.js";
22
- import { TypeRegistryService } from "../services/TypeRegistryService.js";
23
- import { makeTwoslashCache, twoslashEnvHash } from "../twoslash-cache.js";
24
- import path from "node:path";
25
- import { hashContent } from "@tsdoctor/snapshot";
26
- import { Effect, Layer, Metric } from "effect";
27
- import ts from "typescript";
28
- import os from "node:os";
29
- import { createHighlighter } from "shiki";
30
-
31
- //#region src/layers/ConfigServiceLive.ts
32
- const DEFAULT_THRESHOLDS = {
33
- slowCodeBlock: 100,
34
- slowPageGeneration: 500,
35
- slowApiLoad: 1e3,
36
- slowFileOperation: 50,
37
- slowHttpRequest: 2e3,
38
- slowDbOperation: 100
39
- };
40
- /**
41
- * Normalize theme configuration from user input to a consistent format.
42
- */
43
- function normalizeThemeConfig(theme) {
44
- if (!theme) return { ...DEFAULT_SHIKI_THEMES };
45
- if (typeof theme === "string") return {
46
- light: theme,
47
- dark: theme
48
- };
49
- if ("light" in theme && "dark" in theme && typeof theme.light === "string" && typeof theme.dark === "string") return {
50
- light: theme.light,
51
- dark: theme.dark
52
- };
53
- return {
54
- light: theme,
55
- dark: theme
56
- };
57
- }
58
- /**
59
- * Prepend import statements for external type references to the VFS declaration files.
60
- * Returns per-entry payloads for event emission (heavy content/importRefs gated on wantTrace).
61
- */
62
- function prependImportsToVfs(vfs, apiPackage, packageName, wantTrace) {
63
- const extractor = new TypeReferenceExtractor(apiPackage, packageName);
64
- const payloads = [];
65
- for (const entryPoint of apiPackage.entryPoints) {
66
- const entryEp = entryPoint;
67
- const imports = extractor.extractImportsForEntryPoint(entryEp);
68
- const importStatements = TypeReferenceExtractor.formatImports(imports);
69
- const entryName = entryEp.displayName || "";
70
- const file = `node_modules/${packageName}/${entryName ? `${entryName}.d.ts` : "index.d.ts"}`;
71
- const hasImports = importStatements.length > 0;
72
- if (hasImports) {
73
- const existing = vfs.get(file);
74
- if (existing) vfs.set(file, `${importStatements.join("\n")}\n\n${existing}`);
75
- }
76
- const content = vfs.get(file) ?? "";
77
- const declCount = entryEp.members.length;
78
- const contentHash = hashContent(content);
79
- const importRefs = wantTrace && hasImports ? imports.map((i) => ({
80
- from: i.packageName,
81
- symbols: [...i.symbols]
82
- })) : [];
83
- payloads.push({
84
- file,
85
- entryPoint: entryName,
86
- declCount,
87
- contentHash,
88
- content: wantTrace ? content : "",
89
- hasImports,
90
- importRefs
91
- });
92
- }
93
- return payloads;
94
- }
95
- /**
96
- * Validate plugin options and return an Effect that fails with ConfigValidationError.
97
- */
98
- function validateOptions(options, rspressConfig) {
99
- return Effect.gen(function* () {
100
- const api = options.api ?? void 0;
101
- const apis = options.apis != null && options.apis.length > 0 ? options.apis : void 0;
102
- const { multiVersion } = rspressConfig;
103
- if (api && apis) return yield* new ConfigValidationError({
104
- field: "api/apis",
105
- reason: "Cannot provide both 'api' and 'apis'. Use 'api' for single-package sites or 'apis' for multi-package portals."
106
- });
107
- if (!api && !apis) {
108
- if (classifyApiConfig(options) === "missing") return yield* new ConfigValidationError({
109
- field: "api/apis",
110
- reason: "Must provide either 'api' or 'apis'."
111
- });
112
- return;
113
- }
114
- if (apis) {
115
- if (multiVersion) return yield* new ConfigValidationError({
116
- field: "apis",
117
- reason: "multiVersion is not supported with 'apis' (multi-API mode). Use 'api' (single-API mode) for versioned documentation."
118
- });
119
- return;
120
- }
121
- if (api) {
122
- if (multiVersion) {
123
- if (!api.versions) return yield* new ConfigValidationError({
124
- field: "api.versions",
125
- reason: "'versions' is required when multiVersion is active."
126
- });
127
- const pluginKeys = new Set(Object.keys(api.versions));
128
- const rspressKeys = new Set(multiVersion.versions);
129
- if (pluginKeys.size !== rspressKeys.size || ![...pluginKeys].every((k) => rspressKeys.has(k))) return yield* new ConfigValidationError({
130
- field: "api.versions",
131
- reason: `api.versions keys [${[...pluginKeys].join(", ")}] must exactly match multiVersion.versions [${[...rspressKeys].join(", ")}].`
132
- });
133
- } else {
134
- if (api.versions) yield* emit(PluginEvent.ConfigCascadeWarning({
135
- ctx: { buildId: "" },
136
- level: "warn",
137
- field: "versions",
138
- chosen: "(none — multiVersion not configured)",
139
- ignored: ["api.versions"]
140
- }));
141
- if (!api.model) return yield* new ConfigValidationError({
142
- field: "api.model",
143
- reason: "'model' is required when multiVersion is not active."
144
- });
145
- }
146
- }
147
- });
148
- }
149
- /**
150
- * Create ConfigServiceLive from plugin options.
151
- * Resolves plugin options + RSPress config into a fully prepared build context
152
- * with loaded models, type system, and resources.
153
- */
154
- function ConfigServiceLive(options, shikiCrossLinker, buildId = "", resolvedThresholds) {
155
- return Layer.effect(ConfigService, Effect.gen(function* () {
156
- const typeRegistry = yield* TypeRegistryService;
157
- const pathService = yield* PathDerivationService;
158
- return { resolve: (rspressConfig) => Effect.gen(function* () {
159
- const loadStart = performance.now();
160
- const wantTrace = yield* wantsLevel("trace");
161
- yield* validateOptions(options, { ...rspressConfig.multiVersion ? { multiVersion: {
162
- default: rspressConfig.multiVersion.default,
163
- versions: [...rspressConfig.multiVersion.versions]
164
- } } : {} });
165
- const rspressMultiVersion = rspressConfig.multiVersion;
166
- const rspressLocales = rspressConfig.locales?.map((l) => l.lang) ?? [];
167
- const rspressLang = rspressConfig.lang;
168
- const docsRoot = rspressConfig.root;
169
- const rspressRoot = docsRoot || process.cwd();
170
- const categoryResolver = new CategoryResolver();
171
- const pluginDefaults = categoryResolver.mergeCategories(DEFAULT_CATEGORIES, options.defaultCategories);
172
- const apiConfigs = [];
173
- const combinedVfs = /* @__PURE__ */ new Map();
174
- const allExternalPackages = [];
175
- let firstApiTsconfig;
176
- let firstApiCompilerOptions;
177
- /**
178
- * Raw TypeScript config per API scope. Each documented package is
179
- * type-checked under its OWN configuration; the build no longer picks
180
- * one and applies it to everything.
181
- */
182
- const scopeTsConfigs = /* @__PURE__ */ new Map();
183
- /**
184
- * Emit a typed ModelLoadFailed event for a failed model load, then
185
- * convert the typed failure to a defect — a missing or unparsable
186
- * model remains fatal to the build, exactly as before, but the
187
- * event now rides the error channel instead of a sync-island seam.
188
- */
189
- const withModelLoadEvents = (self) => self.pipe(Effect.tapError((error) => emit(PluginEvent.ModelLoadFailed({
190
- ctx: { buildId },
191
- level: "error",
192
- modelPath: "modelPath" in error ? error.modelPath : "<loader function>",
193
- reason: error.message
194
- }))), Effect.orDie);
195
- /**
196
- * Helper to process a single API model (shared by single and multi modes).
197
- */
198
- const processSimpleApi = (api, model, outputDir, fullRoute, wantTrace) => Effect.gen(function* () {
199
- const { apiPackage, source: loaderSource } = yield* withModelLoadEvents(loadApiModel(model));
200
- return yield* Effect.promise(async () => {
201
- const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories);
202
- const resolvedSource = categoryResolver.resolveSourceConfig(api.source, loaderSource);
203
- const resolvedLlms = mergeLlmsPluginConfig(options.llmsPlugin, api.llmsPlugin);
204
- const packageJson = api.packageJson ? await loadPackageJson(api.packageJson) : void 0;
205
- validateExternalPackages(api.externalPackages, packageJson);
206
- const externalPackages = api.externalPackages || extractAutoDetectedPackages(packageJson, api.autoDetectDependencies);
207
- if (externalPackages && externalPackages.length > 0) Effect.runSync(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
- ...options.siteUrl != null ? { siteUrl: options.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 = yield* pathService.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 = (yield* pathService.derivePaths({
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
- return yield* Effect.promise(async () => {
263
- Effect.runSync(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 ? await loadPackageJson(api.packageJson) : void 0);
268
- validateExternalPackages(versionExternalPackages || api.externalPackages, packageJson);
269
- const autoDetectOptions = versionAutoDetectDependencies || api.autoDetectDependencies;
270
- const externalPackages = versionExternalPackages || api.externalPackages || extractAutoDetectedPackages(packageJson, autoDetectOptions);
271
- if (externalPackages && externalPackages.length > 0) Effect.runSync(Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length));
272
- const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
273
- const vfsPayloads = prependImportsToVfs(vfs, apiPackage, api.packageName, wantTrace);
274
- const resolvedOgImage = versionOgImage ?? api.ogImage ?? options.ogImage;
275
- const resolvedTheme = normalizeThemeConfig(api.theme);
276
- const outputDir = versionDp.outputDir;
277
- const fullRoute = versionDp.routeBase;
278
- return {
279
- vfs,
280
- vfsPayloads,
281
- externalPackages: externalPackages || [],
282
- config: {
283
- apiPackage,
284
- packageName: `${api.packageName} (${version})`,
285
- ...api.name != null ? { apiName: api.name } : {},
286
- outputDir,
287
- baseRoute: fullRoute,
288
- categories: resolvedCategories,
289
- ...resolvedSource != null ? { source: resolvedSource } : {},
290
- ...packageJson != null ? { packageJson } : {},
291
- ...resolvedLlms != null ? { llmsPlugin: resolvedLlms } : {},
292
- ...options.siteUrl != null ? { siteUrl: options.siteUrl } : {},
293
- ...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
294
- docsDir: path.dirname(outputDir),
295
- ...docsRoot != null ? { docsRoot } : {},
296
- ...resolvedTheme != null ? { theme: resolvedTheme } : {}
297
- }
298
- };
299
- });
300
- }), { concurrency: "unbounded" });
301
- for (const result of versionResults) {
302
- for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
303
- if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
304
- if (result.config) apiConfigs.push(result.config);
305
- for (const payload of result.vfsPayloads) {
306
- yield* emit(PluginEvent.VfsGenerated({
307
- ctx: {
308
- buildId: "",
309
- packageName: api.packageName,
310
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
311
- },
312
- level: "debug",
313
- file: payload.file,
314
- declCount: payload.declCount,
315
- contentHash: payload.contentHash,
316
- ...wantTrace && payload.content ? { content: payload.content } : {}
317
- }));
318
- if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
319
- ctx: {
320
- buildId: "",
321
- packageName: api.packageName,
322
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
323
- },
324
- level: "debug",
325
- file: payload.file,
326
- imports: wantTrace ? payload.importRefs : []
327
- }));
328
- }
329
- }
330
- } else if (api.model) {
331
- const dp = (yield* pathService.derivePaths({
332
- mode: "single",
333
- docsRoot: rspressRoot,
334
- baseRoute,
335
- apiFolder: api.apiFolder ?? "api",
336
- locales: rspressLocales,
337
- defaultLang: rspressLang,
338
- versions: [],
339
- defaultVersion: void 0
340
- }))[0];
341
- if (dp) {
342
- const result = yield* processSimpleApi(api, api.model, dp.outputDir, dp.routeBase, wantTrace);
343
- for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
344
- if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
345
- apiConfigs.push(result.config);
346
- for (const payload of result.vfsPayloads) {
347
- yield* emit(PluginEvent.VfsGenerated({
348
- ctx: {
349
- buildId: "",
350
- packageName: api.packageName,
351
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
352
- },
353
- level: "debug",
354
- file: payload.file,
355
- declCount: payload.declCount,
356
- contentHash: payload.contentHash,
357
- ...wantTrace && payload.content ? { content: payload.content } : {}
358
- }));
359
- if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
360
- ctx: {
361
- buildId: "",
362
- packageName: api.packageName,
363
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
364
- },
365
- level: "debug",
366
- file: payload.file,
367
- imports: wantTrace ? payload.importRefs : []
368
- }));
369
- }
370
- }
371
- }
372
- } else if (options.apis) {
373
- const apisWithTsconfig = options.apis.filter((a) => a.tsconfig);
374
- if (apisWithTsconfig.length > 0) firstApiTsconfig = apisWithTsconfig[0].tsconfig;
375
- const apisWithCompilerOptions = options.apis.filter((a) => a.compilerOptions);
376
- if (apisWithCompilerOptions.length > 0) firstApiCompilerOptions = apisWithCompilerOptions[0].compilerOptions;
377
- for (const a of options.apis) {
378
- const scopeRoute = yield* pathService.normalizeBaseRoute(a.baseRoute ?? `/${unscopedName(a.packageName)}`);
379
- scopeTsConfigs.set(apiScopeOf(scopeRoute, a.packageName), rawTsConfig(a));
380
- }
381
- const multiResults = yield* Effect.forEach(options.apis, (api) => Effect.gen(function* () {
382
- const apiBaseRoute = yield* pathService.normalizeBaseRoute(api.baseRoute ?? `/${unscopedName(api.packageName)}`);
383
- const dp = (yield* pathService.derivePaths({
384
- mode: "multi",
385
- docsRoot: rspressRoot,
386
- baseRoute: apiBaseRoute,
387
- apiFolder: api.apiFolder ?? "api",
388
- locales: rspressLocales,
389
- defaultLang: rspressLang,
390
- versions: [],
391
- defaultVersion: void 0
392
- }))[0];
393
- if (!dp) return [];
394
- const result = yield* processSimpleApi(api, api.model, dp.outputDir, dp.routeBase, wantTrace);
395
- for (const payload of result.vfsPayloads) {
396
- yield* emit(PluginEvent.VfsGenerated({
397
- ctx: {
398
- buildId: "",
399
- packageName: api.packageName,
400
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
401
- },
402
- level: "debug",
403
- file: payload.file,
404
- declCount: payload.declCount,
405
- contentHash: payload.contentHash,
406
- ...wantTrace && payload.content ? { content: payload.content } : {}
407
- }));
408
- if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
409
- ctx: {
410
- buildId: "",
411
- packageName: api.packageName,
412
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
413
- },
414
- level: "debug",
415
- file: payload.file,
416
- imports: wantTrace ? payload.importRefs : []
417
- }));
418
- }
419
- return [result];
420
- }), { concurrency: "unbounded" });
421
- for (const results of multiResults) for (const result of results) {
422
- for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
423
- if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
424
- apiConfigs.push(result.config);
425
- }
426
- }
427
- }), resolvedThresholds ?? DEFAULT_THRESHOLDS);
428
- const loadMs = performance.now() - loadStart;
429
- yield* emit(PluginEvent.ModelLoaded({
430
- ctx: { buildId: "" },
431
- level: "debug",
432
- entryPoints: apiConfigs.length,
433
- itemCount: apiConfigs.reduce((sum, cfg) => sum + cfg.apiPackage.entryPoints.reduce((s, ep) => s + ep.members.length, 0), 0),
434
- durationMs: Math.round(loadMs)
435
- }));
436
- const projectRoot = process.cwd();
437
- let globalTsConfig;
438
- if (firstApiTsconfig || firstApiCompilerOptions) {
439
- globalTsConfig = {};
440
- if (firstApiTsconfig != null) globalTsConfig.tsconfig = firstApiTsconfig;
441
- if (firstApiCompilerOptions != null) globalTsConfig.compilerOptions = firstApiCompilerOptions;
442
- }
443
- const resolvedCompilerOptions = yield* Effect.promise(() => resolveTypeScriptConfig(projectRoot, globalTsConfig));
444
- yield* emit(PluginEvent.TsCacheCreated({
445
- ctx: { buildId: "" },
446
- level: "debug",
447
- compilerOptions: `target=${resolvedCompilerOptions.target}, module=${resolvedCompilerOptions.module}, lib=[${resolvedCompilerOptions.lib?.join(", ") ?? ""}]`,
448
- durationMs: 0
449
- }));
450
- const documentedPackageNames = new Set(apiConfigs.map((config) => config.packageName));
451
- const externalPackagesToLoad = allExternalPackages.filter((pkg) => !documentedPackageNames.has(pkg.name));
452
- const typeLoadResult = yield* Effect.result(Effect.gen(function* () {
453
- if (externalPackagesToLoad.length > 0) {
454
- const resolvedPackages = yield* typeRegistry.resolveVersions(externalPackagesToLoad);
455
- const droppedCount = externalPackagesToLoad.length - resolvedPackages.length;
456
- if (droppedCount > 0) yield* emit(PluginEvent.ExternalPackageSkipped({
457
- ctx: { buildId: "" },
458
- level: "debug",
459
- reason: `${droppedCount} unresolvable package(s) (unpublished or workspace-only)`
460
- }));
461
- if (resolvedPackages.length > 0) {
462
- const result = yield* typeRegistry.loadPackages(resolvedPackages);
463
- for (const [filePath, content] of result.vfs.entries()) combinedVfs.set(filePath, content);
464
- yield* emit(PluginEvent.VfsMerged({
465
- ctx: { buildId: "" },
466
- level: "debug",
467
- totalFiles: result.vfs.size,
468
- packages: resolvedPackages.map((p) => p.name)
469
- }));
470
- }
471
- }
472
- }));
473
- if (typeLoadResult._tag === "Failure") yield* emit(PluginEvent.ConfigCascadeWarning({
474
- ctx: { buildId: "" },
475
- level: "warn",
476
- field: "externalTypes",
477
- chosen: "empty VFS",
478
- ignored: [typeLoadResult.failure.message ?? String(typeLoadResult.failure)]
479
- }));
480
- const twoslashEnv = twoslashEnvHash(combinedVfs, `typescript@${ts.version}`);
481
- const restored = yield* (yield* TwoslashCacheService).load(twoslashEnv);
482
- const twoslashCache = makeTwoslashCache(restored);
483
- yield* emit(PluginEvent.TwoslashCacheLoaded({
484
- ctx: { buildId: "" },
485
- level: "debug",
486
- envHash: twoslashEnv,
487
- entries: restored.size
488
- }));
489
- const twoslashStartMs = performance.now();
490
- const manager = TwoslashManager.getInstance();
491
- manager.initialize(combinedVfs, void 0, void 0, void 0, resolvedCompilerOptions, twoslashCache);
492
- const resolvedByRawConfig = /* @__PURE__ */ new Map();
493
- for (const [apiScope, rawConfig] of scopeTsConfigs) {
494
- if (rawConfig === void 0) {
495
- manager.registerScope(apiScope, resolvedCompilerOptions);
496
- continue;
497
- }
498
- const rawKey = JSON.stringify([String(rawConfig.tsconfig ?? ""), rawConfig.compilerOptions ?? null]);
499
- let scopeOptions = resolvedByRawConfig.get(rawKey);
500
- if (scopeOptions === void 0) {
501
- scopeOptions = yield* Effect.promise(() => resolveTypeScriptConfig(projectRoot, rawConfig));
502
- resolvedByRawConfig.set(rawKey, scopeOptions);
503
- }
504
- manager.initialize(combinedVfs, void 0, void 0, void 0, scopeOptions, twoslashCache);
505
- manager.registerScope(apiScope, scopeOptions);
506
- }
507
- yield* emit(PluginEvent.TwoslashInitialized({
508
- ctx: { buildId: "" },
509
- level: "debug",
510
- durationMs: Math.round(performance.now() - twoslashStartMs),
511
- vfsFileCount: combinedVfs.size
512
- }));
513
- const shikiStartMs = performance.now();
514
- const themeSet = /* @__PURE__ */ new Set();
515
- const customThemes = [];
516
- for (const config of apiConfigs) {
517
- const theme = config.theme ?? {
518
- light: DEFAULT_SHIKI_THEMES.light,
519
- dark: DEFAULT_SHIKI_THEMES.dark
520
- };
521
- if (typeof theme.light === "string") themeSet.add(theme.light);
522
- else if (typeof theme.light === "object") customThemes.push(theme.light);
523
- if (typeof theme.dark === "string") themeSet.add(theme.dark);
524
- else if (typeof theme.dark === "object") customThemes.push(theme.dark);
525
- }
526
- if (typeof DEFAULT_SHIKI_THEMES.light === "string") themeSet.add(DEFAULT_SHIKI_THEMES.light);
527
- if (typeof DEFAULT_SHIKI_THEMES.dark === "string") themeSet.add(DEFAULT_SHIKI_THEMES.dark);
528
- const themes = [...themeSet, ...customThemes];
529
- const langs = [
530
- "typescript",
531
- "javascript",
532
- "json",
533
- "bash",
534
- "sh"
535
- ];
536
- const highlighter = yield* Effect.promise(() => createHighlighter({
537
- themes,
538
- langs
539
- }));
540
- yield* emit(PluginEvent.PhaseCompleted({
541
- ctx: { buildId: "" },
542
- level: "debug",
543
- phase: "shikiInit",
544
- durationMs: Math.round(performance.now() - shikiStartMs)
545
- }));
546
- const ogResolver = options.siteUrl ? new OpenGraphResolver({
547
- siteUrl: options.siteUrl,
548
- ...docsRoot != null ? { docsRoot } : {}
549
- }) : null;
550
- const hideCutTransformer = MemberFormatTransformer;
551
- const hideCutLinesTransformer = HideCutLinesTransformer;
552
- const twoslashTransformer = TwoslashManager.getInstance().getTransformer() ?? void 0;
553
- const logLevel = options.logLevel ?? "info";
554
- const suppressExampleErrors = options.errors?.example !== "show";
555
- return {
556
- apiConfigs,
557
- combinedVfs,
558
- highlighter,
559
- resolvedCompilerOptions,
560
- ogResolver,
561
- shikiCrossLinker,
562
- hideCutTransformer,
563
- hideCutLinesTransformer,
564
- twoslashTransformer,
565
- twoslashCache,
566
- twoslashEnvHash: twoslashEnv,
567
- pageConcurrency: os.cpus().length,
568
- logLevel: logLevel === "none" ? "info" : logLevel,
569
- suppressExampleErrors,
570
- thresholds: resolvedThresholds ?? DEFAULT_THRESHOLDS,
571
- buildId
572
- };
573
- }) };
574
- }));
575
- }
576
- /**
577
- * Derive the API scope key from a base route, matching the derivation in
578
- * `build-program.ts` so config resolution and the remark plugins agree on the
579
- * name a code block is attributed to.
580
- */
581
- function apiScopeOf(baseRoute, packageName) {
582
- return baseRoute.replace(/^\//, "").split("/")[0] || packageName;
583
- }
584
- /** The raw TypeScript config an API declares, or undefined when it declares none. */
585
- function rawTsConfig(api) {
586
- if (api.tsconfig == null && api.compilerOptions == null) return void 0;
587
- const cfg = {};
588
- if (api.tsconfig != null) cfg.tsconfig = api.tsconfig;
589
- if (api.compilerOptions != null) cfg.compilerOptions = api.compilerOptions;
590
- return cfg;
591
- }
592
- /**
593
- * Strip npm scope from a package name.
594
- */
595
- function unscopedName(packageName) {
596
- return packageName.startsWith("@") ? packageName.split("/")[1] ?? packageName : packageName;
597
- }
598
-
599
- //#endregion
600
- export { ConfigServiceLive };
@@ -1,16 +0,0 @@
1
- import { deriveOutputPaths, normalizeBaseRoute } from "../path-derivation.js";
2
- import { PathDerivationService } from "../services/PathDerivationService.js";
3
- import { Effect, Layer } from "effect";
4
-
5
- //#region src/layers/PathDerivationServiceLive.ts
6
- const PathDerivationServiceLive = Layer.succeed(PathDerivationService, {
7
- derivePaths: (input) => Effect.succeed(deriveOutputPaths({
8
- ...input,
9
- locales: [...input.locales],
10
- versions: [...input.versions]
11
- })),
12
- normalizeBaseRoute: (route) => Effect.succeed(normalizeBaseRoute(route))
13
- });
14
-
15
- //#endregion
16
- export { PathDerivationServiceLive };
@@ -1,53 +0,0 @@
1
- import { TwoslashCacheService } from "../services/TwoslashCacheService.js";
2
- import { decodeTwoslashCache, encodeTwoslashCache, twoslashBlobKey } from "../twoslash-cache.js";
3
- import { NodeFileSystem } from "@effect/platform-node";
4
- import { Effect, Layer, Option, Path } from "effect";
5
- import { Cache } from "@effected/store";
6
- import { AppDirs, Xdg } from "@effected/xdg";
7
-
8
- //#region src/layers/TwoslashCacheServiceLive.ts
9
- const PlatformLive = Layer.mergeAll(NodeFileSystem.layer, Path.layer);
10
- /**
11
- * XDG app dirs under the same `tsdoctor` namespace the type registry uses, so
12
- * every derived-artifact cache this plugin keeps lives in one place.
13
- */
14
- const AppDirsLive = AppDirs.layer({ namespace: "tsdoctor" }).pipe(Layer.provide(Layer.mergeAll(Xdg.layer, PlatformLive)));
15
- /**
16
- * A sqlite-backed `@effected/store` Cache in the XDG cache dir, separate from
17
- * the registry's `metadata.sqlite`.
18
- *
19
- * XDG rather than the repo: these are regenerable results derived from content
20
- * hashes, so they belong with the user's other caches — shared across worktrees
21
- * and checkouts of the same project, and untouched by cleaning `dist/`. Nothing
22
- * here needs to be committed for a build to be correct.
23
- */
24
- const CacheLive = Layer.unwrap(Effect.gen(function* () {
25
- const appDirs = yield* AppDirs;
26
- const path = yield* Path.Path;
27
- const cacheDir = yield* appDirs.ensureCache;
28
- return Cache.layerSqlite({ filename: path.join(cacheDir, "twoslash.sqlite") });
29
- })).pipe(Layer.provide(Layer.mergeAll(AppDirsLive, PlatformLive)));
30
- /**
31
- * Live Twoslash cache persistence.
32
- *
33
- * Both operations swallow every failure by design — see the service docs. A
34
- * missing HOME, an unwritable cache dir or a corrupt database degrades the
35
- * build to "type-check everything", which is exactly the behaviour before this
36
- * cache existed.
37
- */
38
- const TwoslashCacheServiceLive = Layer.succeed(TwoslashCacheService, {
39
- load: (envHash) => Effect.gen(function* () {
40
- const entry = yield* (yield* Cache).get(twoslashBlobKey(envHash));
41
- return Option.isSome(entry) ? decodeTwoslashCache(entry.value.value) : /* @__PURE__ */ new Map();
42
- }).pipe(Effect.provide(CacheLive), Effect.catchCause(() => Effect.succeed(/* @__PURE__ */ new Map()))),
43
- save: (envHash, entries) => Effect.gen(function* () {
44
- yield* (yield* Cache).set({
45
- key: twoslashBlobKey(envHash),
46
- value: encodeTwoslashCache(entries),
47
- tags: ["twoslash"]
48
- });
49
- }).pipe(Effect.provide(CacheLive), Effect.catchCause(() => Effect.void))
50
- });
51
-
52
- //#endregion
53
- export { TwoslashCacheServiceLive };