@tsdoctor/model 0.4.1 → 0.5.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/ApiExtractedPackage.js +477 -0
- package/Frontmatter.js +152 -0
- package/README.md +5 -3
- package/TypeReferenceExtractor.js +201 -0
- package/index.d.ts +366 -2
- package/index.js +4 -1
- package/package.json +4 -8
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { ApiItemKind, ApiModel, ExcerptTokenKind } from "@microsoft/api-extractor-model";
|
|
2
|
+
import { VirtualPackage } from "@tsdoctor/vfs";
|
|
3
|
+
|
|
4
|
+
//#region src/ApiExtractedPackage.ts
|
|
5
|
+
/**
|
|
6
|
+
* Reconstructs TypeScript declaration files from an API Extractor model.
|
|
7
|
+
*
|
|
8
|
+
* Extends `VirtualPackage` with the ability to generate high-fidelity
|
|
9
|
+
* `.d.ts` output from API Extractor's `ApiPackage` — including enum values,
|
|
10
|
+
* full JSDoc, namespace members, and all interface member kinds.
|
|
11
|
+
*
|
|
12
|
+
* Use the factory methods `fromApiModel` or `fromPackage` to create instances.
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
var ApiExtractedPackage = class ApiExtractedPackage extends VirtualPackage {
|
|
17
|
+
apiPackage;
|
|
18
|
+
constructor(apiPackage, packageName, entries) {
|
|
19
|
+
super({
|
|
20
|
+
name: packageName,
|
|
21
|
+
version: "1.0.0",
|
|
22
|
+
entries
|
|
23
|
+
});
|
|
24
|
+
this.apiPackage = apiPackage;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create an ApiExtractedPackage from an API model JSON file path.
|
|
28
|
+
*/
|
|
29
|
+
static fromApiModel(modelPath) {
|
|
30
|
+
const apiPackage = new ApiModel().loadPackage(modelPath);
|
|
31
|
+
return ApiExtractedPackage.fromPackage(apiPackage, apiPackage.name);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create an ApiExtractedPackage from an existing ApiPackage instance.
|
|
35
|
+
*/
|
|
36
|
+
static fromPackage(apiPackage, packageName) {
|
|
37
|
+
const scratch = new ApiExtractedPackage(apiPackage, packageName, /* @__PURE__ */ new Map([["index.d.ts", ""]]));
|
|
38
|
+
const entries = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const ep of apiPackage.entryPoints) {
|
|
40
|
+
const entryName = scratch.getEntryPointName(ep);
|
|
41
|
+
const fileName = entryName ? `${entryName}.d.ts` : "index.d.ts";
|
|
42
|
+
entries.set(fileName, scratch.generateDeclarations(ep));
|
|
43
|
+
}
|
|
44
|
+
return new ApiExtractedPackage(apiPackage, packageName, entries);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generate the .d.ts content for a specific entry point.
|
|
48
|
+
*/
|
|
49
|
+
generateDeclarations(entryPoint) {
|
|
50
|
+
const ep = entryPoint ?? this.apiPackage.entryPoints[0];
|
|
51
|
+
if (!ep) return "";
|
|
52
|
+
const parts = [];
|
|
53
|
+
const packageDoc = this.extractPackageDocumentation();
|
|
54
|
+
if (packageDoc) {
|
|
55
|
+
parts.push(packageDoc);
|
|
56
|
+
parts.push("");
|
|
57
|
+
}
|
|
58
|
+
for (const member of ep.members) {
|
|
59
|
+
const decl = this.generateDeclaration(member);
|
|
60
|
+
if (decl) {
|
|
61
|
+
parts.push(decl);
|
|
62
|
+
parts.push("");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
parts.push("export { }");
|
|
66
|
+
parts.push("");
|
|
67
|
+
return parts.join("\n");
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Generate a TypeScript declaration for a single API item.
|
|
71
|
+
*/
|
|
72
|
+
generateDeclaration(apiItem) {
|
|
73
|
+
switch (apiItem.kind) {
|
|
74
|
+
case ApiItemKind.Class: return this.generateClassDeclaration(apiItem);
|
|
75
|
+
case ApiItemKind.Interface: return this.generateInterfaceDeclaration(apiItem);
|
|
76
|
+
case ApiItemKind.TypeAlias: return this.generateTypeAliasDeclaration(apiItem);
|
|
77
|
+
case ApiItemKind.Function: return this.generateFunctionDeclaration(apiItem);
|
|
78
|
+
case ApiItemKind.Enum: return this.generateEnumDeclaration(apiItem);
|
|
79
|
+
case ApiItemKind.Variable: return this.generateVariableDeclaration(apiItem);
|
|
80
|
+
case ApiItemKind.Namespace: return this.generateNamespaceDeclaration(apiItem);
|
|
81
|
+
default: return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
generateClassDeclaration(apiClass) {
|
|
85
|
+
const lines = [];
|
|
86
|
+
const jsDoc = this.formatJSDoc(apiClass);
|
|
87
|
+
if (jsDoc) lines.push(jsDoc);
|
|
88
|
+
let name = apiClass.displayName;
|
|
89
|
+
if (apiClass.typeParameters?.length) name += this.formatTypeParameters(apiClass.typeParameters);
|
|
90
|
+
const headerParts = apiClass.isAbstract ? ["export declare abstract class", name] : ["export declare class", name];
|
|
91
|
+
if (apiClass.extendsType) headerParts.push(`extends ${this.renderExcerpt(apiClass.extendsType.excerpt)}`);
|
|
92
|
+
if (apiClass.implementsTypes?.length) {
|
|
93
|
+
const impl = apiClass.implementsTypes.map((t) => this.renderExcerpt(t.excerpt)).join(", ");
|
|
94
|
+
headerParts.push(`implements ${impl}`);
|
|
95
|
+
}
|
|
96
|
+
lines.push(`${headerParts.join(" ")} {`);
|
|
97
|
+
for (const member of apiClass.members) {
|
|
98
|
+
const memberDecl = this.generateClassMember(member);
|
|
99
|
+
if (memberDecl) lines.push(memberDecl);
|
|
100
|
+
}
|
|
101
|
+
lines.push("}");
|
|
102
|
+
return lines.join("\n");
|
|
103
|
+
}
|
|
104
|
+
generateInterfaceDeclaration(apiInterface) {
|
|
105
|
+
const lines = [];
|
|
106
|
+
const jsDoc = this.formatJSDoc(apiInterface);
|
|
107
|
+
if (jsDoc) lines.push(jsDoc);
|
|
108
|
+
let name = apiInterface.displayName;
|
|
109
|
+
if (apiInterface.typeParameters?.length) name += this.formatTypeParameters(apiInterface.typeParameters);
|
|
110
|
+
const headerParts = ["export declare interface", name];
|
|
111
|
+
if (apiInterface.extendsTypes?.length) {
|
|
112
|
+
const ext = apiInterface.extendsTypes.map((t) => this.renderExcerpt(t.excerpt)).join(", ");
|
|
113
|
+
headerParts.push(`extends ${ext}`);
|
|
114
|
+
}
|
|
115
|
+
lines.push(`${headerParts.join(" ")} {`);
|
|
116
|
+
for (const member of apiInterface.members) {
|
|
117
|
+
const memberDecl = this.generateInterfaceMember(member);
|
|
118
|
+
if (memberDecl) lines.push(memberDecl);
|
|
119
|
+
}
|
|
120
|
+
lines.push("}");
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
generateTypeAliasDeclaration(typeAlias) {
|
|
124
|
+
const lines = [];
|
|
125
|
+
const jsDoc = this.formatJSDoc(typeAlias);
|
|
126
|
+
if (jsDoc) lines.push(jsDoc);
|
|
127
|
+
let name = typeAlias.displayName;
|
|
128
|
+
if (typeAlias.typeParameters?.length) name += this.formatTypeParameters(typeAlias.typeParameters);
|
|
129
|
+
lines.push(`export declare type ${name} = ${this.renderExcerpt(typeAlias.typeExcerpt)};`);
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
generateFunctionDeclaration(apiFunction) {
|
|
133
|
+
const lines = [];
|
|
134
|
+
const jsDoc = this.formatJSDoc(apiFunction);
|
|
135
|
+
if (jsDoc) lines.push(jsDoc);
|
|
136
|
+
const cleaned = this.cleanExcerpt(this.renderExcerpt(apiFunction.excerpt));
|
|
137
|
+
const decl = cleaned.startsWith("function ") ? cleaned : `const ${cleaned}`;
|
|
138
|
+
lines.push(`export declare ${decl};`);
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
generateEnumDeclaration(apiEnum) {
|
|
142
|
+
const lines = [];
|
|
143
|
+
const jsDoc = this.formatJSDoc(apiEnum);
|
|
144
|
+
if (jsDoc) lines.push(jsDoc);
|
|
145
|
+
lines.push(`export declare enum ${apiEnum.displayName} {`);
|
|
146
|
+
const enumMembers = apiEnum.members.filter((m) => m.kind === ApiItemKind.EnumMember);
|
|
147
|
+
for (let i = 0; i < enumMembers.length; i++) {
|
|
148
|
+
const enumMember = enumMembers[i];
|
|
149
|
+
const memberJsDoc = this.formatJSDoc(enumMember, " ");
|
|
150
|
+
if (memberJsDoc) lines.push(memberJsDoc);
|
|
151
|
+
const suffix = i === enumMembers.length - 1 ? "" : ",";
|
|
152
|
+
const initExcerpt = enumMember.initializerExcerpt;
|
|
153
|
+
if (initExcerpt?.text.trim()) lines.push(` ${enumMember.displayName} = ${initExcerpt.text.trim()}${suffix}`);
|
|
154
|
+
else lines.push(` ${enumMember.displayName}${suffix}`);
|
|
155
|
+
}
|
|
156
|
+
lines.push("}");
|
|
157
|
+
return lines.join("\n");
|
|
158
|
+
}
|
|
159
|
+
generateVariableDeclaration(apiVariable) {
|
|
160
|
+
const lines = [];
|
|
161
|
+
const jsDoc = this.formatJSDoc(apiVariable);
|
|
162
|
+
if (jsDoc) lines.push(jsDoc);
|
|
163
|
+
let cleaned = this.cleanExcerpt(this.renderExcerpt(apiVariable.excerpt));
|
|
164
|
+
if (!cleaned.startsWith("const ") && !cleaned.startsWith("let ") && !cleaned.startsWith("var ")) cleaned = `const ${cleaned}`;
|
|
165
|
+
lines.push(`export declare ${cleaned};`);
|
|
166
|
+
return lines.join("\n");
|
|
167
|
+
}
|
|
168
|
+
generateNamespaceDeclaration(apiNamespace) {
|
|
169
|
+
const lines = [];
|
|
170
|
+
const jsDoc = this.formatJSDoc(apiNamespace);
|
|
171
|
+
if (jsDoc) lines.push(jsDoc);
|
|
172
|
+
lines.push(`export declare namespace ${apiNamespace.displayName} {`);
|
|
173
|
+
for (const member of apiNamespace.members) {
|
|
174
|
+
const memberDecl = this.generateNamespaceMember(member);
|
|
175
|
+
if (memberDecl) lines.push(memberDecl);
|
|
176
|
+
}
|
|
177
|
+
lines.push("}");
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
}
|
|
180
|
+
generateNamespaceMember(apiItem) {
|
|
181
|
+
switch (apiItem.kind) {
|
|
182
|
+
case ApiItemKind.Function: return this.generateNamespaceFunction(apiItem);
|
|
183
|
+
case ApiItemKind.Interface: return this.generateNamespaceInterface(apiItem);
|
|
184
|
+
case ApiItemKind.Enum: return this.generateNamespaceEnum(apiItem);
|
|
185
|
+
case ApiItemKind.TypeAlias: return this.generateNamespaceTypeAlias(apiItem);
|
|
186
|
+
case ApiItemKind.Variable: return this.generateNamespaceVariable(apiItem);
|
|
187
|
+
case ApiItemKind.Class: return this.generateNamespaceClass(apiItem);
|
|
188
|
+
default: return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
generateNamespaceFunction(apiFunction) {
|
|
192
|
+
const lines = [];
|
|
193
|
+
const jsDoc = this.formatJSDoc(apiFunction, " ");
|
|
194
|
+
if (jsDoc) lines.push(jsDoc);
|
|
195
|
+
const cleaned = this.cleanExcerpt(this.renderExcerpt(apiFunction.excerpt));
|
|
196
|
+
lines.push(` export ${cleaned};`);
|
|
197
|
+
return lines.join("\n");
|
|
198
|
+
}
|
|
199
|
+
generateNamespaceInterface(apiInterface) {
|
|
200
|
+
const lines = [];
|
|
201
|
+
const jsDoc = this.formatJSDoc(apiInterface, " ");
|
|
202
|
+
if (jsDoc) lines.push(jsDoc);
|
|
203
|
+
let name = apiInterface.displayName;
|
|
204
|
+
if (apiInterface.typeParameters?.length) name += this.formatTypeParameters(apiInterface.typeParameters);
|
|
205
|
+
const headerParts = ["export interface", name];
|
|
206
|
+
if (apiInterface.extendsTypes?.length) {
|
|
207
|
+
const ext = apiInterface.extendsTypes.map((t) => this.renderExcerpt(t.excerpt)).join(", ");
|
|
208
|
+
headerParts.push(`extends ${ext}`);
|
|
209
|
+
}
|
|
210
|
+
lines.push(` ${headerParts.join(" ")} {`);
|
|
211
|
+
for (const member of apiInterface.members) {
|
|
212
|
+
const memberDecl = this.generateInterfaceMember(member, " ");
|
|
213
|
+
if (memberDecl) lines.push(memberDecl);
|
|
214
|
+
}
|
|
215
|
+
lines.push(" }");
|
|
216
|
+
return lines.join("\n");
|
|
217
|
+
}
|
|
218
|
+
generateNamespaceEnum(apiEnum) {
|
|
219
|
+
const lines = [];
|
|
220
|
+
const jsDoc = this.formatJSDoc(apiEnum, " ");
|
|
221
|
+
if (jsDoc) lines.push(jsDoc);
|
|
222
|
+
lines.push(` export enum ${apiEnum.displayName} {`);
|
|
223
|
+
const enumMembers = apiEnum.members.filter((m) => m.kind === ApiItemKind.EnumMember);
|
|
224
|
+
for (let i = 0; i < enumMembers.length; i++) {
|
|
225
|
+
const enumMember = enumMembers[i];
|
|
226
|
+
const memberJsDoc = this.formatJSDoc(enumMember, " ");
|
|
227
|
+
if (memberJsDoc) lines.push(memberJsDoc);
|
|
228
|
+
const suffix = i === enumMembers.length - 1 ? "" : ",";
|
|
229
|
+
const initExcerpt = enumMember.initializerExcerpt;
|
|
230
|
+
if (initExcerpt?.text.trim()) lines.push(` ${enumMember.displayName} = ${initExcerpt.text.trim()}${suffix}`);
|
|
231
|
+
else lines.push(` ${enumMember.displayName}${suffix}`);
|
|
232
|
+
}
|
|
233
|
+
lines.push(" }");
|
|
234
|
+
return lines.join("\n");
|
|
235
|
+
}
|
|
236
|
+
generateNamespaceTypeAlias(typeAlias) {
|
|
237
|
+
const lines = [];
|
|
238
|
+
const jsDoc = this.formatJSDoc(typeAlias, " ");
|
|
239
|
+
if (jsDoc) lines.push(jsDoc);
|
|
240
|
+
let name = typeAlias.displayName;
|
|
241
|
+
if (typeAlias.typeParameters?.length) name += this.formatTypeParameters(typeAlias.typeParameters);
|
|
242
|
+
lines.push(` export type ${name} = ${this.renderExcerpt(typeAlias.typeExcerpt)};`);
|
|
243
|
+
return lines.join("\n");
|
|
244
|
+
}
|
|
245
|
+
generateNamespaceVariable(apiVariable) {
|
|
246
|
+
const lines = [];
|
|
247
|
+
const jsDoc = this.formatJSDoc(apiVariable, " ");
|
|
248
|
+
if (jsDoc) lines.push(jsDoc);
|
|
249
|
+
let cleaned = this.cleanExcerpt(this.renderExcerpt(apiVariable.excerpt));
|
|
250
|
+
if (!cleaned.startsWith("const ") && !cleaned.startsWith("let ") && !cleaned.startsWith("var ")) cleaned = `const ${cleaned}`;
|
|
251
|
+
lines.push(` export ${cleaned};`);
|
|
252
|
+
return lines.join("\n");
|
|
253
|
+
}
|
|
254
|
+
generateNamespaceClass(apiClass) {
|
|
255
|
+
const decl = this.generateClassDeclaration(apiClass);
|
|
256
|
+
if (!decl) return "";
|
|
257
|
+
return decl.replace(/\bexport declare (abstract )?class\b/, "export $1class").split("\n").map((line) => line.trim() ? ` ${line}` : line).join("\n");
|
|
258
|
+
}
|
|
259
|
+
generateClassMember(member, indent = " ") {
|
|
260
|
+
switch (member.kind) {
|
|
261
|
+
case ApiItemKind.Constructor: return this.generateMemberFromExcerpt(member, indent);
|
|
262
|
+
case ApiItemKind.Method: return this.generateMemberFromExcerpt(member, indent);
|
|
263
|
+
case ApiItemKind.Property: return this.generateMemberFromExcerpt(member, indent);
|
|
264
|
+
default: return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
generateInterfaceMember(member, indent = " ") {
|
|
268
|
+
switch (member.kind) {
|
|
269
|
+
case ApiItemKind.MethodSignature: return this.generateMemberFromExcerpt(member, indent);
|
|
270
|
+
case ApiItemKind.PropertySignature: return this.generateMemberFromExcerpt(member, indent);
|
|
271
|
+
case ApiItemKind.CallSignature: return this.generateMemberFromExcerpt(member, indent);
|
|
272
|
+
case ApiItemKind.ConstructSignature: return this.generateMemberFromExcerpt(member, indent);
|
|
273
|
+
case ApiItemKind.IndexSignature: return this.generateMemberFromExcerpt(member, indent);
|
|
274
|
+
default: return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
generateMemberFromExcerpt(member, indent) {
|
|
278
|
+
const lines = [];
|
|
279
|
+
const jsDoc = this.formatJSDoc(member, indent);
|
|
280
|
+
if (jsDoc) lines.push(jsDoc);
|
|
281
|
+
const cleaned = this.cleanExcerpt(this.renderExcerpt(member.excerpt));
|
|
282
|
+
lines.push(`${indent}${cleaned};`);
|
|
283
|
+
return lines.join("\n");
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Render an excerpt to source text, normalizing dts-rollup disambiguation
|
|
287
|
+
* aliases. The dts rollup renames a re-imported symbol as `Name$1`, but its
|
|
288
|
+
* canonical reference is the un-suffixed `Name` (the same symbol). The import
|
|
289
|
+
* prepender ({@link TypeReferenceExtractor}) imports the canonical name, so
|
|
290
|
+
* emitting the suffixed text would leave `Name$1` undefined (TS2304). Emit the
|
|
291
|
+
* canonical name so the body and the prepended import agree.
|
|
292
|
+
*
|
|
293
|
+
* Equivalent to `excerpt.text` for excerpts without rollup aliases (the text
|
|
294
|
+
* is the concatenation of the spanned tokens), so unaliased output is unchanged.
|
|
295
|
+
*/
|
|
296
|
+
renderExcerpt(excerpt) {
|
|
297
|
+
return excerpt.spannedTokens.map((token) => this.normalizeTokenText(token)).join("");
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Strip a dts-rollup `$N` suffix from a reference token when the de-suffixed
|
|
301
|
+
* text matches the token's canonical symbol. Never touches a non-reference
|
|
302
|
+
* token or a legitimate identifier that genuinely ends in `$N` (its canonical
|
|
303
|
+
* name would carry the suffix too).
|
|
304
|
+
*/
|
|
305
|
+
normalizeTokenText(token) {
|
|
306
|
+
if (token.kind !== ExcerptTokenKind.Reference) return token.text;
|
|
307
|
+
const match = /^(.+)\$\d+$/.exec(token.text);
|
|
308
|
+
if (!match) return token.text;
|
|
309
|
+
const canonical = token.canonicalReference?.toString();
|
|
310
|
+
if (!canonical) return token.text;
|
|
311
|
+
const afterBang = canonical.slice(canonical.indexOf("!") + 1);
|
|
312
|
+
const colon = afterBang.indexOf(":");
|
|
313
|
+
const symbol = colon === -1 ? afterBang : afterBang.slice(0, colon);
|
|
314
|
+
const leaf = symbol.includes(".") ? symbol.slice(symbol.lastIndexOf(".") + 1) : symbol;
|
|
315
|
+
return match[1] === symbol || match[1] === leaf ? match[1] : token.text;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Clean an excerpt text: strip export/declare keywords and trailing semicolons/whitespace.
|
|
319
|
+
*/
|
|
320
|
+
cleanExcerpt(text) {
|
|
321
|
+
return text.replace(/^export\s+/, "").replace(/^declare\s+/, "").replace(/;+\s*$/, "").trim();
|
|
322
|
+
}
|
|
323
|
+
formatTypeParameters(typeParameters) {
|
|
324
|
+
if (!typeParameters.length) return "";
|
|
325
|
+
return `<${typeParameters.map((tp) => {
|
|
326
|
+
const parts = [tp.name];
|
|
327
|
+
if (tp.constraintExcerpt && this.renderExcerpt(tp.constraintExcerpt).trim()) parts.push(`extends ${this.renderExcerpt(tp.constraintExcerpt).trim()}`);
|
|
328
|
+
if (tp.defaultTypeExcerpt && this.renderExcerpt(tp.defaultTypeExcerpt).trim()) parts.push(`= ${this.renderExcerpt(tp.defaultTypeExcerpt).trim()}`);
|
|
329
|
+
return parts.join(" ");
|
|
330
|
+
}).join(", ")}>`;
|
|
331
|
+
}
|
|
332
|
+
extractPackageDocumentation() {
|
|
333
|
+
const pkg = this.apiPackage;
|
|
334
|
+
if (!pkg.tsdocComment?.summarySection) return null;
|
|
335
|
+
const summary = this.extractPlainText(pkg.tsdocComment.summarySection).trim();
|
|
336
|
+
if (!summary) return null;
|
|
337
|
+
const lines = [];
|
|
338
|
+
for (const line of summary.split("\n")) lines.push(line);
|
|
339
|
+
lines.push("");
|
|
340
|
+
lines.push("@packageDocumentation");
|
|
341
|
+
return `/**\n${lines.map((line) => line ? ` * ${line}` : " *").join("\n")}\n */`;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Format JSDoc comment from an API item's TSDoc.
|
|
345
|
+
* Produces output matching the TypeScript compiler's JSDoc style.
|
|
346
|
+
*/
|
|
347
|
+
formatJSDoc(apiItem, indent = "") {
|
|
348
|
+
const item = apiItem;
|
|
349
|
+
if (!item.tsdocComment) return null;
|
|
350
|
+
const tsdoc = item.tsdocComment;
|
|
351
|
+
const lines = [];
|
|
352
|
+
if (tsdoc.summarySection) {
|
|
353
|
+
const summary = this.extractPlainText(tsdoc.summarySection).trim();
|
|
354
|
+
if (summary) for (const line of summary.split("\n")) lines.push(line);
|
|
355
|
+
}
|
|
356
|
+
const typeParamLines = [];
|
|
357
|
+
if (tsdoc.typeParams?.blocks) for (const block of tsdoc.typeParams.blocks) {
|
|
358
|
+
const blockAny = block;
|
|
359
|
+
const name = blockAny.parameterName || "";
|
|
360
|
+
const desc = this.extractPlainText(blockAny.content).replace(/\s+/g, " ").trim();
|
|
361
|
+
if (name && desc) typeParamLines.push(`@typeParam ${name} - ${desc}`);
|
|
362
|
+
}
|
|
363
|
+
const paramLines = [];
|
|
364
|
+
if (tsdoc.params?.blocks) for (const paramBlock of tsdoc.params.blocks) {
|
|
365
|
+
const param = paramBlock;
|
|
366
|
+
const name = param.parameterName || "";
|
|
367
|
+
const desc = this.extractPlainText(param.content).replace(/\s+/g, " ").trim();
|
|
368
|
+
if (name && desc) paramLines.push(`@param ${name} - ${desc}`);
|
|
369
|
+
}
|
|
370
|
+
let returnsLine = null;
|
|
371
|
+
if (tsdoc.returnsBlock) {
|
|
372
|
+
const desc = this.extractPlainText(tsdoc.returnsBlock.content).replace(/\s+/g, " ").trim();
|
|
373
|
+
if (desc) returnsLine = `@returns ${desc}`;
|
|
374
|
+
}
|
|
375
|
+
if (typeParamLines.length || paramLines.length || returnsLine) {
|
|
376
|
+
if (lines.length > 0) lines.push("");
|
|
377
|
+
lines.push(...typeParamLines);
|
|
378
|
+
lines.push(...paramLines);
|
|
379
|
+
if (returnsLine) lines.push(returnsLine);
|
|
380
|
+
}
|
|
381
|
+
if (tsdoc.deprecatedBlock) {
|
|
382
|
+
const msg = this.extractPlainText(tsdoc.deprecatedBlock.content).replace(/\s+/g, " ").trim();
|
|
383
|
+
if (msg) {
|
|
384
|
+
if (lines.length > 0) lines.push("");
|
|
385
|
+
lines.push(`@deprecated ${msg}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (tsdoc.remarksBlock) {
|
|
389
|
+
const remarks = this.extractPlainText(tsdoc.remarksBlock.content).trim();
|
|
390
|
+
if (remarks) {
|
|
391
|
+
if (lines.length > 0) lines.push("");
|
|
392
|
+
lines.push("@remarks");
|
|
393
|
+
for (const line of remarks.split("\n")) lines.push(line);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (tsdoc.customBlocks) for (const block of tsdoc.customBlocks) {
|
|
397
|
+
const blockAny = block;
|
|
398
|
+
if (blockAny.blockTag?.tagName === "@example") {
|
|
399
|
+
const exampleText = this.extractPlainText(blockAny.content).trim();
|
|
400
|
+
if (exampleText) {
|
|
401
|
+
if (lines.length > 0) lines.push("");
|
|
402
|
+
lines.push("@example");
|
|
403
|
+
for (const line of exampleText.split("\n")) lines.push(line);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
if (tsdoc.modifierTagSet?.isPublic?.()) {
|
|
409
|
+
if (lines.length > 0) lines.push("");
|
|
410
|
+
lines.push("@public");
|
|
411
|
+
}
|
|
412
|
+
} catch {}
|
|
413
|
+
if (lines.length === 0) return null;
|
|
414
|
+
if (lines.length === 1 && !lines[0].includes("\n")) return `${indent}/** ${lines[0]} */`;
|
|
415
|
+
return `${indent}/**\n${lines.map((line) => line ? `${indent} * ${line}` : `${indent} *`).join("\n")}\n${indent} */`;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Recursively extract plain text from a TSDoc DocNode tree.
|
|
419
|
+
*
|
|
420
|
+
* @remarks
|
|
421
|
+
* NOT interchangeable with this package's `Tsdoc` prose extraction, despite
|
|
422
|
+
* the overlapping name and shape. This one **preserves** `{@link X.Y}` TSDoc
|
|
423
|
+
* syntax and reconstructs fenced code blocks, because its output is a
|
|
424
|
+
* `.d.ts` file whose JSDoc must survive round-tripping into a virtual
|
|
425
|
+
* TypeScript environment. `Tsdoc`'s flattens `{@link}` to its display text
|
|
426
|
+
* and drops code fences, because its output is rendered prose.
|
|
427
|
+
*
|
|
428
|
+
* They looked like duplicates from two packages away and now sit in one, so
|
|
429
|
+
* this is the note that should stop the merge: collapsing them would either
|
|
430
|
+
* put display text where a declaration reference belongs, or leak link
|
|
431
|
+
* syntax into rendered documentation.
|
|
432
|
+
*/
|
|
433
|
+
extractPlainText(node) {
|
|
434
|
+
const n = node;
|
|
435
|
+
if (n.kind === "PlainText") return n.text || "";
|
|
436
|
+
if (n.kind === "SoftBreak") return "\n";
|
|
437
|
+
if (n.kind === "CodeSpan") return `\`${n.code || ""}\``;
|
|
438
|
+
if (n.kind === "EscapedText") return n.encodedText || n.decodedText || "";
|
|
439
|
+
if (n.kind === "ErrorText") return n.text || "";
|
|
440
|
+
if (n.kind === "FencedCode") return `\`\`\`${n.language || ""}\n${(n.code || "").replace(/\n+$/, "")}\n\`\`\``;
|
|
441
|
+
if (n.kind === "LinkTag") {
|
|
442
|
+
let target = "";
|
|
443
|
+
if (n.codeDestination?.memberReferences) {
|
|
444
|
+
const identifiers = [];
|
|
445
|
+
for (const ref of n.codeDestination.memberReferences) if (ref.memberIdentifier?.identifier) identifiers.push(ref.memberIdentifier.identifier);
|
|
446
|
+
target = identifiers.join(".");
|
|
447
|
+
}
|
|
448
|
+
const displayText = typeof n.linkText === "string" ? n.linkText : "";
|
|
449
|
+
if (target && displayText) return `{@link ${target} | ${displayText}}`;
|
|
450
|
+
if (target) return `{@link ${target}}`;
|
|
451
|
+
if (displayText) return displayText;
|
|
452
|
+
return "";
|
|
453
|
+
}
|
|
454
|
+
if (n.kind === "Section") {
|
|
455
|
+
const children = n.getChildNodes?.() || [];
|
|
456
|
+
const paragraphs = [];
|
|
457
|
+
for (const child of children) {
|
|
458
|
+
const trimmed = this.extractPlainText(child).trim();
|
|
459
|
+
if (trimmed) paragraphs.push(trimmed);
|
|
460
|
+
}
|
|
461
|
+
return paragraphs.join("\n\n");
|
|
462
|
+
}
|
|
463
|
+
const parts = [];
|
|
464
|
+
if (n.getChildNodes && typeof n.getChildNodes === "function") for (const child of n.getChildNodes()) {
|
|
465
|
+
const text = this.extractPlainText(child);
|
|
466
|
+
if (text) parts.push(text);
|
|
467
|
+
}
|
|
468
|
+
return parts.join("");
|
|
469
|
+
}
|
|
470
|
+
getEntryPointName(entryPoint) {
|
|
471
|
+
if (entryPoint.displayName === "") return void 0;
|
|
472
|
+
return entryPoint.displayName;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
//#endregion
|
|
477
|
+
export { ApiExtractedPackage };
|
package/Frontmatter.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { FrontmatterSource, FrontmatterSourceBlock, FrontmatterSourceSplit } from "@effected/markdown";
|
|
3
|
+
import { Yaml, YamlStringifyOptions } from "@effected/yaml";
|
|
4
|
+
|
|
5
|
+
//#region src/Frontmatter.ts
|
|
6
|
+
/**
|
|
7
|
+
* Stringify options shared by both emit sites.
|
|
8
|
+
*
|
|
9
|
+
* `lineWidth: 0` disables wrapping so long titles/descriptions/URLs stay on
|
|
10
|
+
* one line. The quoting matters for downstream consumers: RSPress parses the
|
|
11
|
+
* emitted frontmatter with js-yaml (YAML 1.1-flavored), where an unquoted
|
|
12
|
+
* ISO timestamp such as `2024-01-15T12:00:00.000Z` decodes to a `Date`
|
|
13
|
+
* object instead of a string. `quoteCompat: "yaml-1.1"` quotes exactly the
|
|
14
|
+
* plain scalars a YAML 1.1 resolver would coerce (timestamps, `yes`/`no`/
|
|
15
|
+
* `on`/`off` booleans, legacy octal/sexagesimal numbers), keeping the
|
|
16
|
+
* decoded representation identical across YAML 1.1 and 1.2 parsers without
|
|
17
|
+
* quoting every value; `quoteStyle: "double"` makes the quotes that do
|
|
18
|
+
* appear double quotes.
|
|
19
|
+
*/
|
|
20
|
+
const STRINGIFY_OPTIONS = YamlStringifyOptions.make({
|
|
21
|
+
lineWidth: 0,
|
|
22
|
+
quoteCompat: "yaml-1.1",
|
|
23
|
+
quoteStyle: "double"
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Split markdown source into frontmatter data and body content, preserving
|
|
27
|
+
* gray-matter's exact boundary semantics.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* This is a byte-for-byte port of the `gray-matter` split contract the
|
|
31
|
+
* snapshot system's hashes depend on (see `@tsdoctor/snapshot`
|
|
32
|
+
* `hashContent`/`hashFrontmatter` and the disk-fallback comparison in
|
|
33
|
+
* `build-stages.ts`), with `@effected/yaml` (`Yaml.parse`, YAML 1.2) as the
|
|
34
|
+
* YAML engine instead of js-yaml:
|
|
35
|
+
*
|
|
36
|
+
* - No opening `---` line at offset 0 → `data: {}` and the whole input as
|
|
37
|
+
* `content` (a leading BOM is stripped first, as gray-matter does).
|
|
38
|
+
* - The closing delimiter is the first `\n---` after the opening line
|
|
39
|
+
* (gray-matter uses a plain `indexOf`, so `\n----` also closes and the
|
|
40
|
+
* leftover `-` stays in the body — preserved deliberately).
|
|
41
|
+
* - Exactly one newline (`\n` or `\r\n`) immediately after the closing `---`
|
|
42
|
+
* is consumed; everything else is the body verbatim. A build's generated
|
|
43
|
+
* page (`---\n…\n---\n\n# Title`) therefore yields a body starting with a
|
|
44
|
+
* single `\n`, exactly as gray-matter returned it.
|
|
45
|
+
* - A block with no closing delimiter is all frontmatter and yields an empty
|
|
46
|
+
* body; an empty/blank block yields `data: {}`.
|
|
47
|
+
* - Invalid YAML throws (a defect), matching gray-matter's js-yaml throw.
|
|
48
|
+
*
|
|
49
|
+
* One deliberate delta: gray-matter treats text on the opening line
|
|
50
|
+
* (`---toml`) as an engine name and throws for unregistered engines; this
|
|
51
|
+
* split treats such input as "no frontmatter" instead. The plugin never emits
|
|
52
|
+
* or consumes language-tagged frontmatter.
|
|
53
|
+
*
|
|
54
|
+
* `@effected/markdown`'s `FrontmatterSource.split` was evaluated for this
|
|
55
|
+
* path and deliberately NOT adopted: its grammar is strict by design (a
|
|
56
|
+
* fence line is exactly `---`, an unterminated block is not frontmatter),
|
|
57
|
+
* while this contract pins gray-matter's `indexOf`-based quirks (`\n----`
|
|
58
|
+
* closes, trailing-space close lines close, a missing close means
|
|
59
|
+
* all-frontmatter). The emission half (`stringifyFrontmatter` /
|
|
60
|
+
* `emitFrontmatterBlock`) does use `FrontmatterSource.join`.
|
|
61
|
+
*
|
|
62
|
+
* Representation parity with js-yaml is verified by characterization tests
|
|
63
|
+
* (`__test__/frontmatter.test.ts`) pinning hashes captured under gray-matter.
|
|
64
|
+
* The one input where the engines disagree — an *unquoted* ISO timestamp
|
|
65
|
+
* (js-yaml: `Date`, YAML 1.2: string) — is unreachable from this plugin's
|
|
66
|
+
* emitters, which always quote timestamp values, and hashes identically
|
|
67
|
+
* anyway because `hashFrontmatter` JSON-serializes (a `Date` serializes to
|
|
68
|
+
* the same ISO string).
|
|
69
|
+
*
|
|
70
|
+
* @param source - The markdown source, with or without a frontmatter block
|
|
71
|
+
* @returns The decoded frontmatter data and the body content
|
|
72
|
+
*
|
|
73
|
+
* @public
|
|
74
|
+
*/
|
|
75
|
+
function parseFrontmatter(source) {
|
|
76
|
+
const text = source.charCodeAt(0) === 65279 ? source.slice(1) : source;
|
|
77
|
+
const split = FrontmatterSource.split(text);
|
|
78
|
+
if (split.frontmatter === void 0 || split.frontmatter.format !== "yaml") return {
|
|
79
|
+
data: {},
|
|
80
|
+
content: split.body
|
|
81
|
+
};
|
|
82
|
+
if (split.frontmatter.value.trim() === "") return {
|
|
83
|
+
data: {},
|
|
84
|
+
content: split.body
|
|
85
|
+
};
|
|
86
|
+
const value = Effect.runSync(Yaml.parse(split.frontmatter.value));
|
|
87
|
+
return {
|
|
88
|
+
data: value == null ? {} : value,
|
|
89
|
+
content: split.body
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Serialize frontmatter data and body content back into a markdown document,
|
|
94
|
+
* preserving gray-matter's `matter.stringify` contract.
|
|
95
|
+
*
|
|
96
|
+
* @remarks
|
|
97
|
+
* Emits `---\n<yaml>---\n<content>` with the body's trailing newline ensured,
|
|
98
|
+
* and returns the body unchanged (no fences) when `data` has no keys — both
|
|
99
|
+
* gray-matter behaviors the write path relied on. The YAML is emitted by
|
|
100
|
+
* `@effected/yaml` with every string value double-quoted (see
|
|
101
|
+
* `STRINGIFY_OPTIONS` for why); byte output differs from js-yaml's dump, but
|
|
102
|
+
* the decoded representation is identical, which is the invariant the
|
|
103
|
+
* snapshot hashes depend on. Unchanged pages are never rewritten, so the byte
|
|
104
|
+
* difference only ever lands in files that were being rewritten anyway.
|
|
105
|
+
*
|
|
106
|
+
* @param content - The body content
|
|
107
|
+
* @param data - The frontmatter data to serialize
|
|
108
|
+
* @returns The combined markdown document
|
|
109
|
+
*
|
|
110
|
+
* @public
|
|
111
|
+
*/
|
|
112
|
+
function stringifyFrontmatter(content, data) {
|
|
113
|
+
const body = content.endsWith("\n") ? content : `${content}\n`;
|
|
114
|
+
if (Object.keys(data).length === 0) return body;
|
|
115
|
+
const yaml = Effect.runSync(Yaml.stringify(data, STRINGIFY_OPTIONS));
|
|
116
|
+
return FrontmatterSource.join(FrontmatterSourceSplit.make({
|
|
117
|
+
frontmatter: FrontmatterSourceBlock.make({
|
|
118
|
+
format: "yaml",
|
|
119
|
+
value: yaml
|
|
120
|
+
}),
|
|
121
|
+
body
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Serialize a data object to a YAML frontmatter block (fences included, plus
|
|
126
|
+
* the trailing blank line the page generators emit before the body).
|
|
127
|
+
*
|
|
128
|
+
* @remarks
|
|
129
|
+
* Used by `generateFrontmatter` (`markdown/helpers.ts`) as the emission half
|
|
130
|
+
* of the page generators' frontmatter. Every string value is double-quoted
|
|
131
|
+
* (see `STRINGIFY_OPTIONS`), so values that a YAML 1.1 consumer would
|
|
132
|
+
* otherwise coerce (timestamps, `yes`/`no`, numeric-looking strings) stay
|
|
133
|
+
* strings for RSPress's js-yaml parse.
|
|
134
|
+
*
|
|
135
|
+
* @param data - The frontmatter data to serialize
|
|
136
|
+
* @returns A `---`-fenced YAML block ending with a blank line
|
|
137
|
+
*
|
|
138
|
+
* @public
|
|
139
|
+
*/
|
|
140
|
+
function emitFrontmatterBlock(data) {
|
|
141
|
+
const yaml = Effect.runSync(Yaml.stringify(data, STRINGIFY_OPTIONS));
|
|
142
|
+
return FrontmatterSource.join(FrontmatterSourceSplit.make({
|
|
143
|
+
frontmatter: FrontmatterSourceBlock.make({
|
|
144
|
+
format: "yaml",
|
|
145
|
+
value: yaml
|
|
146
|
+
}),
|
|
147
|
+
body: "\n"
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
export { emitFrontmatterBlock, parseFrontmatter, stringifyFrontmatter };
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://nodejs.org/)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
|
|
8
|
-
Framework-neutral analysis and rendering for Microsoft API Extractor `.api.json` models: Effect-typed loading, pure TSDoc extraction, categorization, multi-entry-point resolution, route/collision computation, synthetic-base detection, type-signature formatting, prose cross-linking and markdown rendering.
|
|
8
|
+
Framework-neutral analysis and rendering for Microsoft API Extractor `.api.json` models: Effect-typed loading, pure TSDoc extraction, categorization, multi-entry-point resolution, route/collision computation, synthetic-base detection, type-signature formatting, prose cross-linking, declaration reconstruction and markdown rendering.
|
|
9
9
|
|
|
10
10
|
## Why @tsdoctor/model
|
|
11
11
|
|
|
@@ -23,7 +23,7 @@ pnpm add @tsdoctor/model
|
|
|
23
23
|
|
|
24
24
|
Requires Node.js >=24.11.0. This is an ESM-only package.
|
|
25
25
|
|
|
26
|
-
`@effected/markdown` and `effect` are required peers
|
|
26
|
+
`@effected/markdown`, `@effected/yaml` and `effect` are required peers — `Render` builds its output as `@effected/markdown` node trees, and the frontmatter helpers parse and emit through `@effected/yaml`. [`@tsdoctor/vfs`](https://www.npmjs.com/package/@tsdoctor/vfs) is an ordinary dependency, resolved for you: `ApiExtractedPackage` extends its `VirtualPackage`.
|
|
27
27
|
|
|
28
28
|
## Quick start
|
|
29
29
|
|
|
@@ -100,7 +100,9 @@ Supplying `filter` fully replaces the default, so compose it with `Render.isEmit
|
|
|
100
100
|
- **`Signature`** — `format(excerpt)` turns an API Extractor `Excerpt` into a clean, line-wrapped type signature string; `stripExportDeclare` strips `export`/`declare` modifiers from declaration text.
|
|
101
101
|
- **`CrossLinker`** — an immutable class that wraps known item names in prose with links, skipping code spans and existing links. Build one per build from a precomputed route map (`CrossLinker.fromRoutes`) or from item refs plus an injected URL scheme (`CrossLinker.fromRefs`); `link` returns markdown links, `linkHtml` returns `<a>` anchors.
|
|
102
102
|
- **`Render`** — the markdown output system. `Render.docs(pkg, opts)` renders a whole package; `Render.item(apiItem, opts)` renders one item; `Render.isEmittable` is the default emit rule. `Render.tree` (`@alpha`) exposes the pre-serialization `@effected/markdown` node tree for a future page-IR consumer.
|
|
103
|
-
- **`
|
|
103
|
+
- **`ApiExtractedPackage`** — reconstructs `.d.ts` text from a model and renders it to a `Vfs`, one declaration file per entry point behind a synthetic `package.json`, so a type-checker can resolve the documented package the way a consumer would. Built on `VirtualPackage` from `@tsdoctor/vfs`; `fromApiModel(path)` loads a model file, `fromPackage(apiPackage, name)` takes one already in memory.
|
|
104
|
+
- **`TypeReferenceExtractor`** — finds the types a package's declarations reference but do not own, and emits the `import type` statements those declarations need. `extractImports` covers a package, `extractImportsForEntryPoint` one entry point, and the static `formatImports` renders an `ImportStatement[]` to source lines. Built-in and self-referencing types are filtered out, and a namespaced reference imports its namespace root rather than the leaf member.
|
|
105
|
+
- **Frontmatter** — `parseFrontmatter(text)` splits a document into `{ data, content }`, `stringifyFrontmatter(data)` emits the YAML block, and `emitFrontmatterBlock(data, body)` assembles a whole document. Quoting is chosen so a YAML 1.1 consumer decodes the same values a YAML 1.2 one does — an unquoted ISO timestamp would otherwise arrive as a `Date` in one and a string in the other.
|
|
104
106
|
|
|
105
107
|
## License
|
|
106
108
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { ApiItemKind } from "@microsoft/api-extractor-model";
|
|
2
|
+
|
|
3
|
+
//#region src/TypeReferenceExtractor.ts
|
|
4
|
+
/**
|
|
5
|
+
* Extracts type references from API Extractor models to generate import statements.
|
|
6
|
+
*
|
|
7
|
+
* This class analyzes API items and their excerpt tokens to identify external type
|
|
8
|
+
* references that need to be imported in the generated TypeScript declaration files.
|
|
9
|
+
*
|
|
10
|
+
* **How it works:**
|
|
11
|
+
* 1. Walks through all API items (classes, interfaces, functions, etc.)
|
|
12
|
+
* 2. Extracts type references from excerpt tokens
|
|
13
|
+
* 3. Filters out built-in types and internal references
|
|
14
|
+
* 4. Groups external references by package
|
|
15
|
+
* 5. Generates `import type` statements
|
|
16
|
+
*
|
|
17
|
+
* **Reference Types:**
|
|
18
|
+
* - **Built-in:** TypeScript types like `Promise`, `Record`, `NonNullable` (skipped)
|
|
19
|
+
* - **Internal:** References to types in the same package (skipped)
|
|
20
|
+
* - **External:** References to types from npm packages (imported)
|
|
21
|
+
*
|
|
22
|
+
* **Canonical Reference Format:**
|
|
23
|
+
* API Extractor uses canonical references like:
|
|
24
|
+
* - `"zod!ZodType:interface"` → External reference to `zod` package
|
|
25
|
+
* - `"mypackage!MyType:type"` → Internal reference (same package)
|
|
26
|
+
* - `"!Promise:interface"` → Built-in TypeScript type
|
|
27
|
+
* - `"!\"node:buffer\".__global.Buffer:interface"` → Node.js built-in (treated as built-in)
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const extractor = new TypeReferenceExtractor(apiPackage, "my-package");
|
|
32
|
+
* const imports = extractor.extractImports();
|
|
33
|
+
*
|
|
34
|
+
* for (const stmt of imports) {
|
|
35
|
+
* console.log(`import type { ${[...stmt.symbols].join(", ")} } from "${stmt.packageName}";`);
|
|
36
|
+
* }
|
|
37
|
+
* // Output:
|
|
38
|
+
* // import type { ZodType } from "zod";
|
|
39
|
+
* // import type { Effect } from "@effect/schema";
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
var TypeReferenceExtractor = class {
|
|
45
|
+
apiPackage;
|
|
46
|
+
currentPackageName;
|
|
47
|
+
/**
|
|
48
|
+
* All type references found in the API package
|
|
49
|
+
*/
|
|
50
|
+
references = /* @__PURE__ */ new Map();
|
|
51
|
+
constructor(apiPackage, currentPackageName) {
|
|
52
|
+
this.apiPackage = apiPackage;
|
|
53
|
+
this.currentPackageName = currentPackageName;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Extract all type references from the API package and generate import statements.
|
|
57
|
+
* Returns an array of import statements grouped by package.
|
|
58
|
+
*/
|
|
59
|
+
extractImports() {
|
|
60
|
+
this.walkApiPackage();
|
|
61
|
+
return this.generateImportStatements();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Extract type references for a specific entry point only.
|
|
65
|
+
* This enables per-entry-point import optimization for multi-entry packages.
|
|
66
|
+
*
|
|
67
|
+
* @param entryPoint - The specific entry point to extract imports for
|
|
68
|
+
* @returns Import statements containing only types used in this entry point
|
|
69
|
+
*/
|
|
70
|
+
extractImportsForEntryPoint(entryPoint) {
|
|
71
|
+
this.references.clear();
|
|
72
|
+
for (const member of entryPoint.members) this.walkApiItem(member);
|
|
73
|
+
return this.generateImportStatements();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Extract type references for a single API item.
|
|
77
|
+
* This enables generating imports for individual signatures.
|
|
78
|
+
*
|
|
79
|
+
* @param apiItem - The specific API item to extract imports for
|
|
80
|
+
* @returns Import statements containing only types used in this item
|
|
81
|
+
*/
|
|
82
|
+
extractImportsForApiItem(apiItem) {
|
|
83
|
+
this.references.clear();
|
|
84
|
+
this.walkApiItem(apiItem);
|
|
85
|
+
return this.generateImportStatements();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Generate import statements from collected references.
|
|
89
|
+
* Used by both extractImports() and extractImportsForEntryPoint().
|
|
90
|
+
*/
|
|
91
|
+
generateImportStatements() {
|
|
92
|
+
const packageMap = /* @__PURE__ */ new Map();
|
|
93
|
+
for (const ref of this.references.values()) {
|
|
94
|
+
if (ref.isBuiltIn || ref.isInternal) continue;
|
|
95
|
+
if (!packageMap.has(ref.packageName)) packageMap.set(ref.packageName, /* @__PURE__ */ new Set());
|
|
96
|
+
packageMap.get(ref.packageName)?.add(ref.symbolName);
|
|
97
|
+
}
|
|
98
|
+
const imports = [];
|
|
99
|
+
for (const [packageName, symbols] of packageMap.entries()) imports.push({
|
|
100
|
+
packageName,
|
|
101
|
+
symbols,
|
|
102
|
+
typeOnly: true
|
|
103
|
+
});
|
|
104
|
+
imports.sort((a, b) => a.packageName.localeCompare(b.packageName));
|
|
105
|
+
return imports;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Generate import statement strings from ImportStatement objects.
|
|
109
|
+
* Returns an array of formatted import statements.
|
|
110
|
+
*/
|
|
111
|
+
static formatImports(imports) {
|
|
112
|
+
const statements = [];
|
|
113
|
+
for (const stmt of imports) {
|
|
114
|
+
const sortedSymbols = Array.from(stmt.symbols).sort();
|
|
115
|
+
const statement = `${stmt.typeOnly ? "import type" : "import"} { ${sortedSymbols.join(", ")} } from "${stmt.packageName}";`;
|
|
116
|
+
statements.push(statement);
|
|
117
|
+
}
|
|
118
|
+
return statements;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Walk through the entire API package and extract all type references
|
|
122
|
+
*/
|
|
123
|
+
walkApiPackage() {
|
|
124
|
+
for (const entryPoint of this.apiPackage.entryPoints) for (const member of entryPoint.members) this.walkApiItem(member);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Recursively walk through an API item and its children to extract type references
|
|
128
|
+
*/
|
|
129
|
+
walkApiItem(apiItem) {
|
|
130
|
+
this.extractFromExcerpt(apiItem);
|
|
131
|
+
if ("members" in apiItem) {
|
|
132
|
+
const members = apiItem.members;
|
|
133
|
+
if (Array.isArray(members)) for (const member of members) this.walkApiItem(member);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Extract type references from an API item using its excerpt
|
|
138
|
+
*/
|
|
139
|
+
extractFromExcerpt(apiItem) {
|
|
140
|
+
const excerpt = this.getExcerpt(apiItem);
|
|
141
|
+
if (!excerpt) return;
|
|
142
|
+
this.extractFromExcerptTokens(excerpt);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get the appropriate excerpt from an API item based on its kind
|
|
146
|
+
*/
|
|
147
|
+
getExcerpt(apiItem) {
|
|
148
|
+
const item = apiItem;
|
|
149
|
+
if (item.excerpt) return item.excerpt;
|
|
150
|
+
if (apiItem.kind === ApiItemKind.TypeAlias && item.typeExcerpt) return item.typeExcerpt;
|
|
151
|
+
if ((apiItem.kind === ApiItemKind.Property || apiItem.kind === ApiItemKind.PropertySignature) && item.propertyTypeExcerpt) return item.propertyTypeExcerpt;
|
|
152
|
+
if (item.returnTypeExcerpt) return item.returnTypeExcerpt;
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Extract type references from excerpt tokens
|
|
157
|
+
*/
|
|
158
|
+
extractFromExcerptTokens(excerpt) {
|
|
159
|
+
if (!excerpt.spannedTokens || excerpt.spannedTokens.length === 0) return;
|
|
160
|
+
for (const token of excerpt.spannedTokens) {
|
|
161
|
+
if (token.kind !== "Reference") continue;
|
|
162
|
+
const canonicalRef = token.canonicalReference?.toString();
|
|
163
|
+
if (!canonicalRef || typeof canonicalRef !== "string") continue;
|
|
164
|
+
const ref = this.parseCanonicalReference(canonicalRef, token.text);
|
|
165
|
+
if (ref) this.references.set(ref.canonicalReference, ref);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Parse a canonical reference string to extract type reference information.
|
|
170
|
+
*
|
|
171
|
+
* Canonical reference format: "packageName!symbolName:kind"
|
|
172
|
+
* Examples:
|
|
173
|
+
* - "zod!ZodType:interface" → External reference
|
|
174
|
+
* - "mypackage!MyType:type" → Internal reference
|
|
175
|
+
* - "!Promise:interface" → Built-in type
|
|
176
|
+
* - "!\"node:buffer\".__global.Buffer:interface" → Node.js built-in
|
|
177
|
+
*/
|
|
178
|
+
parseCanonicalReference(canonicalRef, symbolText) {
|
|
179
|
+
const exclamationIndex = canonicalRef.indexOf("!");
|
|
180
|
+
if (exclamationIndex === -1) return null;
|
|
181
|
+
const packagePart = canonicalRef.substring(0, exclamationIndex);
|
|
182
|
+
const rest = canonicalRef.substring(exclamationIndex + 1);
|
|
183
|
+
const colonIndex = rest.indexOf(":");
|
|
184
|
+
const symbolFromCanonical = colonIndex !== -1 ? rest.substring(0, colonIndex) : rest;
|
|
185
|
+
const isBuiltIn = packagePart === "" || packagePart.startsWith("\"");
|
|
186
|
+
const isInternal = packagePart === this.currentPackageName;
|
|
187
|
+
let symbolName;
|
|
188
|
+
if (symbolText.includes(".")) symbolName = symbolText.split(".")[0].trim();
|
|
189
|
+
else symbolName = symbolFromCanonical.trim();
|
|
190
|
+
return {
|
|
191
|
+
symbolName,
|
|
192
|
+
packageName: packagePart,
|
|
193
|
+
canonicalReference: canonicalRef,
|
|
194
|
+
isBuiltIn,
|
|
195
|
+
isInternal
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
//#endregion
|
|
201
|
+
export { TypeReferenceExtractor };
|
package/index.d.ts
CHANGED
|
@@ -1,7 +1,106 @@
|
|
|
1
|
-
import { ApiClass, ApiInterface, ApiItem, ApiModel, ApiNamespace, ApiPackage, Excerpt } from "@microsoft/api-extractor-model";
|
|
1
|
+
import { ApiClass, ApiEntryPoint, ApiInterface, ApiItem, ApiModel, ApiNamespace, ApiPackage, Excerpt } from "@microsoft/api-extractor-model";
|
|
2
|
+
import { VirtualPackage } from "@tsdoctor/vfs";
|
|
2
3
|
import { Effect, Schema } from "effect";
|
|
3
4
|
import { FlowContent, MarkdownNode } from "@effected/markdown";
|
|
4
5
|
import { DocNode } from "@microsoft/tsdoc";
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/ApiExtractedPackage.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Reconstructs TypeScript declaration files from an API Extractor model.
|
|
10
|
+
*
|
|
11
|
+
* Extends `VirtualPackage` with the ability to generate high-fidelity
|
|
12
|
+
* `.d.ts` output from API Extractor's `ApiPackage` — including enum values,
|
|
13
|
+
* full JSDoc, namespace members, and all interface member kinds.
|
|
14
|
+
*
|
|
15
|
+
* Use the factory methods `fromApiModel` or `fromPackage` to create instances.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
declare class ApiExtractedPackage extends VirtualPackage {
|
|
20
|
+
readonly apiPackage: ApiPackage;
|
|
21
|
+
private constructor();
|
|
22
|
+
/**
|
|
23
|
+
* Create an ApiExtractedPackage from an API model JSON file path.
|
|
24
|
+
*/
|
|
25
|
+
static fromApiModel(modelPath: string): ApiExtractedPackage;
|
|
26
|
+
/**
|
|
27
|
+
* Create an ApiExtractedPackage from an existing ApiPackage instance.
|
|
28
|
+
*/
|
|
29
|
+
static fromPackage(apiPackage: ApiPackage, packageName: string): ApiExtractedPackage;
|
|
30
|
+
/**
|
|
31
|
+
* Generate the .d.ts content for a specific entry point.
|
|
32
|
+
*/
|
|
33
|
+
generateDeclarations(entryPoint?: ApiEntryPoint): string;
|
|
34
|
+
/**
|
|
35
|
+
* Generate a TypeScript declaration for a single API item.
|
|
36
|
+
*/
|
|
37
|
+
private generateDeclaration;
|
|
38
|
+
private generateClassDeclaration;
|
|
39
|
+
private generateInterfaceDeclaration;
|
|
40
|
+
private generateTypeAliasDeclaration;
|
|
41
|
+
private generateFunctionDeclaration;
|
|
42
|
+
private generateEnumDeclaration;
|
|
43
|
+
private generateVariableDeclaration;
|
|
44
|
+
private generateNamespaceDeclaration;
|
|
45
|
+
private generateNamespaceMember;
|
|
46
|
+
private generateNamespaceFunction;
|
|
47
|
+
private generateNamespaceInterface;
|
|
48
|
+
private generateNamespaceEnum;
|
|
49
|
+
private generateNamespaceTypeAlias;
|
|
50
|
+
private generateNamespaceVariable;
|
|
51
|
+
private generateNamespaceClass;
|
|
52
|
+
private generateClassMember;
|
|
53
|
+
private generateInterfaceMember;
|
|
54
|
+
private generateMemberFromExcerpt;
|
|
55
|
+
/**
|
|
56
|
+
* Render an excerpt to source text, normalizing dts-rollup disambiguation
|
|
57
|
+
* aliases. The dts rollup renames a re-imported symbol as `Name$1`, but its
|
|
58
|
+
* canonical reference is the un-suffixed `Name` (the same symbol). The import
|
|
59
|
+
* prepender ({@link TypeReferenceExtractor}) imports the canonical name, so
|
|
60
|
+
* emitting the suffixed text would leave `Name$1` undefined (TS2304). Emit the
|
|
61
|
+
* canonical name so the body and the prepended import agree.
|
|
62
|
+
*
|
|
63
|
+
* Equivalent to `excerpt.text` for excerpts without rollup aliases (the text
|
|
64
|
+
* is the concatenation of the spanned tokens), so unaliased output is unchanged.
|
|
65
|
+
*/
|
|
66
|
+
private renderExcerpt;
|
|
67
|
+
/**
|
|
68
|
+
* Strip a dts-rollup `$N` suffix from a reference token when the de-suffixed
|
|
69
|
+
* text matches the token's canonical symbol. Never touches a non-reference
|
|
70
|
+
* token or a legitimate identifier that genuinely ends in `$N` (its canonical
|
|
71
|
+
* name would carry the suffix too).
|
|
72
|
+
*/
|
|
73
|
+
private normalizeTokenText;
|
|
74
|
+
/**
|
|
75
|
+
* Clean an excerpt text: strip export/declare keywords and trailing semicolons/whitespace.
|
|
76
|
+
*/
|
|
77
|
+
private cleanExcerpt;
|
|
78
|
+
private formatTypeParameters;
|
|
79
|
+
private extractPackageDocumentation;
|
|
80
|
+
/**
|
|
81
|
+
* Format JSDoc comment from an API item's TSDoc.
|
|
82
|
+
* Produces output matching the TypeScript compiler's JSDoc style.
|
|
83
|
+
*/
|
|
84
|
+
private formatJSDoc;
|
|
85
|
+
/**
|
|
86
|
+
* Recursively extract plain text from a TSDoc DocNode tree.
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* NOT interchangeable with this package's `Tsdoc` prose extraction, despite
|
|
90
|
+
* the overlapping name and shape. This one **preserves** `{@link X.Y}` TSDoc
|
|
91
|
+
* syntax and reconstructs fenced code blocks, because its output is a
|
|
92
|
+
* `.d.ts` file whose JSDoc must survive round-tripping into a virtual
|
|
93
|
+
* TypeScript environment. `Tsdoc`'s flattens `{@link}` to its display text
|
|
94
|
+
* and drops code fences, because its output is rendered prose.
|
|
95
|
+
*
|
|
96
|
+
* They looked like duplicates from two packages away and now sit in one, so
|
|
97
|
+
* this is the note that should stop the merge: collapsing them would either
|
|
98
|
+
* put display text where a declaration reference belongs, or leak link
|
|
99
|
+
* syntax into rendered documentation.
|
|
100
|
+
*/
|
|
101
|
+
private extractPlainText;
|
|
102
|
+
private getEntryPointName;
|
|
103
|
+
}
|
|
5
104
|
declare namespace EntryPoints_d_exports {
|
|
6
105
|
export { ResolvedEntryItem, entryPointName, resolve };
|
|
7
106
|
}
|
|
@@ -265,6 +364,109 @@ declare class CrossLinker {
|
|
|
265
364
|
*/
|
|
266
365
|
linkHtml(text: string): string;
|
|
267
366
|
}
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/Frontmatter.d.ts
|
|
369
|
+
/**
|
|
370
|
+
* A parsed frontmatter document: the decoded frontmatter data and the body
|
|
371
|
+
* content that followed the closing delimiter.
|
|
372
|
+
*
|
|
373
|
+
* @public
|
|
374
|
+
*/
|
|
375
|
+
interface ParsedFrontmatter {
|
|
376
|
+
/** Decoded frontmatter data (`{}` when there is no frontmatter block). */
|
|
377
|
+
readonly data: Record<string, unknown>;
|
|
378
|
+
/** Body content after the closing delimiter (whole input when no block). */
|
|
379
|
+
readonly content: string;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Split markdown source into frontmatter data and body content, preserving
|
|
383
|
+
* gray-matter's exact boundary semantics.
|
|
384
|
+
*
|
|
385
|
+
* @remarks
|
|
386
|
+
* This is a byte-for-byte port of the `gray-matter` split contract the
|
|
387
|
+
* snapshot system's hashes depend on (see `@tsdoctor/snapshot`
|
|
388
|
+
* `hashContent`/`hashFrontmatter` and the disk-fallback comparison in
|
|
389
|
+
* `build-stages.ts`), with `@effected/yaml` (`Yaml.parse`, YAML 1.2) as the
|
|
390
|
+
* YAML engine instead of js-yaml:
|
|
391
|
+
*
|
|
392
|
+
* - No opening `---` line at offset 0 → `data: {}` and the whole input as
|
|
393
|
+
* `content` (a leading BOM is stripped first, as gray-matter does).
|
|
394
|
+
* - The closing delimiter is the first `\n---` after the opening line
|
|
395
|
+
* (gray-matter uses a plain `indexOf`, so `\n----` also closes and the
|
|
396
|
+
* leftover `-` stays in the body — preserved deliberately).
|
|
397
|
+
* - Exactly one newline (`\n` or `\r\n`) immediately after the closing `---`
|
|
398
|
+
* is consumed; everything else is the body verbatim. A build's generated
|
|
399
|
+
* page (`---\n…\n---\n\n# Title`) therefore yields a body starting with a
|
|
400
|
+
* single `\n`, exactly as gray-matter returned it.
|
|
401
|
+
* - A block with no closing delimiter is all frontmatter and yields an empty
|
|
402
|
+
* body; an empty/blank block yields `data: {}`.
|
|
403
|
+
* - Invalid YAML throws (a defect), matching gray-matter's js-yaml throw.
|
|
404
|
+
*
|
|
405
|
+
* One deliberate delta: gray-matter treats text on the opening line
|
|
406
|
+
* (`---toml`) as an engine name and throws for unregistered engines; this
|
|
407
|
+
* split treats such input as "no frontmatter" instead. The plugin never emits
|
|
408
|
+
* or consumes language-tagged frontmatter.
|
|
409
|
+
*
|
|
410
|
+
* `@effected/markdown`'s `FrontmatterSource.split` was evaluated for this
|
|
411
|
+
* path and deliberately NOT adopted: its grammar is strict by design (a
|
|
412
|
+
* fence line is exactly `---`, an unterminated block is not frontmatter),
|
|
413
|
+
* while this contract pins gray-matter's `indexOf`-based quirks (`\n----`
|
|
414
|
+
* closes, trailing-space close lines close, a missing close means
|
|
415
|
+
* all-frontmatter). The emission half (`stringifyFrontmatter` /
|
|
416
|
+
* `emitFrontmatterBlock`) does use `FrontmatterSource.join`.
|
|
417
|
+
*
|
|
418
|
+
* Representation parity with js-yaml is verified by characterization tests
|
|
419
|
+
* (`__test__/frontmatter.test.ts`) pinning hashes captured under gray-matter.
|
|
420
|
+
* The one input where the engines disagree — an *unquoted* ISO timestamp
|
|
421
|
+
* (js-yaml: `Date`, YAML 1.2: string) — is unreachable from this plugin's
|
|
422
|
+
* emitters, which always quote timestamp values, and hashes identically
|
|
423
|
+
* anyway because `hashFrontmatter` JSON-serializes (a `Date` serializes to
|
|
424
|
+
* the same ISO string).
|
|
425
|
+
*
|
|
426
|
+
* @param source - The markdown source, with or without a frontmatter block
|
|
427
|
+
* @returns The decoded frontmatter data and the body content
|
|
428
|
+
*
|
|
429
|
+
* @public
|
|
430
|
+
*/
|
|
431
|
+
declare function parseFrontmatter(source: string): ParsedFrontmatter;
|
|
432
|
+
/**
|
|
433
|
+
* Serialize frontmatter data and body content back into a markdown document,
|
|
434
|
+
* preserving gray-matter's `matter.stringify` contract.
|
|
435
|
+
*
|
|
436
|
+
* @remarks
|
|
437
|
+
* Emits `---\n<yaml>---\n<content>` with the body's trailing newline ensured,
|
|
438
|
+
* and returns the body unchanged (no fences) when `data` has no keys — both
|
|
439
|
+
* gray-matter behaviors the write path relied on. The YAML is emitted by
|
|
440
|
+
* `@effected/yaml` with every string value double-quoted (see
|
|
441
|
+
* `STRINGIFY_OPTIONS` for why); byte output differs from js-yaml's dump, but
|
|
442
|
+
* the decoded representation is identical, which is the invariant the
|
|
443
|
+
* snapshot hashes depend on. Unchanged pages are never rewritten, so the byte
|
|
444
|
+
* difference only ever lands in files that were being rewritten anyway.
|
|
445
|
+
*
|
|
446
|
+
* @param content - The body content
|
|
447
|
+
* @param data - The frontmatter data to serialize
|
|
448
|
+
* @returns The combined markdown document
|
|
449
|
+
*
|
|
450
|
+
* @public
|
|
451
|
+
*/
|
|
452
|
+
declare function stringifyFrontmatter(content: string, data: Record<string, unknown>): string;
|
|
453
|
+
/**
|
|
454
|
+
* Serialize a data object to a YAML frontmatter block (fences included, plus
|
|
455
|
+
* the trailing blank line the page generators emit before the body).
|
|
456
|
+
*
|
|
457
|
+
* @remarks
|
|
458
|
+
* Used by `generateFrontmatter` (`markdown/helpers.ts`) as the emission half
|
|
459
|
+
* of the page generators' frontmatter. Every string value is double-quoted
|
|
460
|
+
* (see `STRINGIFY_OPTIONS`), so values that a YAML 1.1 consumer would
|
|
461
|
+
* otherwise coerce (timestamps, `yes`/`no`, numeric-looking strings) stay
|
|
462
|
+
* strings for RSPress's js-yaml parse.
|
|
463
|
+
*
|
|
464
|
+
* @param data - The frontmatter data to serialize
|
|
465
|
+
* @returns A `---`-fenced YAML block ending with a blank line
|
|
466
|
+
*
|
|
467
|
+
* @public
|
|
468
|
+
*/
|
|
469
|
+
declare function emitFrontmatterBlock(data: Record<string, unknown>): string;
|
|
268
470
|
declare namespace Model_d_exports {
|
|
269
471
|
export { EmptyModelError, ModelNotFoundError, ModelParseError, firstPackage, load };
|
|
270
472
|
}
|
|
@@ -724,5 +926,167 @@ declare function seeReferences(item: ApiItem): ReadonlyArray<{
|
|
|
724
926
|
readonly text: string;
|
|
725
927
|
}>;
|
|
726
928
|
//#endregion
|
|
727
|
-
|
|
929
|
+
//#region src/TypeReferenceExtractor.d.ts
|
|
930
|
+
/**
|
|
931
|
+
* Represents a type reference extracted from an API item.
|
|
932
|
+
* Contains information about where the type comes from and how to import it.
|
|
933
|
+
*
|
|
934
|
+
* @public
|
|
935
|
+
*/
|
|
936
|
+
interface TypeReference {
|
|
937
|
+
/**
|
|
938
|
+
* The symbol name to import (e.g., "ZodType", "Effect")
|
|
939
|
+
*/
|
|
940
|
+
symbolName: string;
|
|
941
|
+
/**
|
|
942
|
+
* The package name (e.g., "zod", "\@effect/schema").
|
|
943
|
+
* Empty string for built-in TypeScript types.
|
|
944
|
+
*/
|
|
945
|
+
packageName: string;
|
|
946
|
+
/**
|
|
947
|
+
* The canonical reference from API Extractor
|
|
948
|
+
* Format: "packageName!symbolName:kind"
|
|
949
|
+
*/
|
|
950
|
+
canonicalReference: string;
|
|
951
|
+
/**
|
|
952
|
+
* Whether this is a built-in TypeScript type (Promise, Record, etc.)
|
|
953
|
+
*/
|
|
954
|
+
isBuiltIn: boolean;
|
|
955
|
+
/**
|
|
956
|
+
* Whether this reference is from the current package being documented
|
|
957
|
+
*/
|
|
958
|
+
isInternal: boolean;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Import statement to be generated for a package
|
|
962
|
+
*
|
|
963
|
+
* @public
|
|
964
|
+
*/
|
|
965
|
+
interface ImportStatement {
|
|
966
|
+
/**
|
|
967
|
+
* Package name to import from
|
|
968
|
+
*/
|
|
969
|
+
packageName: string;
|
|
970
|
+
/**
|
|
971
|
+
* Named imports from this package
|
|
972
|
+
*/
|
|
973
|
+
symbols: Set<string>;
|
|
974
|
+
/**
|
|
975
|
+
* Whether to use type-only import
|
|
976
|
+
*/
|
|
977
|
+
typeOnly: boolean;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Extracts type references from API Extractor models to generate import statements.
|
|
981
|
+
*
|
|
982
|
+
* This class analyzes API items and their excerpt tokens to identify external type
|
|
983
|
+
* references that need to be imported in the generated TypeScript declaration files.
|
|
984
|
+
*
|
|
985
|
+
* **How it works:**
|
|
986
|
+
* 1. Walks through all API items (classes, interfaces, functions, etc.)
|
|
987
|
+
* 2. Extracts type references from excerpt tokens
|
|
988
|
+
* 3. Filters out built-in types and internal references
|
|
989
|
+
* 4. Groups external references by package
|
|
990
|
+
* 5. Generates `import type` statements
|
|
991
|
+
*
|
|
992
|
+
* **Reference Types:**
|
|
993
|
+
* - **Built-in:** TypeScript types like `Promise`, `Record`, `NonNullable` (skipped)
|
|
994
|
+
* - **Internal:** References to types in the same package (skipped)
|
|
995
|
+
* - **External:** References to types from npm packages (imported)
|
|
996
|
+
*
|
|
997
|
+
* **Canonical Reference Format:**
|
|
998
|
+
* API Extractor uses canonical references like:
|
|
999
|
+
* - `"zod!ZodType:interface"` → External reference to `zod` package
|
|
1000
|
+
* - `"mypackage!MyType:type"` → Internal reference (same package)
|
|
1001
|
+
* - `"!Promise:interface"` → Built-in TypeScript type
|
|
1002
|
+
* - `"!\"node:buffer\".__global.Buffer:interface"` → Node.js built-in (treated as built-in)
|
|
1003
|
+
*
|
|
1004
|
+
* @example
|
|
1005
|
+
* ```ts
|
|
1006
|
+
* const extractor = new TypeReferenceExtractor(apiPackage, "my-package");
|
|
1007
|
+
* const imports = extractor.extractImports();
|
|
1008
|
+
*
|
|
1009
|
+
* for (const stmt of imports) {
|
|
1010
|
+
* console.log(`import type { ${[...stmt.symbols].join(", ")} } from "${stmt.packageName}";`);
|
|
1011
|
+
* }
|
|
1012
|
+
* // Output:
|
|
1013
|
+
* // import type { ZodType } from "zod";
|
|
1014
|
+
* // import type { Effect } from "@effect/schema";
|
|
1015
|
+
* ```
|
|
1016
|
+
*
|
|
1017
|
+
* @public
|
|
1018
|
+
*/
|
|
1019
|
+
declare class TypeReferenceExtractor {
|
|
1020
|
+
private readonly apiPackage;
|
|
1021
|
+
private readonly currentPackageName;
|
|
1022
|
+
/**
|
|
1023
|
+
* All type references found in the API package
|
|
1024
|
+
*/
|
|
1025
|
+
private readonly references;
|
|
1026
|
+
constructor(apiPackage: ApiPackage, currentPackageName: string);
|
|
1027
|
+
/**
|
|
1028
|
+
* Extract all type references from the API package and generate import statements.
|
|
1029
|
+
* Returns an array of import statements grouped by package.
|
|
1030
|
+
*/
|
|
1031
|
+
extractImports(): ImportStatement[];
|
|
1032
|
+
/**
|
|
1033
|
+
* Extract type references for a specific entry point only.
|
|
1034
|
+
* This enables per-entry-point import optimization for multi-entry packages.
|
|
1035
|
+
*
|
|
1036
|
+
* @param entryPoint - The specific entry point to extract imports for
|
|
1037
|
+
* @returns Import statements containing only types used in this entry point
|
|
1038
|
+
*/
|
|
1039
|
+
extractImportsForEntryPoint(entryPoint: ApiEntryPoint): ImportStatement[];
|
|
1040
|
+
/**
|
|
1041
|
+
* Extract type references for a single API item.
|
|
1042
|
+
* This enables generating imports for individual signatures.
|
|
1043
|
+
*
|
|
1044
|
+
* @param apiItem - The specific API item to extract imports for
|
|
1045
|
+
* @returns Import statements containing only types used in this item
|
|
1046
|
+
*/
|
|
1047
|
+
extractImportsForApiItem(apiItem: ApiItem): ImportStatement[];
|
|
1048
|
+
/**
|
|
1049
|
+
* Generate import statements from collected references.
|
|
1050
|
+
* Used by both extractImports() and extractImportsForEntryPoint().
|
|
1051
|
+
*/
|
|
1052
|
+
private generateImportStatements;
|
|
1053
|
+
/**
|
|
1054
|
+
* Generate import statement strings from ImportStatement objects.
|
|
1055
|
+
* Returns an array of formatted import statements.
|
|
1056
|
+
*/
|
|
1057
|
+
static formatImports(imports: ImportStatement[]): string[];
|
|
1058
|
+
/**
|
|
1059
|
+
* Walk through the entire API package and extract all type references
|
|
1060
|
+
*/
|
|
1061
|
+
private walkApiPackage;
|
|
1062
|
+
/**
|
|
1063
|
+
* Recursively walk through an API item and its children to extract type references
|
|
1064
|
+
*/
|
|
1065
|
+
private walkApiItem;
|
|
1066
|
+
/**
|
|
1067
|
+
* Extract type references from an API item using its excerpt
|
|
1068
|
+
*/
|
|
1069
|
+
private extractFromExcerpt;
|
|
1070
|
+
/**
|
|
1071
|
+
* Get the appropriate excerpt from an API item based on its kind
|
|
1072
|
+
*/
|
|
1073
|
+
private getExcerpt;
|
|
1074
|
+
/**
|
|
1075
|
+
* Extract type references from excerpt tokens
|
|
1076
|
+
*/
|
|
1077
|
+
private extractFromExcerptTokens;
|
|
1078
|
+
/**
|
|
1079
|
+
* Parse a canonical reference string to extract type reference information.
|
|
1080
|
+
*
|
|
1081
|
+
* Canonical reference format: "packageName!symbolName:kind"
|
|
1082
|
+
* Examples:
|
|
1083
|
+
* - "zod!ZodType:interface" → External reference
|
|
1084
|
+
* - "mypackage!MyType:type" → Internal reference
|
|
1085
|
+
* - "!Promise:interface" → Built-in type
|
|
1086
|
+
* - "!\"node:buffer\".__global.Buffer:interface" → Node.js built-in
|
|
1087
|
+
*/
|
|
1088
|
+
private parseCanonicalReference;
|
|
1089
|
+
}
|
|
1090
|
+
//#endregion
|
|
1091
|
+
export { ApiExtractedPackage, type ApiItemRef, ApiItems_d_exports as ApiItems, CrossLinker, type DocMeta, EntryPoints_d_exports as EntryPoints, type FrontmatterRenderer, type ImportStatement, type ItemKindSlug, Model_d_exports as Model, type ParsedFrontmatter, Render_d_exports as Render, type RenderPackageOptions, type RenderedDoc, type RouteFormatter, Routes_d_exports as Routes, Signature_d_exports as Signature, SyntheticBases_d_exports as SyntheticBases, Tsdoc_d_exports as Tsdoc, type TypeReference, TypeReferenceExtractor, emitFrontmatterBlock, parseFrontmatter, stringifyFrontmatter };
|
|
728
1092
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { ApiExtractedPackage } from "./ApiExtractedPackage.js";
|
|
1
2
|
import { Routes_exports } from "./Routes.js";
|
|
2
3
|
import { Tsdoc_exports } from "./Tsdoc.js";
|
|
3
4
|
import { ApiItems_exports } from "./ApiItems.js";
|
|
4
5
|
import { CrossLinker } from "./CrossLinker.js";
|
|
5
6
|
import { EntryPoints_exports } from "./EntryPoints.js";
|
|
7
|
+
import { emitFrontmatterBlock, parseFrontmatter, stringifyFrontmatter } from "./Frontmatter.js";
|
|
6
8
|
import { Model_exports } from "./Model.js";
|
|
7
9
|
import { Signature_exports } from "./Signature.js";
|
|
8
10
|
import { Render_exports } from "./Render.js";
|
|
9
11
|
import { SyntheticBases_exports } from "./SyntheticBases.js";
|
|
12
|
+
import { TypeReferenceExtractor } from "./TypeReferenceExtractor.js";
|
|
10
13
|
|
|
11
|
-
export { ApiItems_exports as ApiItems, CrossLinker, EntryPoints_exports as EntryPoints, Model_exports as Model, Render_exports as Render, Routes_exports as Routes, Signature_exports as Signature, SyntheticBases_exports as SyntheticBases, Tsdoc_exports as Tsdoc };
|
|
14
|
+
export { ApiExtractedPackage, ApiItems_exports as ApiItems, CrossLinker, EntryPoints_exports as EntryPoints, Model_exports as Model, Render_exports as Render, Routes_exports as Routes, Signature_exports as Signature, SyntheticBases_exports as SyntheticBases, Tsdoc_exports as Tsdoc, TypeReferenceExtractor, emitFrontmatterBlock, parseFrontmatter, stringifyFrontmatter };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tsdoctor/model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Render Microsoft API Extractor models into LLM-lean markdown. Pure model loading, TSDoc extraction, type-signature formatting, and per-item markdown rendering.",
|
|
6
6
|
"keywords": [
|
|
@@ -39,18 +39,14 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@microsoft/api-extractor-model": "^7.33.11",
|
|
42
|
-
"@microsoft/tsdoc": "^0.16.0"
|
|
42
|
+
"@microsoft/tsdoc": "^0.16.0",
|
|
43
|
+
"@tsdoctor/vfs": "0.1.0"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
45
46
|
"@effected/markdown": "^0.7.0",
|
|
46
|
-
"@effected/
|
|
47
|
+
"@effected/yaml": "^0.12.0",
|
|
47
48
|
"effect": "4.0.0-rc.109"
|
|
48
49
|
},
|
|
49
|
-
"peerDependenciesMeta": {
|
|
50
|
-
"@effected/package-json": {
|
|
51
|
-
"optional": true
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
50
|
"engines": {
|
|
55
51
|
"node": ">=24.11.0"
|
|
56
52
|
}
|