Highly ambitious ATProtocol AppView service and sdks
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 725 lines 22 kB view raw
1import type { SourceFile } from "ts-morph"; 2import type { Lexicon, LexiconDefinition, LexiconProperty } from "./mod.ts"; 3import { 4 nsidToPascalCase, 5 nsidToNamespace, 6 defNameToPascalCase, 7 isPropertyRequired, 8 isFieldSortable, 9} from "./mod.ts"; 10 11// Convert lexicon type to TypeScript type 12function convertLexiconTypeToTypeScript( 13 def: LexiconDefinition | LexiconProperty, 14 currentLexicon: string, 15 propertyName?: string, 16 lexicons?: Lexicon[] 17): string { 18 const type = def.type; 19 switch (type) { 20 case "string": 21 // For knownValues, return the type alias name 22 if ( 23 def.knownValues && 24 Array.isArray(def.knownValues) && 25 def.knownValues.length > 0 && 26 propertyName 27 ) { 28 // Reference the generated type alias with namespace 29 const namespace = nsidToNamespace(currentLexicon); 30 return `${namespace}${defNameToPascalCase(propertyName)}`; 31 } 32 return "string"; 33 case "integer": 34 return "number"; 35 case "boolean": 36 return "boolean"; 37 case "bytes": 38 return "string"; // Base64 encoded 39 case "cid-link": 40 return "{ $link: string }"; 41 case "blob": 42 return "BlobRef"; 43 case "unknown": 44 return "unknown"; 45 case "ref": 46 return resolveTypeReference(def.ref!, currentLexicon, lexicons); 47 case "array": 48 if (def.items) { 49 const itemType = convertLexiconTypeToTypeScript( 50 def.items, 51 currentLexicon, 52 undefined, 53 lexicons 54 ); 55 return `${itemType}[]`; 56 } 57 return "any[]"; 58 case "union": 59 if (def.refs && def.refs.length > 0) { 60 const unionTypes = def.refs.map((ref: string) => 61 resolveTypeReference(ref, currentLexicon, lexicons) 62 ); 63 // Add unknown type for open unions 64 if (!def.closed) { 65 unionTypes.push("{ $type: string; [key: string]: unknown }"); 66 } 67 return unionTypes.join(" | "); 68 } 69 return "unknown"; 70 case "object": 71 return "Record<string, unknown>"; 72 default: 73 return "unknown"; 74 } 75} 76 77// Resolve lexicon type references 78function resolveTypeReference( 79 ref: string, 80 currentLexicon: string, 81 lexicons?: Lexicon[] 82): string { 83 if (ref.startsWith("#")) { 84 // Local reference: #aspectRatio -> CurrentLexiconAspectRatio 85 const defName = ref.slice(1); 86 const namespace = nsidToNamespace(currentLexicon); 87 return `${namespace}["${defNameToPascalCase(defName)}"]`; 88 } else if (ref.includes("#")) { 89 // Cross-lexicon reference: app.bsky.embed.defs#aspectRatio -> AppBskyEmbedDefs["AspectRatio"] 90 const [nsid, defName] = ref.split("#"); 91 92 if (defName === "main") { 93 // Find the lexicon and check if it has multiple definitions 94 const lexicon = lexicons?.find((lex) => lex.id === nsid); 95 if (lexicon && lexicon.definitions) { 96 const defCount = Object.keys(lexicon.definitions).length; 97 const mainDef = lexicon.definitions.main; 98 99 if (defCount === 1 && mainDef) { 100 // Single definition - use clean name 101 if (mainDef.type === "record") { 102 // For records: AppBskyActorProfile 103 return nsidToPascalCase(nsid); 104 } else { 105 // For objects: ComAtprotoRepoStrongRef 106 return nsidToNamespace(nsid); 107 } 108 } else { 109 // Multiple definitions - use namespace pattern 110 const namespace = nsidToNamespace(nsid); 111 return `${namespace}["Main"]`; 112 } 113 } 114 // Fallback 115 return nsidToNamespace(nsid); 116 } 117 118 const namespace = nsidToNamespace(nsid); 119 return `${namespace}["${defNameToPascalCase(defName)}"]`; 120 } else { 121 // Direct lexicon reference: check if single or multiple definitions 122 const lexicon = lexicons?.find((lex) => lex.id === ref); 123 if (lexicon && lexicon.definitions) { 124 const defCount = Object.keys(lexicon.definitions).length; 125 const mainDef = lexicon.definitions.main; 126 127 if (defCount === 1 && mainDef) { 128 // Single definition - use clean name 129 if (mainDef.type === "record") { 130 // For records: AppBskyActorProfile 131 return nsidToPascalCase(ref); 132 } else { 133 // For objects: ComAtprotoRepoStrongRef 134 return nsidToNamespace(ref); 135 } 136 } else if (mainDef) { 137 // Multiple definitions - use namespace pattern 138 const namespace = nsidToNamespace(ref); 139 return `${namespace}["Main"]`; 140 } 141 } 142 // Fallback 143 return nsidToNamespace(ref); 144 } 145} 146 147// Generate interface for object definitions 148function generateObjectInterface( 149 sourceFile: SourceFile, 150 lexicon: Lexicon, 151 defKey: string, 152 defValue: LexiconDefinition, 153 lexicons: Lexicon[] 154): void { 155 const namespace = nsidToNamespace(lexicon.id); 156 157 // For single-definition lexicons with main, use clean name 158 const defCount = Object.keys(lexicon.definitions || {}).length; 159 const interfaceName = 160 defKey === "main" && defCount === 1 161 ? namespace // Clean name: ComAtprotoRepoStrongRef 162 : `${namespace}${defNameToPascalCase(defKey)}`; // Multi-def: AppBskyRichtextFacetMention 163 164 const properties: Array<{ 165 name: string; 166 type: string; 167 hasQuestionToken: boolean; 168 docs?: string[]; 169 }> = []; 170 171 if (defValue.properties) { 172 for (const [propName, propDef] of Object.entries(defValue.properties)) { 173 const tsType = convertLexiconTypeToTypeScript( 174 propDef, 175 lexicon.id, 176 propName, 177 lexicons 178 ); 179 const required = 180 defValue.required && defValue.required.includes(propName); 181 182 properties.push({ 183 name: propName, 184 type: tsType, 185 hasQuestionToken: !required, 186 docs: (propDef as LexiconProperty).description 187 ? [(propDef as LexiconProperty).description!] 188 : undefined, 189 }); 190 } 191 } 192 193 if (properties.length === 0) { 194 sourceFile.addTypeAlias({ 195 name: interfaceName, 196 isExported: true, 197 type: "Record<string, never>", 198 }); 199 } else { 200 sourceFile.addInterface({ 201 name: interfaceName, 202 isExported: true, 203 properties: properties, 204 }); 205 } 206} 207 208// Generate interface for record definitions 209function generateRecordInterface( 210 sourceFile: SourceFile, 211 lexicon: Lexicon, 212 defKey: string, 213 defValue: LexiconDefinition, 214 lexicons: Lexicon[] 215): void { 216 const recordDef = defValue.record; 217 if (!recordDef) return; 218 219 const properties: Array<{ 220 name: string; 221 type: string; 222 hasQuestionToken: boolean; 223 docs?: string[]; 224 }> = []; 225 const fieldNames: string[] = []; 226 227 if (recordDef.properties) { 228 for (const [propName, propDef] of Object.entries(recordDef.properties)) { 229 const tsType = convertLexiconTypeToTypeScript( 230 propDef, 231 lexicon.id, 232 propName, 233 lexicons 234 ); 235 const required = isPropertyRequired(recordDef, propName); 236 237 properties.push({ 238 name: propName, 239 type: tsType, 240 hasQuestionToken: !required, 241 docs: (propDef as LexiconProperty).description 242 ? [(propDef as LexiconProperty).description!] 243 : undefined, 244 }); 245 246 // Collect sortable field names for sort type 247 if (isFieldSortable(propDef)) { 248 fieldNames.push(propName); 249 } 250 } 251 } 252 253 // For main records, use clean naming without Record suffix 254 const interfaceName = 255 defKey === "main" 256 ? nsidToPascalCase(lexicon.id) 257 : `${nsidToNamespace(lexicon.id)}${defNameToPascalCase(defKey)}`; 258 259 if (properties.length === 0) { 260 sourceFile.addTypeAlias({ 261 name: interfaceName, 262 isExported: true, 263 type: "Record<string, never>", 264 }); 265 } else { 266 sourceFile.addInterface({ 267 name: interfaceName, 268 isExported: true, 269 properties: properties, 270 }); 271 } 272 273 // Generate sort fields type union for records (only if there are sortable fields) 274 if (fieldNames.length > 0) { 275 sourceFile.addTypeAlias({ 276 name: `${interfaceName}SortFields`, 277 isExported: true, 278 type: fieldNames.map((f) => `"${f}"`).join(" | "), 279 }); 280 } 281} 282 283// Generate type alias for union definitions 284function generateUnionType( 285 sourceFile: SourceFile, 286 lexicon: Lexicon, 287 defKey: string, 288 defValue: LexiconDefinition, 289 lexicons: Lexicon[] 290): void { 291 const namespace = nsidToNamespace(lexicon.id); 292 const typeName = `${namespace}${defNameToPascalCase(defKey)}`; 293 294 if (defValue.refs && defValue.refs.length > 0) { 295 const unionTypes = defValue.refs.map((ref: string) => 296 resolveTypeReference(ref, lexicon.id, lexicons) 297 ); 298 299 // Add unknown type for open unions 300 if (!defValue.closed) { 301 unionTypes.push("{ $type: string; [key: string]: unknown }"); 302 } 303 304 sourceFile.addTypeAlias({ 305 name: typeName, 306 isExported: true, 307 type: unionTypes.join(" | "), 308 }); 309 } 310} 311 312// Generate type alias for array definitions 313function generateArrayType( 314 sourceFile: SourceFile, 315 lexicon: Lexicon, 316 defKey: string, 317 defValue: LexiconDefinition, 318 lexicons: Lexicon[] 319): void { 320 const namespace = nsidToNamespace(lexicon.id); 321 const typeName = `${namespace}${defNameToPascalCase(defKey)}`; 322 323 if (defValue.items) { 324 const itemType = convertLexiconTypeToTypeScript( 325 defValue.items, 326 lexicon.id, 327 undefined, 328 lexicons 329 ); 330 sourceFile.addTypeAlias({ 331 name: typeName, 332 isExported: true, 333 type: `${itemType}[]`, 334 }); 335 } 336} 337 338// Generate string literal type for token definitions 339function generateTokenType( 340 sourceFile: SourceFile, 341 lexicon: Lexicon, 342 defKey: string 343): void { 344 const namespace = nsidToNamespace(lexicon.id); 345 const typeName = `${namespace}${defNameToPascalCase(defKey)}`; 346 347 sourceFile.addTypeAlias({ 348 name: typeName, 349 isExported: true, 350 type: `"${lexicon.id}${defKey === "main" ? "" : `#${defKey}`}"`, 351 }); 352} 353 354// Generate type aliases for string fields with knownValues 355function generateKnownValuesTypes( 356 sourceFile: SourceFile, 357 lexicons: Lexicon[] 358): void { 359 for (const lexicon of lexicons) { 360 if (lexicon.definitions && typeof lexicon.definitions === "object") { 361 for (const [defKey, defValue] of Object.entries(lexicon.definitions)) { 362 if (defValue.type === "record" && defValue.record?.properties) { 363 for (const [propName, propDef] of Object.entries( 364 defValue.record.properties 365 )) { 366 const prop = propDef as LexiconProperty; 367 if ( 368 prop.type === "string" && 369 prop.knownValues && 370 Array.isArray(prop.knownValues) && 371 prop.knownValues.length > 0 372 ) { 373 // Generate a type alias for this property, namespaced by lexicon 374 const namespace = nsidToNamespace(lexicon.id); 375 const pascalPropName = defNameToPascalCase(propName); 376 const typeName = `${namespace}${pascalPropName}`; 377 378 const knownValueTypes = prop.knownValues 379 .map((value: string) => `'${value}'`) 380 .join("\n | "); 381 const typeDefinition = `${knownValueTypes}\n | (string & Record<string, never>)`; 382 383 sourceFile.addTypeAlias({ 384 name: typeName, 385 isExported: true, 386 type: typeDefinition, 387 leadingTrivia: "\n", 388 }); 389 } 390 } 391 } else if (defValue.type === "object" && defValue.properties) { 392 for (const [propName, propDef] of Object.entries( 393 defValue.properties 394 )) { 395 const prop = propDef as LexiconProperty; 396 if ( 397 prop.type === "string" && 398 prop.knownValues && 399 Array.isArray(prop.knownValues) && 400 prop.knownValues.length > 0 401 ) { 402 // Generate a type alias for this property, namespaced by lexicon 403 const namespace = nsidToNamespace(lexicon.id); 404 const pascalPropName = defNameToPascalCase(propName); 405 const typeName = `${namespace}${pascalPropName}`; 406 407 const knownValueTypes = prop.knownValues 408 .map((value: string) => `'${value}'`) 409 .join("\n | "); 410 const typeDefinition = `${knownValueTypes}\n | (string & Record<string, never>)`; 411 412 sourceFile.addTypeAlias({ 413 name: typeName, 414 isExported: true, 415 type: typeDefinition, 416 leadingTrivia: "\n", 417 }); 418 } 419 } 420 } else if (defValue.type === "string") { 421 // Handle standalone string definitions with knownValues (like labelValue) 422 const stringDef = defValue as LexiconDefinition; 423 if ( 424 stringDef.knownValues && 425 Array.isArray(stringDef.knownValues) && 426 stringDef.knownValues.length > 0 427 ) { 428 // Generate a type alias for this definition, namespaced by lexicon 429 const namespace = nsidToNamespace(lexicon.id); 430 const typeName = `${namespace}${defNameToPascalCase(defKey)}`; 431 432 const knownValueTypes = stringDef.knownValues 433 .map((value: string) => `'${value}'`) 434 .join("\n | "); 435 const typeDefinition = `${knownValueTypes}\n | (string & Record<string, never>)`; 436 437 sourceFile.addTypeAlias({ 438 name: typeName, 439 isExported: true, 440 type: typeDefinition, 441 leadingTrivia: "\n", 442 }); 443 } 444 } 445 } 446 } 447 } 448} 449 450// Generate namespace interfaces for lexicons with multiple definitions 451function addLexiconNamespaces( 452 sourceFile: SourceFile, 453 lexicons: Lexicon[] 454): void { 455 for (const lexicon of lexicons) { 456 if (!lexicon.definitions || typeof lexicon.definitions !== "object") { 457 continue; 458 } 459 460 const definitions = Object.keys(lexicon.definitions); 461 462 // Skip lexicons with only a main definition (they get traditional Record interfaces) 463 if (definitions.length === 1 && definitions[0] === "main") { 464 continue; 465 } 466 467 const namespace = nsidToNamespace(lexicon.id); 468 const namespaceProperties: Array<{ 469 name: string; 470 type: string; 471 isReadonly: boolean; 472 }> = []; 473 474 // Add properties for each definition 475 for (const [defKey, defValue] of Object.entries(lexicon.definitions)) { 476 const defName = defNameToPascalCase(defKey); 477 478 switch (defValue.type) { 479 case "object": { 480 namespaceProperties.push({ 481 name: defName, 482 type: `${namespace}${defName}`, 483 isReadonly: true, 484 }); 485 break; 486 } 487 case "string": { 488 // Check if this is a string type with knownValues 489 const stringDef = defValue as LexiconDefinition; 490 if ( 491 stringDef.knownValues && 492 Array.isArray(stringDef.knownValues) && 493 stringDef.knownValues.length > 0 494 ) { 495 // This generates a type alias, reference it in the namespace with full name 496 namespaceProperties.push({ 497 name: defName, 498 type: `${namespace}${defNameToPascalCase(defKey)}`, 499 isReadonly: true, 500 }); 501 } 502 break; 503 } 504 case "union": 505 case "array": 506 case "token": 507 // These generate type aliases, so we reference the type itself 508 namespaceProperties.push({ 509 name: defName, 510 type: `${namespace}${defName}`, 511 isReadonly: true, 512 }); 513 break; 514 case "record": 515 // Records get their own interfaces, reference them 516 if (defKey === "main") { 517 namespaceProperties.push({ 518 name: "Main", 519 type: nsidToPascalCase(lexicon.id), 520 isReadonly: true, 521 }); 522 } else { 523 namespaceProperties.push({ 524 name: defName, 525 type: `${namespace}${defName}`, 526 isReadonly: true, 527 }); 528 } 529 break; 530 } 531 } 532 533 // Only create namespace if we have properties 534 if (namespaceProperties.length > 0) { 535 sourceFile.addInterface({ 536 name: namespace, 537 isExported: true, 538 properties: namespaceProperties.map((prop) => ({ 539 name: prop.name, 540 type: prop.type, 541 isReadonly: prop.isReadonly, 542 })), 543 }); 544 } 545 } 546} 547 548// Base interfaces are imported from @slices/client, only add network.slices specific interfaces 549function addBaseInterfaces( 550 _sourceFile: SourceFile 551): void { 552 // All interfaces are now generated from lexicons 553 // This function is kept for future extensibility if needed 554} 555 556// Generate interfaces for query and procedure parameters/input/output 557function generateQueryProcedureInterfaces( 558 sourceFile: SourceFile, 559 lexicon: Lexicon, 560 defValue: LexiconDefinition, 561 lexicons: Lexicon[], 562 type: "query" | "procedure" 563): void { 564 const baseName = nsidToPascalCase(lexicon.id); 565 566 // Generate parameters interface if present and has properties 567 if (defValue.parameters?.properties && Object.keys(defValue.parameters.properties).length > 0) { 568 const interfaceName = `${baseName}Params`; 569 const properties = Object.entries(defValue.parameters.properties).map( 570 ([propName, propDef]) => ({ 571 name: propName, 572 type: convertLexiconTypeToTypeScript( 573 propDef, 574 lexicon.id, 575 propName, 576 lexicons 577 ), 578 hasQuestionToken: !(defValue.parameters?.required || []).includes(propName), 579 }) 580 ); 581 582 sourceFile.addInterface({ 583 name: interfaceName, 584 isExported: true, 585 properties, 586 }); 587 } 588 589 // Generate input interface for procedures 590 if (type === "procedure" && defValue.input?.schema) { 591 const interfaceName = `${baseName}Input`; 592 593 if (defValue.input?.schema?.type === "object" && defValue.input.schema.properties) { 594 const properties = Object.entries(defValue.input.schema.properties).map( 595 ([propName, propDef]) => ({ 596 name: propName, 597 type: convertLexiconTypeToTypeScript( 598 propDef, 599 lexicon.id, 600 propName, 601 lexicons 602 ), 603 hasQuestionToken: !((defValue.input?.schema?.required as string[]) || []).includes(propName), 604 }) 605 ); 606 607 sourceFile.addInterface({ 608 name: interfaceName, 609 isExported: true, 610 properties, 611 }); 612 } 613 } 614 615 // Generate output interface if present 616 if (defValue.output?.schema) { 617 const interfaceName = `${baseName}Output`; 618 619 if (defValue.output?.schema?.type === "object" && defValue.output.schema.properties) { 620 const properties = Object.entries(defValue.output.schema.properties).map( 621 ([propName, propDef]) => ({ 622 name: propName, 623 type: convertLexiconTypeToTypeScript( 624 propDef, 625 lexicon.id, 626 propName, 627 lexicons 628 ), 629 hasQuestionToken: !((defValue.output?.schema?.required as string[]) || []).includes(propName), 630 }) 631 ); 632 633 sourceFile.addInterface({ 634 name: interfaceName, 635 isExported: true, 636 properties, 637 }); 638 } else { 639 // Handle non-object output schemas (like refs) by creating a type alias 640 const outputType = convertLexiconTypeToTypeScript( 641 defValue.output.schema, 642 lexicon.id, 643 undefined, 644 lexicons 645 ); 646 647 sourceFile.addTypeAlias({ 648 name: interfaceName, 649 isExported: true, 650 type: outputType, 651 }); 652 } 653 } 654} 655 656export function generateInterfaces( 657 sourceFile: SourceFile, 658 lexicons: Lexicon[] 659): void { 660 // Base interfaces are imported from @slices/client, only add custom interfaces 661 addBaseInterfaces(sourceFile); 662 663 // Generate type aliases for string fields with knownValues 664 generateKnownValuesTypes(sourceFile, lexicons); 665 666 // First pass: Generate all individual definition interfaces/types 667 for (const lexicon of lexicons) { 668 if (lexicon.definitions && typeof lexicon.definitions === "object") { 669 for (const [defKey, defValue] of Object.entries(lexicon.definitions)) { 670 switch (defValue.type) { 671 case "record": 672 if (defValue.record) { 673 generateRecordInterface( 674 sourceFile, 675 lexicon, 676 defKey, 677 defValue, 678 lexicons 679 ); 680 } 681 break; 682 case "object": 683 generateObjectInterface( 684 sourceFile, 685 lexicon, 686 defKey, 687 defValue, 688 lexicons 689 ); 690 break; 691 case "union": 692 generateUnionType(sourceFile, lexicon, defKey, defValue, lexicons); 693 break; 694 case "array": 695 generateArrayType(sourceFile, lexicon, defKey, defValue, lexicons); 696 break; 697 case "token": 698 generateTokenType(sourceFile, lexicon, defKey); 699 break; 700 case "query": 701 generateQueryProcedureInterfaces( 702 sourceFile, 703 lexicon, 704 defValue, 705 lexicons, 706 "query" 707 ); 708 break; 709 case "procedure": 710 generateQueryProcedureInterfaces( 711 sourceFile, 712 lexicon, 713 defValue, 714 lexicons, 715 "procedure" 716 ); 717 break; 718 } 719 } 720 } 721 } 722 723 // Second pass: Generate namespace interfaces for lexicons with multiple definitions 724 addLexiconNamespaces(sourceFile, lexicons); 725}