File
Blob: types/src/generator/index.ts
| 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 | |
| 5 | import assert from 'node:assert'; |
| 6 | import { |
| 7 | FunctionType, |
| 8 | Member, |
| 9 | Member_Which, |
| 10 | Method, |
| 11 | Structure, |
| 12 | StructureGroups, |
| 13 | Type, |
| 14 | Type_Which, |
| 15 | } from '@workerd/jsg/rtti'; |
| 16 | import ts from 'typescript'; |
| 17 | import { createStructureNode } from './structure'; |
| 18 | |
| 19 | export { getTypeName } from './type'; |
| 20 | |
| 21 | export type StructureMap = Map<string, Structure>; |
| 22 | // Builds a lookup table mapping type names to structures |
| 23 | function collectStructureMap(root: StructureGroups): StructureMap { |
| 24 | const map = new Map<string, Structure>(); |
| 25 | root.groups.forEach((group) => { |
| 26 | group.structures.forEach((structure) => { |
| 27 | map.set(structure.fullyQualifiedName, structure); |
| 28 | }); |
| 29 | }); |
| 30 | return map; |
| 31 | } |
| 32 | |
| 33 | // Builds a set containing the names of structures that should be included |
| 34 | // in the definitions, because they are referenced by root types or any of their |
| 35 | // children. A struct/resource type is marked as a root type using a |
| 36 | // `JSG_(STRUCT_)TS_ROOT` macro. |
| 37 | // |
| 38 | // We need to do this as some types should only be included in the definitions |
| 39 | // when certain compatibility flags are enabled (e.g. `Navigator`, |
| 40 | // standards-compliant `URL`). However, these types are always included in |
| 41 | // the `*_TYPES` macros. |
| 42 | function collectIncluded(map: StructureMap, root?: string): Set<string> { |
| 43 | const included = new Set<string>(); |
| 44 | |
| 45 | function visitType(type: Type): void { |
| 46 | switch (type.which()) { |
| 47 | case Type_Which.PROMISE: { |
| 48 | visitType(type.promise.value); |
| 49 | return; |
| 50 | } |
| 51 | case Type_Which.STRUCTURE: { |
| 52 | const name = type.structure.fullyQualifiedName; |
| 53 | const structure = map.get(name); |
| 54 | assert(structure !== undefined, `Unknown structure type: ${name}`); |
| 55 | { |
| 56 | visitStructure(structure); |
| 57 | return; |
| 58 | } |
| 59 | } |
| 60 | case Type_Which.ARRAY: { |
| 61 | visitType(type.array.element); |
| 62 | return; |
| 63 | } |
| 64 | case Type_Which.MAYBE: { |
| 65 | visitType(type.maybe.value); |
| 66 | return; |
| 67 | } |
| 68 | case Type_Which.DICT: { |
| 69 | const dict = type.dict; |
| 70 | visitType(dict.key); |
| 71 | visitType(dict.value); |
| 72 | return; |
| 73 | } |
| 74 | case Type_Which.ONE_OF: { |
| 75 | type.oneOf.variants.forEach(visitType); |
| 76 | return; |
| 77 | } |
| 78 | case Type_Which.FUNCTION: { |
| 79 | visitFunction(type.function); |
| 80 | return; |
| 81 | } |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | function visitFunction(func: FunctionType | Method): void { |
| 86 | func.args.forEach(visitType); |
| 87 | visitType(func.returnType); |
| 88 | } |
| 89 | |
| 90 | function visitMember(member: Member): void { |
| 91 | switch (member.which()) { |
| 92 | case Member_Which.METHOD: { |
| 93 | visitFunction(member.method); |
| 94 | return; |
| 95 | } |
| 96 | case Member_Which.PROPERTY: { |
| 97 | visitType(member.property.type); |
| 98 | return; |
| 99 | } |
| 100 | case Member_Which.NESTED: { |
| 101 | visitStructure(member.nested.structure); |
| 102 | return; |
| 103 | } |
| 104 | case Member_Which.CONSTRUCTOR: { |
| 105 | member.$constructor.args.forEach(visitType); |
| 106 | return; |
| 107 | } |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | function visitStructure(structure: Structure): void { |
| 112 | const name = structure.fullyQualifiedName; |
| 113 | if (included.has(name)) return; |
| 114 | included.add(name); |
| 115 | structure.members.forEach(visitMember); |
| 116 | if (structure._hasExtends()) { |
| 117 | visitType(structure.extends); |
| 118 | } |
| 119 | if (structure._hasIterator()) { |
| 120 | visitFunction(structure.iterator); |
| 121 | } |
| 122 | if (structure._hasAsyncIterator()) { |
| 123 | visitFunction(structure.asyncIterator); |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | if (root === undefined) { |
| 128 | // If no root was specified, visit all structures with |
| 129 | // `JSG_(STRUCT_)TS_ROOT` macros |
| 130 | for (const structure of map.values()) { |
| 131 | if (structure.tsRoot) visitStructure(structure); |
| 132 | } |
| 133 | } else { |
| 134 | // Otherwise, visit just that root |
| 135 | const structure = map.get(root); |
| 136 | assert(structure !== undefined, `Unknown root: ${root}`); |
| 137 | visitStructure(structure); |
| 138 | } |
| 139 | |
| 140 | return included; |
| 141 | } |
| 142 | |
| 143 | // Builds a set containing the names of structures that must be declared as |
| 144 | // `class`es rather than `interface`s because they either: |
| 145 | // 1) Get inherited by another class (`class` `extends` requires another `class`) |
| 146 | // 2) Are constructible (`constructor(...)`s can only appear in `class`es) |
| 147 | // 3) Have `static` methods (`static`s can only appear in `class`es) |
| 148 | // 4) Are a nested type (users could call `instanceof` with the type) |
| 149 | function collectClasses(map: StructureMap): Set<string> { |
| 150 | const classes = new Set<string>(); |
| 151 | for (const structure of map.values()) { |
| 152 | // 1) Add all classes inherited by this class |
| 153 | if (structure._hasExtends()) { |
| 154 | const extendsType = structure.extends; |
| 155 | if (extendsType._isStructure) { |
| 156 | classes.add(extendsType.structure.fullyQualifiedName); |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | structure.members.forEach((member) => { |
| 161 | // 2) Add this class if it's constructible |
| 162 | if (member._isConstructor) { |
| 163 | classes.add(structure.fullyQualifiedName); |
| 164 | } |
| 165 | // 3) Add this class if it contains static methods |
| 166 | if (member._isMethod && member.method.static) { |
| 167 | classes.add(structure.fullyQualifiedName); |
| 168 | } |
| 169 | // 4) Add all nested types defined by this class |
| 170 | if (member._isNested) { |
| 171 | classes.add(member.nested.structure.fullyQualifiedName); |
| 172 | } |
| 173 | }); |
| 174 | } |
| 175 | return classes; |
| 176 | } |
| 177 | |
| 178 | export function generateDefinitions(root: StructureGroups): { |
| 179 | nodes: ts.Statement[]; |
| 180 | structureMap: StructureMap; |
| 181 | } { |
| 182 | const structureMap = collectStructureMap(root); |
| 183 | const globalIncluded = collectIncluded(structureMap); |
| 184 | const classes = collectClasses(structureMap); |
| 185 | |
| 186 | // Can't use `flatMap()` here as `getGroups()` returns a `capnp.List` |
| 187 | const nodes = root.groups.map((group) => { |
| 188 | const structureNodes: ts.Statement[] = []; |
| 189 | group.structures.forEach((structure) => { |
| 190 | const name = structure.fullyQualifiedName; |
| 191 | if (globalIncluded.has(name)) { |
| 192 | const asClass = classes.has(name); |
| 193 | structureNodes.push(createStructureNode(structure, { asClass })); |
| 194 | } |
| 195 | }); |
| 196 | return structureNodes; |
| 197 | }); |
| 198 | const flatNodes = nodes.flat(); |
| 199 | |
| 200 | return { nodes: flatNodes, structureMap }; |
| 201 | } |
| 202 | |
| 203 | export function collectTypeScriptModules(root: StructureGroups): string { |
| 204 | let result = ''; |
| 205 | |
| 206 | root.modules.forEach((module) => { |
| 207 | if (!module._isTsDeclarations) return; |
| 208 | const declarations = module.tsDeclarations |
| 209 | // Looks for any lines starting with `///`, which indicates a TypeScript |
| 210 | // Triple-Slash Directive (https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) |
| 211 | .replaceAll(/^\/\/\/.+$/gm, (match) => { |
| 212 | assert.strictEqual( |
| 213 | match, |
| 214 | '/// <reference types="@workerd/types-internal" />', |
| 215 | `Unexpected triple-slash directive, got ${match}` |
| 216 | ); |
| 217 | return ''; |
| 218 | }); |
| 219 | |
| 220 | result += `declare module "${module.specifier}" {\n${declarations}\n}\n`; |
| 221 | }); |
| 222 | |
| 223 | return result; |
| 224 | } |