Skip to content
File

Blob: types/src/standards.ts

typescript122 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5import assert from "node:assert";
6import { readFileSync } from "node:fs";
7import * as ts from "typescript";
8import { createMemoryProgram } from "./program";
9import { CommentsData } from "./transforms";
10 
11// Collate comments from standards-based .d.ts files (e.g. lib.webworker.d.ts)
12export function collateStandardComments(
13 ...standardTypes: string[]
14): CommentsData {
15 const combinedLibPath = "/$virtual/standards.d.ts";
16 const combinedLibContents: string = standardTypes
17 .map(
18 (s) =>
19 // Remove the Microsoft copyright notices from the file, to prevent them being copied in as TS comments
20 readFileSync(s, "utf-8").split(`/////////////////////////////`)[2]
21 )
22 .join("\n");
23 
24 const program = createMemoryProgram(
25 new Map([[combinedLibPath, combinedLibContents]])
26 );
27 
28 const combinedLibFile = program.getSourceFile(combinedLibPath);
29 
30 assert(combinedLibFile !== undefined);
31 
32 const result: CommentsData = {};
33 const recordComments = (node: ts.Node, name: string, memberName?: string): void => {
34 const ranges = ts.getLeadingCommentRanges(
35 combinedLibContents,
36 node.getFullStart()
37 );
38 if (ranges === undefined) return;
39 
40 const nodeResult = (result[name] ??= {});
41 const key = memberName ?? "$";
42 nodeResult[key] ??= "";
43 
44 for (const range of ranges) {
45 let text = combinedLibContents.slice(range.pos + 2, range.end);
46 // All lib.*.d.ts file use multiline comment syntax (/** ... */).
47 // For the avoidance of doubt and to make sure the following parsing code is valid,
48 // assert that only multiline comments are included.
49 assert(
50 range.kind === ts.SyntaxKind.MultiLineCommentTrivia,
51 "Unexpected single-line comment in a standards .d.ts file"
52 );
53 text = text
54 // Remove the multiline comment end
55 .slice(0, text.length - 2)
56 // Remove multiline comment prefix lines (e.g. ` * ` -> ` * `)
57 .replaceAll(/^\s+/gm, " ");
58 
59 // Let's make sure comments that are actually only 1 line (usually a link to MDN) don't start
60 // with two * characters (e.g. `/** [MDN Reference] ... */` -> `/* [MDN Reference] ... */`)
61 if (!text.includes("\n") && text.startsWith("*")) {
62 text = text.slice(1);
63 }
64 
65 // Because we load multiple lib files, some types are included multiple times.
66 // This simple check makes sure that we don't add a doc comment twice
67 if (!nodeResult[key]?.includes?.(text)) {
68 nodeResult[key] += text;
69 }
70 }
71 };
72 
73 ts.forEachChild(combinedLibFile, (node) => {
74 if (ts.isInterfaceDeclaration(node)) {
75 // Prototype properties/methods exist on interfaces, for example:
76 // ```ts
77 // /** ... */
78 // interface AbortSignal extends EventTarget {
79 // /** ... */
80 // readonly aborted: boolean;
81 // }
82 // ```
83 recordComments(node, node.name.text);
84 for (const member of node.members) {
85 if (member.name === undefined) continue;
86 if (!ts.isIdentifier(member.name)) continue;
87 recordComments(member, node.name.text, member.name.text);
88 }
89 } else if (ts.isFunctionDeclaration(node)) {
90 if (node.name !== undefined) recordComments(node, node.name.text);
91 } else if (ts.isVariableStatement(node)) {
92 if (node.declarationList.declarations.length > 1) return;
93 const declaration = node.declarationList.declarations[0];
94 const name = declaration.name;
95 if (!ts.isIdentifier(name)) return;
96 recordComments(node, name.text);
97 
98 if (
99 declaration.type !== undefined &&
100 ts.isTypeLiteralNode(declaration.type)
101 ) {
102 // Static properties/methods exist on type literals, for example:
103 // ```ts
104 // declare var AbortSignal: {
105 // prototype: AbortSignal;
106 // new(): AbortSignal;
107 // /** ... */
108 // abort(reason?: any): AbortSignal;
109 // };
110 // ```
111 for (const member of declaration.type.members) {
112 if (member.name === undefined) continue;
113 if (!ts.isIdentifier(member.name)) continue;
114 recordComments(member, name.text, `static:${member.name.text}`);
115 }
116 }
117 }
118 });
119 
120 return result;
121}