rspress-plugin-api-extractor 0.10.0 → 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.
- package/BuildEnv.js +58 -0
- package/build-program.js +33 -31
- package/build-stages.js +47 -39
- package/errors.js +0 -1
- package/layers/ConfigServiceLive.js +339 -433
- package/layers/HighlighterServiceLive.js +52 -0
- package/layers/OgServiceLive.js +134 -0
- package/layers/TwoslashCacheServiceLive.js +74 -19
- package/layers/TwoslashEnvironmentsLive.js +33 -0
- package/layers/TypeRegistryServiceLive.js +54 -47
- package/layers/xdg.js +44 -0
- package/markdown/helpers.js +9 -55
- package/markdown/page-generators/class-page.js +8 -31
- package/markdown/page-generators/index-pages.js +6 -8
- package/markdown/page-generators/interface-page.js +7 -7
- package/markdown/shiki-utils.js +65 -10
- package/observability/EventBus.js +29 -7
- package/observability/spans.js +3 -1
- package/observability/sync-emitter.js +78 -0
- package/og-resolver.js +46 -287
- package/package.json +2 -3
- package/path-derivation.js +19 -1
- package/plugin.js +48 -73
- package/prettier-formatter.js +4 -10
- package/remark-api-codeblocks.js +10 -18
- package/remark-with-api.js +11 -21
- package/services/HighlighterService.js +30 -0
- package/services/OgService.js +23 -0
- package/services/PluginConfig.js +26 -0
- package/services/TwoslashEnvironments.js +7 -0
- package/shiki-transformer.js +53 -234
- package/twoslash-access.js +48 -0
- package/twoslash-transformer.js +106 -83
- package/vfs-registry.js +1 -31
- package/layers/PathDerivationServiceLive.js +0 -16
- package/services/PathDerivationService.js +0 -7
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { PluginEvent } from "../observability/events.js";
|
|
2
|
+
import { emit } from "../observability/EventBus.js";
|
|
3
|
+
import { HighlighterService } from "../services/HighlighterService.js";
|
|
4
|
+
import { SHIKI_LANGS } from "../markdown/shiki-utils.js";
|
|
5
|
+
import { Effect, Layer } from "effect";
|
|
6
|
+
import { createHighlighter } from "shiki";
|
|
7
|
+
|
|
8
|
+
//#region src/layers/HighlighterServiceLive.ts
|
|
9
|
+
/**
|
|
10
|
+
* Acquire the build's highlighter, and release it when the runtime is
|
|
11
|
+
* disposed.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* `Layer.effect` over `Effect.acquireRelease` is the v4 scoped-constructor
|
|
15
|
+
* idiom (`Layer.scoped` is gone; `Layer.effect` strips `Scope` from `R`).
|
|
16
|
+
* Because the layer sits in the `ManagedRuntime`'s stack, the highlighter is
|
|
17
|
+
* created on the runtime's first use and `dispose()`d by
|
|
18
|
+
* `effectRuntime.dispose()` — which `plugin.ts` calls on production builds
|
|
19
|
+
* only, so a dev HMR session keeps one highlighter across rebuilds instead of
|
|
20
|
+
* leaking one per rebuild.
|
|
21
|
+
*
|
|
22
|
+
* **Bind the result to a `const`.** This is a layer FACTORY: each call mints a
|
|
23
|
+
* fresh layer reference, and layers memoize by reference, so calling it twice
|
|
24
|
+
* in one graph acquires two highlighters — the exact leak this layer exists to
|
|
25
|
+
* fix.
|
|
26
|
+
*
|
|
27
|
+
* The themes are passed in rather than read from a resolved build context
|
|
28
|
+
* because the layer builds before `ConfigService.resolve()` runs. Passing them
|
|
29
|
+
* as an argument rather than through a `Context.Reference` is deliberate: a
|
|
30
|
+
* Reference carries a default, so forgetting to wire it would silently load
|
|
31
|
+
* only the default themes and render every custom-themed block wrong. A
|
|
32
|
+
* missing argument is a type error.
|
|
33
|
+
*/
|
|
34
|
+
function HighlighterServiceLive(themes) {
|
|
35
|
+
return Layer.effect(HighlighterService, Effect.gen(function* () {
|
|
36
|
+
const startedMs = performance.now();
|
|
37
|
+
const highlighter = yield* Effect.acquireRelease(Effect.promise(() => createHighlighter({
|
|
38
|
+
themes: [...themes],
|
|
39
|
+
langs: [...SHIKI_LANGS]
|
|
40
|
+
})), (instance) => Effect.sync(() => instance.dispose()));
|
|
41
|
+
yield* emit(PluginEvent.PhaseCompleted({
|
|
42
|
+
ctx: {},
|
|
43
|
+
level: "debug",
|
|
44
|
+
phase: "shikiInit",
|
|
45
|
+
durationMs: Math.round(performance.now() - startedMs)
|
|
46
|
+
}));
|
|
47
|
+
return { highlighter };
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { HighlighterServiceLive };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { PluginEvent } from "../observability/events.js";
|
|
2
|
+
import { emit } from "../observability/EventBus.js";
|
|
3
|
+
import { imageMimeType, ogAltText, resolveOgUrl } from "../og-resolver.js";
|
|
4
|
+
import { OgImageError, OgService } from "../services/OgService.js";
|
|
5
|
+
import { Effect, FileSystem, Layer, Option, Path } from "effect";
|
|
6
|
+
import { imageSize } from "image-size";
|
|
7
|
+
|
|
8
|
+
//#region src/layers/OgServiceLive.ts
|
|
9
|
+
/**
|
|
10
|
+
* Resolve OG images through the core `FileSystem`, with one read per file per
|
|
11
|
+
* build.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* The `node:fs` `existsSync` + `imageSizeFromFile` pair this replaces ran once
|
|
15
|
+
* per PAGE, so a 400-page API re-read the same image 400 times. The memo below
|
|
16
|
+
* keys on the absolute path and removes that entirely.
|
|
17
|
+
*
|
|
18
|
+
* The memo is per build, not persisted. A cross-build cache in the shared XDG
|
|
19
|
+
* store was considered and deliberately deferred: it would need mtime/size
|
|
20
|
+
* invalidation to stay sound, and a stale image dimension is a silent wrong
|
|
21
|
+
* answer. There is nothing expensive enough here to justify that yet — when
|
|
22
|
+
* phase 4 starts GENERATING images, which are expensive and content-addressed,
|
|
23
|
+
* the XDG cache is the right home for them.
|
|
24
|
+
*
|
|
25
|
+
* `imageSize` over the read bytes replaces `imageSizeFromFile`, which took a
|
|
26
|
+
* path and therefore required real `node:fs`. Same parser, same output.
|
|
27
|
+
*/
|
|
28
|
+
const OgServiceLive = Layer.effect(OgService, Effect.gen(function* () {
|
|
29
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
30
|
+
const path = yield* Path.Path;
|
|
31
|
+
/** Absolute path → facts, or `null` for "looked, could not use it". */
|
|
32
|
+
const factsByPath = /* @__PURE__ */ new Map();
|
|
33
|
+
/** Locate a root-relative image under the docs `public/` directory. */
|
|
34
|
+
const findLocalImage = (imagePath, docsRoot) => {
|
|
35
|
+
if (docsRoot == null || !imagePath.startsWith("/")) return Effect.succeed(Option.none());
|
|
36
|
+
const candidate = path.join(docsRoot, "public", imagePath);
|
|
37
|
+
return fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false), Effect.map((found) => found ? Option.some(candidate) : Option.none()));
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Read dimensions and MIME type. A file that cannot be parsed warns and
|
|
41
|
+
* yields nothing — the page still gets its `og:image`, just without
|
|
42
|
+
* dimensions, which is what the class this replaced did.
|
|
43
|
+
*/
|
|
44
|
+
const readImageFacts = (filePath) => Effect.gen(function* () {
|
|
45
|
+
const memoed = factsByPath.get(filePath);
|
|
46
|
+
if (memoed !== void 0) return memoed;
|
|
47
|
+
const result = yield* Effect.result(fileSystem.readFile(filePath).pipe(Effect.flatMap((bytes) => Effect.try(() => imageSize(bytes)))));
|
|
48
|
+
if (result._tag === "Failure") {
|
|
49
|
+
const error = new OgImageError({
|
|
50
|
+
code: "unreadable-image",
|
|
51
|
+
field: "ogImage",
|
|
52
|
+
value: filePath,
|
|
53
|
+
cause: result.failure
|
|
54
|
+
});
|
|
55
|
+
yield* emit(PluginEvent.ConfigValidationWarning({
|
|
56
|
+
ctx: {},
|
|
57
|
+
field: "ogImage",
|
|
58
|
+
value: filePath,
|
|
59
|
+
reason: error.message,
|
|
60
|
+
level: "warn"
|
|
61
|
+
}));
|
|
62
|
+
factsByPath.set(filePath, null);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const size = result.success;
|
|
66
|
+
const mimeType = imageMimeType(size.type);
|
|
67
|
+
const facts = {
|
|
68
|
+
...size.width != null ? { width: size.width } : {},
|
|
69
|
+
...size.height != null ? { height: size.height } : {},
|
|
70
|
+
...mimeType != null ? { type: mimeType } : {}
|
|
71
|
+
};
|
|
72
|
+
factsByPath.set(filePath, facts);
|
|
73
|
+
return facts;
|
|
74
|
+
});
|
|
75
|
+
const resolveFromString = (imageUrl, request) => Effect.gen(function* () {
|
|
76
|
+
const resolvedUrl = resolveOgUrl(request.siteUrl, imageUrl);
|
|
77
|
+
if (resolvedUrl == null) return yield* Effect.fail(new OgImageError({
|
|
78
|
+
code: "invalid-url",
|
|
79
|
+
field: "ogImage",
|
|
80
|
+
value: imageUrl
|
|
81
|
+
}));
|
|
82
|
+
const localPath = yield* findLocalImage(imageUrl, request.docsRoot);
|
|
83
|
+
const facts = Option.isSome(localPath) ? yield* readImageFacts(localPath.value) : null;
|
|
84
|
+
return Option.some({
|
|
85
|
+
url: resolvedUrl,
|
|
86
|
+
type: facts?.type,
|
|
87
|
+
width: facts?.width,
|
|
88
|
+
height: facts?.height,
|
|
89
|
+
alt: ogAltText(request.packageName, request.apiName)
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
const resolveFromMetadata = (metadata, request) => Effect.gen(function* () {
|
|
93
|
+
const { url, secureUrl, type, width, height, alt } = metadata;
|
|
94
|
+
const resolvedUrl = resolveOgUrl(request.siteUrl, url);
|
|
95
|
+
if (resolvedUrl == null) return yield* Effect.fail(new OgImageError({
|
|
96
|
+
code: "invalid-url",
|
|
97
|
+
field: "ogImage.url",
|
|
98
|
+
value: url
|
|
99
|
+
}));
|
|
100
|
+
let resolvedSecureUrl;
|
|
101
|
+
if (secureUrl != null) {
|
|
102
|
+
if (secureUrl.startsWith("https://")) resolvedSecureUrl = secureUrl;
|
|
103
|
+
else {
|
|
104
|
+
const error = new OgImageError({
|
|
105
|
+
code: "invalid-secure-url",
|
|
106
|
+
field: "ogImage.secureUrl",
|
|
107
|
+
value: secureUrl
|
|
108
|
+
});
|
|
109
|
+
yield* emit(PluginEvent.ConfigValidationWarning({
|
|
110
|
+
ctx: {},
|
|
111
|
+
field: "ogImage.secureUrl",
|
|
112
|
+
value: secureUrl,
|
|
113
|
+
reason: error.message,
|
|
114
|
+
level: "warn"
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return Option.some({
|
|
119
|
+
url: resolvedUrl,
|
|
120
|
+
secureUrl: resolvedSecureUrl,
|
|
121
|
+
type,
|
|
122
|
+
width,
|
|
123
|
+
height,
|
|
124
|
+
alt: alt ?? ogAltText(request.packageName, request.apiName)
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
return { resolveImage: (request) => {
|
|
128
|
+
if (request.config == null) return Effect.succeed(Option.none());
|
|
129
|
+
return typeof request.config === "object" ? resolveFromMetadata(request.config, request) : resolveFromString(request.config, request);
|
|
130
|
+
} };
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
export { OgServiceLive };
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import { TwoslashCacheService } from "../services/TwoslashCacheService.js";
|
|
2
|
-
import { decodeTwoslashCache, encodeTwoslashCache, twoslashBlobKey } from "../twoslash-cache.js";
|
|
3
|
-
import {
|
|
2
|
+
import { decodeTwoslashCache, encodeTwoslashCache, makeTwoslashCache, twoslashBlobKey } from "../twoslash-cache.js";
|
|
3
|
+
import { AppDirsLive, PlatformLive } from "./xdg.js";
|
|
4
4
|
import { Effect, Layer, Option, Path } from "effect";
|
|
5
5
|
import { Cache } from "@effected/store";
|
|
6
|
-
import { AppDirs
|
|
6
|
+
import { AppDirs } from "@effected/xdg";
|
|
7
7
|
|
|
8
8
|
//#region src/layers/TwoslashCacheServiceLive.ts
|
|
9
|
-
const PlatformLive = Layer.mergeAll(NodeFileSystem.layer, Path.layer);
|
|
10
|
-
/**
|
|
11
|
-
* XDG app dirs under the same `tsdoctor` namespace the type registry uses, so
|
|
12
|
-
* every derived-artifact cache this plugin keeps lives in one place.
|
|
13
|
-
*/
|
|
14
|
-
const AppDirsLive = AppDirs.layer({ namespace: "tsdoctor" }).pipe(Layer.provide(Layer.mergeAll(Xdg.layer, PlatformLive)));
|
|
15
9
|
/**
|
|
16
10
|
* A sqlite-backed `@effected/store` Cache in the XDG cache dir, separate from
|
|
17
11
|
* the registry's `metadata.sqlite`.
|
|
@@ -35,19 +29,80 @@ const CacheLive = Layer.unwrap(Effect.gen(function* () {
|
|
|
35
29
|
* build to "type-check everything", which is exactly the behaviour before this
|
|
36
30
|
* cache existed.
|
|
37
31
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Add the build-generation half of the service on top of a load/save pair.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Shared by the real and degraded layers so the two cannot drift: a degraded
|
|
37
|
+
* build must still hand out a working in-memory cache, otherwise the
|
|
38
|
+
* transformers have nothing to read or write and the render pass changes
|
|
39
|
+
* shape rather than merely losing persistence.
|
|
40
|
+
*/
|
|
41
|
+
function withGeneration(base) {
|
|
42
|
+
let open = null;
|
|
43
|
+
return {
|
|
44
|
+
...base,
|
|
45
|
+
open: (envHash) => base.load(envHash).pipe(Effect.map((restored) => {
|
|
46
|
+
const cache = makeTwoslashCache(restored);
|
|
47
|
+
open = {
|
|
48
|
+
cache,
|
|
49
|
+
envHash
|
|
50
|
+
};
|
|
51
|
+
return cache;
|
|
52
|
+
})),
|
|
53
|
+
persist: () => Effect.suspend(() => {
|
|
54
|
+
if (open === null) return Effect.succeed(Option.none());
|
|
55
|
+
const { cache, envHash } = open;
|
|
56
|
+
const stats = cache.stats();
|
|
57
|
+
const report = Option.some({
|
|
58
|
+
...stats,
|
|
59
|
+
envHash
|
|
60
|
+
});
|
|
61
|
+
return stats.dirty ? base.save(envHash, cache.entries()).pipe(Effect.as(report)) : Effect.succeed(report);
|
|
62
|
+
})
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const CacheBackedLive = Layer.effect(TwoslashCacheService, Effect.gen(function* () {
|
|
66
|
+
const cache = yield* Cache;
|
|
67
|
+
return withGeneration({
|
|
68
|
+
load: (envHash) => cache.get(twoslashBlobKey(envHash)).pipe(Effect.map((entry) => Option.isSome(entry) ? decodeTwoslashCache(entry.value.value) : /* @__PURE__ */ new Map()), Effect.catch(() => Effect.succeed(/* @__PURE__ */ new Map()))),
|
|
69
|
+
save: (envHash, entries) => cache.set({
|
|
45
70
|
key: twoslashBlobKey(envHash),
|
|
46
71
|
value: encodeTwoslashCache(entries),
|
|
47
72
|
tags: ["twoslash"]
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
});
|
|
73
|
+
}).pipe(Effect.catch(() => Effect.void))
|
|
74
|
+
});
|
|
75
|
+
})).pipe(Layer.provide(CacheLive));
|
|
76
|
+
/**
|
|
77
|
+
* A cache that holds nothing, for when the real one cannot be opened.
|
|
78
|
+
*
|
|
79
|
+
* @remarks
|
|
80
|
+
* `load` returns empty and `save` discards, which is precisely the behaviour
|
|
81
|
+
* before this cache existed: type-check everything, persist nothing.
|
|
82
|
+
*/
|
|
83
|
+
const DegradedLive = Layer.succeed(TwoslashCacheService, withGeneration({
|
|
84
|
+
load: () => Effect.succeed(/* @__PURE__ */ new Map()),
|
|
85
|
+
save: () => Effect.void
|
|
86
|
+
}));
|
|
87
|
+
/**
|
|
88
|
+
* Live Twoslash cache persistence.
|
|
89
|
+
*
|
|
90
|
+
* @remarks
|
|
91
|
+
* Failure is absorbed at TWO levels, and both are load-bearing. Inside the
|
|
92
|
+
* service, a failed read or write degrades that one operation. Around the
|
|
93
|
+
* layer, a failed CONSTRUCTION — no HOME for XDG, an unwritable cache
|
|
94
|
+
* directory, a corrupt database — degrades to {@link DegradedLive}.
|
|
95
|
+
*
|
|
96
|
+
* The second is why `Layer.catchCause` wraps this at all. While the sqlite
|
|
97
|
+
* layer was provided inside each method, a construction failure surfaced as
|
|
98
|
+
* that method's failure and the in-method handler swallowed it. Hoisting
|
|
99
|
+
* acquisition to layer construction moved the failure to `ManagedRuntime`
|
|
100
|
+
* build time, where it would abort the entire build — breaking the contract
|
|
101
|
+
* this service documents, that an unreachable cache must never fail a build
|
|
102
|
+
* that would otherwise succeed. `catchCause` rather than a failure-only catch
|
|
103
|
+
* because a defect thrown by the sqlite driver must degrade too.
|
|
104
|
+
*/
|
|
105
|
+
const TwoslashCacheServiceLive = CacheBackedLive.pipe(Layer.catchCause(() => DegradedLive));
|
|
51
106
|
|
|
52
107
|
//#endregion
|
|
53
108
|
export { TwoslashCacheServiceLive };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TwoslashEnvironments } from "../services/TwoslashEnvironments.js";
|
|
2
|
+
import { TwoslashEnvironmentRegistry } from "../twoslash-transformer.js";
|
|
3
|
+
import { Layer } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/layers/TwoslashEnvironmentsLive.ts
|
|
6
|
+
/**
|
|
7
|
+
* One environment registry per runtime.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* `Layer.sync` rather than `Layer.succeed`: the registry is mutable, and a
|
|
11
|
+
* `Layer.succeed` would capture a single instance shared by every layer graph
|
|
12
|
+
* that referenced this const — including, in a test run, every test file in
|
|
13
|
+
* the process. Building it when the layer builds is what makes substitution
|
|
14
|
+
* work: a test that wants an isolated registry provides its own layer, which
|
|
15
|
+
* is what the old static `TwoslashManager.reset()` was standing in for.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately NOT `Layer.effect` with a finalizer. The registry holds Shiki
|
|
18
|
+
* transformers, which the render pass uses AFTER `config()` returns — the same
|
|
19
|
+
* lifetime constraint the highlighter has, and for the same reason.
|
|
20
|
+
*/
|
|
21
|
+
const TwoslashEnvironmentsLive = Layer.sync(TwoslashEnvironments, () => {
|
|
22
|
+
const registry = new TwoslashEnvironmentRegistry();
|
|
23
|
+
return {
|
|
24
|
+
registerEnvironment: (options) => registry.registerEnvironment(options),
|
|
25
|
+
registerScope: (apiScope, compilerOptions) => registry.registerScope(apiScope, compilerOptions),
|
|
26
|
+
transformerFor: (apiScope) => registry.transformerFor(apiScope),
|
|
27
|
+
setCurrentFile: (path) => registry.setCurrentFile(path),
|
|
28
|
+
reportErrorForTest: (error, code, file) => registry.reportErrorForTest(error, code, file)
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { TwoslashEnvironmentsLive };
|
|
@@ -3,11 +3,12 @@ import { emit } from "../observability/EventBus.js";
|
|
|
3
3
|
import { resolveExternalPackageVersions } from "../config-utils.js";
|
|
4
4
|
import { TypeRegistryError } from "../errors.js";
|
|
5
5
|
import { TypeRegistryService } from "../services/TypeRegistryService.js";
|
|
6
|
-
import {
|
|
6
|
+
import { AppDirsLive, PlatformLive } from "./xdg.js";
|
|
7
|
+
import { NodeHttpClient } from "@effect/platform-node";
|
|
7
8
|
import { Duration, Effect, Layer, Path } from "effect";
|
|
8
9
|
import { PackageFetcher, PackageSpec, RegistryObserver, TypeCache, TypeRegistry } from "@tsdoctor/registry";
|
|
9
10
|
import { Cache } from "@effected/store";
|
|
10
|
-
import { AppDirs
|
|
11
|
+
import { AppDirs } from "@effected/xdg";
|
|
11
12
|
|
|
12
13
|
//#region src/layers/TypeRegistryServiceLive.ts
|
|
13
14
|
/**
|
|
@@ -22,19 +23,13 @@ import { AppDirs, Xdg } from "@effected/xdg";
|
|
|
22
23
|
const RegistryObserverLayer = Layer.succeed(RegistryObserver, { emit: (event) => {
|
|
23
24
|
switch (event._tag) {
|
|
24
25
|
case "VersionResolved": return emit(PluginEvent.TypeRegistryEvent({
|
|
25
|
-
ctx: {
|
|
26
|
-
buildId: "",
|
|
27
|
-
packageName: event.package
|
|
28
|
-
},
|
|
26
|
+
ctx: { packageName: event.package },
|
|
29
27
|
level: "debug",
|
|
30
28
|
kind: "VersionResolved",
|
|
31
29
|
detail: `${event.requested} -> ${event.resolved}`
|
|
32
30
|
}));
|
|
33
31
|
case "VersionResolveFailed": return emit(PluginEvent.TypeRegistryEvent({
|
|
34
|
-
ctx: {
|
|
35
|
-
buildId: "",
|
|
36
|
-
packageName: event.package
|
|
37
|
-
},
|
|
32
|
+
ctx: { packageName: event.package },
|
|
38
33
|
level: "debug",
|
|
39
34
|
kind: "VersionResolveFailed",
|
|
40
35
|
detail: `${event.requested}: ${event.kind}`
|
|
@@ -43,7 +38,6 @@ const RegistryObserverLayer = Layer.succeed(RegistryObserver, { emit: (event) =>
|
|
|
43
38
|
case "CacheMiss":
|
|
44
39
|
case "FetchStart": return emit(PluginEvent.TypeRegistryEvent({
|
|
45
40
|
ctx: {
|
|
46
|
-
buildId: "",
|
|
47
41
|
packageName: event.package,
|
|
48
42
|
version: event.version
|
|
49
43
|
},
|
|
@@ -53,7 +47,6 @@ const RegistryObserverLayer = Layer.succeed(RegistryObserver, { emit: (event) =>
|
|
|
53
47
|
}));
|
|
54
48
|
case "CacheStale": return emit(PluginEvent.TypeRegistryEvent({
|
|
55
49
|
ctx: {
|
|
56
|
-
buildId: "",
|
|
57
50
|
packageName: event.package,
|
|
58
51
|
version: event.version
|
|
59
52
|
},
|
|
@@ -62,14 +55,13 @@ const RegistryObserverLayer = Layer.succeed(RegistryObserver, { emit: (event) =>
|
|
|
62
55
|
detail: ""
|
|
63
56
|
}));
|
|
64
57
|
case "FetchFailed": return emit(PluginEvent.TypeRegistryEvent({
|
|
65
|
-
ctx: {
|
|
58
|
+
ctx: {},
|
|
66
59
|
level: "debug",
|
|
67
60
|
kind: "FetchFailed",
|
|
68
61
|
detail: `HTTP ${event.status}: ${event.url}${event.bodySnippet ? ` — ${event.bodySnippet}` : ""}`
|
|
69
62
|
}));
|
|
70
63
|
case "PackageLoaded": return emit(PluginEvent.TypeRegistryEvent({
|
|
71
64
|
ctx: {
|
|
72
|
-
buildId: "",
|
|
73
65
|
packageName: event.package,
|
|
74
66
|
version: event.version
|
|
75
67
|
},
|
|
@@ -79,7 +71,6 @@ const RegistryObserverLayer = Layer.succeed(RegistryObserver, { emit: (event) =>
|
|
|
79
71
|
}));
|
|
80
72
|
case "PackageLoadFailed": return emit(PluginEvent.TypeRegistryEvent({
|
|
81
73
|
ctx: {
|
|
82
|
-
buildId: "",
|
|
83
74
|
packageName: event.package,
|
|
84
75
|
version: event.version
|
|
85
76
|
},
|
|
@@ -88,35 +79,19 @@ const RegistryObserverLayer = Layer.succeed(RegistryObserver, { emit: (event) =>
|
|
|
88
79
|
detail: `[${event.kind}] ${event.error instanceof Error ? event.error.message : String(event.error)}`
|
|
89
80
|
}));
|
|
90
81
|
case "BatchStart": return emit(PluginEvent.TypeRegistryEvent({
|
|
91
|
-
ctx: {
|
|
82
|
+
ctx: {},
|
|
92
83
|
level: "debug",
|
|
93
84
|
kind: "BatchStart",
|
|
94
85
|
detail: `${event.total} package(s)`
|
|
95
86
|
}));
|
|
96
87
|
case "BatchComplete": return emit(PluginEvent.TypeRegistryEvent({
|
|
97
|
-
ctx: {
|
|
88
|
+
ctx: {},
|
|
98
89
|
level: "info",
|
|
99
90
|
kind: "BatchComplete",
|
|
100
91
|
detail: `${event.loaded}/${event.total} packages, ${event.totalFiles} files, ${Math.round(Duration.toMillis(event.duration))}ms`
|
|
101
92
|
}));
|
|
102
93
|
}
|
|
103
94
|
} });
|
|
104
|
-
/**
|
|
105
|
-
* @tsdoctor/registry composes at the edge: the library ships no platform
|
|
106
|
-
* layer of its own, so the plugin wires FileSystem/Path, the XDG directories,
|
|
107
|
-
* the sqlite metadata Cache and the HTTP client here.
|
|
108
|
-
*
|
|
109
|
-
* All layers are bound to module-level consts (never rebuilt per call) per the
|
|
110
|
-
* v4 layer memoization discipline.
|
|
111
|
-
*/
|
|
112
|
-
const PlatformLive = Layer.mergeAll(NodeFileSystem.layer, Path.layer);
|
|
113
|
-
/**
|
|
114
|
-
* XDG app directories under the tsdoctor-wide namespace. Renamed from the
|
|
115
|
-
* legacy "type-registry-effect" namespace in phase 2 per the resolved identity
|
|
116
|
-
* decision (see tsdoctor-package-architecture.md) — a deliberate one-time
|
|
117
|
-
* on-disk cache invalidation: existing caches go cold and refetch.
|
|
118
|
-
*/
|
|
119
|
-
const AppDirsLive = AppDirs.layer({ namespace: "tsdoctor" }).pipe(Layer.provide(Layer.mergeAll(Xdg.layer, PlatformLive)));
|
|
120
95
|
/** Metadata plane: a sqlite-backed `@effected/store` Cache rooted in the XDG cache dir. */
|
|
121
96
|
const MetadataCacheLive = Layer.unwrap(Effect.gen(function* () {
|
|
122
97
|
const appDirs = yield* AppDirs;
|
|
@@ -133,23 +108,55 @@ const RegistryLayer = TypeRegistry.layer.pipe(Layer.provideMerge(Layer.mergeAll(
|
|
|
133
108
|
/**
|
|
134
109
|
* TypeRegistryServiceLive: uses @tsdoctor/registry Effect programs directly.
|
|
135
110
|
*/
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
111
|
+
const RegistryBackedLive = Layer.effect(TypeRegistryService, Effect.gen(function* () {
|
|
112
|
+
const registry = yield* TypeRegistry;
|
|
113
|
+
return {
|
|
114
|
+
resolveVersions: (packages) => resolveExternalPackageVersions(packages, (pkg) => registry.resolveVersion(pkg.name, pkg.version)).pipe(Effect.catch(() => Effect.succeed([...packages]))),
|
|
115
|
+
loadPackages: (packages) => packages.length === 0 ? Effect.succeed({ vfs: /* @__PURE__ */ new Map() }) : Effect.gen(function* () {
|
|
116
|
+
const specs = packages.map((pkg) => new PackageSpec({
|
|
117
|
+
name: pkg.name,
|
|
118
|
+
version: pkg.version
|
|
119
|
+
}));
|
|
120
|
+
return { vfs: yield* registry.getVfs(specs, { autoFetch: true }) };
|
|
121
|
+
}).pipe(Effect.catch((error) => Effect.fail(new TypeRegistryError({
|
|
122
|
+
packageName: packages.map((p) => p.name).join(", "),
|
|
123
|
+
version: packages.map((p) => p.version).join(", "),
|
|
124
|
+
reason: error instanceof Error ? error.message ?? String(error) : String(error)
|
|
125
|
+
}))))
|
|
126
|
+
};
|
|
127
|
+
})).pipe(Layer.provide(RegistryLayer));
|
|
128
|
+
/**
|
|
129
|
+
* The service when the registry stack cannot be built at all.
|
|
130
|
+
*
|
|
131
|
+
* @remarks
|
|
132
|
+
* Preserves exactly the split the working service documents: `resolveVersions`
|
|
133
|
+
* passes its specs through unresolved rather than swallowing the problem, so
|
|
134
|
+
* the failure surfaces from `loadPackages` as a {@link PluginTypeRegistryError}
|
|
135
|
+
* with a message, which `ConfigServiceLive` turns into a build-continues
|
|
136
|
+
* warning. Before acquisition moved to layer construction this fell out of the
|
|
137
|
+
* per-method handlers; it has to be stated explicitly now.
|
|
138
|
+
*/
|
|
139
|
+
const DegradedLive = Layer.succeed(TypeRegistryService, {
|
|
140
|
+
resolveVersions: (packages) => Effect.succeed([...packages]),
|
|
141
|
+
loadPackages: (packages) => Effect.fail(new TypeRegistryError({
|
|
148
142
|
packageName: packages.map((p) => p.name).join(", "),
|
|
149
143
|
version: packages.map((p) => p.version).join(", "),
|
|
150
|
-
reason:
|
|
151
|
-
}))
|
|
144
|
+
reason: "type registry unavailable: its cache directory or metadata database could not be opened"
|
|
145
|
+
}))
|
|
152
146
|
});
|
|
147
|
+
/**
|
|
148
|
+
* TypeRegistryServiceLive: the `@tsdoctor/registry` stack, acquired once.
|
|
149
|
+
*
|
|
150
|
+
* @remarks
|
|
151
|
+
* `Layer.catchCause` keeps a broken environment — no HOME for XDG, an
|
|
152
|
+
* unwritable cache directory — from aborting the build at `ManagedRuntime`
|
|
153
|
+
* construction. External type loading is an enhancement: without it code
|
|
154
|
+
* blocks render without Twoslash enrichment, which is a degradation, not a
|
|
155
|
+
* failure. That was true while the stack was provided per method and the
|
|
156
|
+
* in-method handlers absorbed it; hoisting acquisition made it something the
|
|
157
|
+
* layer has to say for itself.
|
|
158
|
+
*/
|
|
159
|
+
const TypeRegistryServiceLive = RegistryBackedLive.pipe(Layer.catchCause(() => DegradedLive));
|
|
153
160
|
|
|
154
161
|
//#endregion
|
|
155
162
|
export { TypeRegistryServiceLive };
|
package/layers/xdg.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NodeFileSystem } from "@effect/platform-node";
|
|
2
|
+
import { Layer, Path } from "effect";
|
|
3
|
+
import { AppDirs, Xdg } from "@effected/xdg";
|
|
4
|
+
|
|
5
|
+
//#region src/layers/xdg.ts
|
|
6
|
+
/**
|
|
7
|
+
* The plugin's shared platform and XDG layers.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Both cache-backed services — the type registry and the Twoslash result cache
|
|
11
|
+
* — need a Node platform and an XDG app-directory root, and both used to
|
|
12
|
+
* declare their own. Two consequences, both fixed by having one home:
|
|
13
|
+
*
|
|
14
|
+
* 1. **Two distinct layer references build twice.** Layer memoization is by
|
|
15
|
+
* reference, so a second `Layer.mergeAll(NodeFileSystem.layer, Path.layer)`
|
|
16
|
+
* is a different layer as far as the memo map is concerned, and the XDG
|
|
17
|
+
* resolution ran once per consumer.
|
|
18
|
+
* 2. **The namespace literal was copy-pasted.** The house style bans exactly
|
|
19
|
+
* this: when two sibling layers must agree on an identity string, the
|
|
20
|
+
* agreement has to be structural rather than textual. A drift here is
|
|
21
|
+
* silent and permanent — the caches move to a different directory, every
|
|
22
|
+
* lookup misses, and a build that should hit a warm Twoslash cache goes
|
|
23
|
+
* cold forever with no error and nothing in the output to notice.
|
|
24
|
+
*
|
|
25
|
+
* @packageDocumentation
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* The XDG namespace every cache this plugin keeps lives under.
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* One definition, deliberately. Changing it invalidates every on-disk cache —
|
|
32
|
+
* the type registry's `metadata.sqlite` and the Twoslash result cache's
|
|
33
|
+
* `twoslash.sqlite` — which is a cold refetch and a full re-type-check, not an
|
|
34
|
+
* error. That was accepted once, at the phase-2 rename from
|
|
35
|
+
* `type-registry-effect`; do not do it casually.
|
|
36
|
+
*/
|
|
37
|
+
const TSDOCTOR_NAMESPACE = "tsdoctor";
|
|
38
|
+
/** Node platform services: the filesystem and path implementations. */
|
|
39
|
+
const PlatformLive = Layer.mergeAll(NodeFileSystem.layer, Path.layer);
|
|
40
|
+
/** XDG application directories rooted at {@link TSDOCTOR_NAMESPACE}. */
|
|
41
|
+
const AppDirsLive = AppDirs.layer({ namespace: TSDOCTOR_NAMESPACE }).pipe(Layer.provide(Layer.mergeAll(Xdg.layer, PlatformLive)));
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { AppDirsLive, PlatformLive, TSDOCTOR_NAMESPACE };
|
package/markdown/helpers.js
CHANGED
|
@@ -4,7 +4,6 @@ import { formatCode } from "../prettier-formatter.js";
|
|
|
4
4
|
import { TypeReferenceExtractor } from "../type-reference-extractor.js";
|
|
5
5
|
|
|
6
6
|
//#region src/markdown/helpers.ts
|
|
7
|
-
/* v8 ignore start -- markdown generation helpers, tested via page generator integration tests */
|
|
8
7
|
/**
|
|
9
8
|
* Helper utilities for generating markdown API documentation.
|
|
10
9
|
*
|
|
@@ -50,63 +49,18 @@ function prepareExampleCode(example, apiItemName, packageName, suppressErrors =
|
|
|
50
49
|
};
|
|
51
50
|
}
|
|
52
51
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* @returns URL-safe ID string
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* ```ts
|
|
65
|
-
* sanitizeId("myMethod"); // "mymethod"
|
|
66
|
-
* sanitizeId("get value"); // "get-value"
|
|
67
|
-
* sanitizeId("run", "static"); // "static-run"
|
|
68
|
-
* ```
|
|
69
|
-
*/
|
|
70
|
-
function sanitizeId(displayName, prefix = "") {
|
|
71
|
-
const baseName = displayName.replace(/["']/g, "").replace(/[^\w-]/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
72
|
-
return prefix ? `${prefix}-${baseName}` : baseName;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Escape a YAML string value by handling special characters.
|
|
76
|
-
*
|
|
77
|
-
* Normalizes whitespace and wraps strings in double quotes if they contain
|
|
78
|
-
* characters that could break YAML parsing (colons, quotes, hashes, pipes,
|
|
79
|
-
* brackets, braces, Unicode characters, etc.).
|
|
80
|
-
*
|
|
81
|
-
* @param value - The string value to escape
|
|
82
|
-
* @returns YAML-safe string
|
|
83
|
-
*
|
|
84
|
-
* @example
|
|
85
|
-
* ```ts
|
|
86
|
-
* escapeYamlString("Hello World"); // "Hello World"
|
|
87
|
-
* escapeYamlString("Type: string"); // "\"Type: string\""
|
|
88
|
-
* escapeYamlString("He said \"hello\""); // "\"He said \\\"hello\\\"\""
|
|
89
|
-
* escapeYamlString("@pkg/name。:"); // "\"@pkg/name。:\""
|
|
90
|
-
* ```
|
|
91
|
-
*/
|
|
92
|
-
/**
|
|
93
|
-
* Normalize a string for use as a YAML frontmatter value: collapse newlines
|
|
94
|
-
* and repeated whitespace to single spaces and trim.
|
|
95
|
-
*
|
|
96
|
-
* This is the cleaning half of the former hand-rolled YAML escaping. It is
|
|
97
|
-
* applied to every frontmatter value BEFORE serialization so the parsed data
|
|
98
|
-
* (and therefore the snapshot frontmatter hash — see `@tsdoctor/snapshot`)
|
|
99
|
-
* is byte-identical to what the previous emitter produced; the quoting half
|
|
100
|
-
* is now owned by the real YAML emitter in `../frontmatter.ts`.
|
|
52
|
+
* Collapse newlines and runs of whitespace to single spaces, and trim.
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* Applied to every frontmatter scalar before emission. This is NOT quoting —
|
|
56
|
+
* `@effected/yaml` owns that — it is whitespace normalization, and it is
|
|
57
|
+
* load-bearing: the snapshot system hashes the PARSED frontmatter, so a value
|
|
58
|
+
* that folds differently between builds would churn the hash. Survives the
|
|
59
|
+
* removal of `escapeYamlString`, which was its other caller.
|
|
101
60
|
*/
|
|
102
61
|
function cleanYamlValue(value) {
|
|
103
62
|
return value.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim();
|
|
104
63
|
}
|
|
105
|
-
function escapeYamlString(value) {
|
|
106
|
-
const cleaned = cleanYamlValue(value);
|
|
107
|
-
if (/["':#|>&*!%@`[\]{},?-]/.test(cleaned) || /[\u0080-\uFFFF]/.test(cleaned) || /^(true|false|null|~|yes|no|on|off)$/i.test(cleaned) || /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(cleaned)) return `"${cleaned.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
108
|
-
return cleaned;
|
|
109
|
-
}
|
|
110
64
|
/**
|
|
111
65
|
* Escape generic type parameters in MDX by wrapping them in backticks.
|
|
112
66
|
*
|
|
@@ -334,4 +288,4 @@ async function formatExampleCode(code, language, _context) {
|
|
|
334
288
|
}
|
|
335
289
|
|
|
336
290
|
//#endregion
|
|
337
|
-
export { escapeMdxGenerics,
|
|
291
|
+
export { escapeMdxGenerics, formatExampleCode, generateAvailableFrom, generateFrontmatter, prepareExampleCode, prependHiddenImports, stripTwoslashDirectives };
|