Skip to content
File

Blob: types/src/transforms/globals.ts

typescript202 lines
1// Copyright (c) 2022-2023 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 ts from "typescript";
7 
8// Copies all properties of `ServiceWorkerGlobalScope` and its superclasses into
9// the global scope:
10//
11// ```ts
12// export declare class EventTarget {
13// constructor();
14// addEventListener(...): ...;
15// }
16// export declare abstract WorkerGlobalScope extends EventTarget {
17// ...
18// }
19// export interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
20// DOMException: typeof DOMException;
21// btoa(value: string): string;
22// crypto: Crypto;
23// ...
24// }
25// ```
26//
27// --- transforms to --->
28//
29// ```ts
30// export declare class EventTarget { ... }
31// export declare abstract WorkerGlobalScope extends EventTarget { ... }
32// export interface ServiceWorkerGlobalScope extends WorkerGlobalScope { ... }
33//
34// export declare function addEventListener(...): ...;
35// export declare function btoa(value: string): string;
36// export declare const crypto: Crypto;
37// ```
38export function createGlobalScopeTransformer(
39 checker: ts.TypeChecker
40): ts.TransformerFactory<ts.SourceFile> {
41 return (ctx) => {
42 return (node) => {
43 const visitor = createGlobalScopeVisitor(ctx, checker);
44 return ts.visitEachChild(node, visitor, ctx);
45 };
46 };
47}
48 
49// Copy type nodes everywhere they are referenced
50function createInlineVisitor(
51 ctx: ts.TransformationContext,
52 inlines: Map<string, ts.TypeNode>
53): ts.Visitor<ts.Node, ts.Node> {
54 // If there's nothing to inline, just return identity visitor
55 if (inlines.size === 0) return (node) => node;
56 
57 const visitor: ts.Visitor<ts.Node, ts.Node> = (node) => {
58 // Recursively visit all nodes
59 node = ts.visitEachChild(node, visitor, ctx);
60 
61 // Inline all matching type references
62 if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
63 const inline = inlines.get(node.typeName.text);
64 if (inline !== undefined) return inline;
65 }
66 
67 return node;
68 };
69 return visitor;
70}
71 
72// Call with each potential method/property that could be extracted into a
73// global function/const.
74export function maybeExtractGlobalNode(
75 ctx: ts.TransformationContext,
76 node: ts.Node,
77 modifiers?: readonly ts.ModifierLike[]
78): ts.Statement | undefined {
79 if (
80 (ts.isMethodSignature(node) || ts.isMethodDeclaration(node)) &&
81 ts.isIdentifier(node.name)
82 ) {
83 return ctx.factory.createFunctionDeclaration(
84 modifiers,
85 /* asteriskToken */ undefined,
86 node.name,
87 node.typeParameters,
88 node.parameters,
89 node.type,
90 /* body */ undefined
91 );
92 }
93 if (
94 (ts.isPropertySignature(node) ||
95 ts.isPropertyDeclaration(node) ||
96 ts.isGetAccessorDeclaration(node)) &&
97 ts.isIdentifier(node.name)
98 ) {
99 assert(node.type !== undefined);
100 // Don't create global nodes for nested types, they'll already be there
101 if (!ts.isTypeQueryNode(node.type)) {
102 const varDeclaration = ctx.factory.createVariableDeclaration(
103 node.name,
104 /* exclamationToken */ undefined,
105 node.type
106 );
107 const varDeclarationList = ctx.factory.createVariableDeclarationList(
108 [varDeclaration],
109 ts.NodeFlags.Const // Use `const` instead of `var`
110 );
111 return ctx.factory.createVariableStatement(modifiers, varDeclarationList);
112 }
113 }
114}
115 
116function createGlobalScopeVisitor(
117 ctx: ts.TransformationContext,
118 checker: ts.TypeChecker
119): ts.Visitor {
120 // Called with each class/interface that should have its methods/properties
121 // extracted into global functions/consts. Recursively visits superclasses.
122 function extractGlobalNodes(
123 node: ts.InterfaceDeclaration | ts.ClassDeclaration,
124 typeArgs?: ts.NodeArray<ts.TypeNode>
125 ): ts.Node[] {
126 const nodes: ts.Node[] = [];
127 
128 // If this declaration has type parameters, we'll need to inline them when
129 // extracting members.
130 const typeArgInlines = new Map<string, ts.TypeNode>();
131 if (node.typeParameters) {
132 assert(
133 node.typeParameters.length === typeArgs?.length,
134 `Expected ${node.typeParameters.length} type argument(s), got ${typeArgs?.length}`
135 );
136 node.typeParameters.forEach((typeParam, index) => {
137 typeArgInlines.set(typeParam.name.text, typeArgs[index]);
138 });
139 }
140 const inlineVisitor = createInlineVisitor(ctx, typeArgInlines);
141 
142 // Recursively extract from all superclasses
143 if (node.heritageClauses !== undefined) {
144 for (let clause of node.heritageClauses) {
145 // Handle case where type param appears in heritage clause:
146 // ```ts
147 // class A<T> {} // ↓
148 // class B<T> extends A<T> {}
149 // class C extends B<string> {}
150 // ```
151 clause = ts.visitNode(clause, inlineVisitor, ts.isHeritageClause);
152 
153 for (const superType of clause.types) {
154 const superTypeSymbol = checker.getSymbolAtLocation(
155 superType.expression
156 );
157 assert(superTypeSymbol !== undefined);
158 const superTypeDeclarations = superTypeSymbol.getDeclarations();
159 assert.strictEqual(superTypeDeclarations?.length, 1);
160 const superTypeDeclaration = superTypeDeclarations[0];
161 assert(
162 ts.isInterfaceDeclaration(superTypeDeclaration) ||
163 ts.isClassDeclaration(superTypeDeclaration)
164 );
165 nodes.push(
166 // Pass any defined type arguments for inlining in extracted nodes
167 // (e.g. `...extends EventTarget<WorkerGlobalScopeEventMap>`).
168 ...extractGlobalNodes(superTypeDeclaration, superType.typeArguments)
169 );
170 }
171 }
172 }
173 
174 // Extract methods/properties
175 const modifiers: ts.Modifier[] = [
176 ctx.factory.createToken(ts.SyntaxKind.DeclareKeyword),
177 ];
178 for (const member of node.members) {
179 const maybeNode = maybeExtractGlobalNode(ctx, member, modifiers);
180 if (maybeNode !== undefined) {
181 nodes.push(ts.visitNode(maybeNode, inlineVisitor));
182 }
183 }
184 
185 return nodes;
186 }
187 
188 // Finds the `ServiceWorkerGlobalScope` declaration, calls
189 // `extractGlobalNodes` with it, and inserts all extracted nodes.
190 const serviceWorkerGlobalScopeVisitor: ts.Visitor = (node) => {
191 if (
192 (ts.isInterfaceDeclaration(node) || ts.isClassDeclaration(node)) &&
193 node.name !== undefined &&
194 node.name.text === "ServiceWorkerGlobalScope"
195 ) {
196 return [node, ...extractGlobalNodes(node)];
197 }
198 return node;
199 };
200 return serviceWorkerGlobalScopeVisitor;
201}