rspress-plugin-api-extractor 0.10.0 → 0.12.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 +34 -33
- package/build-stages.js +48 -42
- package/config-helpers.js +7 -7
- package/errors.js +1 -6
- package/index.d.ts +84 -86
- package/layers/AppLayer.js +67 -0
- package/layers/api-results.js +83 -0
- package/layers/build-metrics.js +1 -1
- package/layers/config-resolution.js +407 -0
- 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 +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/model-loader.js +3 -3
- package/observability/EventBus.js +29 -7
- package/observability/heartbeat.js +1 -1
- package/observability/sinks/metrics-sink.js +1 -1
- package/observability/sinks/trace-sink.js +4 -4
- package/observability/spans.js +3 -1
- package/observability/sync-emitter.js +78 -0
- package/og-resolver.js +74 -284
- package/package.json +3 -4
- package/path-derivation.js +19 -1
- package/plugin.js +63 -91
- package/prettier-formatter.js +5 -11
- package/remark-api-codeblocks.js +11 -19
- package/remark-with-api.js +11 -21
- package/schemas/config.js +0 -2
- package/services/ConfigService.js +37 -2
- package/services/HighlighterService.js +75 -0
- package/services/OgService.js +190 -0
- package/services/PluginConfig.js +26 -0
- package/services/TwoslashCacheService.js +128 -2
- package/services/TwoslashEnvironments.js +35 -0
- package/services/TypeRegistryService.js +178 -2
- package/shiki-transformer.js +53 -234
- package/sync-node-fs.js +6 -6
- package/tsconfig-parser.js +77 -95
- package/twoslash-access.js +48 -0
- package/twoslash-transformer.js +106 -83
- package/vfs-registry.js +1 -31
- package/layers/ConfigServiceLive.js +0 -600
- package/layers/PathDerivationServiceLive.js +0 -16
- package/layers/TwoslashCacheServiceLive.js +0 -53
- package/layers/TypeRegistryServiceLive.js +0 -155
- package/markdown/index.js +0 -11
- package/schemas/index.js +0 -6
- package/services/PathDerivationService.js +0 -7
package/shiki-transformer.js
CHANGED
|
@@ -43,100 +43,53 @@
|
|
|
43
43
|
* @see the `@tsdoctor/model` CrossLinker for the markdown equivalent
|
|
44
44
|
* @see {@link TwoslashManager} for type-aware documentation features
|
|
45
45
|
*/
|
|
46
|
-
var ShikiCrossLinker = class {
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
*/
|
|
61
|
-
constructor(routes, kinds, apiScope) {
|
|
62
|
-
if (routes && kinds && apiScope) this.reinitialize(routes, kinds, apiScope);
|
|
46
|
+
var ShikiCrossLinker = class ShikiCrossLinker {
|
|
47
|
+
/** API item name to route, for THIS scope. */
|
|
48
|
+
apiItemRoutes;
|
|
49
|
+
/** API item name to kind (Class, Interface, …), for THIS scope. */
|
|
50
|
+
apiItemKinds;
|
|
51
|
+
/** Parent name to its member names, longest first, for THIS scope. */
|
|
52
|
+
classMembersMap;
|
|
53
|
+
/** The scope this linker links for. Read-only after construction. */
|
|
54
|
+
apiScope;
|
|
55
|
+
constructor(apiScope, apiItemRoutes, apiItemKinds, classMembersMap) {
|
|
56
|
+
this.apiScope = apiScope;
|
|
57
|
+
this.apiItemRoutes = apiItemRoutes;
|
|
58
|
+
this.apiItemKinds = apiItemKinds;
|
|
59
|
+
this.classMembersMap = classMembersMap;
|
|
63
60
|
}
|
|
64
61
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
62
|
+
* One linker for one API's routes.
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* Mirrors `@tsdoctor/model`'s `CrossLinker.fromRoutes`, deliberately: the
|
|
66
|
+
* two halves of cross-linking — prose and code blocks — are built the same
|
|
67
|
+
* way and both are immutable per build.
|
|
68
68
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
69
|
+
* This replaces a single long-lived instance created at plugin-factory time,
|
|
70
|
+
* threaded through `ConfigService.layer`'s constructor and the build context,
|
|
71
|
+
* and mutated per API by `reinitialize()`. Scope isolation used to be a
|
|
72
|
+
* property of internal `…ByScope` maps plus a mutable `currentApiScope` that
|
|
73
|
+
* any caller could reassign between a lookup and a render; it is now a
|
|
74
|
+
* property of the instance, which cannot be pointed at another package's
|
|
75
|
+
* routes at all.
|
|
72
76
|
*/
|
|
73
|
-
|
|
74
|
-
this.apiItemRoutesByScope.set(apiScope, new Map(routes));
|
|
75
|
-
this.apiItemKindsByScope.set(apiScope, new Map(kinds));
|
|
77
|
+
static fromRoutes(routes, kinds, apiScope) {
|
|
76
78
|
const classMembersMap = /* @__PURE__ */ new Map();
|
|
77
|
-
for (const [name] of routes.entries())
|
|
79
|
+
for (const [name] of routes.entries()) {
|
|
78
80
|
const dotIndex = name.indexOf(".");
|
|
81
|
+
if (dotIndex === -1) continue;
|
|
79
82
|
const className = name.substring(0, dotIndex);
|
|
80
83
|
const memberName = name.substring(dotIndex + 1);
|
|
81
|
-
if (!classMembersMap.has(className)) classMembersMap.set(className, []);
|
|
82
84
|
const members = classMembersMap.get(className);
|
|
83
|
-
if (members
|
|
85
|
+
if (members === void 0) classMembersMap.set(className, [memberName]);
|
|
86
|
+
else if (!members.includes(memberName)) members.push(memberName);
|
|
84
87
|
}
|
|
85
88
|
for (const members of classMembersMap.values()) members.sort((a, b) => b.length - a.length);
|
|
86
|
-
|
|
87
|
-
this.currentApiScope = apiScope;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Set the current API scope for cross-linking.
|
|
91
|
-
* This should be called before rendering each file to ensure links are scoped correctly.
|
|
92
|
-
*
|
|
93
|
-
* @param apiScope - The API scope identifier (e.g., "claude-binary-plugin")
|
|
94
|
-
*/
|
|
95
|
-
setApiScope(apiScope) {
|
|
96
|
-
this.currentApiScope = apiScope;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Get the routes map for the current API scope
|
|
100
|
-
*/
|
|
101
|
-
getRoutesForCurrentScope() {
|
|
102
|
-
if (!this.currentApiScope) return /* @__PURE__ */ new Map();
|
|
103
|
-
return this.apiItemRoutesByScope.get(this.currentApiScope) || /* @__PURE__ */ new Map();
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Get the kinds map for the current API scope
|
|
107
|
-
*/
|
|
108
|
-
getKindsForCurrentScope() {
|
|
109
|
-
if (!this.currentApiScope) return /* @__PURE__ */ new Map();
|
|
110
|
-
return this.apiItemKindsByScope.get(this.currentApiScope) || /* @__PURE__ */ new Map();
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Get the class members map for the current API scope
|
|
114
|
-
*/
|
|
115
|
-
getClassMembersForCurrentScope() {
|
|
116
|
-
if (!this.currentApiScope) return /* @__PURE__ */ new Map();
|
|
117
|
-
return this.classMembersMapByScope.get(this.currentApiScope) || /* @__PURE__ */ new Map();
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Get the routes map for a specific API scope
|
|
121
|
-
*/
|
|
122
|
-
getRoutesForScope(scope) {
|
|
123
|
-
if (!scope) return /* @__PURE__ */ new Map();
|
|
124
|
-
return this.apiItemRoutesByScope.get(scope) || /* @__PURE__ */ new Map();
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Get the kinds map for a specific API scope
|
|
128
|
-
*/
|
|
129
|
-
getKindsForScope(scope) {
|
|
130
|
-
if (!scope) return /* @__PURE__ */ new Map();
|
|
131
|
-
return this.apiItemKindsByScope.get(scope) || /* @__PURE__ */ new Map();
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Get the class members map for a specific API scope
|
|
135
|
-
*/
|
|
136
|
-
getClassMembersForScope(scope) {
|
|
137
|
-
if (!scope) return /* @__PURE__ */ new Map();
|
|
138
|
-
return this.classMembersMapByScope.get(scope) || /* @__PURE__ */ new Map();
|
|
89
|
+
return new ShikiCrossLinker(apiScope, new Map(routes), new Map(kinds), classMembersMap);
|
|
139
90
|
}
|
|
91
|
+
/** A linker that links nothing — for a scope with no documented routes. */
|
|
92
|
+
static empty = new ShikiCrossLinker("", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map());
|
|
140
93
|
/**
|
|
141
94
|
* Transform a finalized HAST tree to add cross-links to type references.
|
|
142
95
|
*
|
|
@@ -145,162 +98,28 @@ var ShikiCrossLinker = class {
|
|
|
145
98
|
* positions before we add anchor links.
|
|
146
99
|
*
|
|
147
100
|
* @param hast - The finalized HAST root node from Shiki
|
|
148
|
-
* @param apiScope - Optional API scope to use for lookups. If not provided, uses currentApiScope.
|
|
149
101
|
* @returns The transformed HAST with cross-links added
|
|
150
102
|
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* There is no scope parameter. A linker IS a scope — the caller picks the
|
|
105
|
+
* right instance (from the `VfsRegistry` entry for the page's scope) rather
|
|
106
|
+
* than picking the right argument. The parameter it replaces was optional
|
|
107
|
+
* and fell back to a mutable `currentApiScope`, so omitting it linked
|
|
108
|
+
* against whichever package happened to render last.
|
|
109
|
+
*
|
|
151
110
|
* @example
|
|
152
111
|
* ```ts
|
|
153
112
|
* const hast = await highlighter.codeToHast(code, { transformers: [twoslashTransformer] });
|
|
154
|
-
* const linkedHast = crossLinker.transformHast(hast
|
|
113
|
+
* const linkedHast = crossLinker.transformHast(hast);
|
|
155
114
|
* ```
|
|
156
115
|
*/
|
|
157
|
-
transformHast(hast
|
|
158
|
-
|
|
159
|
-
return this.transformRootWithScope(hast, effectiveScope);
|
|
116
|
+
transformHast(hast) {
|
|
117
|
+
return this.transformRoot(hast);
|
|
160
118
|
}
|
|
161
|
-
/**
|
|
162
|
-
* Transform the root node of the syntax tree with explicit scope
|
|
163
|
-
*/
|
|
164
|
-
transformRootWithScope(node, scope) {
|
|
165
|
-
const apiItemRoutes = this.getRoutesForScope(scope);
|
|
166
|
-
const apiItemKinds = this.getKindsForScope(scope);
|
|
167
|
-
const classMembersMap = this.getClassMembersForScope(scope);
|
|
168
|
-
const scopeStack = [];
|
|
169
|
-
const preElement = node.children.find((child) => child.type === "element" && child.tagName === "pre");
|
|
170
|
-
if (preElement?.type !== "element") return node;
|
|
171
|
-
const codeElement = preElement.children.find((child) => child.type === "element" && child.tagName === "code");
|
|
172
|
-
if (codeElement?.type !== "element") return node;
|
|
173
|
-
for (const lineElement of codeElement.children) {
|
|
174
|
-
if (lineElement.type !== "element" || lineElement.tagName !== "span") continue;
|
|
175
|
-
const getText = (node) => {
|
|
176
|
-
if (node.type === "text") return node.value;
|
|
177
|
-
if (node.type === "element") return node.children.map(getText).join("");
|
|
178
|
-
return "";
|
|
179
|
-
};
|
|
180
|
-
const lineText = lineElement.children.map(getText).join("");
|
|
181
|
-
const currentScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
|
|
182
|
-
if (currentScope) {
|
|
183
|
-
const members = classMembersMap.get(currentScope);
|
|
184
|
-
if (members) for (const spanElement of lineElement.children) {
|
|
185
|
-
if (spanElement.type !== "element" || spanElement.tagName !== "span") continue;
|
|
186
|
-
if (spanElement.children?.length !== 1) continue;
|
|
187
|
-
const textNode = spanElement.children[0];
|
|
188
|
-
if (textNode.type !== "text") continue;
|
|
189
|
-
const rawContent = textNode.value;
|
|
190
|
-
const content = rawContent.trim();
|
|
191
|
-
if (!content) continue;
|
|
192
|
-
if (members.includes(content)) {
|
|
193
|
-
const fullMemberName = `${currentScope}.${content}`;
|
|
194
|
-
const memberRoute = apiItemRoutes.get(fullMemberName);
|
|
195
|
-
if (memberRoute) {
|
|
196
|
-
const memberKind = apiItemKinds.get(fullMemberName);
|
|
197
|
-
const memberSemanticClass = memberKind ? this.getSemanticClass(memberKind) : null;
|
|
198
|
-
const leadingSpace = rawContent.match(/^\s*/)?.[0] || "";
|
|
199
|
-
const trailingSpace = rawContent.match(/\s*$/)?.[0] || "";
|
|
200
|
-
const classNames = ["api-type-link"];
|
|
201
|
-
if (memberSemanticClass) classNames.push(memberSemanticClass);
|
|
202
|
-
const newChildren = [];
|
|
203
|
-
if (leadingSpace) newChildren.push({
|
|
204
|
-
type: "text",
|
|
205
|
-
value: leadingSpace
|
|
206
|
-
});
|
|
207
|
-
newChildren.push({
|
|
208
|
-
type: "element",
|
|
209
|
-
tagName: "a",
|
|
210
|
-
properties: {
|
|
211
|
-
href: memberRoute,
|
|
212
|
-
class: classNames.join(" ")
|
|
213
|
-
},
|
|
214
|
-
children: [{
|
|
215
|
-
type: "text",
|
|
216
|
-
value: content
|
|
217
|
-
}]
|
|
218
|
-
});
|
|
219
|
-
if (trailingSpace) newChildren.push({
|
|
220
|
-
type: "text",
|
|
221
|
-
value: trailingSpace
|
|
222
|
-
});
|
|
223
|
-
spanElement.children = newChildren;
|
|
224
|
-
spanElement.properties = {
|
|
225
|
-
...spanElement.properties,
|
|
226
|
-
"data-api-processed": "true"
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
const classMatch = lineText.match(/(?:class|interface|namespace)\s+(\w+)\s*(?:<[^>]*>)?\s*(?:extends|implements)?[^{]*\{/);
|
|
233
|
-
if (classMatch) {
|
|
234
|
-
if ((lineText.match(/\{/g) || []).length > (lineText.match(/\}/g) || []).length) scopeStack.push(classMatch[1]);
|
|
235
|
-
}
|
|
236
|
-
const openBraces = (lineText.match(/\{/g) || []).length;
|
|
237
|
-
const excessCloses = (lineText.match(/\}/g) || []).length - openBraces;
|
|
238
|
-
for (let i = 0; i < excessCloses && scopeStack.length > 0; i++) scopeStack.pop();
|
|
239
|
-
}
|
|
240
|
-
const findTwoslashSpans = (element) => {
|
|
241
|
-
const results = [];
|
|
242
|
-
if (element.properties?.class && String(element.properties.class).includes("twoslash-hover")) results.push(element);
|
|
243
|
-
if (element.children) {
|
|
244
|
-
for (const child of element.children) if (child.type === "element") results.push(...findTwoslashSpans(child));
|
|
245
|
-
}
|
|
246
|
-
return results;
|
|
247
|
-
};
|
|
248
|
-
const twoslashSpans = findTwoslashSpans(codeElement);
|
|
249
|
-
for (const twoslashSpan of twoslashSpans) {
|
|
250
|
-
if (twoslashSpan.properties?.["data-api-processed"] === "true") continue;
|
|
251
|
-
const methodInfo = this.extractMethodInfoFromTwoslashTooltip(twoslashSpan);
|
|
252
|
-
if (!methodInfo) continue;
|
|
253
|
-
const { className, methodName } = methodInfo;
|
|
254
|
-
const fullMemberName = `${className}.${methodName}`;
|
|
255
|
-
const memberRoute = apiItemRoutes.get(fullMemberName);
|
|
256
|
-
if (!memberRoute) continue;
|
|
257
|
-
const memberKind = apiItemKinds.get(fullMemberName);
|
|
258
|
-
const memberSemanticClass = memberKind ? this.getSemanticClass(memberKind) : null;
|
|
259
|
-
const memberClassNames = ["api-type-link"];
|
|
260
|
-
if (memberSemanticClass) memberClassNames.push(memberSemanticClass);
|
|
261
|
-
const textContent = this.extractTextFromTwoslash(twoslashSpan);
|
|
262
|
-
if (!textContent) continue;
|
|
263
|
-
this.wrapTwoslashTextInAnchor(twoslashSpan, textContent.trim(), memberRoute, memberClassNames);
|
|
264
|
-
twoslashSpan.properties = {
|
|
265
|
-
...twoslashSpan.properties,
|
|
266
|
-
"data-api-processed": "true"
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
const typeNames = Array.from(apiItemRoutes.keys()).filter((name) => !name.includes(".")).sort((a, b) => b.length - a.length);
|
|
270
|
-
if (typeNames.length > 0) {
|
|
271
|
-
for (const twoslashSpan of twoslashSpans) {
|
|
272
|
-
if (twoslashSpan.properties?.["data-api-processed"] === "true") continue;
|
|
273
|
-
const text = this.extractTextFromTwoslash(twoslashSpan);
|
|
274
|
-
if (!text) continue;
|
|
275
|
-
const content = text.trim();
|
|
276
|
-
const route = apiItemRoutes.get(content);
|
|
277
|
-
if (!route) continue;
|
|
278
|
-
const kind = apiItemKinds.get(content);
|
|
279
|
-
const semanticClass = kind ? this.getSemanticClass(kind) : null;
|
|
280
|
-
const classNames = ["api-type-link"];
|
|
281
|
-
if (semanticClass) classNames.push(semanticClass);
|
|
282
|
-
this.wrapTwoslashTextInAnchor(twoslashSpan, content, route, classNames);
|
|
283
|
-
twoslashSpan.properties = {
|
|
284
|
-
...twoslashSpan.properties,
|
|
285
|
-
"data-api-processed": "true"
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
const escapedNames = typeNames.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
289
|
-
const typePattern = new RegExp(`\\b(${escapedNames.join("|")})\\b`, "g");
|
|
290
|
-
for (const lineElement of codeElement.children) {
|
|
291
|
-
if (lineElement.type !== "element" || lineElement.tagName !== "span") continue;
|
|
292
|
-
this.linkTypeReferencesInLine(lineElement, typePattern, apiItemRoutes, apiItemKinds);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
return node;
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Transform the root node of the syntax tree
|
|
299
|
-
*/
|
|
300
119
|
transformRoot(node) {
|
|
301
|
-
const apiItemRoutes = this.
|
|
302
|
-
const apiItemKinds = this.
|
|
303
|
-
const classMembersMap = this.
|
|
120
|
+
const apiItemRoutes = this.apiItemRoutes;
|
|
121
|
+
const apiItemKinds = this.apiItemKinds;
|
|
122
|
+
const classMembersMap = this.classMembersMap;
|
|
304
123
|
const scopeStack = [];
|
|
305
124
|
const preElement = node.children.find((child) => child.type === "element" && child.tagName === "pre");
|
|
306
125
|
if (preElement?.type !== "element") return node;
|
|
@@ -434,9 +253,9 @@ var ShikiCrossLinker = class {
|
|
|
434
253
|
* Transform a line element
|
|
435
254
|
*/
|
|
436
255
|
transformLine(node) {
|
|
437
|
-
const apiItemRoutes = this.
|
|
438
|
-
const apiItemKinds = this.
|
|
439
|
-
const classMembersMap = this.
|
|
256
|
+
const apiItemRoutes = this.apiItemRoutes;
|
|
257
|
+
const apiItemKinds = this.apiItemKinds;
|
|
258
|
+
const classMembersMap = this.classMembersMap;
|
|
440
259
|
if (!node.children) return;
|
|
441
260
|
for (let i = 0; i < node.children.length; i++) {
|
|
442
261
|
const child = node.children[i];
|
|
@@ -521,8 +340,8 @@ var ShikiCrossLinker = class {
|
|
|
521
340
|
* Transform a span element
|
|
522
341
|
*/
|
|
523
342
|
transformSpan(node, _line, _col) {
|
|
524
|
-
const apiItemRoutes = this.
|
|
525
|
-
const apiItemKinds = this.
|
|
343
|
+
const apiItemRoutes = this.apiItemRoutes;
|
|
344
|
+
const apiItemKinds = this.apiItemKinds;
|
|
526
345
|
if (node.properties?.["data-api-processed"] === "true") return;
|
|
527
346
|
const firstChild = node.children?.[0];
|
|
528
347
|
if (firstChild && firstChild.type === "element" && firstChild.tagName === "a") return;
|
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 };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/twoslash-access.ts
|
|
2
|
+
/** The uninstalled state: no environments, so nothing to hand out. */
|
|
3
|
+
const NOT_INSTALLED = {
|
|
4
|
+
transformerFor: () => null,
|
|
5
|
+
setCurrentFile: () => {}
|
|
6
|
+
};
|
|
7
|
+
let current = NOT_INSTALLED;
|
|
8
|
+
/**
|
|
9
|
+
* Bind the render pass to this build's environments.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* Called from `plugin.ts`'s Effect program, beside the other seam wiring, and
|
|
13
|
+
* NOT from `ConfigService.layer` — config resolution should compute a value, not
|
|
14
|
+
* also mutate module state on the side.
|
|
15
|
+
*/
|
|
16
|
+
function installTwoslashAccess(environments) {
|
|
17
|
+
current = environments;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Reset to the uninstalled state.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* Called at the start of every build, next to `VfsRegistry.clear()`, for the
|
|
24
|
+
* same reason that call exists: a dev HMR session reuses the process, so a
|
|
25
|
+
* holder from the previous build would otherwise outlive it and hand the
|
|
26
|
+
* render pass transformers built against declarations that have since changed.
|
|
27
|
+
*/
|
|
28
|
+
function clearTwoslashAccess() {
|
|
29
|
+
current = NOT_INSTALLED;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The transformer for a scope, for the remark plugins.
|
|
33
|
+
*
|
|
34
|
+
* @returns `null` when nothing is installed — an inert build, which registers
|
|
35
|
+
* no environments at all. A `with-api` fence still renders; it just renders
|
|
36
|
+
* without type information, which is the honest answer when the build
|
|
37
|
+
* documents no API.
|
|
38
|
+
*/
|
|
39
|
+
function twoslashTransformerFor(apiScope) {
|
|
40
|
+
return current.transformerFor(apiScope);
|
|
41
|
+
}
|
|
42
|
+
/** Attribute subsequent Twoslash diagnostics to a source file. */
|
|
43
|
+
function setTwoslashFile(path) {
|
|
44
|
+
current.setCurrentFile(path);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { clearTwoslashAccess, installTwoslashAccess, setTwoslashFile, twoslashTransformerFor };
|