extract.ts (10493B)
1 /* 2 This file is part of GNU Taler 3 (C) 2022 Taler Systems S.A. 4 5 GNU Taler is free software; you can redistribute it and/or modify it under the 6 terms of the GNU General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 13 You should have received a copy of the GNU General Public License along with 14 GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 import * as ts from "typescript"; 18 import * as fs from "fs/promises"; 19 import * as path from "path"; 20 import * as prettier from "prettier"; 21 22 if (process.argv.length != 4) { 23 console.log( 24 `usage: ${process.argv[0]} ${process.argv[1]} WALLET_CORE_REPO OUTFILE` 25 ); 26 process.exit(2); 27 } 28 29 const walletRootDir = process.argv[2]; 30 const outfile = process.argv[3]; 31 32 const walletCoreDir = path.join(walletRootDir, "packages/taler-wallet-core"); 33 const excludedNames = new Set([ 34 "TalerErrorCode", 35 "WalletBackupContentV1", 36 "Array", 37 ]); 38 39 const configFile = ts.findConfigFile( 40 walletCoreDir, 41 ts.sys.fileExists, 42 "tsconfig.json" 43 ); 44 if (!configFile) throw Error("tsconfig.json not found"); 45 const { config } = ts.readConfigFile(configFile, ts.sys.readFile); 46 47 const { options, fileNames, errors } = ts.parseJsonConfigFileContent( 48 config, 49 ts.sys, 50 walletCoreDir 51 ); 52 53 const program = ts.createProgram({ 54 options, 55 rootNames: fileNames, 56 configFileParsingDiagnostics: errors, 57 }); 58 59 const checker = program.getTypeChecker(); 60 61 const walletApiTypesFiles = `${walletCoreDir}/src/wallet-api-types.ts`; 62 console.log("api types file:", walletApiTypesFiles); 63 64 const sourceFile = program.getSourceFile(walletApiTypesFiles); 65 66 if (!sourceFile) { 67 throw Error(); 68 } 69 70 const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); 71 72 const fileSymbol = program.getTypeChecker().getSymbolAtLocation(sourceFile); 73 74 const expo = fileSymbol?.exports; 75 if (!expo) { 76 throw Error(); 77 } 78 79 interface PerOpGatherState { 80 opName: string; 81 nameSet: Set<string>; 82 group: string; 83 /** 84 * Enum member declaration in the form 'Foo = "bar"'. 85 */ 86 enumMemberDecl: string | undefined; 87 } 88 89 interface GatherState { 90 declTexts: Map<string, string>; 91 } 92 93 function gatherDecls( 94 node: ts.Node, 95 gatherState: GatherState, 96 perOpState: PerOpGatherState 97 ): void { 98 switch (node.kind) { 99 case ts.SyntaxKind.EnumDeclaration: 100 // Always handled via parent 101 return; 102 case ts.SyntaxKind.Identifier: 103 case ts.SyntaxKind.TypeReference: { 104 console.log(`start typeref-or-id ${node.getText()}`); 105 const type = checker.getTypeAtLocation(node); 106 if (type.flags === ts.TypeFlags.String) { 107 console.log("string!"); 108 break; 109 } 110 const symbol = type.aliasSymbol || type.symbol; 111 if (!symbol) { 112 console.log(`no type symbol for ${node.getText()}`); 113 break; 114 } 115 const name = symbol.name; 116 console.log(`symbol name: ${type.symbol?.name}`); 117 console.log(`alias symbol name: ${type.aliasSymbol?.name}`); 118 if (perOpState.nameSet.has(name)) { 119 console.log(`already found ${name}`); 120 break; 121 } 122 perOpState.nameSet.add(name); 123 if (excludedNames.has(name)) { 124 console.log("excluded!"); 125 break; 126 } 127 const decls = symbol.getDeclarations(); 128 decls?.forEach((decl) => { 129 const sourceFilename = decl.getSourceFile().fileName; 130 if (path.basename(sourceFilename).startsWith("lib.")) { 131 return; 132 } 133 switch (decl.kind) { 134 case ts.SyntaxKind.EnumMember: { 135 gatherDecls(decl.parent, gatherState, perOpState); 136 console.log("enum member", decl.getText()); 137 break; 138 } 139 case ts.SyntaxKind.InterfaceDeclaration: 140 case ts.SyntaxKind.EnumDeclaration: 141 case ts.SyntaxKind.TypeAliasDeclaration: { 142 const declText = printer.printNode( 143 ts.EmitHint.Unspecified, 144 decl, 145 decl.getSourceFile()! 146 ); 147 gatherState.declTexts.set(name, declText); 148 console.log(declText); 149 break; 150 } 151 case ts.SyntaxKind.TypeLiteral: 152 if (!type.aliasSymbol) { 153 // Just free-standing type literal, no need to emit! 154 break; 155 } 156 console.log(`got TypeLiteral for ${name}`); 157 const declText = printer.printNode( 158 ts.EmitHint.Unspecified, 159 decl, 160 decl.getSourceFile()! 161 ); 162 gatherState.declTexts.set(name, `type ${name} = ${declText};`); 163 console.log(declText); 164 break; 165 default: 166 console.log(`unknown decl kind ${ts.SyntaxKind[decl.kind]}`); 167 break; 168 } 169 gatherDecls(decl, gatherState, perOpState); 170 console.log(`end typeref-or-id ${node.getText()}`); 171 }); 172 break; 173 } 174 default: 175 break; 176 } 177 console.log(`start syntax children for ${node.getText()}`); 178 node.forEachChild((child) => { 179 console.log(`syntax child: ${ts.SyntaxKind[child.kind]}`); 180 gatherDecls(child, gatherState, perOpState); 181 }); 182 console.log(`end syntax children for ${node.getText()}`); 183 //console.log(`// unknown node kind ${ts.SyntaxKind[node.kind]}`); 184 return; 185 } 186 187 function getOpEnumDecl(decl: ts.Declaration): string | undefined { 188 console.log("getting OpEnumDecl") 189 let enumMemberDecl: undefined | string = undefined; 190 function walk(node: ts.Node, level: number = 0) { 191 node.forEachChild((x) => { 192 console.log(`child kind [${level}]: ${ts.SyntaxKind[x.kind]}`); 193 console.log(x.getText()); 194 switch (x.kind) { 195 case ts.SyntaxKind.PropertySignature: { 196 const sig = x as ts.PropertySignature; 197 if (sig.name.getText() == "op") { 198 const type = checker.getTypeFromTypeNode(sig.type!); 199 enumMemberDecl = type.symbol.declarations![0]!.getText(); 200 } 201 break; 202 } 203 } 204 walk(x, level + 1); 205 }); 206 } 207 walk(decl); 208 return enumMemberDecl; 209 } 210 211 const main = async () => { 212 const f = await fs.open(outfile, "w"); 213 const gatherState: GatherState = { 214 declTexts: new Map<string, string>(), 215 }; 216 const perOpStates: PerOpGatherState[] = []; 217 218 let currentGroup: string = "Unknown Group"; 219 220 expo.forEach((v, k) => { 221 if (!v.name.endsWith("Op")) { 222 return; 223 } 224 console.log(`gathering documentation for export ${v.name}`); 225 const decls = v.getDeclarations(); 226 if (!decls) { 227 return; 228 } 229 console.log(`has ${decls.length} declarations`); 230 decls.forEach((decl) => { 231 console.log(`export decl, kind ${ts.SyntaxKind[decl.kind]}`); 232 233 const commentRanges = ts.getLeadingCommentRanges( 234 sourceFile.getFullText(), 235 decl.getFullStart() 236 ); 237 commentRanges?.forEach((r) => { 238 const text = sourceFile.getFullText().slice(r.pos, r.end); 239 console.log("comment text:", text); 240 const groupPrefix = "group:"; 241 const loc = text.indexOf(groupPrefix); 242 if (loc >= 0) { 243 const groupName = text.slice(loc + groupPrefix.length); 244 console.log("got new group", groupName); 245 currentGroup = groupName; 246 } 247 }); 248 249 const perOpState: PerOpGatherState = { 250 opName: v.name, 251 nameSet: new Set<string>(), 252 group: currentGroup, 253 enumMemberDecl: getOpEnumDecl(decl), 254 }; 255 let declText = printer.printNode( 256 ts.EmitHint.Unspecified, 257 decl, 258 decl.getSourceFile()! 259 ); 260 if (perOpState.enumMemberDecl) { 261 declText = declText + `\n// ${perOpState.enumMemberDecl}\n`; 262 } 263 console.log("replacing group in", declText); 264 // Remove group comments 265 declText = declText.replace(/\/\/ group: [^\n]*[\n]/m, ""); 266 perOpState.nameSet.add(v.name); 267 gatherState.declTexts.set(v.name, declText); 268 gatherDecls(decl, gatherState, perOpState); 269 perOpStates.push(perOpState); 270 }); 271 }); 272 273 const allNames: Set<string> = new Set(); 274 275 for (const g of perOpStates) { 276 for (const k of g.nameSet.values()) { 277 allNames.add(k); 278 } 279 } 280 281 const commonNames: Set<string> = new Set(); 282 283 for (const name of allNames) { 284 let count = 0; 285 for (const g of perOpStates) { 286 for (const k of g.nameSet.values()) { 287 if (name === k) { 288 count++; 289 } 290 } 291 } 292 if (count > 1) { 293 console.log(`common name: ${name}`); 294 commonNames.add(name); 295 } 296 } 297 298 const groups = new Set<string>(); 299 for (const g of perOpStates) { 300 groups.add(g.group); 301 } 302 303 await f.write(`# Wallet-Core API Documentation\n`); 304 305 await f.write( 306 `This file is auto-generated from the [taler-typescript-core](https://git.taler.net/taler-typescript-core.git/tree/packages/taler-wallet-core/src/wallet-api-types.ts) repository.\n` 307 ); 308 309 await f.write(`## Overview\n`); 310 for (const g of groups.values()) { 311 await f.write(`### ${g}\n`); 312 for (const op of perOpStates) { 313 if (op.group !== g) { 314 continue; 315 } 316 await f.write(`* [${op.opName}](#${op.opName.toLowerCase()})\n`); 317 } 318 } 319 320 await f.write(`## Operation Reference\n`); 321 for (const g of perOpStates) { 322 // Not yet supported, switch to myst first! 323 // await f.write(`(${g.opName.toLowerCase()})=\n`); 324 await f.write(`### ${g.opName}\n`); 325 for (const name of g.nameSet.values()) { 326 if (commonNames.has(name)) { 327 continue; 328 } 329 const text = gatherState.declTexts.get(name); 330 if (!text) { 331 continue; 332 } 333 await f.write("```typescript\n"); 334 const formatted = prettier.format(text, { 335 semi: true, 336 parser: "typescript", 337 }); 338 await f.write(`${formatted}\n`); 339 await f.write("```\n"); 340 } 341 await f.write("\n"); 342 } 343 344 await f.write(`## Common Declarations\n`); 345 for (const name of commonNames.values()) { 346 const text = gatherState.declTexts.get(name); 347 if (!text) { 348 continue; 349 } 350 await f.write("```typescript\n"); 351 const formatted = prettier.format(text, { 352 semi: true, 353 parser: "typescript", 354 }); 355 await f.write(`${formatted}`); 356 await f.write("```\n"); 357 } 358 359 await f.close(); 360 }; 361 362 main();