rspress-plugin-api-extractor 0.9.2 → 0.11.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 (52) hide show
  1. package/BuildEnv.js +58 -0
  2. package/README.md +2 -1
  3. package/build-program.js +33 -30
  4. package/build-stages.js +47 -39
  5. package/errors.js +0 -1
  6. package/index.d.ts +22 -14
  7. package/layers/ConfigServiceLive.js +349 -400
  8. package/layers/HighlighterServiceLive.js +52 -0
  9. package/layers/ObservabilityLive.js +26 -7
  10. package/layers/OgServiceLive.js +134 -0
  11. package/layers/TwoslashCacheServiceLive.js +108 -0
  12. package/layers/TwoslashEnvironmentsLive.js +33 -0
  13. package/layers/TypeRegistryServiceLive.js +54 -47
  14. package/layers/build-metrics.js +32 -5
  15. package/layers/xdg.js +44 -0
  16. package/markdown/helpers.js +9 -55
  17. package/markdown/page-generators/class-page.js +8 -31
  18. package/markdown/page-generators/index-pages.js +6 -8
  19. package/markdown/page-generators/interface-page.js +7 -7
  20. package/markdown/shiki-utils.js +65 -10
  21. package/observability/EventBus.js +29 -9
  22. package/observability/heartbeat.js +1 -1
  23. package/observability/metric-report.js +124 -0
  24. package/observability/sinks/console-sink.js +6 -0
  25. package/observability/sinks/metrics-sink.js +64 -21
  26. package/observability/sinks/render-sink.js +86 -0
  27. package/observability/sinks/trace-sink.js +10 -17
  28. package/observability/spans.js +4 -2
  29. package/observability/sync-emitter.js +78 -0
  30. package/og-resolver.js +46 -287
  31. package/package.json +4 -5
  32. package/path-derivation.js +19 -1
  33. package/plugin.js +64 -52
  34. package/prettier-formatter.js +4 -10
  35. package/remark-api-codeblocks.js +33 -15
  36. package/remark-with-api.js +24 -27
  37. package/schemas/config.js +11 -7
  38. package/services/HighlighterService.js +30 -0
  39. package/services/OgService.js +23 -0
  40. package/services/PluginConfig.js +26 -0
  41. package/services/TwoslashCacheService.js +15 -0
  42. package/services/TwoslashEnvironments.js +7 -0
  43. package/shiki-transformer.js +55 -256
  44. package/twoslash-access.js +48 -0
  45. package/twoslash-cache.js +174 -0
  46. package/twoslash-patterns.js +1 -1
  47. package/twoslash-timing-wrapper.js +23 -0
  48. package/twoslash-transformer.js +153 -89
  49. package/vfs-registry.js +1 -31
  50. package/layers/PathDerivationServiceLive.js +0 -16
  51. package/runtime/components/MarkdownText/index.js +0 -34
  52. package/services/PathDerivationService.js +0 -7
@@ -1,57 +1,32 @@
1
+ import { BuildId } from "../BuildEnv.js";
1
2
  import { PluginEvent } from "../observability/events.js";
2
3
  import { emit, wantsLevel } from "../observability/EventBus.js";
3
4
  import { BuildMetrics } from "./build-metrics.js";
4
5
  import "./ObservabilityLive.js";
5
6
  import { TypeReferenceExtractor } from "../type-reference-extractor.js";
6
- import { OpenGraphResolver } from "../og-resolver.js";
7
7
  import { withPhase } from "../observability/spans.js";
8
+ import { TwoslashEnvironments } from "../services/TwoslashEnvironments.js";
8
9
  import { resolveTypeScriptConfig } from "../typescript-config.js";
9
- import { TwoslashManager } from "../twoslash-transformer.js";
10
+ import { apiScopeOf, deriveOutputPaths, normalizeBaseRoute, unscopedName } from "../path-derivation.js";
10
11
  import { classifyApiConfig, extractAutoDetectedPackages, isVersionConfig, mergeLlmsPluginConfig, validateExternalPackages } from "../config-utils.js";
11
12
  import { ApiExtractedPackage } from "../api-extracted-package.js";
12
13
  import { CategoryResolver } from "../category-resolver.js";
13
14
  import { ConfigValidationError } from "../errors.js";
14
- import { HideCutLinesTransformer, MemberFormatTransformer } from "../hide-cut-transformer.js";
15
- import { DEFAULT_SHIKI_THEMES } from "../markdown/shiki-utils.js";
15
+ import { normalizeThemeConfig } from "../markdown/shiki-utils.js";
16
16
  import { loadApiModel, loadPackageJson, loadVersionModel } from "../model-loader.js";
17
17
  import { DEFAULT_CATEGORIES } from "../schemas/config.js";
18
18
  import "../schemas/index.js";
19
19
  import { ConfigService } from "../services/ConfigService.js";
20
- import { PathDerivationService } from "../services/PathDerivationService.js";
20
+ import { PluginConfig } from "../services/PluginConfig.js";
21
+ import { TwoslashCacheService } from "../services/TwoslashCacheService.js";
21
22
  import { TypeRegistryService } from "../services/TypeRegistryService.js";
23
+ import { twoslashEnvHash } from "../twoslash-cache.js";
22
24
  import path from "node:path";
23
25
  import { hashContent } from "@tsdoctor/snapshot";
24
26
  import { Effect, Layer, Metric } from "effect";
25
- import os from "node:os";
26
- import { createHighlighter } from "shiki";
27
+ import ts from "typescript";
27
28
 
28
29
  //#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
- };
37
- /**
38
- * Normalize theme configuration from user input to a consistent format.
39
- */
40
- function normalizeThemeConfig(theme) {
41
- if (!theme) return { ...DEFAULT_SHIKI_THEMES };
42
- if (typeof theme === "string") return {
43
- light: theme,
44
- dark: theme
45
- };
46
- if ("light" in theme && "dark" in theme && typeof theme.light === "string" && typeof theme.dark === "string") return {
47
- light: theme.light,
48
- dark: theme.dark
49
- };
50
- return {
51
- light: theme,
52
- dark: theme
53
- };
54
- }
55
30
  /**
56
31
  * Prepend import statements for external type references to the VFS declaration files.
57
32
  * Returns per-entry payloads for event emission (heavy content/importRefs gated on wantTrace).
@@ -129,7 +104,7 @@ function validateOptions(options, rspressConfig) {
129
104
  });
130
105
  } else {
131
106
  if (api.versions) yield* emit(PluginEvent.ConfigCascadeWarning({
132
- ctx: { buildId: "" },
107
+ ctx: {},
133
108
  level: "warn",
134
109
  field: "versions",
135
110
  chosen: "(none — multiVersion not configured)",
@@ -144,258 +119,210 @@ function validateOptions(options, rspressConfig) {
144
119
  });
145
120
  }
146
121
  /**
147
- * Create ConfigServiceLive from plugin options.
148
- * Resolves plugin options + RSPress config into a fully prepared build context
149
- * with loaded models, type system, and resources.
122
+ * Resolve plugin options + RSPress config into the API configs the pipeline
123
+ * runs over.
124
+ *
125
+ * @remarks
126
+ * A module-level `const`, not a factory. It took the plugin options as an
127
+ * argument and was called inline at the merge site, which made it a
128
+ * layer-returning function: layers memoize by reference, so a second call
129
+ * would build a second `ConfigService` with its own captured `TypeRegistry`.
130
+ * The options come from {@link PluginConfig} now, so there is nothing to pass
131
+ * and nothing to call twice.
150
132
  */
151
- function ConfigServiceLive(options, shikiCrossLinker, buildId = "", resolvedThresholds) {
152
- return Layer.effect(ConfigService, Effect.gen(function* () {
153
- const typeRegistry = yield* TypeRegistryService;
154
- const pathService = yield* PathDerivationService;
155
- return { resolve: (rspressConfig) => Effect.gen(function* () {
156
- const loadStart = performance.now();
157
- const wantTrace = yield* wantsLevel("trace");
158
- yield* validateOptions(options, { ...rspressConfig.multiVersion ? { multiVersion: {
159
- default: rspressConfig.multiVersion.default,
160
- versions: [...rspressConfig.multiVersion.versions]
161
- } } : {} });
162
- const rspressMultiVersion = rspressConfig.multiVersion;
163
- const rspressLocales = rspressConfig.locales?.map((l) => l.lang) ?? [];
164
- const rspressLang = rspressConfig.lang;
165
- const docsRoot = rspressConfig.root;
166
- const rspressRoot = docsRoot || process.cwd();
167
- const categoryResolver = new CategoryResolver();
168
- const pluginDefaults = categoryResolver.mergeCategories(DEFAULT_CATEGORIES, options.defaultCategories);
169
- const apiConfigs = [];
170
- const combinedVfs = /* @__PURE__ */ new Map();
171
- const allExternalPackages = [];
172
- let firstApiTsconfig;
173
- let firstApiCompilerOptions;
174
- /**
175
- * Emit a typed ModelLoadFailed event for a failed model load, then
176
- * convert the typed failure to a defect a missing or unparsable
177
- * model remains fatal to the build, exactly as before, but the
178
- * event now rides the error channel instead of a sync-island seam.
179
- */
180
- const withModelLoadEvents = (self) => self.pipe(Effect.tapError((error) => emit(PluginEvent.ModelLoadFailed({
181
- ctx: { buildId },
182
- level: "error",
183
- modelPath: "modelPath" in error ? error.modelPath : "<loader function>",
184
- reason: error.message
185
- }))), Effect.orDie);
186
- /**
187
- * Helper to process a single API model (shared by single and multi modes).
188
- */
189
- const processSimpleApi = (api, model, outputDir, fullRoute, wantTrace) => Effect.gen(function* () {
190
- const { apiPackage, source: loaderSource } = yield* withModelLoadEvents(loadApiModel(model));
191
- return yield* Effect.promise(async () => {
192
- const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories);
193
- const resolvedSource = categoryResolver.resolveSourceConfig(api.source, loaderSource);
194
- const resolvedLlms = mergeLlmsPluginConfig(options.llmsPlugin, api.llmsPlugin);
195
- const packageJson = api.packageJson ? await loadPackageJson(api.packageJson) : void 0;
196
- validateExternalPackages(api.externalPackages, packageJson);
197
- const externalPackages = api.externalPackages || extractAutoDetectedPackages(packageJson, api.autoDetectDependencies);
198
- if (externalPackages && externalPackages.length > 0) Effect.runSync(Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length));
199
- const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
200
- const vfsPayloads = prependImportsToVfs(vfs, apiPackage, api.packageName, wantTrace);
201
- const resolvedOgImage = api.ogImage ?? options.ogImage;
202
- const resolvedTheme = normalizeThemeConfig(api.theme);
203
- return {
204
- vfs,
205
- vfsPayloads,
206
- externalPackages: externalPackages || [],
207
- config: {
208
- apiPackage,
209
- packageName: api.packageName,
210
- ...api.name != null ? { apiName: api.name } : {},
211
- outputDir,
212
- baseRoute: fullRoute,
213
- categories: resolvedCategories,
214
- ...resolvedSource != null ? { source: resolvedSource } : {},
215
- ...packageJson != null ? { packageJson } : {},
216
- ...resolvedLlms != null ? { llmsPlugin: resolvedLlms } : {},
217
- ...options.siteUrl != null ? { siteUrl: options.siteUrl } : {},
218
- ...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
219
- docsDir: path.dirname(outputDir),
220
- ...docsRoot != null ? { docsRoot } : {},
221
- ...resolvedTheme != null ? { theme: resolvedTheme } : {}
222
- }
223
- };
224
- });
133
+ const ConfigServiceLive = Layer.effect(ConfigService, Effect.gen(function* () {
134
+ const typeRegistry = yield* TypeRegistryService;
135
+ const options = yield* PluginConfig;
136
+ return { resolve: (rspressConfig) => Effect.gen(function* () {
137
+ const buildId = yield* BuildId;
138
+ const loadStart = performance.now();
139
+ const wantTrace = yield* wantsLevel("trace");
140
+ yield* validateOptions(options, { ...rspressConfig.multiVersion ? { multiVersion: {
141
+ default: rspressConfig.multiVersion.default,
142
+ versions: [...rspressConfig.multiVersion.versions]
143
+ } } : {} });
144
+ const rspressMultiVersion = rspressConfig.multiVersion;
145
+ const rspressLocales = rspressConfig.locales?.map((l) => l.lang) ?? [];
146
+ const rspressLang = rspressConfig.lang;
147
+ const docsRoot = rspressConfig.root;
148
+ const rspressRoot = docsRoot || process.cwd();
149
+ const categoryResolver = new CategoryResolver();
150
+ const pluginDefaults = categoryResolver.mergeCategories(DEFAULT_CATEGORIES, options.defaultCategories);
151
+ const apiConfigs = [];
152
+ const combinedVfs = /* @__PURE__ */ new Map();
153
+ const allExternalPackages = [];
154
+ let firstApiTsconfig;
155
+ let firstApiCompilerOptions;
156
+ /**
157
+ * Raw TypeScript config per API scope. Each documented package is
158
+ * type-checked under its OWN configuration; the build no longer picks
159
+ * one and applies it to everything.
160
+ */
161
+ const scopeTsConfigs = /* @__PURE__ */ new Map();
162
+ /**
163
+ * Emit a typed ModelLoadFailed event for a failed model load, then
164
+ * convert the typed failure to a defect — a missing or unparsable
165
+ * model remains fatal to the build, exactly as before, but the
166
+ * event now rides the error channel instead of a sync-island seam.
167
+ */
168
+ const withModelLoadEvents = (self) => self.pipe(Effect.tapError((error) => emit(PluginEvent.ModelLoadFailed({
169
+ ctx: { buildId },
170
+ level: "error",
171
+ modelPath: "modelPath" in error ? error.modelPath : "<loader function>",
172
+ reason: error.message
173
+ }))), Effect.orDie);
174
+ /**
175
+ * Helper to process a single API model (shared by single and multi modes).
176
+ */
177
+ const processSimpleApi = (api, model, outputDir, fullRoute, wantTrace) => Effect.gen(function* () {
178
+ const { apiPackage, source: loaderSource } = yield* withModelLoadEvents(loadApiModel(model));
179
+ return yield* Effect.promise(async () => {
180
+ const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories);
181
+ const resolvedSource = categoryResolver.resolveSourceConfig(api.source, loaderSource);
182
+ const resolvedLlms = mergeLlmsPluginConfig(options.llmsPlugin, api.llmsPlugin);
183
+ const packageJson = api.packageJson ? await loadPackageJson(api.packageJson) : void 0;
184
+ validateExternalPackages(api.externalPackages, packageJson);
185
+ const externalPackages = api.externalPackages || extractAutoDetectedPackages(packageJson, api.autoDetectDependencies);
186
+ if (externalPackages && externalPackages.length > 0) Effect.runSync(Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length));
187
+ const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
188
+ const vfsPayloads = prependImportsToVfs(vfs, apiPackage, api.packageName, wantTrace);
189
+ const resolvedOgImage = api.ogImage ?? options.ogImage;
190
+ const resolvedTheme = normalizeThemeConfig(api.theme);
191
+ return {
192
+ vfs,
193
+ vfsPayloads,
194
+ externalPackages: externalPackages || [],
195
+ config: {
196
+ apiPackage,
197
+ packageName: api.packageName,
198
+ ...api.name != null ? { apiName: api.name } : {},
199
+ outputDir,
200
+ baseRoute: fullRoute,
201
+ categories: resolvedCategories,
202
+ ...resolvedSource != null ? { source: resolvedSource } : {},
203
+ ...packageJson != null ? { packageJson } : {},
204
+ ...resolvedLlms != null ? { llmsPlugin: resolvedLlms } : {},
205
+ ...options.siteUrl != null ? { siteUrl: options.siteUrl } : {},
206
+ ...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
207
+ docsDir: path.dirname(outputDir),
208
+ ...docsRoot != null ? { docsRoot } : {},
209
+ ...resolvedTheme != null ? { theme: resolvedTheme } : {}
210
+ }
211
+ };
225
212
  });
226
- yield* withPhase("modelLoad", { buildId }, Effect.gen(function* () {
227
- if (options.api) {
228
- const api = options.api;
229
- const baseRoute = yield* pathService.normalizeBaseRoute(api.baseRoute ?? "/");
230
- firstApiTsconfig = api.tsconfig;
231
- firstApiCompilerOptions = api.compilerOptions;
232
- if (rspressMultiVersion && api.versions) {
233
- const versionResults = yield* Effect.forEach(Object.entries(api.versions), ([version, versionValue]) => Effect.gen(function* () {
234
- const versionDp = (yield* pathService.derivePaths({
235
- mode: "single",
236
- docsRoot: rspressRoot,
237
- baseRoute,
238
- apiFolder: api.apiFolder ?? "api",
239
- locales: rspressLocales,
240
- defaultLang: rspressLang,
241
- versions: [version],
242
- defaultVersion: rspressMultiVersion?.default
243
- }))[0];
244
- if (!versionDp) return {
245
- vfs: /* @__PURE__ */ new Map(),
246
- vfsPayloads: [],
247
- externalPackages: [],
248
- config: null
249
- };
250
- const versionConfig = isVersionConfig(versionValue) ? versionValue : { model: versionValue };
251
- const { apiPackage, packageJson: versionPackageJson, categories: versionCategories, source: versionSource, externalPackages: versionExternalPackages, autoDetectDependencies: versionAutoDetectDependencies, llmsPlugin: versionLlms, ogImage: versionOgImage } = yield* withModelLoadEvents(loadVersionModel(versionConfig));
252
- return yield* Effect.promise(async () => {
253
- Effect.runSync(Metric.update(BuildMetrics.apiVersionsLoaded, 1));
254
- const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories, versionCategories);
255
- const resolvedSource = categoryResolver.resolveSourceConfig(api.source, versionSource);
256
- const resolvedLlms = mergeLlmsPluginConfig(options.llmsPlugin, api.llmsPlugin, versionLlms);
257
- const packageJson = versionPackageJson || (api.packageJson ? await loadPackageJson(api.packageJson) : void 0);
258
- validateExternalPackages(versionExternalPackages || api.externalPackages, packageJson);
259
- const autoDetectOptions = versionAutoDetectDependencies || api.autoDetectDependencies;
260
- const externalPackages = versionExternalPackages || api.externalPackages || extractAutoDetectedPackages(packageJson, autoDetectOptions);
261
- if (externalPackages && externalPackages.length > 0) Effect.runSync(Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length));
262
- const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
263
- const vfsPayloads = prependImportsToVfs(vfs, apiPackage, api.packageName, wantTrace);
264
- const resolvedOgImage = versionOgImage ?? api.ogImage ?? options.ogImage;
265
- const resolvedTheme = normalizeThemeConfig(api.theme);
266
- const outputDir = versionDp.outputDir;
267
- const fullRoute = versionDp.routeBase;
268
- return {
269
- vfs,
270
- vfsPayloads,
271
- externalPackages: externalPackages || [],
272
- config: {
273
- apiPackage,
274
- packageName: `${api.packageName} (${version})`,
275
- ...api.name != null ? { apiName: api.name } : {},
276
- outputDir,
277
- baseRoute: fullRoute,
278
- categories: resolvedCategories,
279
- ...resolvedSource != null ? { source: resolvedSource } : {},
280
- ...packageJson != null ? { packageJson } : {},
281
- ...resolvedLlms != null ? { llmsPlugin: resolvedLlms } : {},
282
- ...options.siteUrl != null ? { siteUrl: options.siteUrl } : {},
283
- ...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
284
- docsDir: path.dirname(outputDir),
285
- ...docsRoot != null ? { docsRoot } : {},
286
- ...resolvedTheme != null ? { theme: resolvedTheme } : {}
287
- }
288
- };
289
- });
290
- }), { concurrency: "unbounded" });
291
- for (const result of versionResults) {
292
- for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
293
- if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
294
- if (result.config) apiConfigs.push(result.config);
295
- for (const payload of result.vfsPayloads) {
296
- yield* emit(PluginEvent.VfsGenerated({
297
- ctx: {
298
- buildId: "",
299
- packageName: api.packageName,
300
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
301
- },
302
- level: "debug",
303
- file: payload.file,
304
- declCount: payload.declCount,
305
- contentHash: payload.contentHash,
306
- ...wantTrace && payload.content ? { content: payload.content } : {}
307
- }));
308
- if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
309
- ctx: {
310
- buildId: "",
311
- packageName: api.packageName,
312
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
313
- },
314
- level: "debug",
315
- file: payload.file,
316
- imports: wantTrace ? payload.importRefs : []
317
- }));
318
- }
319
- }
320
- } else if (api.model) {
321
- const dp = (yield* pathService.derivePaths({
213
+ });
214
+ yield* withPhase("modelLoad", { buildId }, Effect.gen(function* () {
215
+ if (options.api) {
216
+ const api = options.api;
217
+ const baseRoute = normalizeBaseRoute(api.baseRoute ?? "/");
218
+ firstApiTsconfig = api.tsconfig;
219
+ firstApiCompilerOptions = api.compilerOptions;
220
+ scopeTsConfigs.set(apiScopeOf(baseRoute, api.packageName), rawTsConfig(api));
221
+ if (rspressMultiVersion && api.versions) {
222
+ const versionResults = yield* Effect.forEach(Object.entries(api.versions), ([version, versionValue]) => Effect.gen(function* () {
223
+ const versionDp = deriveOutputPaths({
322
224
  mode: "single",
323
225
  docsRoot: rspressRoot,
324
226
  baseRoute,
325
227
  apiFolder: api.apiFolder ?? "api",
326
228
  locales: rspressLocales,
327
229
  defaultLang: rspressLang,
328
- versions: [],
329
- defaultVersion: void 0
330
- }))[0];
331
- if (dp) {
332
- const result = yield* processSimpleApi(api, api.model, dp.outputDir, dp.routeBase, wantTrace);
333
- for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
334
- if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
335
- apiConfigs.push(result.config);
336
- for (const payload of result.vfsPayloads) {
337
- yield* emit(PluginEvent.VfsGenerated({
338
- ctx: {
339
- buildId: "",
340
- packageName: api.packageName,
341
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
342
- },
343
- level: "debug",
344
- file: payload.file,
345
- declCount: payload.declCount,
346
- contentHash: payload.contentHash,
347
- ...wantTrace && payload.content ? { content: payload.content } : {}
348
- }));
349
- if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
350
- ctx: {
351
- buildId: "",
352
- packageName: api.packageName,
353
- ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
354
- },
355
- level: "debug",
356
- file: payload.file,
357
- imports: wantTrace ? payload.importRefs : []
358
- }));
359
- }
360
- }
361
- }
362
- } else if (options.apis) {
363
- const apisWithTsconfig = options.apis.filter((a) => a.tsconfig);
364
- if (apisWithTsconfig.length > 0) {
365
- firstApiTsconfig = apisWithTsconfig[0].tsconfig;
366
- const uniqueTsconfigs = new Set(apisWithTsconfig.map((a) => String(a.tsconfig)));
367
- if (uniqueTsconfigs.size > 1) {
368
- const chosen = String(firstApiTsconfig);
369
- const ignored = [...uniqueTsconfigs].filter((t) => t !== chosen);
370
- yield* emit(PluginEvent.ConfigCascadeWarning({
371
- ctx: { buildId: "" },
372
- level: "warn",
373
- field: "tsconfig",
374
- chosen,
375
- ignored
230
+ versions: [version],
231
+ defaultVersion: rspressMultiVersion?.default
232
+ })[0];
233
+ if (!versionDp) return {
234
+ vfs: /* @__PURE__ */ new Map(),
235
+ vfsPayloads: [],
236
+ externalPackages: [],
237
+ config: null
238
+ };
239
+ const versionConfig = isVersionConfig(versionValue) ? versionValue : { model: versionValue };
240
+ const { apiPackage, packageJson: versionPackageJson, categories: versionCategories, source: versionSource, externalPackages: versionExternalPackages, autoDetectDependencies: versionAutoDetectDependencies, llmsPlugin: versionLlms, ogImage: versionOgImage } = yield* withModelLoadEvents(loadVersionModel(versionConfig));
241
+ return yield* Effect.promise(async () => {
242
+ Effect.runSync(Metric.update(BuildMetrics.apiVersionsLoaded, 1));
243
+ const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories, versionCategories);
244
+ const resolvedSource = categoryResolver.resolveSourceConfig(api.source, versionSource);
245
+ const resolvedLlms = mergeLlmsPluginConfig(options.llmsPlugin, api.llmsPlugin, versionLlms);
246
+ const packageJson = versionPackageJson || (api.packageJson ? await loadPackageJson(api.packageJson) : void 0);
247
+ validateExternalPackages(versionExternalPackages || api.externalPackages, packageJson);
248
+ const autoDetectOptions = versionAutoDetectDependencies || api.autoDetectDependencies;
249
+ const externalPackages = versionExternalPackages || api.externalPackages || extractAutoDetectedPackages(packageJson, autoDetectOptions);
250
+ if (externalPackages && externalPackages.length > 0) Effect.runSync(Metric.update(BuildMetrics.externalPackagesTotal, externalPackages.length));
251
+ const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
252
+ const vfsPayloads = prependImportsToVfs(vfs, apiPackage, api.packageName, wantTrace);
253
+ const resolvedOgImage = versionOgImage ?? api.ogImage ?? options.ogImage;
254
+ const resolvedTheme = normalizeThemeConfig(api.theme);
255
+ const outputDir = versionDp.outputDir;
256
+ const fullRoute = versionDp.routeBase;
257
+ return {
258
+ vfs,
259
+ vfsPayloads,
260
+ externalPackages: externalPackages || [],
261
+ config: {
262
+ apiPackage,
263
+ packageName: `${api.packageName} (${version})`,
264
+ ...api.name != null ? { apiName: api.name } : {},
265
+ outputDir,
266
+ baseRoute: fullRoute,
267
+ categories: resolvedCategories,
268
+ ...resolvedSource != null ? { source: resolvedSource } : {},
269
+ ...packageJson != null ? { packageJson } : {},
270
+ ...resolvedLlms != null ? { llmsPlugin: resolvedLlms } : {},
271
+ ...options.siteUrl != null ? { siteUrl: options.siteUrl } : {},
272
+ ...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
273
+ docsDir: path.dirname(outputDir),
274
+ ...docsRoot != null ? { docsRoot } : {},
275
+ ...resolvedTheme != null ? { theme: resolvedTheme } : {}
276
+ }
277
+ };
278
+ });
279
+ }), { concurrency: "unbounded" });
280
+ for (const result of versionResults) {
281
+ for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
282
+ if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
283
+ if (result.config) apiConfigs.push(result.config);
284
+ for (const payload of result.vfsPayloads) {
285
+ yield* emit(PluginEvent.VfsGenerated({
286
+ ctx: {
287
+ packageName: api.packageName,
288
+ ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
289
+ },
290
+ level: "debug",
291
+ file: payload.file,
292
+ declCount: payload.declCount,
293
+ contentHash: payload.contentHash,
294
+ ...wantTrace && payload.content ? { content: payload.content } : {}
295
+ }));
296
+ if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
297
+ ctx: {
298
+ packageName: api.packageName,
299
+ ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
300
+ },
301
+ level: "debug",
302
+ file: payload.file,
303
+ imports: wantTrace ? payload.importRefs : []
376
304
  }));
377
305
  }
378
306
  }
379
- const apisWithCompilerOptions = options.apis.filter((a) => a.compilerOptions);
380
- if (apisWithCompilerOptions.length > 0) firstApiCompilerOptions = apisWithCompilerOptions[0].compilerOptions;
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 [];
307
+ } else if (api.model) {
308
+ const dp = deriveOutputPaths({
309
+ mode: "single",
310
+ docsRoot: rspressRoot,
311
+ baseRoute,
312
+ apiFolder: api.apiFolder ?? "api",
313
+ locales: rspressLocales,
314
+ defaultLang: rspressLang,
315
+ versions: [],
316
+ defaultVersion: void 0
317
+ })[0];
318
+ if (dp) {
394
319
  const result = yield* processSimpleApi(api, api.model, dp.outputDir, dp.routeBase, wantTrace);
320
+ for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
321
+ if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
322
+ apiConfigs.push(result.config);
395
323
  for (const payload of result.vfsPayloads) {
396
324
  yield* emit(PluginEvent.VfsGenerated({
397
325
  ctx: {
398
- buildId: "",
399
326
  packageName: api.packageName,
400
327
  ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
401
328
  },
@@ -407,7 +334,6 @@ function ConfigServiceLive(options, shikiCrossLinker, buildId = "", resolvedThre
407
334
  }));
408
335
  if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
409
336
  ctx: {
410
- buildId: "",
411
337
  packageName: api.packageName,
412
338
  ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
413
339
  },
@@ -416,141 +342,164 @@ function ConfigServiceLive(options, shikiCrossLinker, buildId = "", resolvedThre
416
342
  imports: wantTrace ? payload.importRefs : []
417
343
  }));
418
344
  }
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
345
  }
426
346
  }
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: "" },
347
+ } else if (options.apis) {
348
+ const apisWithTsconfig = options.apis.filter((a) => a.tsconfig);
349
+ if (apisWithTsconfig.length > 0) firstApiTsconfig = apisWithTsconfig[0].tsconfig;
350
+ const apisWithCompilerOptions = options.apis.filter((a) => a.compilerOptions);
351
+ if (apisWithCompilerOptions.length > 0) firstApiCompilerOptions = apisWithCompilerOptions[0].compilerOptions;
352
+ for (const a of options.apis) {
353
+ const scopeRoute = normalizeBaseRoute(a.baseRoute ?? `/${unscopedName(a.packageName)}`);
354
+ scopeTsConfigs.set(apiScopeOf(scopeRoute, a.packageName), rawTsConfig(a));
355
+ }
356
+ const multiResults = yield* Effect.forEach(options.apis, (api) => Effect.gen(function* () {
357
+ const apiBaseRoute = normalizeBaseRoute(api.baseRoute ?? `/${unscopedName(api.packageName)}`);
358
+ const dp = deriveOutputPaths({
359
+ mode: "multi",
360
+ docsRoot: rspressRoot,
361
+ baseRoute: apiBaseRoute,
362
+ apiFolder: api.apiFolder ?? "api",
363
+ locales: rspressLocales,
364
+ defaultLang: rspressLang,
365
+ versions: [],
366
+ defaultVersion: void 0
367
+ })[0];
368
+ if (!dp) return [];
369
+ const result = yield* processSimpleApi(api, api.model, dp.outputDir, dp.routeBase, wantTrace);
370
+ for (const payload of result.vfsPayloads) {
371
+ yield* emit(PluginEvent.VfsGenerated({
372
+ ctx: {
373
+ packageName: api.packageName,
374
+ ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
375
+ },
466
376
  level: "debug",
467
- totalFiles: result.vfs.size,
468
- packages: resolvedPackages.map((p) => p.name)
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 : []
469
390
  }));
470
391
  }
392
+ return [result];
393
+ }), { 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);
471
398
  }
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 twoslashStartMs = performance.now();
481
- TwoslashManager.getInstance().initialize(combinedVfs, void 0, void 0, void 0, resolvedCompilerOptions);
482
- yield* emit(PluginEvent.TwoslashInitialized({
483
- ctx: { buildId: "" },
484
- level: "debug",
485
- durationMs: Math.round(performance.now() - twoslashStartMs),
486
- vfsFileCount: combinedVfs.size
487
- }));
488
- const shikiStartMs = performance.now();
489
- const themeSet = /* @__PURE__ */ new Set();
490
- const customThemes = [];
491
- for (const config of apiConfigs) {
492
- const theme = config.theme ?? {
493
- light: DEFAULT_SHIKI_THEMES.light,
494
- dark: DEFAULT_SHIKI_THEMES.dark
495
- };
496
- if (typeof theme.light === "string") themeSet.add(theme.light);
497
- else if (typeof theme.light === "object") customThemes.push(theme.light);
498
- if (typeof theme.dark === "string") themeSet.add(theme.dark);
499
- else if (typeof theme.dark === "object") customThemes.push(theme.dark);
500
399
  }
501
- if (typeof DEFAULT_SHIKI_THEMES.light === "string") themeSet.add(DEFAULT_SHIKI_THEMES.light);
502
- if (typeof DEFAULT_SHIKI_THEMES.dark === "string") themeSet.add(DEFAULT_SHIKI_THEMES.dark);
503
- const themes = [...themeSet, ...customThemes];
504
- const langs = [
505
- "typescript",
506
- "javascript",
507
- "json",
508
- "bash",
509
- "sh"
510
- ];
511
- const highlighter = yield* Effect.promise(() => createHighlighter({
512
- themes,
513
- langs
514
- }));
515
- yield* emit(PluginEvent.PhaseCompleted({
516
- ctx: { buildId: "" },
517
- level: "debug",
518
- phase: "shikiInit",
519
- durationMs: Math.round(performance.now() - shikiStartMs)
520
- }));
521
- const ogResolver = options.siteUrl ? new OpenGraphResolver({
522
- siteUrl: options.siteUrl,
523
- ...docsRoot != null ? { docsRoot } : {}
524
- }) : null;
525
- const hideCutTransformer = MemberFormatTransformer;
526
- const hideCutLinesTransformer = HideCutLinesTransformer;
527
- const twoslashTransformer = TwoslashManager.getInstance().getTransformer() ?? void 0;
528
- const logLevel = options.logLevel ?? "info";
529
- const suppressExampleErrors = options.errors?.example !== "show";
530
- return {
531
- apiConfigs,
532
- combinedVfs,
533
- highlighter,
534
- resolvedCompilerOptions,
535
- ogResolver,
536
- shikiCrossLinker,
537
- hideCutTransformer,
538
- hideCutLinesTransformer,
539
- twoslashTransformer,
540
- pageConcurrency: os.cpus().length,
541
- logLevel: logLevel === "none" ? "info" : logLevel,
542
- suppressExampleErrors,
543
- thresholds: resolvedThresholds ?? DEFAULT_THRESHOLDS,
544
- buildId
545
- };
546
- }) };
547
- }));
548
- }
549
- /**
550
- * Strip npm scope from a package name.
551
- */
552
- function unscopedName(packageName) {
553
- return packageName.startsWith("@") ? packageName.split("/")[1] ?? packageName : packageName;
400
+ }));
401
+ const loadMs = performance.now() - loadStart;
402
+ yield* emit(PluginEvent.ModelLoaded({
403
+ ctx: {},
404
+ level: "debug",
405
+ entryPoints: apiConfigs.length,
406
+ itemCount: apiConfigs.reduce((sum, cfg) => sum + cfg.apiPackage.entryPoints.reduce((s, ep) => s + ep.members.length, 0), 0),
407
+ durationMs: Math.round(loadMs)
408
+ }));
409
+ const projectRoot = process.cwd();
410
+ let globalTsConfig;
411
+ if (firstApiTsconfig || firstApiCompilerOptions) {
412
+ globalTsConfig = {};
413
+ if (firstApiTsconfig != null) globalTsConfig.tsconfig = firstApiTsconfig;
414
+ if (firstApiCompilerOptions != null) globalTsConfig.compilerOptions = firstApiCompilerOptions;
415
+ }
416
+ const resolvedCompilerOptions = yield* Effect.promise(() => resolveTypeScriptConfig(projectRoot, globalTsConfig));
417
+ yield* emit(PluginEvent.TsCacheCreated({
418
+ ctx: {},
419
+ level: "debug",
420
+ compilerOptions: `target=${resolvedCompilerOptions.target}, module=${resolvedCompilerOptions.module}, lib=[${resolvedCompilerOptions.lib?.join(", ") ?? ""}]`,
421
+ durationMs: 0
422
+ }));
423
+ const documentedPackageNames = new Set(apiConfigs.map((config) => config.packageName));
424
+ const externalPackagesToLoad = allExternalPackages.filter((pkg) => !documentedPackageNames.has(pkg.name));
425
+ const typeLoadResult = yield* Effect.result(Effect.gen(function* () {
426
+ if (externalPackagesToLoad.length > 0) {
427
+ const resolvedPackages = yield* typeRegistry.resolveVersions(externalPackagesToLoad);
428
+ const droppedCount = externalPackagesToLoad.length - resolvedPackages.length;
429
+ if (droppedCount > 0) yield* emit(PluginEvent.ExternalPackageSkipped({
430
+ ctx: {},
431
+ level: "debug",
432
+ reason: `${droppedCount} unresolvable package(s) (unpublished or workspace-only)`
433
+ }));
434
+ if (resolvedPackages.length > 0) {
435
+ const result = yield* typeRegistry.loadPackages(resolvedPackages);
436
+ for (const [filePath, content] of result.vfs.entries()) combinedVfs.set(filePath, content);
437
+ yield* emit(PluginEvent.VfsMerged({
438
+ ctx: {},
439
+ level: "debug",
440
+ totalFiles: result.vfs.size,
441
+ packages: resolvedPackages.map((p) => p.name)
442
+ }));
443
+ }
444
+ }
445
+ }));
446
+ if (typeLoadResult._tag === "Failure") yield* emit(PluginEvent.ConfigCascadeWarning({
447
+ ctx: {},
448
+ level: "warn",
449
+ field: "externalTypes",
450
+ chosen: "empty VFS",
451
+ ignored: [typeLoadResult.failure.message ?? String(typeLoadResult.failure)]
452
+ }));
453
+ const twoslashEnv = twoslashEnvHash(combinedVfs, `typescript@${ts.version}`);
454
+ const twoslashCache = yield* (yield* TwoslashCacheService).open(twoslashEnv);
455
+ yield* emit(PluginEvent.TwoslashCacheLoaded({
456
+ ctx: {},
457
+ level: "debug",
458
+ envHash: twoslashEnv,
459
+ entries: twoslashCache.entries().size
460
+ }));
461
+ const twoslashStartMs = performance.now();
462
+ const environments = yield* TwoslashEnvironments;
463
+ environments.registerEnvironment({
464
+ vfs: combinedVfs,
465
+ compilerOptions: resolvedCompilerOptions,
466
+ typesCache: twoslashCache
467
+ });
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
+ return apiConfigs;
494
+ }) };
495
+ }));
496
+ /** The raw TypeScript config an API declares, or undefined when it declares none. */
497
+ function rawTsConfig(api) {
498
+ if (api.tsconfig == null && api.compilerOptions == null) return void 0;
499
+ const cfg = {};
500
+ if (api.tsconfig != null) cfg.tsconfig = api.tsconfig;
501
+ if (api.compilerOptions != null) cfg.compilerOptions = api.compilerOptions;
502
+ return cfg;
554
503
  }
555
504
 
556
505
  //#endregion