@tsdoctor/pages 0.1.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/Build.js ADDED
@@ -0,0 +1,525 @@
1
+ import { AvailableFrom, BaseClass, CodeText, EnumMemberRow, EnumMemberTable, Example, ExampleGroup, Member, MemberGroup, MemberIndex, MemberIndexEntry, ParameterRow, ParameterTable, ProseBlock, SeeAlso, Signature as Signature$1, SourceLink, Title } from "./Blocks.js";
2
+ import { codeText, formatExampleCode, prepareExampleCode, prependHiddenImports } from "./Examples.js";
3
+ import { NavEntry } from "./Nav.js";
4
+ import { Page } from "./Page.js";
5
+ import { Markdown, Paragraph, Text } from "@effected/markdown";
6
+ import { Effect, Option, Result, Schema } from "effect";
7
+ import { ApiItemKind } from "@microsoft/api-extractor-model";
8
+ import { ApiItems, Routes, Signature, Tsdoc, TypeReferenceExtractor } from "@tsdoctor/model";
9
+
10
+ //#region src/Build.ts
11
+ /**
12
+ * The description a page carries when its item has no summary.
13
+ *
14
+ * @public
15
+ */
16
+ const NO_DESCRIPTION = "No description available.";
17
+ /**
18
+ * The API landing page: frontmatter facts only, no blocks.
19
+ *
20
+ * @public
21
+ */
22
+ var IndexPage = class extends Schema.Class("IndexPage")({
23
+ /** The page route (`{baseRoute}/index`). */
24
+ route: Schema.String,
25
+ /** The page title. */
26
+ title: Schema.String,
27
+ /** The page description. */
28
+ description: Schema.String
29
+ }) {};
30
+ /**
31
+ * The label of the index page.
32
+ *
33
+ * @public
34
+ */
35
+ const INDEX_PAGE_TITLE = "API Reference";
36
+ /**
37
+ * Build the API landing page.
38
+ *
39
+ * @public
40
+ */
41
+ function buildIndexPage(input) {
42
+ return IndexPage.make({
43
+ route: `${input.baseRoute}/index`,
44
+ title: INDEX_PAGE_TITLE,
45
+ description: `Auto-generated API documentation for ${input.packageName}`
46
+ });
47
+ }
48
+ const PARSE_OPTIONS = { dialect: "commonmark" };
49
+ /**
50
+ * Parse a one-line prose string as phrasing content. Total: a parse failure
51
+ * (a hardening-guard trip on a pathological string) degrades to the raw
52
+ * text.
53
+ */
54
+ function phrasing(text) {
55
+ const parsed = Markdown.parsePhrasingResult(text, PARSE_OPTIONS);
56
+ return Result.isSuccess(parsed) ? parsed.success : [Text.make({ value: text })];
57
+ }
58
+ /** Cross-link a prose string, then parse it as phrasing content. */
59
+ function linked(linker, text) {
60
+ return phrasing(linker.link(text));
61
+ }
62
+ /** Parse a prose string as flow content. Total, on the same terms as {@link phrasing}. */
63
+ function flow(text) {
64
+ const parsed = Markdown.parseResult(text, PARSE_OPTIONS);
65
+ if (Result.isSuccess(parsed)) {
66
+ const nodes = parsed.success.children.filter((node) => node.type !== "frontmatter" && node.type !== "mdxjsEsm");
67
+ if (nodes.length > 0) return nodes;
68
+ }
69
+ return [Paragraph.make({ children: [Text.make({ value: text })] })];
70
+ }
71
+ /** The formatted declaration excerpt, or `""` when the item has none. */
72
+ function excerptOf(item) {
73
+ const declared = item;
74
+ return declared.excerpt?.text ? Signature.format(declared.excerpt).trim() : "";
75
+ }
76
+ /**
77
+ * Prepend the hidden `import type` lines a code block needs for the external
78
+ * types `scope` references, resolved against `owner`'s package.
79
+ */
80
+ function withHiddenImports(code, owner, scope, packageName) {
81
+ const apiPackage = owner.getAssociatedPackage?.();
82
+ if (!apiPackage) return code;
83
+ const imports = new TypeReferenceExtractor(apiPackage, packageName).extractImportsForApiItem(scope);
84
+ return prependHiddenImports(code, imports);
85
+ }
86
+ /** The `class X extends … implements … {` / `interface X<T> extends … {` opening line. */
87
+ function containerDeclaration(item) {
88
+ const inheritance = ApiItems.inheritance(item);
89
+ let declaration = item.kind === ApiItemKind.Class ? `class ${item.displayName}` : `interface ${item.displayName}`;
90
+ if (item.kind === ApiItemKind.Interface) {
91
+ const typeParameters = item.typeParameters;
92
+ if (typeParameters && typeParameters.length > 0) declaration += `<${typeParameters.map((parameter) => parameter.name).join(", ")}>`;
93
+ }
94
+ if (inheritance.extends && inheritance.extends.length > 0) declaration += ` extends ${inheritance.extends.join(", ")}`;
95
+ if (item.kind === ApiItemKind.Class && inheritance.implements && inheritance.implements.length > 0) declaration += ` implements ${inheritance.implements.join(", ")}`;
96
+ return `${declaration} {`;
97
+ }
98
+ /** The full skeleton: the opening line, one indented line per member, the closing brace. */
99
+ function containerSkeleton(item, members) {
100
+ const lines = [containerDeclaration(item)];
101
+ for (const member of members) {
102
+ const signature = excerptOf(member);
103
+ if (signature) lines.push(` ${signature}`);
104
+ }
105
+ lines.push("}");
106
+ return lines.join("\n");
107
+ }
108
+ /** The three-line member context the hide-cut transformer trims: opening, member, closing. */
109
+ function memberContext(item, member, packageName) {
110
+ return withHiddenImports(`${containerDeclaration(item)}\n${excerptOf(member)}\n}`, item, member, packageName);
111
+ }
112
+ const isStatic = (member) => member.isStatic === true;
113
+ const isAccessorName = (member) => member.displayName.startsWith("get ") || member.displayName.startsWith("set ");
114
+ /** Group a class's members the way the class page lists and skeletons them. */
115
+ function classMembers(apiClass) {
116
+ const properties = apiClass.members.filter((m) => m.kind === "Property" || m.kind === "PropertySignature");
117
+ const methods = apiClass.members.filter((m) => m.kind === "Method" || m.kind === "MethodSignature");
118
+ return {
119
+ constructors: apiClass.members.filter((m) => m.kind === "Constructor"),
120
+ staticProperties: properties.filter(isStatic),
121
+ staticMethods: methods.filter((m) => !(m.kind === "Method" && isAccessorName(m)) && isStatic(m)),
122
+ instanceProperties: properties.filter((m) => !isStatic(m) && !isAccessorName(m)),
123
+ getters: methods.filter((m) => m.kind === "Method" && isAccessorName(m)),
124
+ instanceMethods: methods.filter((m) => !(m.kind === "Method" && isAccessorName(m)) && !isStatic(m))
125
+ };
126
+ }
127
+ /** The anchor a member's page element carries — data from the work item, or the model's own algorithm. */
128
+ function anchorOf(ctx, member) {
129
+ return ctx.anchors.get(member.canonicalReference?.toString() ?? member.displayName) ?? Routes.memberAnchor(member.displayName);
130
+ }
131
+ function parameterRows(linker, item) {
132
+ return Tsdoc.params(item).map((parameter) => ParameterRow.make({
133
+ name: parameter.name,
134
+ ...parameter.type !== void 0 ? { type: parameter.type } : {},
135
+ description: linked(linker, parameter.description)
136
+ }));
137
+ }
138
+ /**
139
+ * One member block, or none when the member has no declaration excerpt —
140
+ * the generators rendered nothing for such a member.
141
+ */
142
+ function buildMember(ctx, member, role, name, anchor, options) {
143
+ const signature = excerptOf(member);
144
+ if (!signature) return Option.none();
145
+ const summary = Tsdoc.summary(member);
146
+ const parameters = options.parameters ? parameterRows(ctx.linker, member) : [];
147
+ const returns = options.returns ? Tsdoc.returns(member) : null;
148
+ return Option.some(Member.make({
149
+ role,
150
+ name,
151
+ anchor,
152
+ code: CodeText.make({
153
+ display: signature,
154
+ source: memberContext(ctx.owner, member, ctx.packageName)
155
+ }),
156
+ ...summary ? { summary: linked(ctx.linker, summary) } : {},
157
+ ...parameters.length > 0 ? { parameters } : {},
158
+ ...returns ? { returns: linked(ctx.linker, returns.description) } : {}
159
+ }));
160
+ }
161
+ const fixedMember = (name, anchorName) => ({
162
+ name,
163
+ anchor: Routes.memberAnchor(anchorName)
164
+ });
165
+ /** A heading group, or none when no member of the list renders. */
166
+ function memberGroup(ctx, title, members, role, options, fixed) {
167
+ if (members.length === 0) return Option.none();
168
+ const built = [];
169
+ for (const member of members) {
170
+ const anchor = fixed ? fixed.anchor : anchorOf(ctx, member);
171
+ const result = buildMember(ctx, member, role, fixed ? fixed.name : member.displayName, anchor, options);
172
+ if (Option.isSome(result)) built.push(result.value);
173
+ }
174
+ return Option.some(MemberGroup.make({
175
+ title,
176
+ members: built
177
+ }));
178
+ }
179
+ function titleBlock(item, linker) {
180
+ const deprecation = Tsdoc.deprecation(item);
181
+ return Title.make({
182
+ name: item.displayName,
183
+ releaseTag: Tsdoc.releaseTag(item),
184
+ ...deprecation ? { deprecation: linked(linker, deprecation.message) } : {}
185
+ });
186
+ }
187
+ function headBlocks(input, summary) {
188
+ const blocks = [titleBlock(input.item, input.linker), ProseBlock.make({
189
+ role: "summary",
190
+ content: flow(summary)
191
+ })];
192
+ if (input.availableFrom && input.availableFrom.length > 1) blocks.push(AvailableFrom.make({
193
+ packageName: input.packageName,
194
+ entryPoints: input.availableFrom
195
+ }));
196
+ const href = ApiItems.sourceLink(input.item, input.source);
197
+ if (href) blocks.push(SourceLink.make({ href }));
198
+ return blocks;
199
+ }
200
+ const buildExampleWithFallback = (input, example) => Effect.gen(function* () {
201
+ const prepared = prepareExampleCode(example, input.item.displayName, input.packageName, input.suppressExampleErrors);
202
+ const formatted = yield* formatExampleCode(prepared.code, prepared.language).pipe(Effect.catchTag("ExampleFormatError", (error) => (input.onExampleFormatError ? input.onExampleFormatError(error) : Effect.void).pipe(Effect.as(prepared.code))));
203
+ return Example.make({
204
+ language: prepared.language,
205
+ code: prepared.isTypeScript ? codeText(formatted) : CodeText.make({
206
+ display: formatted,
207
+ source: formatted
208
+ }),
209
+ typeChecked: prepared.isTypeScript
210
+ });
211
+ });
212
+ const tailBlocks = (input) => Effect.gen(function* () {
213
+ const blocks = [];
214
+ const examples = Tsdoc.examples(input.item);
215
+ if (examples.length > 0) {
216
+ const items = yield* Effect.forEach(examples, (example) => buildExampleWithFallback(input, example));
217
+ blocks.push(ExampleGroup.make({ items }));
218
+ }
219
+ const references = Tsdoc.seeReferences(input.item);
220
+ if (references.length > 0) blocks.push(SeeAlso.make({ references: references.map((reference) => linked(input.linker, reference.text)) }));
221
+ return blocks;
222
+ });
223
+ function classBody(input, apiClass) {
224
+ const { packageName, linker } = input;
225
+ const members = classMembers(apiClass);
226
+ const ctx = {
227
+ owner: apiClass,
228
+ packageName,
229
+ linker,
230
+ anchors: input.memberAnchors ?? ApiItems.memberAnchors(apiClass)
231
+ };
232
+ const blocks = [];
233
+ const skeleton = containerSkeleton(apiClass, [
234
+ ...members.constructors,
235
+ ...members.staticProperties,
236
+ ...members.staticMethods,
237
+ ...members.instanceProperties,
238
+ ...members.getters,
239
+ ...members.instanceMethods
240
+ ]);
241
+ blocks.push(Signature$1.make({ code: codeText(withHiddenImports(skeleton, apiClass, apiClass, packageName)) }));
242
+ const base = input.syntheticBase;
243
+ if (base?.excerpt?.text) {
244
+ const signature = Signature.format(base.excerpt).trim();
245
+ blocks.push(BaseClass.make({
246
+ className: apiClass.displayName,
247
+ baseName: base.displayName,
248
+ packageName,
249
+ code: codeText(withHiddenImports(signature, apiClass, base, packageName))
250
+ }));
251
+ }
252
+ const withParameters = {
253
+ parameters: true,
254
+ returns: false
255
+ };
256
+ const methodOptions = {
257
+ parameters: true,
258
+ returns: true
259
+ };
260
+ const propertyOptions = {
261
+ parameters: false,
262
+ returns: false
263
+ };
264
+ const groups = [
265
+ memberGroup(ctx, "Constructors", members.constructors, "constructor", withParameters, fixedMember("constructor", "constructor")),
266
+ memberGroup(ctx, "Static Properties", members.staticProperties, "property", propertyOptions),
267
+ memberGroup(ctx, "Static Methods", members.staticMethods, "method", methodOptions),
268
+ memberGroup(ctx, "Properties", members.instanceProperties, "property", propertyOptions),
269
+ memberGroup(ctx, "Getters & Setters", members.getters, "getter", methodOptions),
270
+ memberGroup(ctx, "Methods", members.instanceMethods, "method", methodOptions)
271
+ ];
272
+ for (const group of groups) if (Option.isSome(group)) blocks.push(group.value);
273
+ return blocks;
274
+ }
275
+ function interfaceBody(input, apiInterface) {
276
+ const { packageName, linker } = input;
277
+ const ctx = {
278
+ owner: apiInterface,
279
+ packageName,
280
+ linker,
281
+ anchors: input.memberAnchors ?? ApiItems.memberAnchors(apiInterface)
282
+ };
283
+ const ofKind = (kind) => apiInterface.members.filter((m) => m.kind === kind);
284
+ const callSignatures = ofKind("CallSignature");
285
+ const constructSignatures = ofKind("ConstructSignature");
286
+ const indexSignatures = ofKind("IndexSignature");
287
+ const properties = ofKind("PropertySignature");
288
+ const methods = ofKind("MethodSignature");
289
+ const blocks = [];
290
+ const skeleton = containerSkeleton(apiInterface, [
291
+ ...callSignatures,
292
+ ...constructSignatures,
293
+ ...indexSignatures,
294
+ ...properties,
295
+ ...methods
296
+ ]);
297
+ blocks.push(Signature$1.make({ code: codeText(withHiddenImports(skeleton, apiInterface, apiInterface, packageName)) }));
298
+ const plain = {
299
+ parameters: false,
300
+ returns: false
301
+ };
302
+ const groups = [
303
+ memberGroup(ctx, "Call Signatures", callSignatures, "call-signature", plain, fixedMember("Call Signature", "call-signature")),
304
+ memberGroup(ctx, "Construct Signatures", constructSignatures, "construct-signature", plain, fixedMember("Construct Signature", "construct-signature")),
305
+ memberGroup(ctx, "Index Signature", indexSignatures, "index-signature", plain, fixedMember("Index Signature", "index-signature")),
306
+ memberGroup(ctx, "Properties", properties, "property", plain),
307
+ memberGroup(ctx, "Methods", methods, "method", {
308
+ parameters: true,
309
+ returns: true
310
+ })
311
+ ];
312
+ for (const group of groups) if (Option.isSome(group)) blocks.push(group.value);
313
+ return blocks;
314
+ }
315
+ function functionBody(input) {
316
+ const { item, packageName, linker } = input;
317
+ const blocks = [];
318
+ const rows = parameterRows(linker, item);
319
+ const signature = excerptOf(item);
320
+ if (signature) blocks.push(Signature$1.make({
321
+ code: codeText(withHiddenImports(signature, item, item, packageName)),
322
+ hasParameters: rows.length > 0
323
+ }));
324
+ if (rows.length > 0) blocks.push(ParameterTable.make({ rows }));
325
+ const returns = Tsdoc.returns(item);
326
+ if (returns) blocks.push(ProseBlock.make({
327
+ role: "returns",
328
+ content: [Paragraph.make({ children: [...linked(linker, returns.description)] })]
329
+ }));
330
+ return blocks;
331
+ }
332
+ function declarationBody(input) {
333
+ const { item, packageName } = input;
334
+ const signature = excerptOf(item);
335
+ if (!signature) return [];
336
+ return [Signature$1.make({ code: codeText(withHiddenImports(signature, item, item, packageName)) })];
337
+ }
338
+ /** The initializer after `=` in an enum member's excerpt, without a trailing comma. */
339
+ function enumMemberValue(member) {
340
+ const text = member.excerpt?.text?.trim();
341
+ if (!text) return void 0;
342
+ const equals = text.indexOf("=");
343
+ if (equals === -1) return void 0;
344
+ return text.substring(equals + 1).trim().replace(/,\s*$/, "");
345
+ }
346
+ function enumBody(input, apiEnum) {
347
+ const { linker } = input;
348
+ const members = apiEnum.members;
349
+ const lines = [`enum ${apiEnum.displayName} {`];
350
+ members.forEach((member, index) => {
351
+ const value = enumMemberValue(member);
352
+ const line = ` ${member.displayName}${value !== void 0 ? ` = ${value}` : ""}`;
353
+ lines.push(index < members.length - 1 ? `${line},` : line);
354
+ });
355
+ lines.push("}");
356
+ const hasMembers = members.length > 0;
357
+ const blocks = [Signature$1.make({
358
+ code: codeText(lines.join("\n")),
359
+ hasMembers
360
+ })];
361
+ if (hasMembers) blocks.push(EnumMemberTable.make({ rows: members.map((member) => {
362
+ const value = enumMemberValue(member);
363
+ return EnumMemberRow.make({
364
+ name: member.displayName,
365
+ ...value !== void 0 ? { value } : {},
366
+ description: linked(linker, Tsdoc.summary(member) || "")
367
+ });
368
+ }) }));
369
+ return blocks;
370
+ }
371
+ /** The namespace member index sections, in the order the namespace page lists them. */
372
+ const NAMESPACE_SECTIONS = [
373
+ [
374
+ "Classes",
375
+ ApiItemKind.Class,
376
+ "class"
377
+ ],
378
+ [
379
+ "Interfaces",
380
+ ApiItemKind.Interface,
381
+ "interface"
382
+ ],
383
+ [
384
+ "Functions",
385
+ ApiItemKind.Function,
386
+ "function"
387
+ ],
388
+ [
389
+ "Variables",
390
+ ApiItemKind.Variable,
391
+ "variable"
392
+ ],
393
+ [
394
+ "Types",
395
+ ApiItemKind.TypeAlias,
396
+ "type"
397
+ ],
398
+ [
399
+ "Enums",
400
+ ApiItemKind.Enum,
401
+ "enum"
402
+ ],
403
+ [
404
+ "Namespaces",
405
+ ApiItemKind.Namespace,
406
+ "namespace"
407
+ ]
408
+ ];
409
+ /** A declaration abbreviated to its header — everything before the opening brace. */
410
+ function abbreviate(signature) {
411
+ const brace = signature.indexOf("{");
412
+ return brace === -1 ? signature : signature.substring(0, brace).trim();
413
+ }
414
+ function namespaceBody(input, apiNamespace) {
415
+ const { packageName, linker, baseRoute } = input;
416
+ const name = apiNamespace.displayName;
417
+ const lines = [`namespace ${name} {`];
418
+ const bodied = /* @__PURE__ */ new Set([
419
+ ApiItemKind.Class,
420
+ ApiItemKind.Interface,
421
+ ApiItemKind.Enum,
422
+ ApiItemKind.Namespace
423
+ ]);
424
+ for (const [, kind] of NAMESPACE_SECTIONS) for (const member of apiNamespace.members) {
425
+ if (member.kind !== kind) continue;
426
+ const signature = excerptOf(member);
427
+ if (!signature) continue;
428
+ lines.push(bodied.has(kind) ? ` ${abbreviate(signature)} { }` : ` ${signature}`);
429
+ }
430
+ lines.push("}");
431
+ const blocks = [Signature$1.make({ code: codeText(withHiddenImports(lines.join("\n"), apiNamespace, apiNamespace, packageName)) })];
432
+ for (const [title, kind, folder] of NAMESPACE_SECTIONS) {
433
+ const members = apiNamespace.members.filter((member) => member.kind === kind);
434
+ if (members.length === 0) continue;
435
+ blocks.push(MemberIndex.make({
436
+ title,
437
+ entries: members.map((member) => {
438
+ const summary = Tsdoc.summary(member);
439
+ return MemberIndexEntry.make({
440
+ name: member.displayName,
441
+ route: `${baseRoute}/${folder}/${`${name}.${member.displayName}`.toLowerCase()}`,
442
+ ...summary ? { summary: linked(linker, summary) } : {}
443
+ });
444
+ })
445
+ }));
446
+ }
447
+ return blocks;
448
+ }
449
+ /** The page kind for a supported item kind, or none for anything else. */
450
+ function pageKindOf(kind) {
451
+ switch (kind) {
452
+ case ApiItemKind.Class: return Option.some("class");
453
+ case ApiItemKind.Interface: return Option.some("interface");
454
+ case ApiItemKind.Function: return Option.some("function");
455
+ case ApiItemKind.TypeAlias: return Option.some("type-alias");
456
+ case ApiItemKind.Enum: return Option.some("enum");
457
+ case ApiItemKind.Variable: return Option.some("variable");
458
+ case ApiItemKind.Namespace: return Option.some("namespace");
459
+ default: return Option.none();
460
+ }
461
+ }
462
+ /**
463
+ * Whether {@link buildPage} produces a page for an item of this kind.
464
+ *
465
+ * @public
466
+ */
467
+ function isPageKind(kind) {
468
+ return Option.isSome(pageKindOf(kind));
469
+ }
470
+ function bodyFor(input, kind) {
471
+ switch (kind) {
472
+ case "class": return classBody(input, input.item);
473
+ case "interface": return interfaceBody(input, input.item);
474
+ case "function": return functionBody(input);
475
+ case "enum": return enumBody(input, input.item);
476
+ case "namespace": return namespaceBody(input, input.item);
477
+ case "type-alias":
478
+ case "variable": return declarationBody(input);
479
+ }
480
+ }
481
+ /**
482
+ * Build the page for one item, or none for an item kind that gets no page.
483
+ *
484
+ * @remarks
485
+ * The route is `{baseRoute}/{folderName}/{name}` with the name lowercased;
486
+ * a namespace member's last segment is its lowercased qualified name, which
487
+ * is also its sidebar label. Only Prettier can fail here, and that failure
488
+ * degrades through {@link BuildPageInput.onExampleFormatError}, so the
489
+ * error channel is `never`.
490
+ *
491
+ * @public
492
+ */
493
+ const buildPage = Effect.fn("Build.buildPage")(function* (input) {
494
+ const kind = pageKindOf(input.item.kind);
495
+ if (Option.isNone(kind)) return Option.none();
496
+ const { item } = input;
497
+ const summary = Tsdoc.summary(item) || "No description available.";
498
+ const label = input.namespaceMember ? input.namespaceMember.qualifiedName : item.displayName;
499
+ const name = label.toLowerCase();
500
+ const route = `${input.baseRoute}/${input.folderName}/${name}`;
501
+ const blocks = [
502
+ ...headBlocks(input, summary),
503
+ ...bodyFor(input, kind.value),
504
+ ...yield* tailBlocks(input)
505
+ ];
506
+ return Option.some(Page.make({
507
+ kind: kind.value,
508
+ entityName: item.displayName,
509
+ singularName: input.singularName,
510
+ ...input.apiName !== void 0 ? { apiName: input.apiName } : {},
511
+ description: summary,
512
+ route,
513
+ headTags: input.headTags ?? [],
514
+ blocks,
515
+ nav: NavEntry.make({
516
+ categoryKey: input.categoryKey,
517
+ label,
518
+ name,
519
+ route
520
+ })
521
+ }));
522
+ });
523
+
524
+ //#endregion
525
+ export { INDEX_PAGE_TITLE, IndexPage, NO_DESCRIPTION, buildIndexPage, buildPage, isPageKind };