rspress-plugin-api-extractor 0.2.2 → 0.3.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 (43) hide show
  1. package/api-extracted-package.js +2 -1
  2. package/build-program.js +20 -12
  3. package/build-stages.js +123 -29
  4. package/config-utils.js +36 -7
  5. package/index.d.ts +329 -202
  6. package/layers/ConfigServiceLive.js +300 -136
  7. package/layers/ObservabilityLive.js +49 -85
  8. package/layers/TypeRegistryServiceLive.js +122 -21
  9. package/layers/build-metrics.js +61 -0
  10. package/llms-program.js +29 -7
  11. package/loader.js +16 -2
  12. package/markdown/shiki-utils.js +16 -2
  13. package/observability/EventBus.js +38 -0
  14. package/observability/events.js +17 -0
  15. package/observability/sinks/console-sink.js +63 -0
  16. package/observability/sinks/metrics-sink.js +68 -0
  17. package/observability/sinks/trace-sink.js +38 -0
  18. package/observability/spans.js +57 -0
  19. package/og-resolver.js +37 -5
  20. package/package.json +4 -4
  21. package/plugin.js +73 -19
  22. package/prettier-formatter.js +15 -4
  23. package/remark-api-codeblocks.js +22 -3
  24. package/remark-with-api.js +27 -14
  25. package/runtime/components/ApiExample/index.js +5 -5
  26. package/runtime/components/ApiMember/index.js +5 -7
  27. package/runtime/components/ApiSignature/index.js +4 -6
  28. package/runtime/components/EnumMembersTable/index.js +5 -0
  29. package/runtime/components/ExampleBlock/index.js +5 -3
  30. package/runtime/components/MemberSignature/index.js +4 -2
  31. package/runtime/components/ParametersTable/index.js +5 -0
  32. package/runtime/components/SignatureBlock/index.js +4 -2
  33. package/runtime/components/shared/variables.css +0 -15
  34. package/runtime/index.d.ts +65 -399
  35. package/runtime/index.js +1 -5
  36. package/runtime/utils/hast-renderer.js +1 -0
  37. package/schemas/config.js +105 -4
  38. package/schemas/index.js +2 -1
  39. package/schemas/observability.js +62 -0
  40. package/schemas/opengraph.js +30 -0
  41. package/schemas/performance.js +1 -1
  42. package/serve.js +13 -0
  43. package/twoslash-transformer.js +93 -8
@@ -1,6 +1,11 @@
1
- import { BuildMetrics } from "./ObservabilityLive.js";
1
+ import { hashContent } from "../content-hash.js";
2
+ import { PluginEvent } from "../observability/events.js";
3
+ import { emit, wantsLevel } from "../observability/EventBus.js";
4
+ import { BuildMetrics } from "./build-metrics.js";
5
+ import "./ObservabilityLive.js";
2
6
  import { TypeReferenceExtractor } from "../type-reference-extractor.js";
3
7
  import { OpenGraphResolver } from "../og-resolver.js";
8
+ import { withPhase } from "../observability/spans.js";
4
9
  import { resolveTypeScriptConfig } from "../typescript-config.js";
5
10
  import { TwoslashManager } from "../twoslash-transformer.js";
6
11
  import { extractAutoDetectedPackages, isVersionConfig, mergeLlmsPluginConfig, validateExternalPackages } from "../config-utils.js";
@@ -21,6 +26,14 @@ import os from "node:os";
21
26
  import { createHighlighter } from "shiki";
22
27
 
23
28
  //#region src/layers/ConfigServiceLive.ts
29
+ const DEFAULT_THRESHOLDS = {
30
+ slowCodeBlock: 100,
31
+ slowPageGeneration: 500,
32
+ slowApiLoad: 1e3,
33
+ slowFileOperation: 50,
34
+ slowHttpRequest: 2e3,
35
+ slowDbOperation: 100
36
+ };
24
37
  /**
25
38
  * Normalize theme configuration from user input to a consistent format.
26
39
  */
@@ -41,18 +54,40 @@ function normalizeThemeConfig(theme) {
41
54
  }
42
55
  /**
43
56
  * Prepend import statements for external type references to the VFS declaration files.
57
+ * Returns per-entry payloads for event emission (heavy content/importRefs gated on wantTrace).
44
58
  */
45
- function prependImportsToVfs(vfs, apiPackage, packageName) {
59
+ function prependImportsToVfs(vfs, apiPackage, packageName, wantTrace) {
46
60
  const extractor = new TypeReferenceExtractor(apiPackage, packageName);
61
+ const payloads = [];
47
62
  for (const entryPoint of apiPackage.entryPoints) {
48
- const imports = extractor.extractImportsForEntryPoint(entryPoint);
63
+ const entryEp = entryPoint;
64
+ const imports = extractor.extractImportsForEntryPoint(entryEp);
49
65
  const importStatements = TypeReferenceExtractor.formatImports(imports);
50
- if (importStatements.length === 0) continue;
51
- const entryName = entryPoint.displayName || "";
52
- const key = `node_modules/${packageName}/${entryName ? `${entryName}.d.ts` : "index.d.ts"}`;
53
- const existing = vfs.get(key);
54
- if (existing) vfs.set(key, `${importStatements.join("\n")}\n\n${existing}`);
66
+ const entryName = entryEp.displayName || "";
67
+ const file = `node_modules/${packageName}/${entryName ? `${entryName}.d.ts` : "index.d.ts"}`;
68
+ const hasImports = importStatements.length > 0;
69
+ if (hasImports) {
70
+ const existing = vfs.get(file);
71
+ if (existing) vfs.set(file, `${importStatements.join("\n")}\n\n${existing}`);
72
+ }
73
+ const content = vfs.get(file) ?? "";
74
+ const declCount = entryEp.members.length;
75
+ const contentHash = hashContent(content);
76
+ const importRefs = wantTrace && hasImports ? imports.map((i) => ({
77
+ from: i.packageName,
78
+ symbols: [...i.symbols]
79
+ })) : [];
80
+ payloads.push({
81
+ file,
82
+ entryPoint: entryName,
83
+ declCount,
84
+ contentHash,
85
+ content: wantTrace ? content : "",
86
+ hasImports,
87
+ importRefs
88
+ });
55
89
  }
90
+ return payloads;
56
91
  }
57
92
  /**
58
93
  * Validate plugin options and return an Effect that fails with ConfigValidationError.
@@ -92,7 +127,13 @@ function validateOptions(options, rspressConfig) {
92
127
  reason: `api.versions keys [${[...pluginKeys].join(", ")}] must exactly match multiVersion.versions [${[...rspressKeys].join(", ")}].`
93
128
  });
94
129
  } else {
95
- if (api.versions) yield* Effect.logWarning("api.versions is provided but RSPress multiVersion is not configured. Versions will be ignored.");
130
+ if (api.versions) yield* emit(PluginEvent.ConfigCascadeWarning({
131
+ ctx: { buildId: "" },
132
+ level: "warn",
133
+ field: "versions",
134
+ chosen: "(none — multiVersion not configured)",
135
+ ignored: ["api.versions"]
136
+ }));
96
137
  if (!api.model) return yield* new ConfigValidationError({
97
138
  field: "api.model",
98
139
  reason: "'model' is required when multiVersion is not active."
@@ -105,12 +146,13 @@ function validateOptions(options, rspressConfig) {
105
146
  * Resolves plugin options + RSPress config into a fully prepared build context
106
147
  * with loaded models, type system, and resources.
107
148
  */
108
- function ConfigServiceLive(options, shikiCrossLinker) {
149
+ function ConfigServiceLive(options, shikiCrossLinker, buildId = "", resolvedThresholds) {
109
150
  return Layer.effect(ConfigService, Effect.gen(function* () {
110
151
  const typeRegistry = yield* TypeRegistryService;
111
152
  const pathService = yield* PathDerivationService;
112
153
  return { resolve: (rspressConfig) => Effect.gen(function* () {
113
154
  const loadStart = performance.now();
155
+ const wantTrace = yield* wantsLevel("trace");
114
156
  yield* validateOptions(options, { ...rspressConfig.multiVersion ? { multiVersion: {
115
157
  default: rspressConfig.multiVersion.default,
116
158
  versions: [...rspressConfig.multiVersion.versions]
@@ -130,7 +172,7 @@ function ConfigServiceLive(options, shikiCrossLinker) {
130
172
  /**
131
173
  * Helper to process a single API model (shared by single and multi modes).
132
174
  */
133
- const processSimpleApi = (api, model, outputDir, fullRoute) => Effect.promise(async () => {
175
+ const processSimpleApi = (api, model, outputDir, fullRoute, wantTrace) => Effect.promise(async () => {
134
176
  const { apiPackage, source: loaderSource } = await ApiModelLoader.loadApiModel(model);
135
177
  const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories);
136
178
  const resolvedSource = categoryResolver.resolveSourceConfig(api.source, loaderSource);
@@ -140,11 +182,12 @@ function ConfigServiceLive(options, shikiCrossLinker) {
140
182
  const externalPackages = api.externalPackages || extractAutoDetectedPackages(packageJson, api.autoDetectDependencies);
141
183
  if (externalPackages && externalPackages.length > 0) Effect.runSync(Metric.incrementBy(BuildMetrics.externalPackagesTotal, externalPackages.length));
142
184
  const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
143
- prependImportsToVfs(vfs, apiPackage, api.packageName);
185
+ const vfsPayloads = prependImportsToVfs(vfs, apiPackage, api.packageName, wantTrace);
144
186
  const resolvedOgImage = api.ogImage ?? options.ogImage;
145
187
  const resolvedTheme = normalizeThemeConfig(api.theme);
146
188
  return {
147
189
  vfs,
190
+ vfsPayloads,
148
191
  externalPackages: externalPackages || [],
149
192
  config: {
150
193
  apiPackage,
@@ -164,123 +207,216 @@ function ConfigServiceLive(options, shikiCrossLinker) {
164
207
  }
165
208
  };
166
209
  });
167
- if (options.api) {
168
- const api = options.api;
169
- const baseRoute = yield* pathService.normalizeBaseRoute(api.baseRoute ?? "/");
170
- firstApiTsconfig = api.tsconfig;
171
- firstApiCompilerOptions = api.compilerOptions;
172
- if (rspressMultiVersion && api.versions) {
173
- const versionResults = yield* Effect.forEach(Object.entries(api.versions), ([version, versionValue]) => Effect.gen(function* () {
174
- const versionDp = (yield* pathService.derivePaths({
210
+ yield* withPhase("modelLoad", { buildId }, Effect.gen(function* () {
211
+ if (options.api) {
212
+ const api = options.api;
213
+ const baseRoute = yield* pathService.normalizeBaseRoute(api.baseRoute ?? "/");
214
+ firstApiTsconfig = api.tsconfig;
215
+ firstApiCompilerOptions = api.compilerOptions;
216
+ if (rspressMultiVersion && api.versions) {
217
+ const versionResults = yield* Effect.forEach(Object.entries(api.versions), ([version, versionValue]) => Effect.gen(function* () {
218
+ const versionDp = (yield* pathService.derivePaths({
219
+ mode: "single",
220
+ docsRoot: rspressRoot,
221
+ baseRoute,
222
+ apiFolder: api.apiFolder ?? "api",
223
+ locales: rspressLocales,
224
+ defaultLang: rspressLang,
225
+ versions: [version],
226
+ defaultVersion: rspressMultiVersion?.default
227
+ }))[0];
228
+ if (!versionDp) return {
229
+ vfs: /* @__PURE__ */ new Map(),
230
+ vfsPayloads: [],
231
+ externalPackages: [],
232
+ config: null
233
+ };
234
+ return yield* Effect.promise(async () => {
235
+ const versionConfig = isVersionConfig(versionValue) ? versionValue : { model: versionValue };
236
+ const { apiPackage, packageJson: versionPackageJson, categories: versionCategories, source: versionSource, externalPackages: versionExternalPackages, autoDetectDependencies: versionAutoDetectDependencies, llmsPlugin: versionLlms, ogImage: versionOgImage } = await ApiModelLoader.loadVersionModel(versionConfig);
237
+ Effect.runSync(Metric.increment(BuildMetrics.apiVersionsLoaded));
238
+ const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories, versionCategories);
239
+ const resolvedSource = categoryResolver.resolveSourceConfig(api.source, versionSource);
240
+ const resolvedLlms = mergeLlmsPluginConfig(options.llmsPlugin, api.llmsPlugin, versionLlms);
241
+ const packageJson = versionPackageJson || (api.packageJson ? await ApiModelLoader.loadPackageJson(api.packageJson) : void 0);
242
+ validateExternalPackages(versionExternalPackages || api.externalPackages, packageJson);
243
+ const autoDetectOptions = versionAutoDetectDependencies || api.autoDetectDependencies;
244
+ const externalPackages = versionExternalPackages || api.externalPackages || extractAutoDetectedPackages(packageJson, autoDetectOptions);
245
+ if (externalPackages && externalPackages.length > 0) Effect.runSync(Metric.incrementBy(BuildMetrics.externalPackagesTotal, externalPackages.length));
246
+ const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
247
+ const vfsPayloads = prependImportsToVfs(vfs, apiPackage, api.packageName, wantTrace);
248
+ const resolvedOgImage = versionOgImage ?? api.ogImage ?? options.ogImage;
249
+ const resolvedTheme = normalizeThemeConfig(api.theme);
250
+ const outputDir = versionDp.outputDir;
251
+ const fullRoute = versionDp.routeBase;
252
+ return {
253
+ vfs,
254
+ vfsPayloads,
255
+ externalPackages: externalPackages || [],
256
+ config: {
257
+ apiPackage,
258
+ packageName: `${api.packageName} (${version})`,
259
+ ...api.name != null ? { apiName: api.name } : {},
260
+ outputDir,
261
+ baseRoute: fullRoute,
262
+ categories: resolvedCategories,
263
+ ...resolvedSource != null ? { source: resolvedSource } : {},
264
+ ...packageJson != null ? { packageJson } : {},
265
+ ...resolvedLlms != null ? { llmsPlugin: resolvedLlms } : {},
266
+ ...options.siteUrl != null ? { siteUrl: options.siteUrl } : {},
267
+ ...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
268
+ docsDir: path.dirname(outputDir),
269
+ ...docsRoot != null ? { docsRoot } : {},
270
+ ...resolvedTheme != null ? { theme: resolvedTheme } : {}
271
+ }
272
+ };
273
+ });
274
+ }), { concurrency: "unbounded" });
275
+ for (const result of versionResults) {
276
+ for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
277
+ if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
278
+ if (result.config) apiConfigs.push(result.config);
279
+ for (const payload of result.vfsPayloads) {
280
+ yield* emit(PluginEvent.VfsGenerated({
281
+ ctx: {
282
+ buildId: "",
283
+ packageName: api.packageName,
284
+ ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
285
+ },
286
+ level: "debug",
287
+ file: payload.file,
288
+ declCount: payload.declCount,
289
+ contentHash: payload.contentHash,
290
+ ...wantTrace && payload.content ? { content: payload.content } : {}
291
+ }));
292
+ if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
293
+ ctx: {
294
+ buildId: "",
295
+ packageName: api.packageName,
296
+ ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
297
+ },
298
+ level: "debug",
299
+ file: payload.file,
300
+ imports: wantTrace ? payload.importRefs : []
301
+ }));
302
+ }
303
+ }
304
+ } else if (api.model) {
305
+ const dp = (yield* pathService.derivePaths({
175
306
  mode: "single",
176
307
  docsRoot: rspressRoot,
177
308
  baseRoute,
178
309
  apiFolder: api.apiFolder ?? "api",
179
310
  locales: rspressLocales,
180
311
  defaultLang: rspressLang,
181
- versions: [version],
182
- defaultVersion: rspressMultiVersion?.default
312
+ versions: [],
313
+ defaultVersion: void 0
183
314
  }))[0];
184
- if (!versionDp) return {
185
- vfs: /* @__PURE__ */ new Map(),
186
- externalPackages: [],
187
- config: null
188
- };
189
- return yield* Effect.promise(async () => {
190
- const versionConfig = isVersionConfig(versionValue) ? versionValue : { model: versionValue };
191
- const { apiPackage, packageJson: versionPackageJson, categories: versionCategories, source: versionSource, externalPackages: versionExternalPackages, autoDetectDependencies: versionAutoDetectDependencies, llmsPlugin: versionLlms, ogImage: versionOgImage } = await ApiModelLoader.loadVersionModel(versionConfig);
192
- Effect.runSync(Metric.increment(BuildMetrics.apiVersionsLoaded));
193
- const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories, versionCategories);
194
- const resolvedSource = categoryResolver.resolveSourceConfig(api.source, versionSource);
195
- const resolvedLlms = mergeLlmsPluginConfig(options.llmsPlugin, api.llmsPlugin, versionLlms);
196
- const packageJson = versionPackageJson || (api.packageJson ? await ApiModelLoader.loadPackageJson(api.packageJson) : void 0);
197
- validateExternalPackages(versionExternalPackages || api.externalPackages, packageJson);
198
- const autoDetectOptions = versionAutoDetectDependencies || api.autoDetectDependencies;
199
- const externalPackages = versionExternalPackages || api.externalPackages || extractAutoDetectedPackages(packageJson, autoDetectOptions);
200
- if (externalPackages && externalPackages.length > 0) Effect.runSync(Metric.incrementBy(BuildMetrics.externalPackagesTotal, externalPackages.length));
201
- const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
202
- prependImportsToVfs(vfs, apiPackage, api.packageName);
203
- const resolvedOgImage = versionOgImage ?? api.ogImage ?? options.ogImage;
204
- const resolvedTheme = normalizeThemeConfig(api.theme);
205
- const outputDir = versionDp.outputDir;
206
- const fullRoute = versionDp.routeBase;
207
- return {
208
- vfs,
209
- externalPackages: externalPackages || [],
210
- config: {
211
- apiPackage,
212
- packageName: `${api.packageName} (${version})`,
213
- ...api.name != null ? { apiName: api.name } : {},
214
- outputDir,
215
- baseRoute: fullRoute,
216
- categories: resolvedCategories,
217
- ...resolvedSource != null ? { source: resolvedSource } : {},
218
- ...packageJson != null ? { packageJson } : {},
219
- ...resolvedLlms != null ? { llmsPlugin: resolvedLlms } : {},
220
- ...options.siteUrl != null ? { siteUrl: options.siteUrl } : {},
221
- ...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
222
- docsDir: path.dirname(outputDir),
223
- ...docsRoot != null ? { docsRoot } : {},
224
- ...resolvedTheme != null ? { theme: resolvedTheme } : {}
225
- }
226
- };
227
- });
228
- }), { concurrency: "unbounded" });
229
- for (const result of versionResults) {
230
- for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
231
- if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
232
- if (result.config) apiConfigs.push(result.config);
315
+ if (dp) {
316
+ const result = yield* processSimpleApi(api, api.model, dp.outputDir, dp.routeBase, wantTrace);
317
+ for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
318
+ if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
319
+ apiConfigs.push(result.config);
320
+ for (const payload of result.vfsPayloads) {
321
+ yield* emit(PluginEvent.VfsGenerated({
322
+ ctx: {
323
+ buildId: "",
324
+ packageName: api.packageName,
325
+ ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
326
+ },
327
+ level: "debug",
328
+ file: payload.file,
329
+ declCount: payload.declCount,
330
+ contentHash: payload.contentHash,
331
+ ...wantTrace && payload.content ? { content: payload.content } : {}
332
+ }));
333
+ if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
334
+ ctx: {
335
+ buildId: "",
336
+ packageName: api.packageName,
337
+ ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
338
+ },
339
+ level: "debug",
340
+ file: payload.file,
341
+ imports: wantTrace ? payload.importRefs : []
342
+ }));
343
+ }
344
+ }
345
+ }
346
+ } else if (options.apis) {
347
+ const apisWithTsconfig = options.apis.filter((a) => a.tsconfig);
348
+ if (apisWithTsconfig.length > 0) {
349
+ firstApiTsconfig = apisWithTsconfig[0].tsconfig;
350
+ const uniqueTsconfigs = new Set(apisWithTsconfig.map((a) => String(a.tsconfig)));
351
+ if (uniqueTsconfigs.size > 1) {
352
+ const chosen = String(firstApiTsconfig);
353
+ const ignored = [...uniqueTsconfigs].filter((t) => t !== chosen);
354
+ yield* emit(PluginEvent.ConfigCascadeWarning({
355
+ ctx: { buildId: "" },
356
+ level: "warn",
357
+ field: "tsconfig",
358
+ chosen,
359
+ ignored
360
+ }));
361
+ }
233
362
  }
234
- } else if (api.model) {
235
- const dp = (yield* pathService.derivePaths({
236
- mode: "single",
237
- docsRoot: rspressRoot,
238
- baseRoute,
239
- apiFolder: api.apiFolder ?? "api",
240
- locales: rspressLocales,
241
- defaultLang: rspressLang,
242
- versions: [],
243
- defaultVersion: void 0
244
- }))[0];
245
- if (dp) {
246
- const result = yield* processSimpleApi(api, api.model, dp.outputDir, dp.routeBase);
363
+ const apisWithCompilerOptions = options.apis.filter((a) => a.compilerOptions);
364
+ if (apisWithCompilerOptions.length > 0) firstApiCompilerOptions = apisWithCompilerOptions[0].compilerOptions;
365
+ const multiResults = yield* Effect.forEach(options.apis, (api) => Effect.gen(function* () {
366
+ const apiBaseRoute = yield* pathService.normalizeBaseRoute(api.baseRoute ?? `/${unscopedName(api.packageName)}`);
367
+ const dp = (yield* pathService.derivePaths({
368
+ mode: "multi",
369
+ docsRoot: rspressRoot,
370
+ baseRoute: apiBaseRoute,
371
+ apiFolder: api.apiFolder ?? "api",
372
+ locales: rspressLocales,
373
+ defaultLang: rspressLang,
374
+ versions: [],
375
+ defaultVersion: void 0
376
+ }))[0];
377
+ if (!dp) return [];
378
+ const result = yield* processSimpleApi(api, api.model, dp.outputDir, dp.routeBase, wantTrace);
379
+ for (const payload of result.vfsPayloads) {
380
+ yield* emit(PluginEvent.VfsGenerated({
381
+ ctx: {
382
+ buildId: "",
383
+ packageName: api.packageName,
384
+ ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
385
+ },
386
+ level: "debug",
387
+ file: payload.file,
388
+ declCount: payload.declCount,
389
+ contentHash: payload.contentHash,
390
+ ...wantTrace && payload.content ? { content: payload.content } : {}
391
+ }));
392
+ if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
393
+ ctx: {
394
+ buildId: "",
395
+ packageName: api.packageName,
396
+ ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
397
+ },
398
+ level: "debug",
399
+ file: payload.file,
400
+ imports: wantTrace ? payload.importRefs : []
401
+ }));
402
+ }
403
+ return [result];
404
+ }), { concurrency: "unbounded" });
405
+ for (const results of multiResults) for (const result of results) {
247
406
  for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
248
407
  if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
249
408
  apiConfigs.push(result.config);
250
409
  }
251
410
  }
252
- } else if (options.apis) {
253
- const apisWithTsconfig = options.apis.filter((a) => a.tsconfig);
254
- if (apisWithTsconfig.length > 0) {
255
- firstApiTsconfig = apisWithTsconfig[0].tsconfig;
256
- const uniqueTsconfigs = new Set(apisWithTsconfig.map((a) => String(a.tsconfig)));
257
- if (uniqueTsconfigs.size > 1) yield* Effect.logWarning(`Multiple APIs specify different tsconfig values: ${[...uniqueTsconfigs].join(", ")}. Using '${String(firstApiTsconfig)}' for TypeScript resolution. Per-API tsconfig resolution will be supported in a future release.`);
258
- }
259
- const apisWithCompilerOptions = options.apis.filter((a) => a.compilerOptions);
260
- if (apisWithCompilerOptions.length > 0) firstApiCompilerOptions = apisWithCompilerOptions[0].compilerOptions;
261
- const multiResults = yield* Effect.forEach(options.apis, (api) => Effect.gen(function* () {
262
- const apiBaseRoute = yield* pathService.normalizeBaseRoute(api.baseRoute ?? `/${unscopedName(api.packageName)}`);
263
- const dp = (yield* pathService.derivePaths({
264
- mode: "multi",
265
- docsRoot: rspressRoot,
266
- baseRoute: apiBaseRoute,
267
- apiFolder: api.apiFolder ?? "api",
268
- locales: rspressLocales,
269
- defaultLang: rspressLang,
270
- versions: [],
271
- defaultVersion: void 0
272
- }))[0];
273
- if (!dp) return [];
274
- return [yield* processSimpleApi(api, api.model, dp.outputDir, dp.routeBase)];
275
- }), { concurrency: "unbounded" });
276
- for (const results of multiResults) for (const result of results) {
277
- for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
278
- if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
279
- apiConfigs.push(result.config);
280
- }
281
- }
411
+ }), resolvedThresholds ?? DEFAULT_THRESHOLDS);
282
412
  const loadMs = performance.now() - loadStart;
283
- yield* Effect.logDebug(`Loading API models: ${loadMs.toFixed(0)}ms`);
413
+ yield* emit(PluginEvent.ModelLoaded({
414
+ ctx: { buildId: "" },
415
+ level: "debug",
416
+ entryPoints: apiConfigs.length,
417
+ itemCount: apiConfigs.reduce((sum, cfg) => sum + cfg.apiPackage.entryPoints.reduce((s, ep) => s + ep.members.length, 0), 0),
418
+ durationMs: Math.round(loadMs)
419
+ }));
284
420
  const projectRoot = process.cwd();
285
421
  let globalTsConfig;
286
422
  if (firstApiTsconfig || firstApiCompilerOptions) {
@@ -289,28 +425,50 @@ function ConfigServiceLive(options, shikiCrossLinker) {
289
425
  if (firstApiCompilerOptions != null) globalTsConfig.compilerOptions = firstApiCompilerOptions;
290
426
  }
291
427
  const resolvedCompilerOptions = yield* Effect.promise(() => resolveTypeScriptConfig(projectRoot, globalTsConfig));
292
- yield* Effect.logDebug(`Resolved TypeScript config: target=${resolvedCompilerOptions.target}, module=${resolvedCompilerOptions.module}, lib=[${resolvedCompilerOptions.lib?.join(", ")}]`);
293
- let tsEnvCache = /* @__PURE__ */ new Map();
428
+ yield* emit(PluginEvent.TsCacheCreated({
429
+ ctx: { buildId: "" },
430
+ level: "debug",
431
+ compilerOptions: `target=${resolvedCompilerOptions.target}, module=${resolvedCompilerOptions.module}, lib=[${resolvedCompilerOptions.lib?.join(", ") ?? ""}]`,
432
+ durationMs: 0
433
+ }));
434
+ const documentedPackageNames = new Set(apiConfigs.map((config) => config.packageName));
435
+ const externalPackagesToLoad = allExternalPackages.filter((pkg) => !documentedPackageNames.has(pkg.name));
294
436
  const typeLoadResult = yield* Effect.either(Effect.gen(function* () {
295
- if (allExternalPackages.length > 0) {
296
- const typesStart = performance.now();
297
- const result = yield* typeRegistry.loadPackages(allExternalPackages);
298
- const cache = yield* typeRegistry.createTypeScriptCache(allExternalPackages, resolvedCompilerOptions);
299
- for (const [filePath, content] of result.vfs.entries()) combinedVfs.set(filePath, content);
300
- yield* Effect.logDebug(`Loading external package types: ${(performance.now() - typesStart).toFixed(0)}ms`);
301
- return cache;
437
+ if (externalPackagesToLoad.length > 0) {
438
+ const resolvedPackages = yield* typeRegistry.resolveVersions(externalPackagesToLoad);
439
+ const droppedCount = externalPackagesToLoad.length - resolvedPackages.length;
440
+ if (droppedCount > 0) yield* emit(PluginEvent.ExternalPackageSkipped({
441
+ ctx: { buildId: "" },
442
+ level: "debug",
443
+ reason: `${droppedCount} unresolvable package(s) (unpublished or workspace-only)`
444
+ }));
445
+ if (resolvedPackages.length > 0) {
446
+ const result = yield* typeRegistry.loadPackages(resolvedPackages);
447
+ for (const [filePath, content] of result.vfs.entries()) combinedVfs.set(filePath, content);
448
+ yield* emit(PluginEvent.VfsMerged({
449
+ ctx: { buildId: "" },
450
+ level: "debug",
451
+ totalFiles: result.vfs.size,
452
+ packages: resolvedPackages.map((p) => p.name)
453
+ }));
454
+ }
302
455
  }
303
- return yield* typeRegistry.createTypeScriptCache([], resolvedCompilerOptions);
304
456
  }));
305
- if (typeLoadResult._tag === "Right") tsEnvCache = typeLoadResult.right;
306
- else {
307
- yield* Effect.logWarning(`Failed to load external types: ${typeLoadResult.left.message}. Continuing with empty VFS.`);
308
- const fallbackCache = yield* Effect.either(typeRegistry.createTypeScriptCache([], resolvedCompilerOptions));
309
- if (fallbackCache._tag === "Right") tsEnvCache = fallbackCache.right;
310
- }
457
+ if (typeLoadResult._tag === "Left") yield* emit(PluginEvent.ConfigCascadeWarning({
458
+ ctx: { buildId: "" },
459
+ level: "warn",
460
+ field: "externalTypes",
461
+ chosen: "empty VFS",
462
+ ignored: [typeLoadResult.left.message ?? String(typeLoadResult.left)]
463
+ }));
311
464
  const twoslashStartMs = performance.now();
312
- TwoslashManager.getInstance().initialize(combinedVfs, void 0, void 0, tsEnvCache, resolvedCompilerOptions);
313
- yield* Effect.logDebug(`Initializing Twoslash: ${(performance.now() - twoslashStartMs).toFixed(0)}ms`);
465
+ TwoslashManager.getInstance().initialize(combinedVfs, void 0, void 0, void 0, resolvedCompilerOptions);
466
+ yield* emit(PluginEvent.TwoslashInitialized({
467
+ ctx: { buildId: "" },
468
+ level: "debug",
469
+ durationMs: Math.round(performance.now() - twoslashStartMs),
470
+ vfsFileCount: combinedVfs.size
471
+ }));
314
472
  const shikiStartMs = performance.now();
315
473
  const themeSet = /* @__PURE__ */ new Set();
316
474
  const customThemes = [];
@@ -338,7 +496,12 @@ function ConfigServiceLive(options, shikiCrossLinker) {
338
496
  themes,
339
497
  langs
340
498
  }));
341
- yield* Effect.logDebug(`Initializing Shiki highlighter: ${(performance.now() - shikiStartMs).toFixed(0)}ms`);
499
+ yield* emit(PluginEvent.PhaseCompleted({
500
+ ctx: { buildId: "" },
501
+ level: "debug",
502
+ phase: "shikiInit",
503
+ durationMs: Math.round(performance.now() - shikiStartMs)
504
+ }));
342
505
  const ogResolver = options.siteUrl ? new OpenGraphResolver({
343
506
  siteUrl: options.siteUrl,
344
507
  ...docsRoot != null ? { docsRoot } : {}
@@ -352,7 +515,6 @@ function ConfigServiceLive(options, shikiCrossLinker) {
352
515
  apiConfigs,
353
516
  combinedVfs,
354
517
  highlighter,
355
- tsEnvCache,
356
518
  resolvedCompilerOptions,
357
519
  ogResolver,
358
520
  shikiCrossLinker,
@@ -361,7 +523,9 @@ function ConfigServiceLive(options, shikiCrossLinker) {
361
523
  twoslashTransformer,
362
524
  pageConcurrency: os.cpus().length,
363
525
  logLevel: logLevel === "none" ? "info" : logLevel,
364
- suppressExampleErrors
526
+ suppressExampleErrors,
527
+ thresholds: resolvedThresholds ?? DEFAULT_THRESHOLDS,
528
+ buildId
365
529
  };
366
530
  }) };
367
531
  }));