A tool for conquest of ATProto lexicons. https://jsr.io/@hotsocket/lexiconqueror
0

Configure Feed

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

lexiconqueror / src / generator / shared.ts
8.0 kB 269 lines
1/* 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 */ 6 7import ts, { factory as f, SyntaxKind } from "typescript"; 8import type { lexicon as lex } from "@hotsocket/atproto-common"; 9 10/* need to work out the flow 11 12if theres a main def, wrap the rest in a namespace, since the default export needs to be the main def 13^> If a main definition exists, it can be referenced without a fragment, just using the NSID. 14 > For references in the $type fields in data objects themselves (eg, records or contents of a union), 15 > this is a "must" (use of a #main suffix is invalid). For example, com.example.record not com.example.record#main. 16 17then we rename defs not starting with _ to start with $, to create that visual differentiation like lexicon refs 18^ com.atproto.label.defs#selfLabels -> AT.com.atproto.label.defs.$selfLabels 19 20also with defs containing an "unknown" type, add a type parameter to the output type, and set the field type T 21 22 */ 23 24export const PRINTER = ts.createPrinter({ 25 newLine: ts.NewLineKind.LineFeed, 26 noEmitHelpers: false, 27 omitTrailingSemicolon: false, 28 removeComments: false, 29}); 30 31export const CONSTRAINT_SERIALIZABLEOBJECT = f.createTypeParameterDeclaration( 32 undefined, 33 "T", 34 f.createTypeReferenceNode("common.types.SerializableObject"), 35 f.createTypeReferenceNode("common.types.SerializableObject"), 36); 37export const CONSTRAINT_SERIALIZABLEPARAMS = f.createTypeParameterDeclaration( 38 undefined, 39 "T", 40 f.createTypeReferenceNode("common.types.SerializableParams"), 41); 42 43export type ImportInfo = { 44 defaultImport?: string; 45 namedImports?: ts.ImportSpecifier[]; 46 nsImport?: string; 47}; 48export function finalizeImports( 49 imports: Record<string, ImportInfo>, 50): ts.Statement[] { 51 const output: ts.Statement[] = []; 52 for ( 53 const [module, { defaultImport, namedImports, nsImport }] of Object.entries(imports) 54 ) { 55 if (nsImport) { 56 output.push( 57 f.createImportDeclaration( 58 undefined, 59 f.createImportClause( 60 undefined, 61 undefined, 62 f.createNamespaceImport(f.createIdentifier(nsImport)), 63 ), 64 f.createStringLiteral(module), 65 ), 66 ); 67 } else { 68 output.push(f.createImportDeclaration( 69 undefined, 70 f.createImportClause( 71 undefined, 72 defaultImport ? f.createIdentifier(defaultImport) : undefined, 73 namedImports ? f.createNamedImports(namedImports) : undefined, 74 ), 75 f.createStringLiteral(module), 76 )); 77 } 78 } 79 return output; 80} 81 82export function addComments<T extends lex.SchemaObject>( 83 def: T, 84 declaration: ts.Statement | ts.Declaration, 85) { 86 const commentParts: string[] = []; 87 if (def.description) commentParts.push(def.description); 88 // if ( 89 // def.type == "query" || def.type == "procedure" || def.type == "subscription" 90 // ) commentParts.push("@" + def.type); 91 commentParts.push(`@lextype ${def.type}`); 92 if (def.type === "string" && "format" in def && (def as lex.String).format) { 93 commentParts.push("@format " + (def as lex.String).format); 94 } 95 if (commentParts.length > 0) { 96 ts.addSyntheticLeadingComment( 97 declaration, 98 ts.SyntaxKind.MultiLineCommentTrivia, 99 "* " + commentParts.join("\n * ") + " ", 100 true, 101 ); 102 } 103} 104export function addNamedImport( 105 imports: Record<string, ImportInfo>, 106 name: string, 107 file: string, 108) { 109 if (!imports[file]) imports[file] = {}; 110 if (!imports[file].namedImports) imports[file].namedImports = []; 111 if (imports[file].namedImports.find((x) => x.name.text == name)) return; 112 imports[file].namedImports.push( 113 f.createImportSpecifier(false, undefined, f.createIdentifier(name)), 114 ); 115} 116 117export function lastNSIDPart(nsid: string): string { 118 return nsid.substring(nsid.lastIndexOf(".") + 1); 119} 120 121export function refToNode( 122 ref: string, 123 lex: lex.Lexicon, 124 imports: Record<string, ImportInfo>, 125) { 126 const fixedRef = ref.replace("#", ".$"); 127 // #something means it's local to "here" 128 if (ref.startsWith("#")) { 129 if (Object.hasOwn(lex.defs, "main")) { 130 //${lastNSIDPart(lex.id)}${fixedRef} 131 return f.createTypeReferenceNode(`${lastNSIDPart(lex.id)}${fixedRef}`); 132 } else { 133 return f.createTypeReferenceNode(fixedRef.replace(".", "")); 134 } 135 } else { 136 imports["@/index.ts"] = { nsImport: "AT" }; 137 return f.createTypeReferenceNode("AT." + fixedRef); 138 } 139} 140 141export function lookupType( 142 object: lex.AnySchemaObject, 143 lex: lex.Lexicon, 144 name: string, 145 imports: Record<string, ImportInfo>, 146): ts.TypeNode { 147 switch (object.type) { 148 case "cid-link": 149 case "bytes": 150 case "token": 151 case "string": 152 return f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); 153 case "blob": 154 imports["@hotsocket/atproto-common"] = { nsImport: "common" }; 155 return f.createTypeReferenceNode("common.types.XBlob"); 156 case "boolean": 157 return f.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword); 158 case "integer": 159 return f.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); 160 case "unknown": 161 return f.createTypeReferenceNode("T"); 162 case "ref": 163 return refToNode(object.ref, lex, imports); 164 case "union": 165 // TODO: Handle union types properly 166 if (object.refs.length == 0) { 167 // Record<PropertyKey, never> 168 return f.createTypeReferenceNode( 169 "Record", 170 [ 171 f.createTypeReferenceNode("PropertyKey"), 172 f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword), 173 ], 174 ); 175 } else { 176 return f.createUnionTypeNode( 177 object.refs.map((ref) => refToNode(ref, lex, imports)), 178 ); 179 } 180 case "array": 181 // TODO: Handle array types properly 182 return f.createArrayTypeNode( 183 lookupType(object.items, lex, name + ">array", imports), 184 ); 185 // objects, params are handled differently 186 default: 187 throw new Error(`unhandled type ${object.type} for ${lex.id}#${name}`); 188 } 189} 190 191export function propsContainUnknown( 192 props: Record<string, lex.AnySchemaObject>, 193): boolean { 194 for (const name in props) { 195 const prop = props[name]!; 196 if (prop.type == "unknown") return true; 197 if (prop.type == "array" && prop.items.type == "unknown") return true; 198 } 199 return false; 200} 201 202export function convertProperty( 203 lex: lex.Lexicon, 204 name: string, 205 def: lex.FieldTypes, 206 imports: Record<string, ImportInfo>, 207): ts.Statement { 208 return f.createTypeAliasDeclaration( 209 [ 210 f.createModifier(ts.SyntaxKind.ExportKeyword), 211 ], 212 name, 213 undefined, 214 lookupType(def, lex, name, imports), 215 ); 216} 217 218export function convertSchemaObject( 219 lex: lex.Lexicon, 220 name: string, 221 def: lex.Object, 222 imports: Record<string, ImportInfo>, 223): ts.Statement | void { 224 const members: ts.TypeElement[] = []; 225 imports["@hotsocket/atproto-common"] = { nsImport: "common" }; 226 // members.push( 227 // `\t[key: string]: ${name == "_parameters" ? "string | number | boolean | null | undefined" : "Serializable"};`, 228 // ); 229 function requiredLookup(propName: string): ts.QuestionToken | undefined { 230 if (def.required && def.required.indexOf(propName) != -1) { 231 return undefined; 232 } else { 233 return f.createToken(ts.SyntaxKind.QuestionToken); 234 } 235 } 236 // detect whether any property contains `unknown` (including arrays of unknown) 237 // so we can add the generic `T` type parameter to the interface when needed. 238 const hasUnknown = propsContainUnknown(def.properties); 239 for (const propName in def.properties) { 240 const prop = def.properties[propName]!; 241 242 const member = f.createPropertySignature( 243 undefined, 244 propName, 245 requiredLookup(propName), 246 lookupType(prop, lex, propName, imports), 247 ); 248 addComments(prop, member); 249 members.push(member); 250 } 251 let inheritID; 252 if (name == "_parameters") { 253 inheritID = f.createIdentifier("common.types.SerializableParams"); 254 } else { 255 inheritID = f.createIdentifier("common.types.SerializableObject"); 256 } 257 return f.createInterfaceDeclaration( 258 [f.createModifier(ts.SyntaxKind.ExportKeyword)], 259 name, 260 hasUnknown ? [CONSTRAINT_SERIALIZABLEOBJECT] : undefined, 261 [f.createHeritageClause(SyntaxKind.ExtendsKeyword, [ 262 f.createExpressionWithTypeArguments( 263 inheritID, 264 undefined, 265 ), 266 ])], 267 members, 268 ); 269}