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.
- package/BuildEnv.js +3 -3
- package/build-program.js +2 -3
- package/build-stages.js +3 -5
- package/config-helpers.js +7 -7
- package/errors.js +1 -5
- package/index.d.ts +84 -86
- package/layers/AppLayer.js +67 -0
- package/layers/api-results.js +83 -0
- package/layers/build-metrics.js +1 -1
- package/layers/{ConfigServiceLive.js → config-resolution.js} +76 -175
- package/layers/external-types.js +74 -0
- package/layers/{ObservabilityLive.js → observability.js} +3 -3
- package/layers/type-environment.js +109 -0
- package/layers/xdg.js +1 -1
- package/markdown/shiki-utils.js +1 -1
- package/model-loader.js +3 -3
- package/observability/EventBus.js +2 -2
- package/observability/heartbeat.js +1 -1
- package/observability/sinks/metrics-sink.js +1 -1
- package/observability/sinks/trace-sink.js +4 -4
- package/og-resolver.js +32 -1
- package/package.json +2 -2
- package/plugin.js +30 -33
- package/prettier-formatter.js +1 -1
- package/remark-api-codeblocks.js +1 -1
- package/remark-with-api.js +1 -1
- package/schemas/config.js +0 -2
- package/services/ConfigService.js +37 -2
- package/services/HighlighterService.js +48 -3
- package/services/OgService.js +169 -2
- package/services/PluginConfig.js +1 -1
- package/services/TwoslashCacheService.js +128 -2
- package/services/TwoslashEnvironments.js +30 -2
- package/services/TypeRegistryService.js +178 -2
- package/shiki-transformer.js +1 -1
- package/sync-node-fs.js +6 -6
- package/tsconfig-parser.js +77 -95
- package/twoslash-access.js +1 -1
- package/twoslash-transformer.js +1 -1
- package/layers/HighlighterServiceLive.js +0 -52
- package/layers/OgServiceLive.js +0 -134
- package/layers/TwoslashCacheServiceLive.js +0 -108
- package/layers/TwoslashEnvironmentsLive.js +0 -33
- package/layers/TypeRegistryServiceLive.js +0 -162
- package/markdown/index.js +0 -11
- 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 {
|
|
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 {
|
|
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/
|
|
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
|
|
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
|
-
|
|
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 ?
|
|
184
|
-
|
|
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)
|
|
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
|
-
...
|
|
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
|
-
|
|
242
|
-
|
|
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 ?
|
|
247
|
-
|
|
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)
|
|
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
|
-
...
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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*
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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 {
|
|
407
|
+
export { makeConfigService };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { PluginEvent } from "../observability/events.js";
|
|
2
|
+
import { emit } from "../observability/EventBus.js";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/layers/external-types.ts
|
|
6
|
+
/**
|
|
7
|
+
* Merging external package declarations into the build's VFS.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The one phase of config resolution that **degrades rather than fails**.
|
|
11
|
+
* External types are an enhancement: without them, code blocks render without
|
|
12
|
+
* Twoslash enrichment, which is a worse page rather than a broken build. The
|
|
13
|
+
* caller therefore never sees this fail — the failure is reported as a warning
|
|
14
|
+
* and the VFS is left as it was.
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Fetch external package declarations and merge them into `combinedVfs`.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* **First-party packages are excluded, and that exclusion is load-bearing.**
|
|
23
|
+
* The packages being documented are already served from their api.json-derived
|
|
24
|
+
* virtual VFS, which is authoritative. Their published version may not exist
|
|
25
|
+
* yet (an optimistic next version), and if it did, fetching it would clobber
|
|
26
|
+
* the generated declarations with the previous release's — silently
|
|
27
|
+
* documenting the wrong API.
|
|
28
|
+
*
|
|
29
|
+
* Versions are resolved to exact published ones first: the CDN behind
|
|
30
|
+
* `loadPackages` 404s on a range or an unpublished package, so a spec that
|
|
31
|
+
* cannot be resolved is dropped with a debug event rather than failing the
|
|
32
|
+
* batch it is in.
|
|
33
|
+
*
|
|
34
|
+
* Mutates `combinedVfs` in place, matching the other resolution phases.
|
|
35
|
+
*
|
|
36
|
+
* The registry arrives as an argument rather than being pulled from context.
|
|
37
|
+
* `ConfigService.layer` resolves it ONCE at layer construction, and yielding
|
|
38
|
+
* the tag here instead would move it into `resolve`'s per-call requirement
|
|
39
|
+
* channel — a different resolution point, and a widened public signature, for
|
|
40
|
+
* a dependency that does not vary per call.
|
|
41
|
+
*/
|
|
42
|
+
const mergeExternalTypes = (typeRegistry, combinedVfs, apiConfigs, allExternalPackages) => Effect.gen(function* () {
|
|
43
|
+
const documentedPackageNames = new Set(apiConfigs.map((config) => config.packageName));
|
|
44
|
+
const externalPackagesToLoad = allExternalPackages.filter((pkg) => !documentedPackageNames.has(pkg.name));
|
|
45
|
+
const typeLoadResult = yield* Effect.result(Effect.gen(function* () {
|
|
46
|
+
if (externalPackagesToLoad.length === 0) return;
|
|
47
|
+
const resolvedPackages = yield* typeRegistry.resolveVersions(externalPackagesToLoad);
|
|
48
|
+
const droppedCount = externalPackagesToLoad.length - resolvedPackages.length;
|
|
49
|
+
if (droppedCount > 0) yield* emit(PluginEvent.ExternalPackageSkipped({
|
|
50
|
+
ctx: {},
|
|
51
|
+
level: "debug",
|
|
52
|
+
reason: `${droppedCount} unresolvable package(s) (unpublished or workspace-only)`
|
|
53
|
+
}));
|
|
54
|
+
if (resolvedPackages.length === 0) return;
|
|
55
|
+
const result = yield* typeRegistry.loadPackages(resolvedPackages);
|
|
56
|
+
for (const [filePath, content] of result.vfs.entries()) combinedVfs.set(filePath, content);
|
|
57
|
+
yield* emit(PluginEvent.VfsMerged({
|
|
58
|
+
ctx: {},
|
|
59
|
+
level: "debug",
|
|
60
|
+
totalFiles: result.vfs.size,
|
|
61
|
+
packages: resolvedPackages.map((p) => p.name)
|
|
62
|
+
}));
|
|
63
|
+
}));
|
|
64
|
+
if (typeLoadResult._tag === "Failure") yield* emit(PluginEvent.ConfigCascadeWarning({
|
|
65
|
+
ctx: {},
|
|
66
|
+
level: "warn",
|
|
67
|
+
field: "externalTypes",
|
|
68
|
+
chosen: "empty VFS",
|
|
69
|
+
ignored: [typeLoadResult.failure.message ?? String(typeLoadResult.failure)]
|
|
70
|
+
}));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
export { mergeExternalTypes };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { BuildMetrics, makeMetricStore } from "./build-metrics.js";
|
|
1
2
|
import { makeEventBusLayer } from "../observability/EventBus.js";
|
|
2
3
|
import { formatCodeBlockReport, seriesFor } from "../observability/metric-report.js";
|
|
3
|
-
import { BuildMetrics, makeMetricStore } from "./build-metrics.js";
|
|
4
4
|
import { makeConsoleSink } from "../observability/sinks/console-sink.js";
|
|
5
5
|
import { makeIssuesSink } from "../observability/sinks/issues-sink.js";
|
|
6
6
|
import { makeMetricsSink } from "../observability/sinks/metrics-sink.js";
|
|
@@ -8,7 +8,7 @@ import { makeRenderSink } from "../observability/sinks/render-sink.js";
|
|
|
8
8
|
import { makeTraceSink } from "../observability/sinks/trace-sink.js";
|
|
9
9
|
import { Effect, Layer, Logger, Metric, References } from "effect";
|
|
10
10
|
|
|
11
|
-
//#region src/layers/
|
|
11
|
+
//#region src/layers/observability.ts
|
|
12
12
|
function formatTime(date) {
|
|
13
13
|
return date.toTimeString().slice(0, 8);
|
|
14
14
|
}
|
|
@@ -117,4 +117,4 @@ const logBuildSummary = (slowCodeBlockMs, renderPhase) => Effect.gen(function* (
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
//#endregion
|
|
120
|
-
export {
|
|
120
|
+
export { buildEventBus, logBuildSummary, makeSummaryLoggerLayer };
|
|
@@ -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
package/markdown/shiki-utils.js
CHANGED
|
@@ -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
|
-
* `
|
|
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
|
|
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 (!
|
|
14
|
-
const content =
|
|
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 `
|
|
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
|
-
* `
|
|
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 `
|
|
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
|