rspress-plugin-api-extractor 0.1.2 → 0.2.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 (109) hide show
  1. package/README.md +2 -2
  2. package/api-extracted-package.js +426 -0
  3. package/build-program.js +120 -0
  4. package/build-stages.js +664 -0
  5. package/category-resolver.js +50 -0
  6. package/code-post-processor.js +38 -0
  7. package/config-helpers.js +106 -0
  8. package/config-utils.js +258 -0
  9. package/content-hash.js +79 -0
  10. package/errors.js +29 -0
  11. package/formatter.js +69 -0
  12. package/hide-cut-transformer.js +96 -0
  13. package/index.d.ts +901 -831
  14. package/index.js +4 -6128
  15. package/layers/ConfigServiceLive.js +377 -0
  16. package/layers/ObservabilityLive.js +136 -0
  17. package/layers/PathDerivationServiceLive.js +16 -0
  18. package/layers/SnapshotServiceLive.js +94 -0
  19. package/layers/TypeRegistryServiceLive.js +46 -0
  20. package/llms-processing.js +270 -0
  21. package/llms-program.js +262 -0
  22. package/loader.js +186 -0
  23. package/markdown/cross-linker.js +156 -0
  24. package/markdown/helpers.js +364 -0
  25. package/markdown/index.js +11 -0
  26. package/markdown/page-generators/class-page.js +357 -0
  27. package/markdown/page-generators/enum-page.js +152 -0
  28. package/markdown/page-generators/function-page.js +127 -0
  29. package/markdown/page-generators/index-pages.js +27 -0
  30. package/markdown/page-generators/interface-page.js +307 -0
  31. package/markdown/page-generators/namespace-page.js +280 -0
  32. package/markdown/page-generators/type-alias-page.js +110 -0
  33. package/markdown/page-generators/variable-page.js +110 -0
  34. package/markdown/shiki-utils.js +48 -0
  35. package/migrations/001_create_snapshots.js +25 -0
  36. package/model-loader.js +95 -0
  37. package/multi-entry-resolver.js +70 -0
  38. package/og-resolver.js +271 -0
  39. package/package.json +63 -73
  40. package/path-derivation.js +48 -0
  41. package/plugin.js +218 -0
  42. package/prettier-formatter.js +73 -0
  43. package/public/tsconfig/rspress.json +44 -0
  44. package/remark-api-codeblocks.js +130 -0
  45. package/remark-with-api.js +172 -0
  46. package/route-collisions.js +52 -0
  47. package/runtime/components/ApiExample/index.js +29 -25
  48. package/runtime/components/ApiLlmsPackageActions/index.js +252 -323
  49. package/runtime/components/ApiLlmsViewOptions/index.js +269 -340
  50. package/runtime/components/ApiMember/index.js +49 -47
  51. package/runtime/components/ApiSignature/index.js +32 -28
  52. package/runtime/components/EnumMembersTable/{index_module.css → index.css} +18 -19
  53. package/runtime/components/EnumMembersTable/index.js +36 -67
  54. package/runtime/components/EnumMembersTable/index.module.js +9 -6
  55. package/runtime/components/ExampleBlock/{index_module.css → index.css} +2 -3
  56. package/runtime/components/ExampleBlock/index.js +23 -28
  57. package/runtime/components/ExampleBlock/index.module.js +8 -5
  58. package/runtime/components/MarkdownContent/index.js +26 -18
  59. package/runtime/components/MarkdownText/index.js +28 -22
  60. package/runtime/components/MemberSignature/{index_module.css → index.css} +5 -6
  61. package/runtime/components/MemberSignature/index.js +46 -46
  62. package/runtime/components/MemberSignature/index.module.js +9 -6
  63. package/runtime/components/ParametersTable/{index_module.css → index.css} +19 -20
  64. package/runtime/components/ParametersTable/index.js +36 -67
  65. package/runtime/components/ParametersTable/index.module.js +9 -6
  66. package/runtime/components/SignatureBlock/{index_module.css → index.css} +5 -6
  67. package/runtime/components/SignatureBlock/index.js +30 -29
  68. package/runtime/components/SignatureBlock/index.module.js +9 -6
  69. package/runtime/components/SignatureCode/{index_module.css → index.css} +9 -10
  70. package/runtime/components/SignatureCode/index.js +40 -32
  71. package/runtime/components/SignatureCode/index.module.js +9 -6
  72. package/runtime/components/SignatureToolbar/{index_module.css → index.css} +18 -20
  73. package/runtime/components/SignatureToolbar/index.js +48 -51
  74. package/runtime/components/SignatureToolbar/index.module.js +13 -10
  75. package/runtime/components/buttons/ButtonGroup.js +13 -6
  76. package/runtime/components/buttons/CopyCodeButton.js +40 -38
  77. package/runtime/components/buttons/WrapSignatureButton.js +20 -16
  78. package/runtime/components/buttons/{index_module.css → index.css} +5 -6
  79. package/runtime/components/buttons/index.module.js +8 -5
  80. package/runtime/components/icons/CheckIcon/index.js +20 -17
  81. package/runtime/components/icons/CopyIcon/index.js +20 -17
  82. package/runtime/components/icons/UnwrapIcon/index.js +22 -18
  83. package/runtime/components/icons/WrapIcon/index.js +20 -17
  84. package/runtime/components/shared/_twoslash.css +3 -10
  85. package/runtime/components/shared/variables.css +0 -3
  86. package/runtime/hooks/useWrapToggle.js +32 -9
  87. package/runtime/index.d.ts +513 -173
  88. package/runtime/index.js +11 -9
  89. package/runtime/utils/decode-hast.js +31 -16
  90. package/runtime/utils/hast-renderer.js +21 -7
  91. package/schemas/config.js +199 -0
  92. package/schemas/index.js +5 -0
  93. package/schemas/opengraph.js +26 -0
  94. package/schemas/performance.js +19 -0
  95. package/serve.js +133 -0
  96. package/services/ConfigService.js +7 -0
  97. package/services/PathDerivationService.js +7 -0
  98. package/services/SnapshotService.js +7 -0
  99. package/services/TypeRegistryService.js +7 -0
  100. package/shiki-transformer.js +758 -0
  101. package/tsconfig-parser.js +127 -0
  102. package/tsdoc-metadata.json +11 -11
  103. package/twoslash-patterns.js +87 -0
  104. package/twoslash-transformer.js +316 -0
  105. package/type-reference-extractor.js +201 -0
  106. package/typescript-config.js +168 -0
  107. package/vfs-registry.js +121 -0
  108. package/0~llms-program.js +0 -344
  109. package/runtime/components/ApiLlmsPackageActions/index.module.js +0 -2
@@ -0,0 +1,377 @@
1
+ import { BuildMetrics } from "./ObservabilityLive.js";
2
+ import { TypeReferenceExtractor } from "../type-reference-extractor.js";
3
+ import { OpenGraphResolver } from "../og-resolver.js";
4
+ import { resolveTypeScriptConfig } from "../typescript-config.js";
5
+ import { TwoslashManager } from "../twoslash-transformer.js";
6
+ import { extractAutoDetectedPackages, isVersionConfig, mergeLlmsPluginConfig, validateExternalPackages } from "../config-utils.js";
7
+ import { ApiExtractedPackage } from "../api-extracted-package.js";
8
+ import { CategoryResolver } from "../category-resolver.js";
9
+ import { ConfigValidationError } from "../errors.js";
10
+ import { HideCutLinesTransformer, MemberFormatTransformer } from "../hide-cut-transformer.js";
11
+ import { DEFAULT_SHIKI_THEMES } from "../markdown/shiki-utils.js";
12
+ import { ApiModelLoader } from "../model-loader.js";
13
+ import { DEFAULT_CATEGORIES } from "../schemas/config.js";
14
+ import "../schemas/index.js";
15
+ import { ConfigService } from "../services/ConfigService.js";
16
+ import { PathDerivationService } from "../services/PathDerivationService.js";
17
+ import { TypeRegistryService } from "../services/TypeRegistryService.js";
18
+ import path from "node:path";
19
+ import { Effect, Layer, Metric } from "effect";
20
+ import os from "node:os";
21
+ import { createHighlighter } from "shiki";
22
+
23
+ //#region src/layers/ConfigServiceLive.ts
24
+ /**
25
+ * Normalize theme configuration from user input to a consistent format.
26
+ */
27
+ function normalizeThemeConfig(theme) {
28
+ if (!theme) return { ...DEFAULT_SHIKI_THEMES };
29
+ if (typeof theme === "string") return {
30
+ light: theme,
31
+ dark: theme
32
+ };
33
+ if ("light" in theme && "dark" in theme && typeof theme.light === "string" && typeof theme.dark === "string") return {
34
+ light: theme.light,
35
+ dark: theme.dark
36
+ };
37
+ return {
38
+ light: theme,
39
+ dark: theme
40
+ };
41
+ }
42
+ /**
43
+ * Prepend import statements for external type references to the VFS declaration files.
44
+ */
45
+ function prependImportsToVfs(vfs, apiPackage, packageName) {
46
+ const extractor = new TypeReferenceExtractor(apiPackage, packageName);
47
+ for (const entryPoint of apiPackage.entryPoints) {
48
+ const imports = extractor.extractImportsForEntryPoint(entryPoint);
49
+ 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}`);
55
+ }
56
+ }
57
+ /**
58
+ * Validate plugin options and return an Effect that fails with ConfigValidationError.
59
+ */
60
+ function validateOptions(options, rspressConfig) {
61
+ return Effect.gen(function* () {
62
+ const { api, apis } = options;
63
+ const { multiVersion } = rspressConfig;
64
+ if (api && apis) return yield* new ConfigValidationError({
65
+ field: "api/apis",
66
+ reason: "Cannot provide both 'api' and 'apis'. Use 'api' for single-package sites or 'apis' for multi-package portals."
67
+ });
68
+ if (!api && !apis) return yield* new ConfigValidationError({
69
+ field: "api/apis",
70
+ reason: "Must provide either 'api' or 'apis'."
71
+ });
72
+ if (apis) {
73
+ if (apis.length === 0) return yield* new ConfigValidationError({
74
+ field: "apis",
75
+ reason: "'apis' must contain at least one API configuration."
76
+ });
77
+ if (multiVersion) return yield* new ConfigValidationError({
78
+ field: "apis",
79
+ reason: "multiVersion is not supported with 'apis' (multi-API mode). Use 'api' (single-API mode) for versioned documentation."
80
+ });
81
+ return;
82
+ }
83
+ if (api) if (multiVersion) {
84
+ if (!api.versions) return yield* new ConfigValidationError({
85
+ field: "api.versions",
86
+ reason: "'versions' is required when multiVersion is active."
87
+ });
88
+ const pluginKeys = new Set(Object.keys(api.versions));
89
+ const rspressKeys = new Set(multiVersion.versions);
90
+ if (pluginKeys.size !== rspressKeys.size || ![...pluginKeys].every((k) => rspressKeys.has(k))) return yield* new ConfigValidationError({
91
+ field: "api.versions",
92
+ reason: `api.versions keys [${[...pluginKeys].join(", ")}] must exactly match multiVersion.versions [${[...rspressKeys].join(", ")}].`
93
+ });
94
+ } else {
95
+ if (api.versions) yield* Effect.logWarning("api.versions is provided but RSPress multiVersion is not configured. Versions will be ignored.");
96
+ if (!api.model) return yield* new ConfigValidationError({
97
+ field: "api.model",
98
+ reason: "'model' is required when multiVersion is not active."
99
+ });
100
+ }
101
+ });
102
+ }
103
+ /**
104
+ * Create ConfigServiceLive from plugin options.
105
+ * Resolves plugin options + RSPress config into a fully prepared build context
106
+ * with loaded models, type system, and resources.
107
+ */
108
+ function ConfigServiceLive(options, shikiCrossLinker) {
109
+ return Layer.effect(ConfigService, Effect.gen(function* () {
110
+ const typeRegistry = yield* TypeRegistryService;
111
+ const pathService = yield* PathDerivationService;
112
+ return { resolve: (rspressConfig) => Effect.gen(function* () {
113
+ const loadStart = performance.now();
114
+ yield* validateOptions(options, { ...rspressConfig.multiVersion ? { multiVersion: {
115
+ default: rspressConfig.multiVersion.default,
116
+ versions: [...rspressConfig.multiVersion.versions]
117
+ } } : {} });
118
+ const rspressMultiVersion = rspressConfig.multiVersion;
119
+ const rspressLocales = rspressConfig.locales?.map((l) => l.lang) ?? [];
120
+ const rspressLang = rspressConfig.lang;
121
+ const docsRoot = rspressConfig.root;
122
+ const rspressRoot = docsRoot || process.cwd();
123
+ const categoryResolver = new CategoryResolver();
124
+ const pluginDefaults = categoryResolver.mergeCategories(DEFAULT_CATEGORIES, options.defaultCategories);
125
+ const apiConfigs = [];
126
+ const combinedVfs = /* @__PURE__ */ new Map();
127
+ const allExternalPackages = [];
128
+ let firstApiTsconfig;
129
+ let firstApiCompilerOptions;
130
+ /**
131
+ * Helper to process a single API model (shared by single and multi modes).
132
+ */
133
+ const processSimpleApi = (api, model, outputDir, fullRoute) => Effect.promise(async () => {
134
+ const { apiPackage, source: loaderSource } = await ApiModelLoader.loadApiModel(model);
135
+ const resolvedCategories = categoryResolver.resolveCategoryConfig(pluginDefaults, api.categories);
136
+ const resolvedSource = categoryResolver.resolveSourceConfig(api.source, loaderSource);
137
+ const resolvedLlms = mergeLlmsPluginConfig(options.llmsPlugin, api.llmsPlugin);
138
+ const packageJson = api.packageJson ? await ApiModelLoader.loadPackageJson(api.packageJson) : void 0;
139
+ validateExternalPackages(api.externalPackages, packageJson);
140
+ const externalPackages = api.externalPackages || extractAutoDetectedPackages(packageJson, api.autoDetectDependencies);
141
+ if (externalPackages && externalPackages.length > 0) Effect.runSync(Metric.incrementBy(BuildMetrics.externalPackagesTotal, externalPackages.length));
142
+ const vfs = ApiExtractedPackage.fromPackage(apiPackage, api.packageName).generateVfs();
143
+ prependImportsToVfs(vfs, apiPackage, api.packageName);
144
+ const resolvedOgImage = api.ogImage ?? options.ogImage;
145
+ const resolvedTheme = normalizeThemeConfig(api.theme);
146
+ return {
147
+ vfs,
148
+ externalPackages: externalPackages || [],
149
+ config: {
150
+ apiPackage,
151
+ packageName: api.packageName,
152
+ ...api.name != null ? { apiName: api.name } : {},
153
+ outputDir,
154
+ baseRoute: fullRoute,
155
+ categories: resolvedCategories,
156
+ ...resolvedSource != null ? { source: resolvedSource } : {},
157
+ ...packageJson != null ? { packageJson } : {},
158
+ ...resolvedLlms != null ? { llmsPlugin: resolvedLlms } : {},
159
+ ...options.siteUrl != null ? { siteUrl: options.siteUrl } : {},
160
+ ...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
161
+ docsDir: path.dirname(outputDir),
162
+ ...docsRoot != null ? { docsRoot } : {},
163
+ ...resolvedTheme != null ? { theme: resolvedTheme } : {}
164
+ }
165
+ };
166
+ });
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({
175
+ mode: "single",
176
+ docsRoot: rspressRoot,
177
+ baseRoute,
178
+ apiFolder: api.apiFolder ?? "api",
179
+ locales: rspressLocales,
180
+ defaultLang: rspressLang,
181
+ versions: [version],
182
+ defaultVersion: rspressMultiVersion?.default
183
+ }))[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);
233
+ }
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);
247
+ for (const [filepath, content] of result.vfs.entries()) combinedVfs.set(filepath, content);
248
+ if (result.externalPackages.length > 0) allExternalPackages.push(...result.externalPackages);
249
+ apiConfigs.push(result.config);
250
+ }
251
+ }
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
+ }
282
+ const loadMs = performance.now() - loadStart;
283
+ yield* Effect.logDebug(`Loading API models: ${loadMs.toFixed(0)}ms`);
284
+ const projectRoot = process.cwd();
285
+ let globalTsConfig;
286
+ if (firstApiTsconfig || firstApiCompilerOptions) {
287
+ globalTsConfig = {};
288
+ if (firstApiTsconfig != null) globalTsConfig.tsconfig = firstApiTsconfig;
289
+ if (firstApiCompilerOptions != null) globalTsConfig.compilerOptions = firstApiCompilerOptions;
290
+ }
291
+ 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();
294
+ 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;
302
+ }
303
+ return yield* typeRegistry.createTypeScriptCache([], resolvedCompilerOptions);
304
+ }));
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
+ }
311
+ 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`);
314
+ const shikiStartMs = performance.now();
315
+ const themeSet = /* @__PURE__ */ new Set();
316
+ const customThemes = [];
317
+ for (const config of apiConfigs) {
318
+ const theme = config.theme ?? {
319
+ light: DEFAULT_SHIKI_THEMES.light,
320
+ dark: DEFAULT_SHIKI_THEMES.dark
321
+ };
322
+ if (typeof theme.light === "string") themeSet.add(theme.light);
323
+ else if (typeof theme.light === "object") customThemes.push(theme.light);
324
+ if (typeof theme.dark === "string") themeSet.add(theme.dark);
325
+ else if (typeof theme.dark === "object") customThemes.push(theme.dark);
326
+ }
327
+ if (typeof DEFAULT_SHIKI_THEMES.light === "string") themeSet.add(DEFAULT_SHIKI_THEMES.light);
328
+ if (typeof DEFAULT_SHIKI_THEMES.dark === "string") themeSet.add(DEFAULT_SHIKI_THEMES.dark);
329
+ const themes = [...themeSet, ...customThemes];
330
+ const langs = [
331
+ "typescript",
332
+ "javascript",
333
+ "json",
334
+ "bash",
335
+ "sh"
336
+ ];
337
+ const highlighter = yield* Effect.promise(() => createHighlighter({
338
+ themes,
339
+ langs
340
+ }));
341
+ yield* Effect.logDebug(`Initializing Shiki highlighter: ${(performance.now() - shikiStartMs).toFixed(0)}ms`);
342
+ const ogResolver = options.siteUrl ? new OpenGraphResolver({
343
+ siteUrl: options.siteUrl,
344
+ ...docsRoot != null ? { docsRoot } : {}
345
+ }) : null;
346
+ const hideCutTransformer = MemberFormatTransformer;
347
+ const hideCutLinesTransformer = HideCutLinesTransformer;
348
+ const twoslashTransformer = TwoslashManager.getInstance().getTransformer() ?? void 0;
349
+ const logLevel = options.logLevel ?? "info";
350
+ const suppressExampleErrors = options.errors?.example !== "show";
351
+ return {
352
+ apiConfigs,
353
+ combinedVfs,
354
+ highlighter,
355
+ tsEnvCache,
356
+ resolvedCompilerOptions,
357
+ ogResolver,
358
+ shikiCrossLinker,
359
+ hideCutTransformer,
360
+ hideCutLinesTransformer,
361
+ twoslashTransformer,
362
+ pageConcurrency: os.cpus().length,
363
+ logLevel: logLevel === "none" ? "info" : logLevel,
364
+ suppressExampleErrors
365
+ };
366
+ }) };
367
+ }));
368
+ }
369
+ /**
370
+ * Strip npm scope from a package name.
371
+ */
372
+ function unscopedName(packageName) {
373
+ return packageName.startsWith("@") ? packageName.split("/")[1] ?? packageName : packageName;
374
+ }
375
+
376
+ //#endregion
377
+ export { ConfigServiceLive };
@@ -0,0 +1,136 @@
1
+ import { Effect, Layer, LogLevel, Logger, Metric, MetricBoundaries } from "effect";
2
+
3
+ //#region src/layers/ObservabilityLive.ts
4
+ /**
5
+ * All build metrics as named counters/histograms.
6
+ *
7
+ * Note: Effect Metrics use a process-wide registry. In tests, counters
8
+ * accumulate across test cases within the same process. Test assertions
9
+ * should use loose matching (toContain) rather than exact count checks.
10
+ */
11
+ const BuildMetrics = {
12
+ filesTotal: Metric.counter("files.total"),
13
+ filesNew: Metric.counter("files.new"),
14
+ filesModified: Metric.counter("files.modified"),
15
+ filesUnchanged: Metric.counter("files.unchanged"),
16
+ codeblockDuration: Metric.histogram("codeblock.duration", MetricBoundaries.fromIterable([
17
+ 10,
18
+ 25,
19
+ 50,
20
+ 100,
21
+ 200,
22
+ 500,
23
+ 1e3
24
+ ])),
25
+ codeblockShikiDuration: Metric.histogram("codeblock.shiki.duration", MetricBoundaries.fromIterable([
26
+ 5,
27
+ 10,
28
+ 25,
29
+ 50,
30
+ 100,
31
+ 250
32
+ ])),
33
+ codeblockTotal: Metric.counter("codeblock.total"),
34
+ codeblockSlow: Metric.counter("codeblock.slow"),
35
+ twoslashErrors: Metric.counter("twoslash.errors"),
36
+ prettierErrors: Metric.counter("prettier.errors"),
37
+ pagesGenerated: Metric.counter("pages.generated"),
38
+ apiVersionsLoaded: Metric.counter("api.versions.loaded"),
39
+ externalPackagesTotal: Metric.counter("external.packages.total")
40
+ };
41
+ /**
42
+ * Format a Date as HH:MM:SS for console output.
43
+ */
44
+ function formatTime(date) {
45
+ return date.toTimeString().slice(0, 8);
46
+ }
47
+ /**
48
+ * Extract annotations from HashMap to a plain object.
49
+ */
50
+ function annotationsToObject(annotations) {
51
+ const obj = {};
52
+ for (const [key, value] of annotations) obj[key] = value;
53
+ return obj;
54
+ }
55
+ /**
56
+ * Create a custom plugin logger for the given mode.
57
+ * Uses a closure to capture debugMode — no mutable module state.
58
+ */
59
+ function makePluginLogger(debugMode) {
60
+ return Logger.make(({ logLevel, message, date, annotations }) => {
61
+ if (debugMode) {
62
+ const entry = {
63
+ timestamp: date.getTime(),
64
+ level: logLevel.label.toLowerCase(),
65
+ message: typeof message === "string" ? message : String(message),
66
+ ...annotationsToObject(annotations)
67
+ };
68
+ console.log(JSON.stringify(entry));
69
+ } else {
70
+ const time = formatTime(date);
71
+ const msg = typeof message === "string" ? message : String(message);
72
+ const prefix = logLevel._tag === "Warning" ? "⚠️ " : logLevel._tag === "Error" ? "🔴 " : "";
73
+ console.log(`[${time}] ${prefix}${msg}`);
74
+ }
75
+ });
76
+ }
77
+ /**
78
+ * Create the complete observability layer for the plugin.
79
+ * Replaces the default Effect logger with a custom one and sets minimum log level.
80
+ *
81
+ * @param logLevel - Plugin log level from options
82
+ */
83
+ function PluginLoggerLayer(logLevel = "info") {
84
+ const pluginLogger = makePluginLogger(logLevel === "debug");
85
+ const effectLogLevel = {
86
+ debug: LogLevel.Debug,
87
+ verbose: LogLevel.Debug,
88
+ info: LogLevel.Info,
89
+ warn: LogLevel.Warning,
90
+ error: LogLevel.Error,
91
+ none: LogLevel.None
92
+ }[logLevel];
93
+ return Layer.mergeAll(Logger.replace(Logger.defaultLogger, pluginLogger), Logger.minimumLogLevel(effectLogLevel));
94
+ }
95
+ /**
96
+ * Log a build summary by reading all metric snapshots.
97
+ * Replaces the 4 separate logSummary() calls in afterBuild.
98
+ */
99
+ const logBuildSummary = Effect.gen(function* () {
100
+ const filesTotal = yield* Metric.value(BuildMetrics.filesTotal);
101
+ const filesNew = yield* Metric.value(BuildMetrics.filesNew);
102
+ const filesModified = yield* Metric.value(BuildMetrics.filesModified);
103
+ const filesUnchanged = yield* Metric.value(BuildMetrics.filesUnchanged);
104
+ const twoslashErrors = yield* Metric.value(BuildMetrics.twoslashErrors);
105
+ const prettierErrors = yield* Metric.value(BuildMetrics.prettierErrors);
106
+ const codeblockTotal = yield* Metric.value(BuildMetrics.codeblockTotal);
107
+ const codeblockSlow = yield* Metric.value(BuildMetrics.codeblockSlow);
108
+ const total = filesTotal.count;
109
+ const newCount = filesNew.count;
110
+ const modified = filesModified.count;
111
+ const unchanged = filesUnchanged.count;
112
+ const tsErrors = twoslashErrors.count;
113
+ const prErrors = prettierErrors.count;
114
+ const blocks = codeblockTotal.count;
115
+ const slowBlocks = codeblockSlow.count;
116
+ if (total === 0) yield* Effect.log("📝 No files generated");
117
+ else if (newCount === 0 && modified === 0) yield* Effect.log(`📝 ${total} files (all unchanged)`);
118
+ else {
119
+ const parts = [];
120
+ if (newCount > 0) parts.push(`${newCount} new`);
121
+ if (modified > 0) parts.push(`${modified} modified`);
122
+ if (unchanged > 0) parts.push(`${unchanged} unchanged`);
123
+ yield* Effect.log(`📝 ${total} files (${parts.join(", ")})`);
124
+ }
125
+ if (blocks > 0 && slowBlocks > 0) yield* Effect.logWarning(`⚠️ Code block performance: ${slowBlocks} of ${blocks} blocks were slow (>100ms)`);
126
+ const totalErrors = tsErrors + prErrors;
127
+ if (totalErrors > 0) {
128
+ const errorParts = [];
129
+ if (tsErrors > 0) errorParts.push(`${tsErrors} Twoslash`);
130
+ if (prErrors > 0) errorParts.push(`${prErrors} Prettier`);
131
+ yield* Effect.logWarning(`🔴 ${totalErrors} error(s) in code blocks (${errorParts.join(", ")})`);
132
+ }
133
+ });
134
+
135
+ //#endregion
136
+ export { BuildMetrics, PluginLoggerLayer, logBuildSummary };
@@ -0,0 +1,16 @@
1
+ import { deriveOutputPaths, normalizeBaseRoute } from "../path-derivation.js";
2
+ import { PathDerivationService } from "../services/PathDerivationService.js";
3
+ import { Effect, Layer } from "effect";
4
+
5
+ //#region src/layers/PathDerivationServiceLive.ts
6
+ const PathDerivationServiceLive = Layer.succeed(PathDerivationService, {
7
+ derivePaths: (input) => Effect.succeed(deriveOutputPaths({
8
+ ...input,
9
+ locales: [...input.locales],
10
+ versions: [...input.versions]
11
+ })),
12
+ normalizeBaseRoute: (route) => Effect.succeed(normalizeBaseRoute(route))
13
+ });
14
+
15
+ //#endregion
16
+ export { PathDerivationServiceLive };
@@ -0,0 +1,94 @@
1
+ import { hashContent } from "../content-hash.js";
2
+ import { SnapshotService } from "../services/SnapshotService.js";
3
+ import { SnapshotDbError } from "../errors.js";
4
+ import migration from "../migrations/001_create_snapshots.js";
5
+ import { NodeContext } from "@effect/platform-node";
6
+ import { Effect, Layer, Option } from "effect";
7
+ import { Migrator } from "@effect/sql";
8
+ import * as SqlClient from "@effect/sql/SqlClient";
9
+ import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node";
10
+
11
+ //#region src/layers/SnapshotServiceLive.ts
12
+ function toFileSnapshot(row) {
13
+ return {
14
+ outputDir: row.output_dir,
15
+ filePath: row.file_path,
16
+ publishedTime: row.published_time,
17
+ modifiedTime: row.modified_time,
18
+ contentHash: row.content_hash,
19
+ frontmatterHash: row.frontmatter_hash,
20
+ buildTime: row.build_time
21
+ };
22
+ }
23
+ function toSnapshotDbError(error) {
24
+ return new SnapshotDbError({
25
+ operation: "query",
26
+ dbPath: "snapshot-db",
27
+ reason: error instanceof Error ? error.message : String(error)
28
+ });
29
+ }
30
+ const SnapshotServiceLive = (dbPath) => {
31
+ const SqlLive = SqliteClient.layer({ filename: dbPath });
32
+ const MigratorLive = SqliteMigrator.layer({ loader: Migrator.fromRecord({ "001_create_snapshots": migration }) }).pipe(Layer.provide(Layer.merge(SqlLive, NodeContext.layer)));
33
+ const ServiceImpl = Layer.scoped(SnapshotService, Effect.gen(function* () {
34
+ const sql = yield* SqlClient.SqlClient;
35
+ yield* Effect.addFinalizer(() => sql`PRAGMA wal_checkpoint(TRUNCATE)`.pipe(Effect.ignore));
36
+ return {
37
+ hashContent,
38
+ getSnapshot: (outputDir, filePath) => sql`SELECT * FROM file_snapshots WHERE output_dir = ${outputDir} AND file_path = ${filePath}`.pipe(Effect.map((rows) => rows.length > 0 ? Option.some(toFileSnapshot(rows[0])) : Option.none()), Effect.mapError(toSnapshotDbError)),
39
+ getAllForDirectory: (outputDir) => sql`SELECT * FROM file_snapshots WHERE output_dir = ${outputDir}`.pipe(Effect.map((rows) => rows.map(toFileSnapshot)), Effect.mapError(toSnapshotDbError)),
40
+ getFilePaths: (outputDir) => sql`SELECT file_path FROM file_snapshots WHERE output_dir = ${outputDir}`.pipe(Effect.map((rows) => rows.map((r) => r.file_path)), Effect.mapError(toSnapshotDbError)),
41
+ upsert: (snapshot) => sql`INSERT INTO file_snapshots
42
+ (output_dir, file_path, published_time, modified_time,
43
+ content_hash, frontmatter_hash, build_time)
44
+ VALUES (${snapshot.outputDir}, ${snapshot.filePath},
45
+ ${snapshot.publishedTime}, ${snapshot.modifiedTime},
46
+ ${snapshot.contentHash}, ${snapshot.frontmatterHash},
47
+ ${snapshot.buildTime})
48
+ ON CONFLICT(output_dir, file_path) DO UPDATE SET
49
+ published_time = ${snapshot.publishedTime},
50
+ modified_time = ${snapshot.modifiedTime},
51
+ content_hash = ${snapshot.contentHash},
52
+ frontmatter_hash = ${snapshot.frontmatterHash},
53
+ build_time = ${snapshot.buildTime}
54
+ WHERE published_time != ${snapshot.publishedTime}
55
+ OR modified_time != ${snapshot.modifiedTime}
56
+ OR content_hash != ${snapshot.contentHash}
57
+ OR frontmatter_hash != ${snapshot.frontmatterHash}`.pipe(Effect.as(true), Effect.mapError(toSnapshotDbError)),
58
+ batchUpsert: (snapshots) => (snapshots.length === 0 ? Effect.succeed(0) : sql.withTransaction(Effect.forEach(snapshots, (s) => sql`INSERT INTO file_snapshots
59
+ (output_dir, file_path, published_time, modified_time,
60
+ content_hash, frontmatter_hash, build_time)
61
+ VALUES (${s.outputDir}, ${s.filePath},
62
+ ${s.publishedTime}, ${s.modifiedTime},
63
+ ${s.contentHash}, ${s.frontmatterHash},
64
+ ${s.buildTime})
65
+ ON CONFLICT(output_dir, file_path) DO UPDATE SET
66
+ published_time = ${s.publishedTime},
67
+ modified_time = ${s.modifiedTime},
68
+ content_hash = ${s.contentHash},
69
+ frontmatter_hash = ${s.frontmatterHash},
70
+ build_time = ${s.buildTime}
71
+ WHERE published_time != ${s.publishedTime}
72
+ OR modified_time != ${s.modifiedTime}
73
+ OR content_hash != ${s.contentHash}
74
+ OR frontmatter_hash != ${s.frontmatterHash}`, { concurrency: 1 })).pipe(Effect.map(() => snapshots.length))).pipe(Effect.mapError(toSnapshotDbError)),
75
+ deleteSnapshot: (outputDir, filePath) => sql`DELETE FROM file_snapshots WHERE output_dir = ${outputDir} AND file_path = ${filePath}`.pipe(Effect.asVoid, Effect.mapError(toSnapshotDbError)),
76
+ cleanupStale: (outputDir, currentFiles) => Effect.gen(function* () {
77
+ const rows = yield* sql`SELECT file_path FROM file_snapshots WHERE output_dir = ${outputDir}`;
78
+ const staleFiles = [];
79
+ for (const row of rows) {
80
+ const fp = row.file_path;
81
+ if (!currentFiles.has(fp)) {
82
+ yield* sql`DELETE FROM file_snapshots WHERE output_dir = ${outputDir} AND file_path = ${fp}`;
83
+ staleFiles.push(fp);
84
+ }
85
+ }
86
+ return staleFiles;
87
+ }).pipe(Effect.mapError(toSnapshotDbError))
88
+ };
89
+ }));
90
+ return Layer.provide(ServiceImpl, Layer.merge(SqlLive, MigratorLive));
91
+ };
92
+
93
+ //#endregion
94
+ export { SnapshotServiceLive };
@@ -0,0 +1,46 @@
1
+ import { TypeRegistryError } from "../errors.js";
2
+ import { TypeRegistryService } from "../services/TypeRegistryService.js";
3
+ import { Effect, Layer } from "effect";
4
+ import { PackageSpec, TypeRegistry } from "type-registry-effect";
5
+ import { NodeLayer, createTypeScriptCache } from "type-registry-effect/node";
6
+
7
+ //#region src/layers/TypeRegistryServiceLive.ts
8
+ /**
9
+ * TypeRegistryServiceLive: uses type-registry-effect Effect programs directly.
10
+ *
11
+ * The NodeLayer from type-registry-effect provides CacheService, PackageFetcher,
12
+ * and TypeResolver with Node.js platform implementations (FileSystem, HttpClient).
13
+ *
14
+ * Built-in metrics (packagesLoaded, packagesFailed, cacheHits, etc.) are
15
+ * automatically tracked by the upstream library.
16
+ */
17
+ const TypeRegistryServiceLive = Layer.succeed(TypeRegistryService, {
18
+ loadPackages: (packages) => Effect.gen(function* () {
19
+ if (packages.length === 0) return { vfs: /* @__PURE__ */ new Map() };
20
+ const specs = packages.map((pkg) => new PackageSpec({
21
+ name: pkg.name,
22
+ version: pkg.version
23
+ }));
24
+ return { vfs: yield* TypeRegistry.getVFS(specs, { autoFetch: true }).pipe(Effect.catchAll((error) => Effect.fail(new TypeRegistryError({
25
+ packageName: packages.map((p) => p.name).join(", "),
26
+ version: packages.map((p) => p.version).join(", "),
27
+ reason: error.message ?? String(error)
28
+ })))) };
29
+ }).pipe(Effect.provide(NodeLayer)),
30
+ createTypeScriptCache: (packages, compilerOptions) => Effect.tryPromise({
31
+ try: () => {
32
+ return createTypeScriptCache(packages.map((pkg) => new PackageSpec({
33
+ name: pkg.name,
34
+ version: pkg.version
35
+ })), compilerOptions);
36
+ },
37
+ catch: (error) => new TypeRegistryError({
38
+ packageName: packages.map((p) => p.name).join(", "),
39
+ version: "",
40
+ reason: error instanceof Error ? error.message : String(error)
41
+ })
42
+ })
43
+ });
44
+
45
+ //#endregion
46
+ export { TypeRegistryServiceLive };