rspress-plugin-api-extractor 0.11.0 → 0.13.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 +3 -3
- package/README.md +1 -0
- package/build-program.js +14 -5
- package/build-stages.js +88 -49
- package/config-helpers.js +7 -7
- package/errors.js +1 -5
- package/index.d.ts +1 -86
- package/layers/AppLayer.js +67 -0
- package/layers/api-results.js +83 -0
- package/layers/build-metrics.js +1 -1
- package/layers/{ConfigServiceLive.js → config-resolution.js} +115 -175
- package/layers/external-types.js +74 -0
- package/layers/{ObservabilityLive.js → observability.js} +3 -3
- package/layers/type-environment.js +109 -0
- package/layers/xdg.js +1 -1
- package/markdown/helpers.js +8 -33
- package/markdown/shiki-utils.js +1 -1
- package/model-loader.js +3 -3
- package/observability/EventBus.js +2 -2
- package/observability/heartbeat.js +1 -1
- package/observability/sinks/metrics-sink.js +1 -1
- package/observability/sinks/trace-sink.js +4 -4
- package/package.json +5 -4
- package/plugin.js +30 -33
- package/prettier-formatter.js +1 -1
- package/remark-api-codeblocks.js +1 -1
- package/remark-with-api.js +1 -1
- package/schemas/config.js +1 -3
- package/services/ConfigService.js +37 -2
- package/services/HighlighterService.js +48 -3
- package/services/OgService.js +154 -2
- package/services/PluginConfig.js +1 -1
- package/services/TwoslashCacheService.js +128 -2
- package/services/TwoslashEnvironments.js +30 -2
- package/services/TypeRegistryService.js +178 -2
- package/shiki-transformer.js +1 -1
- package/sync-node-fs.js +6 -6
- package/tsconfig-parser.js +77 -95
- package/twoslash-access.js +1 -1
- package/twoslash-transformer.js +1 -1
- package/layers/HighlighterServiceLive.js +0 -52
- package/layers/OgServiceLive.js +0 -134
- package/layers/TwoslashCacheServiceLive.js +0 -108
- package/layers/TwoslashEnvironmentsLive.js +0 -33
- package/layers/TypeRegistryServiceLive.js +0 -162
- package/markdown/index.js +0 -11
- package/og-resolver.js +0 -64
- package/schemas/index.js +0 -6
- package/schemas/opengraph.js +0 -56
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AppDirsLive, PlatformLive } from "../layers/xdg.js";
|
|
2
|
+
import { decodeTwoslashCache, encodeTwoslashCache, makeTwoslashCache, twoslashBlobKey } from "../twoslash-cache.js";
|
|
3
|
+
import { Context, Effect, Layer, Option, Path } from "effect";
|
|
4
|
+
import { Cache } from "@effected/store";
|
|
5
|
+
import { AppDirs } from "@effected/xdg";
|
|
2
6
|
|
|
3
7
|
//#region src/services/TwoslashCacheService.ts
|
|
4
8
|
/**
|
|
@@ -9,7 +13,129 @@ import { Context } from "effect";
|
|
|
9
13
|
* hook and cannot await: the service loads once before the render phase and
|
|
10
14
|
* saves once after it, while every lookup in between is a synchronous map hit.
|
|
11
15
|
*/
|
|
12
|
-
var TwoslashCacheService = class extends Context.Service()("rspress-plugin-api-extractor/TwoslashCacheService") {
|
|
16
|
+
var TwoslashCacheService = class TwoslashCacheService extends Context.Service()("rspress-plugin-api-extractor/TwoslashCacheService") {
|
|
17
|
+
/**
|
|
18
|
+
* Live Twoslash cache persistence.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* Failure is absorbed at TWO levels, and both are load-bearing. Inside the
|
|
22
|
+
* service, a failed read or write degrades that one operation. Around the
|
|
23
|
+
* layer, a failed CONSTRUCTION — no HOME for XDG, an unwritable cache
|
|
24
|
+
* directory, a corrupt database — degrades to {@link DegradedLive}.
|
|
25
|
+
*
|
|
26
|
+
* The second is why `Layer.catchCause` wraps this at all. While the sqlite
|
|
27
|
+
* layer was provided inside each method, a construction failure surfaced as
|
|
28
|
+
* that method's failure and the in-method handler swallowed it. Hoisting
|
|
29
|
+
* acquisition to layer construction moved the failure to `ManagedRuntime`
|
|
30
|
+
* build time, where it would abort the entire build — breaking the contract
|
|
31
|
+
* this service documents, that an unreachable cache must never fail a build
|
|
32
|
+
* that would otherwise succeed. `catchCause` rather than a failure-only catch
|
|
33
|
+
* because a defect thrown by the sqlite driver must degrade too.
|
|
34
|
+
*
|
|
35
|
+
* `Layer.suspend` because the composition below is declared after this class:
|
|
36
|
+
* a static initializer runs while the module body is still evaluating, so
|
|
37
|
+
* naming those consts directly throws at import time with a clean typecheck.
|
|
38
|
+
*/
|
|
39
|
+
static layer = Layer.suspend(() => CacheBackedLive.pipe(Layer.catchCause(() => DegradedLive)));
|
|
40
|
+
/**
|
|
41
|
+
* An always-cold in-memory double.
|
|
42
|
+
*
|
|
43
|
+
* @remarks
|
|
44
|
+
* Tests that resolve config must not touch the user's real XDG cache, and
|
|
45
|
+
* must not have their results depend on whether a previous run warmed it.
|
|
46
|
+
*
|
|
47
|
+
* `open` returns a REAL in-memory generation rather than a stub, and that is
|
|
48
|
+
* not an accident: `registerEnvironment` hands this object to the Twoslash
|
|
49
|
+
* transformers, so a cache that could not be read or written would change
|
|
50
|
+
* the render path's SHAPE rather than merely dropping its persistence.
|
|
51
|
+
*/
|
|
52
|
+
static makeTest = (overrides = {}) => ({
|
|
53
|
+
load: overrides.load ?? (() => Effect.succeed(/* @__PURE__ */ new Map())),
|
|
54
|
+
save: overrides.save ?? (() => Effect.void),
|
|
55
|
+
open: overrides.open ?? (() => Effect.succeed(makeTwoslashCache())),
|
|
56
|
+
persist: overrides.persist ?? (() => Effect.succeed(Option.none()))
|
|
57
|
+
});
|
|
58
|
+
/** {@link TwoslashCacheService.makeTest} behind a `Layer`. */
|
|
59
|
+
static layerTest = (overrides = {}) => Layer.succeed(TwoslashCacheService, TwoslashCacheService.makeTest(overrides));
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* A sqlite-backed `@effected/store` Cache in the XDG cache dir, separate from
|
|
63
|
+
* the registry's `metadata.sqlite`.
|
|
64
|
+
*
|
|
65
|
+
* XDG rather than the repo: these are regenerable results derived from content
|
|
66
|
+
* hashes, so they belong with the user's other caches — shared across worktrees
|
|
67
|
+
* and checkouts of the same project, and untouched by cleaning `dist/`. Nothing
|
|
68
|
+
* here needs to be committed for a build to be correct.
|
|
69
|
+
*/
|
|
70
|
+
const CacheLive = Layer.unwrap(Effect.gen(function* () {
|
|
71
|
+
const appDirs = yield* AppDirs;
|
|
72
|
+
const path = yield* Path.Path;
|
|
73
|
+
const cacheDir = yield* appDirs.ensureCache;
|
|
74
|
+
return Cache.layerSqlite({ filename: path.join(cacheDir, "twoslash.sqlite") });
|
|
75
|
+
})).pipe(Layer.provide(Layer.mergeAll(AppDirsLive, PlatformLive)));
|
|
76
|
+
/**
|
|
77
|
+
* Live Twoslash cache persistence.
|
|
78
|
+
*
|
|
79
|
+
* Both operations swallow every failure by design — see the service docs. A
|
|
80
|
+
* missing HOME, an unwritable cache dir or a corrupt database degrades the
|
|
81
|
+
* build to "type-check everything", which is exactly the behaviour before this
|
|
82
|
+
* cache existed.
|
|
83
|
+
*/
|
|
84
|
+
/**
|
|
85
|
+
* Add the build-generation half of the service on top of a load/save pair.
|
|
86
|
+
*
|
|
87
|
+
* @remarks
|
|
88
|
+
* Shared by the real and degraded layers so the two cannot drift: a degraded
|
|
89
|
+
* build must still hand out a working in-memory cache, otherwise the
|
|
90
|
+
* transformers have nothing to read or write and the render pass changes
|
|
91
|
+
* shape rather than merely losing persistence.
|
|
92
|
+
*/
|
|
93
|
+
function withGeneration(base) {
|
|
94
|
+
let open = null;
|
|
95
|
+
return {
|
|
96
|
+
...base,
|
|
97
|
+
open: (envHash) => base.load(envHash).pipe(Effect.map((restored) => {
|
|
98
|
+
const cache = makeTwoslashCache(restored);
|
|
99
|
+
open = {
|
|
100
|
+
cache,
|
|
101
|
+
envHash
|
|
102
|
+
};
|
|
103
|
+
return cache;
|
|
104
|
+
})),
|
|
105
|
+
persist: () => Effect.suspend(() => {
|
|
106
|
+
if (open === null) return Effect.succeed(Option.none());
|
|
107
|
+
const { cache, envHash } = open;
|
|
108
|
+
const stats = cache.stats();
|
|
109
|
+
const report = Option.some({
|
|
110
|
+
...stats,
|
|
111
|
+
envHash
|
|
112
|
+
});
|
|
113
|
+
return stats.dirty ? base.save(envHash, cache.entries()).pipe(Effect.as(report)) : Effect.succeed(report);
|
|
114
|
+
})
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const CacheBackedLive = Layer.effect(TwoslashCacheService, Effect.gen(function* () {
|
|
118
|
+
const cache = yield* Cache;
|
|
119
|
+
return withGeneration({
|
|
120
|
+
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()))),
|
|
121
|
+
save: (envHash, entries) => cache.set({
|
|
122
|
+
key: twoslashBlobKey(envHash),
|
|
123
|
+
value: encodeTwoslashCache(entries),
|
|
124
|
+
tags: ["twoslash"]
|
|
125
|
+
}).pipe(Effect.catch(() => Effect.void))
|
|
126
|
+
});
|
|
127
|
+
})).pipe(Layer.provide(CacheLive));
|
|
128
|
+
/**
|
|
129
|
+
* A cache that holds nothing, for when the real one cannot be opened.
|
|
130
|
+
*
|
|
131
|
+
* @remarks
|
|
132
|
+
* `load` returns empty and `save` discards, which is precisely the behaviour
|
|
133
|
+
* before this cache existed: type-check everything, persist nothing.
|
|
134
|
+
*/
|
|
135
|
+
const DegradedLive = Layer.succeed(TwoslashCacheService, withGeneration({
|
|
136
|
+
load: () => Effect.succeed(/* @__PURE__ */ new Map()),
|
|
137
|
+
save: () => Effect.void
|
|
138
|
+
}));
|
|
13
139
|
|
|
14
140
|
//#endregion
|
|
15
141
|
export { TwoslashCacheService };
|
|
@@ -1,7 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TwoslashEnvironmentRegistry } from "../twoslash-transformer.js";
|
|
2
|
+
import { Context, Layer } from "effect";
|
|
2
3
|
|
|
3
4
|
//#region src/services/TwoslashEnvironments.ts
|
|
4
|
-
var TwoslashEnvironments = class extends Context.Service()("rspress-plugin-api-extractor/TwoslashEnvironments") {
|
|
5
|
+
var TwoslashEnvironments = class extends Context.Service()("rspress-plugin-api-extractor/TwoslashEnvironments") {
|
|
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
|
+
static layer = Layer.sync(this, () => make());
|
|
22
|
+
};
|
|
23
|
+
const make = () => {
|
|
24
|
+
const registry = new TwoslashEnvironmentRegistry();
|
|
25
|
+
return {
|
|
26
|
+
registerEnvironment: (options) => registry.registerEnvironment(options),
|
|
27
|
+
registerScope: (apiScope, compilerOptions) => registry.registerScope(apiScope, compilerOptions),
|
|
28
|
+
transformerFor: (apiScope) => registry.transformerFor(apiScope),
|
|
29
|
+
setCurrentFile: (path) => registry.setCurrentFile(path),
|
|
30
|
+
reportErrorForTest: (error, code, file) => registry.reportErrorForTest(error, code, file)
|
|
31
|
+
};
|
|
32
|
+
};
|
|
5
33
|
|
|
6
34
|
//#endregion
|
|
7
35
|
export { TwoslashEnvironments };
|
|
@@ -1,7 +1,183 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PluginEvent } from "../observability/events.js";
|
|
2
|
+
import { emit } from "../observability/EventBus.js";
|
|
3
|
+
import { resolveExternalPackageVersions } from "../config-utils.js";
|
|
4
|
+
import { TypeRegistryError } from "../errors.js";
|
|
5
|
+
import { AppDirsLive, PlatformLive } from "../layers/xdg.js";
|
|
6
|
+
import { Context, Duration, Effect, Layer, Path } from "effect";
|
|
7
|
+
import { NodeHttpClient } from "@effect/platform-node";
|
|
8
|
+
import { PackageFetcher, PackageSpec, RegistryObserver, TypeCache, TypeRegistry } from "@tsdoctor/registry";
|
|
9
|
+
import { Cache } from "@effected/store";
|
|
10
|
+
import { AppDirs } from "@effected/xdg";
|
|
2
11
|
|
|
3
12
|
//#region src/services/TypeRegistryService.ts
|
|
4
|
-
var TypeRegistryService = class extends Context.Service()("rspress-plugin-api-extractor/TypeRegistryService") {
|
|
13
|
+
var TypeRegistryService = class TypeRegistryService extends Context.Service()("rspress-plugin-api-extractor/TypeRegistryService") {
|
|
14
|
+
/**
|
|
15
|
+
* The `@tsdoctor/registry` stack, acquired once.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* `Layer.catchCause` keeps a broken environment — no HOME for XDG, an
|
|
19
|
+
* unwritable cache directory — from aborting the build at `ManagedRuntime`
|
|
20
|
+
* construction. External type loading is an enhancement: without it code
|
|
21
|
+
* blocks render without Twoslash enrichment, which is a degradation, not a
|
|
22
|
+
* failure. That was true while the stack was provided per method and the
|
|
23
|
+
* in-method handlers absorbed it; hoisting acquisition made it something the
|
|
24
|
+
* layer has to say for itself.
|
|
25
|
+
*
|
|
26
|
+
* `Layer.suspend` because the composition below is declared after this class: a
|
|
27
|
+
* static initializer runs while the module body is still evaluating, so naming
|
|
28
|
+
* those consts directly throws at import time with a clean typecheck.
|
|
29
|
+
*/
|
|
30
|
+
static layer = Layer.suspend(() => RegistryBackedLive.pipe(Layer.catchCause(() => DegradedLive)));
|
|
31
|
+
/**
|
|
32
|
+
* An in-memory double: no network, no XDG cache, no sqlite.
|
|
33
|
+
*
|
|
34
|
+
* @remarks
|
|
35
|
+
* Defaults resolve every spec unchanged and load an empty VFS — the shape a
|
|
36
|
+
* build takes when nothing external is configured. Override a member to
|
|
37
|
+
* exercise a specific path; an override for one member leaves the other at
|
|
38
|
+
* its default rather than forcing the test to restate it, which is the whole
|
|
39
|
+
* difference from the hand-written `Layer.succeed` doubles this replaces.
|
|
40
|
+
*/
|
|
41
|
+
static makeTest = (overrides = {}) => ({
|
|
42
|
+
resolveVersions: overrides.resolveVersions ?? ((packages) => Effect.succeed([...packages])),
|
|
43
|
+
loadPackages: overrides.loadPackages ?? (() => Effect.succeed({ vfs: /* @__PURE__ */ new Map() }))
|
|
44
|
+
});
|
|
45
|
+
/** {@link TypeRegistryService.makeTest} behind a `Layer`. */
|
|
46
|
+
static layerTest = (overrides = {}) => Layer.succeed(TypeRegistryService, TypeRegistryService.makeTest(overrides));
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Forward @tsdoctor/registry's typed `RegistryEvent`s to the plugin's Effect
|
|
50
|
+
* logger. Since v1 the library emits no logs of its own — observers are the only
|
|
51
|
+
* diagnostic surface — so this restores the build output and routes it through
|
|
52
|
+
* the plugin's configured log level/format (a single source, no duplication).
|
|
53
|
+
*
|
|
54
|
+
* The summary (`BatchComplete`) and failures are surfaced at info/warning;
|
|
55
|
+
* per-package detail stays at debug so a normal build is quiet.
|
|
56
|
+
*/
|
|
57
|
+
const RegistryObserverLayer = Layer.succeed(RegistryObserver, { emit: (event) => {
|
|
58
|
+
switch (event._tag) {
|
|
59
|
+
case "VersionResolved": return emit(PluginEvent.TypeRegistryEvent({
|
|
60
|
+
ctx: { packageName: event.package },
|
|
61
|
+
level: "debug",
|
|
62
|
+
kind: "VersionResolved",
|
|
63
|
+
detail: `${event.requested} -> ${event.resolved}`
|
|
64
|
+
}));
|
|
65
|
+
case "VersionResolveFailed": return emit(PluginEvent.TypeRegistryEvent({
|
|
66
|
+
ctx: { packageName: event.package },
|
|
67
|
+
level: "debug",
|
|
68
|
+
kind: "VersionResolveFailed",
|
|
69
|
+
detail: `${event.requested}: ${event.kind}`
|
|
70
|
+
}));
|
|
71
|
+
case "CacheHit":
|
|
72
|
+
case "CacheMiss":
|
|
73
|
+
case "FetchStart": return emit(PluginEvent.TypeRegistryEvent({
|
|
74
|
+
ctx: {
|
|
75
|
+
packageName: event.package,
|
|
76
|
+
version: event.version
|
|
77
|
+
},
|
|
78
|
+
level: "debug",
|
|
79
|
+
kind: event._tag,
|
|
80
|
+
detail: ""
|
|
81
|
+
}));
|
|
82
|
+
case "CacheStale": return emit(PluginEvent.TypeRegistryEvent({
|
|
83
|
+
ctx: {
|
|
84
|
+
packageName: event.package,
|
|
85
|
+
version: event.version
|
|
86
|
+
},
|
|
87
|
+
level: "debug",
|
|
88
|
+
kind: "CacheStale",
|
|
89
|
+
detail: ""
|
|
90
|
+
}));
|
|
91
|
+
case "FetchFailed": return emit(PluginEvent.TypeRegistryEvent({
|
|
92
|
+
ctx: {},
|
|
93
|
+
level: "debug",
|
|
94
|
+
kind: "FetchFailed",
|
|
95
|
+
detail: `HTTP ${event.status}: ${event.url}${event.bodySnippet ? ` — ${event.bodySnippet}` : ""}`
|
|
96
|
+
}));
|
|
97
|
+
case "PackageLoaded": return emit(PluginEvent.TypeRegistryEvent({
|
|
98
|
+
ctx: {
|
|
99
|
+
packageName: event.package,
|
|
100
|
+
version: event.version
|
|
101
|
+
},
|
|
102
|
+
level: "debug",
|
|
103
|
+
kind: "PackageLoaded",
|
|
104
|
+
detail: `${event.files} files, ${event.source}`
|
|
105
|
+
}));
|
|
106
|
+
case "PackageLoadFailed": return emit(PluginEvent.TypeRegistryEvent({
|
|
107
|
+
ctx: {
|
|
108
|
+
packageName: event.package,
|
|
109
|
+
version: event.version
|
|
110
|
+
},
|
|
111
|
+
level: "warn",
|
|
112
|
+
kind: "PackageLoadFailed",
|
|
113
|
+
detail: `[${event.kind}] ${event.error instanceof Error ? event.error.message : String(event.error)}`
|
|
114
|
+
}));
|
|
115
|
+
case "BatchStart": return emit(PluginEvent.TypeRegistryEvent({
|
|
116
|
+
ctx: {},
|
|
117
|
+
level: "debug",
|
|
118
|
+
kind: "BatchStart",
|
|
119
|
+
detail: `${event.total} package(s)`
|
|
120
|
+
}));
|
|
121
|
+
case "BatchComplete": return emit(PluginEvent.TypeRegistryEvent({
|
|
122
|
+
ctx: {},
|
|
123
|
+
level: "info",
|
|
124
|
+
kind: "BatchComplete",
|
|
125
|
+
detail: `${event.loaded}/${event.total} packages, ${event.totalFiles} files, ${Math.round(Duration.toMillis(event.duration))}ms`
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
} });
|
|
129
|
+
/** Metadata plane: a sqlite-backed `@effected/store` Cache rooted in the XDG cache dir. */
|
|
130
|
+
const MetadataCacheLive = Layer.unwrap(Effect.gen(function* () {
|
|
131
|
+
const appDirs = yield* AppDirs;
|
|
132
|
+
const path = yield* Path.Path;
|
|
133
|
+
const cacheDir = yield* appDirs.ensureCache;
|
|
134
|
+
return Cache.layerSqlite({ filename: path.join(cacheDir, "metadata.sqlite") });
|
|
135
|
+
})).pipe(Layer.provide(Layer.mergeAll(AppDirsLive, PlatformLive)));
|
|
136
|
+
/**
|
|
137
|
+
* The full registry runtime: TypeRegistry over an XDG-rooted TypeCache and the
|
|
138
|
+
* jsDelivr PackageFetcher, with the observer that forwards registry events to
|
|
139
|
+
* the plugin's EventBus (found ambiently via serviceOption at emit time).
|
|
140
|
+
*/
|
|
141
|
+
const RegistryLayer = TypeRegistry.layer.pipe(Layer.provideMerge(Layer.mergeAll(TypeCache.layerXdg(), PackageFetcher.layer)), Layer.provideMerge(RegistryObserverLayer), Layer.provide(Layer.mergeAll(MetadataCacheLive, AppDirsLive, PlatformLive, NodeHttpClient.layerUndici)));
|
|
142
|
+
/**
|
|
143
|
+
* TypeRegistryService.layer: uses @tsdoctor/registry Effect programs directly.
|
|
144
|
+
*/
|
|
145
|
+
const RegistryBackedLive = Layer.effect(TypeRegistryService, Effect.gen(function* () {
|
|
146
|
+
const registry = yield* TypeRegistry;
|
|
147
|
+
return {
|
|
148
|
+
resolveVersions: (packages) => resolveExternalPackageVersions(packages, (pkg) => registry.resolveVersion(pkg.name, pkg.version)).pipe(Effect.catch(() => Effect.succeed([...packages]))),
|
|
149
|
+
loadPackages: (packages) => packages.length === 0 ? Effect.succeed({ vfs: /* @__PURE__ */ new Map() }) : Effect.gen(function* () {
|
|
150
|
+
const specs = packages.map((pkg) => new PackageSpec({
|
|
151
|
+
name: pkg.name,
|
|
152
|
+
version: pkg.version
|
|
153
|
+
}));
|
|
154
|
+
return { vfs: yield* registry.getVfs(specs, { autoFetch: true }) };
|
|
155
|
+
}).pipe(Effect.catch((error) => Effect.fail(new TypeRegistryError({
|
|
156
|
+
packageName: packages.map((p) => p.name).join(", "),
|
|
157
|
+
version: packages.map((p) => p.version).join(", "),
|
|
158
|
+
reason: error instanceof Error ? error.message ?? String(error) : String(error)
|
|
159
|
+
}))))
|
|
160
|
+
};
|
|
161
|
+
})).pipe(Layer.provide(RegistryLayer));
|
|
162
|
+
/**
|
|
163
|
+
* The service when the registry stack cannot be built at all.
|
|
164
|
+
*
|
|
165
|
+
* @remarks
|
|
166
|
+
* Preserves exactly the split the working service documents: `resolveVersions`
|
|
167
|
+
* passes its specs through unresolved rather than swallowing the problem, so
|
|
168
|
+
* the failure surfaces from `loadPackages` as a {@link PluginTypeRegistryError}
|
|
169
|
+
* with a message, which `ConfigService.layer` turns into a build-continues
|
|
170
|
+
* warning. Before acquisition moved to layer construction this fell out of the
|
|
171
|
+
* per-method handlers; it has to be stated explicitly now.
|
|
172
|
+
*/
|
|
173
|
+
const DegradedLive = Layer.succeed(TypeRegistryService, {
|
|
174
|
+
resolveVersions: (packages) => Effect.succeed([...packages]),
|
|
175
|
+
loadPackages: (packages) => Effect.fail(new TypeRegistryError({
|
|
176
|
+
packageName: packages.map((p) => p.name).join(", "),
|
|
177
|
+
version: packages.map((p) => p.version).join(", "),
|
|
178
|
+
reason: "type registry unavailable: its cache directory or metadata database could not be opened"
|
|
179
|
+
}))
|
|
180
|
+
});
|
|
5
181
|
|
|
6
182
|
//#endregion
|
|
7
183
|
export { TypeRegistryService };
|
package/shiki-transformer.js
CHANGED
|
@@ -67,7 +67,7 @@ var ShikiCrossLinker = class ShikiCrossLinker {
|
|
|
67
67
|
* way and both are immutable per build.
|
|
68
68
|
*
|
|
69
69
|
* This replaces a single long-lived instance created at plugin-factory time,
|
|
70
|
-
* threaded through `
|
|
70
|
+
* threaded through `ConfigService.layer`'s constructor and the build context,
|
|
71
71
|
* and mutated per API by `reinitialize()`. Scope isolation used to be a
|
|
72
72
|
* property of internal `…ByScope` maps plus a mutable `currentApiScope` that
|
|
73
73
|
* any caller could reassign between a lookup and a render; it is now a
|
package/sync-node-fs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
2
|
import { Effect, FileSystem, Layer, Option, Path, PlatformError } from "effect";
|
|
3
3
|
|
|
4
4
|
//#region src/sync-node-fs.ts
|
|
@@ -51,21 +51,21 @@ const infoFromStats = (stats) => ({
|
|
|
51
51
|
blocks: Option.some(stats.blocks)
|
|
52
52
|
});
|
|
53
53
|
const syncFileSystem = FileSystem.layerNoop({
|
|
54
|
-
exists: (path) => Effect.sync(() =>
|
|
54
|
+
exists: (path) => Effect.sync(() => fsSync.existsSync(path)),
|
|
55
55
|
stat: (path) => Effect.try({
|
|
56
|
-
try: () => infoFromStats(
|
|
56
|
+
try: () => infoFromStats(fsSync.statSync(path)),
|
|
57
57
|
catch: (cause) => fail("stat", path, cause)
|
|
58
58
|
}),
|
|
59
59
|
readFileString: (path) => Effect.try({
|
|
60
|
-
try: () =>
|
|
60
|
+
try: () => fsSync.readFileSync(path, "utf8"),
|
|
61
61
|
catch: (cause) => fail("readFileString", path, cause)
|
|
62
62
|
}),
|
|
63
63
|
readDirectory: (path) => Effect.try({
|
|
64
|
-
try: () =>
|
|
64
|
+
try: () => fsSync.readdirSync(path),
|
|
65
65
|
catch: (cause) => fail("readDirectory", path, cause)
|
|
66
66
|
}),
|
|
67
67
|
readLink: (path) => Effect.try({
|
|
68
|
-
try: () =>
|
|
68
|
+
try: () => fsSync.readlinkSync(path),
|
|
69
69
|
catch: (cause) => fail("readLink", path, cause)
|
|
70
70
|
})
|
|
71
71
|
});
|
package/tsconfig-parser.js
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { TsconfigLoaderSync } from "@effected/tsconfig-json";
|
|
4
4
|
|
|
5
5
|
//#region src/tsconfig-parser.ts
|
|
6
6
|
/**
|
|
7
|
+
* Reading a `tsconfig.json` into the compiler options the plugin consumes.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* A thin adapter over `@effected/tsconfig-json`'s `TsconfigLoaderSync`, which
|
|
11
|
+
* owns `extends` chain resolution (including package specifiers), JSONC
|
|
12
|
+
* parsing and relative-path handling. This module used to hand-roll all three
|
|
13
|
+
* over TypeScript's `parseJsonConfigFileContent`.
|
|
14
|
+
*
|
|
15
|
+
* **The loader returns the tsconfig SPELLING, not the programmatic one.**
|
|
16
|
+
* `target` is `"es2025"` rather than `ts.ScriptTarget.ES2025`, and `lib` is
|
|
17
|
+
* `["esnext"]` rather than `["lib.esnext.d.ts"]`. That is fine, and it is why
|
|
18
|
+
* the normalization seam had to land first: `toProgrammaticCompilerOptions`
|
|
19
|
+
* (`twoslash-transformer.ts`) converts at ONE place, and
|
|
20
|
+
* {@link TypeResolutionCompilerOptions} accepts both spellings by design. Do
|
|
21
|
+
* not convert here — a second conversion site is exactly the drift that made
|
|
22
|
+
* three of four resolution paths load zero lib files once already.
|
|
23
|
+
*
|
|
24
|
+
* @packageDocumentation
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
7
27
|
* Error thrown when tsconfig.json parsing fails.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Retained as the plugin's own type rather than surfacing the kit's
|
|
31
|
+
* `TsconfigParseError`/`TsconfigExtendsError` directly: `typescript-config.ts`
|
|
32
|
+
* branches on `instanceof TsConfigParseError` to decide whether a failure is
|
|
33
|
+
* already reported, and both kit errors mean the same thing to that caller.
|
|
8
34
|
*/
|
|
9
35
|
var TsConfigParseError = class extends Error {
|
|
10
36
|
configPath;
|
|
@@ -17,117 +43,73 @@ var TsConfigParseError = class extends Error {
|
|
|
17
43
|
}
|
|
18
44
|
};
|
|
19
45
|
/**
|
|
20
|
-
*
|
|
46
|
+
* The sync host the kit loader reads through.
|
|
21
47
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* - Relative path resolution
|
|
26
|
-
*
|
|
27
|
-
* @param configPath - Path to tsconfig.json (relative or absolute)
|
|
28
|
-
* @param projectRoot - Project root directory for resolving relative paths
|
|
29
|
-
* @returns Parsed compiler options
|
|
30
|
-
* @throws TsConfigParseError if the config cannot be read or parsed
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* ```ts
|
|
34
|
-
* const options = parseTsConfig("tsconfig.json", "/path/to/project");
|
|
35
|
-
* // Returns: { target: 99, module: 99, lib: ["ESNext", "DOM"], ... }
|
|
36
|
-
* ```
|
|
48
|
+
* @remarks
|
|
49
|
+
* `node:path` satisfies `SyncPath` verbatim. The filesystem half is two
|
|
50
|
+
* functions, so no shim module is needed.
|
|
37
51
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
const syncHost = {
|
|
53
|
+
fileSystem: {
|
|
54
|
+
exists: existsSync,
|
|
55
|
+
readFile: (filePath) => readFileSync(filePath, "utf8")
|
|
56
|
+
},
|
|
57
|
+
path
|
|
58
|
+
};
|
|
41
59
|
/**
|
|
42
|
-
* Parse a tsconfig.json
|
|
60
|
+
* Parse a `tsconfig.json` and extract the compiler options used for type
|
|
61
|
+
* resolution.
|
|
43
62
|
*
|
|
44
63
|
* @param configPath - Path to tsconfig.json (relative or absolute)
|
|
45
64
|
* @param projectRoot - Project root directory for resolving relative paths
|
|
46
|
-
* @returns
|
|
65
|
+
* @returns The declared compiler options, in the tsconfig spelling
|
|
47
66
|
* @throws TsConfigParseError if the config cannot be read or parsed
|
|
48
67
|
*
|
|
49
68
|
* @example
|
|
50
69
|
* ```ts
|
|
51
|
-
* const
|
|
52
|
-
*
|
|
53
|
-
* console.log(result.extendedPaths); // ["base.json", "tsconfig.json"]
|
|
54
|
-
* console.log(result.compilerOptions); // Merged compiler options
|
|
70
|
+
* const options = parseTsConfig("tsconfig.json", "/path/to/project");
|
|
71
|
+
* // Returns: { target: "es2025", module: "nodenext", lib: ["esnext"], ... }
|
|
55
72
|
* ```
|
|
56
73
|
*/
|
|
57
|
-
function
|
|
58
|
-
const absolutePath = isAbsolute(configPath) ? configPath : resolve(projectRoot, configPath);
|
|
74
|
+
function parseTsConfig(configPath, projectRoot) {
|
|
75
|
+
const absolutePath = path.isAbsolute(configPath) ? configPath : path.resolve(projectRoot, configPath);
|
|
59
76
|
if (!existsSync(absolutePath)) throw new TsConfigParseError(absolutePath, "File not found");
|
|
60
|
-
|
|
61
|
-
if (configFileContent.error) {
|
|
62
|
-
const message = ts.flattenDiagnosticMessageText(configFileContent.error.messageText, "\n");
|
|
63
|
-
throw new TsConfigParseError(absolutePath, message, configFileContent.error);
|
|
64
|
-
}
|
|
65
|
-
const configDir = dirname(absolutePath);
|
|
66
|
-
const parsedConfig = ts.parseJsonConfigFileContent(configFileContent.config, ts.sys, configDir, void 0, absolutePath);
|
|
67
|
-
const significantErrors = parsedConfig.errors.filter((error) => {
|
|
68
|
-
const message = ts.flattenDiagnosticMessageText(error.messageText, "\n");
|
|
69
|
-
return error.code !== 18003 && !message.includes("No inputs were found");
|
|
70
|
-
});
|
|
71
|
-
if (significantErrors.length > 0) {
|
|
72
|
-
const errorMessages = significantErrors.map((error) => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("; ");
|
|
73
|
-
throw new TsConfigParseError(absolutePath, errorMessages, significantErrors);
|
|
74
|
-
}
|
|
75
|
-
const extendedPaths = [absolutePath];
|
|
76
|
-
collectExtendedPaths(configFileContent.config, configDir, extendedPaths);
|
|
77
|
-
const tsOptions = parsedConfig.options;
|
|
78
|
-
return {
|
|
79
|
-
compilerOptions: extractTypeResolutionOptions(tsOptions),
|
|
80
|
-
configPath: absolutePath,
|
|
81
|
-
extendedPaths
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Recursively collect extended config paths.
|
|
86
|
-
* @internal
|
|
87
|
-
*/
|
|
88
|
-
function collectExtendedPaths(config, baseDir, paths) {
|
|
89
|
-
if (!config || typeof config !== "object") return;
|
|
90
|
-
const extendsValue = config.extends;
|
|
91
|
-
if (typeof extendsValue === "string") {
|
|
92
|
-
const extendedPath = resolveExtendedPath(extendsValue, baseDir);
|
|
93
|
-
if (extendedPath && !paths.includes(extendedPath)) paths.unshift(extendedPath);
|
|
94
|
-
} else if (Array.isArray(extendsValue)) {
|
|
95
|
-
for (const ext of extendsValue) if (typeof ext === "string") {
|
|
96
|
-
const extendedPath = resolveExtendedPath(ext, baseDir);
|
|
97
|
-
if (extendedPath && !paths.includes(extendedPath)) paths.unshift(extendedPath);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Resolve an extended config path.
|
|
103
|
-
* @internal
|
|
104
|
-
*/
|
|
105
|
-
function resolveExtendedPath(extendsValue, baseDir) {
|
|
77
|
+
let options;
|
|
106
78
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return null;
|
|
79
|
+
options = TsconfigLoaderSync.compilerOptions(absolutePath, syncHost);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new TsConfigParseError(absolutePath, error instanceof Error ? error.message : String(error), error);
|
|
111
82
|
}
|
|
83
|
+
return extractTypeResolutionOptions(options);
|
|
112
84
|
}
|
|
113
85
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
86
|
+
* Narrow the full compiler options to the ones the plugin actually consumes.
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* Deliberately a whitelist. Everything here reaches Twoslash's TypeScript
|
|
90
|
+
* environment, and passing through options the plugin does not understand
|
|
91
|
+
* would let a consumer's unrelated build setting change how examples
|
|
92
|
+
* type-check.
|
|
116
93
|
*/
|
|
117
|
-
function extractTypeResolutionOptions(
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
if (
|
|
129
|
-
|
|
94
|
+
function extractTypeResolutionOptions(options) {
|
|
95
|
+
const result = {};
|
|
96
|
+
const scalar = (value) => typeof value === "string" || typeof value === "number" ? value : void 0;
|
|
97
|
+
const target = scalar(options.target);
|
|
98
|
+
if (target !== void 0) result.target = target;
|
|
99
|
+
const module_ = scalar(options.module);
|
|
100
|
+
if (module_ !== void 0) result.module = module_;
|
|
101
|
+
const moduleResolution = scalar(options.moduleResolution);
|
|
102
|
+
if (moduleResolution !== void 0) result.moduleResolution = moduleResolution;
|
|
103
|
+
const jsx = scalar(options.jsx);
|
|
104
|
+
if (jsx !== void 0) result.jsx = jsx;
|
|
105
|
+
if (typeof options.strict === "boolean") result.strict = options.strict;
|
|
106
|
+
if (typeof options.skipLibCheck === "boolean") result.skipLibCheck = options.skipLibCheck;
|
|
107
|
+
if (typeof options.esModuleInterop === "boolean") result.esModuleInterop = options.esModuleInterop;
|
|
108
|
+
if (typeof options.allowSyntheticDefaultImports === "boolean") result.allowSyntheticDefaultImports = options.allowSyntheticDefaultImports;
|
|
109
|
+
if (Array.isArray(options.lib) && options.lib.length > 0) result.lib = options.lib.map(String);
|
|
110
|
+
if (Array.isArray(options.types) && options.types.length > 0) result.types = options.types.map(String);
|
|
111
|
+
return result;
|
|
130
112
|
}
|
|
131
113
|
|
|
132
114
|
//#endregion
|
|
133
|
-
export { TsConfigParseError, parseTsConfig
|
|
115
|
+
export { TsConfigParseError, parseTsConfig };
|
package/twoslash-access.js
CHANGED
|
@@ -10,7 +10,7 @@ let current = NOT_INSTALLED;
|
|
|
10
10
|
*
|
|
11
11
|
* @remarks
|
|
12
12
|
* Called from `plugin.ts`'s Effect program, beside the other seam wiring, and
|
|
13
|
-
* NOT from `
|
|
13
|
+
* NOT from `ConfigService.layer` — config resolution should compute a value, not
|
|
14
14
|
* also mutate module state on the side.
|
|
15
15
|
*/
|
|
16
16
|
function installTwoslashAccess(environments) {
|
package/twoslash-transformer.js
CHANGED
|
@@ -456,7 +456,7 @@ function addTypeRoutes(routes) {
|
|
|
456
456
|
*
|
|
457
457
|
* Clears ONLY the routes. The environments are per-build too, but they are
|
|
458
458
|
* owned by the layer now, so nothing here can discard the per-scope
|
|
459
|
-
* transformers `
|
|
459
|
+
* transformers `ConfigService.layer` just built.
|
|
460
460
|
*/
|
|
461
461
|
function clearTypeRoutes() {
|
|
462
462
|
typeRoutes.clear();
|