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.
- package/README.md +2 -2
- package/api-extracted-package.js +426 -0
- package/build-program.js +120 -0
- package/build-stages.js +664 -0
- package/category-resolver.js +50 -0
- package/code-post-processor.js +38 -0
- package/config-helpers.js +106 -0
- package/config-utils.js +258 -0
- package/content-hash.js +79 -0
- package/errors.js +29 -0
- package/formatter.js +69 -0
- package/hide-cut-transformer.js +96 -0
- package/index.d.ts +901 -831
- package/index.js +4 -6128
- package/layers/ConfigServiceLive.js +377 -0
- package/layers/ObservabilityLive.js +136 -0
- package/layers/PathDerivationServiceLive.js +16 -0
- package/layers/SnapshotServiceLive.js +94 -0
- package/layers/TypeRegistryServiceLive.js +46 -0
- package/llms-processing.js +270 -0
- package/llms-program.js +262 -0
- package/loader.js +186 -0
- package/markdown/cross-linker.js +156 -0
- package/markdown/helpers.js +364 -0
- package/markdown/index.js +11 -0
- package/markdown/page-generators/class-page.js +357 -0
- package/markdown/page-generators/enum-page.js +152 -0
- package/markdown/page-generators/function-page.js +127 -0
- package/markdown/page-generators/index-pages.js +27 -0
- package/markdown/page-generators/interface-page.js +307 -0
- package/markdown/page-generators/namespace-page.js +280 -0
- package/markdown/page-generators/type-alias-page.js +110 -0
- package/markdown/page-generators/variable-page.js +110 -0
- package/markdown/shiki-utils.js +48 -0
- package/migrations/001_create_snapshots.js +25 -0
- package/model-loader.js +95 -0
- package/multi-entry-resolver.js +70 -0
- package/og-resolver.js +271 -0
- package/package.json +63 -73
- package/path-derivation.js +48 -0
- package/plugin.js +218 -0
- package/prettier-formatter.js +73 -0
- package/public/tsconfig/rspress.json +44 -0
- package/remark-api-codeblocks.js +130 -0
- package/remark-with-api.js +172 -0
- package/route-collisions.js +52 -0
- package/runtime/components/ApiExample/index.js +29 -25
- package/runtime/components/ApiLlmsPackageActions/index.js +252 -323
- package/runtime/components/ApiLlmsViewOptions/index.js +269 -340
- package/runtime/components/ApiMember/index.js +49 -47
- package/runtime/components/ApiSignature/index.js +32 -28
- package/runtime/components/EnumMembersTable/{index_module.css → index.css} +18 -19
- package/runtime/components/EnumMembersTable/index.js +36 -67
- package/runtime/components/EnumMembersTable/index.module.js +9 -6
- package/runtime/components/ExampleBlock/{index_module.css → index.css} +2 -3
- package/runtime/components/ExampleBlock/index.js +23 -28
- package/runtime/components/ExampleBlock/index.module.js +8 -5
- package/runtime/components/MarkdownContent/index.js +26 -18
- package/runtime/components/MarkdownText/index.js +28 -22
- package/runtime/components/MemberSignature/{index_module.css → index.css} +5 -6
- package/runtime/components/MemberSignature/index.js +46 -46
- package/runtime/components/MemberSignature/index.module.js +9 -6
- package/runtime/components/ParametersTable/{index_module.css → index.css} +19 -20
- package/runtime/components/ParametersTable/index.js +36 -67
- package/runtime/components/ParametersTable/index.module.js +9 -6
- package/runtime/components/SignatureBlock/{index_module.css → index.css} +5 -6
- package/runtime/components/SignatureBlock/index.js +30 -29
- package/runtime/components/SignatureBlock/index.module.js +9 -6
- package/runtime/components/SignatureCode/{index_module.css → index.css} +9 -10
- package/runtime/components/SignatureCode/index.js +40 -32
- package/runtime/components/SignatureCode/index.module.js +9 -6
- package/runtime/components/SignatureToolbar/{index_module.css → index.css} +18 -20
- package/runtime/components/SignatureToolbar/index.js +48 -51
- package/runtime/components/SignatureToolbar/index.module.js +13 -10
- package/runtime/components/buttons/ButtonGroup.js +13 -6
- package/runtime/components/buttons/CopyCodeButton.js +40 -38
- package/runtime/components/buttons/WrapSignatureButton.js +20 -16
- package/runtime/components/buttons/{index_module.css → index.css} +5 -6
- package/runtime/components/buttons/index.module.js +8 -5
- package/runtime/components/icons/CheckIcon/index.js +20 -17
- package/runtime/components/icons/CopyIcon/index.js +20 -17
- package/runtime/components/icons/UnwrapIcon/index.js +22 -18
- package/runtime/components/icons/WrapIcon/index.js +20 -17
- package/runtime/components/shared/_twoslash.css +3 -10
- package/runtime/components/shared/variables.css +0 -3
- package/runtime/hooks/useWrapToggle.js +32 -9
- package/runtime/index.d.ts +513 -173
- package/runtime/index.js +11 -9
- package/runtime/utils/decode-hast.js +31 -16
- package/runtime/utils/hast-renderer.js +21 -7
- package/schemas/config.js +199 -0
- package/schemas/index.js +5 -0
- package/schemas/opengraph.js +26 -0
- package/schemas/performance.js +19 -0
- package/serve.js +133 -0
- package/services/ConfigService.js +7 -0
- package/services/PathDerivationService.js +7 -0
- package/services/SnapshotService.js +7 -0
- package/services/TypeRegistryService.js +7 -0
- package/shiki-transformer.js +758 -0
- package/tsconfig-parser.js +127 -0
- package/tsdoc-metadata.json +11 -11
- package/twoslash-patterns.js +87 -0
- package/twoslash-transformer.js +316 -0
- package/type-reference-extractor.js +201 -0
- package/typescript-config.js +168 -0
- package/vfs-registry.js +121 -0
- package/0~llms-program.js +0 -344
- 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 };
|