Something went wrong. Try again.
Highly ambitious ATProtocol AppView service and sdks forked from slices.network/slices
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726import type { SourceFile } from "ts-morph";import type { Lexicon, LexiconDefinition, LexiconProperty } from "./mod.ts";import { nsidToPascalCase, nsidToNamespace, defNameToPascalCase, isPropertyRequired, isFieldSortable,} from "./mod.ts";
// Convert lexicon type to TypeScript typefunction convertLexiconTypeToTypeScript( def: LexiconDefinition | LexiconProperty, currentLexicon: string, propertyName?: string, lexicons?: Lexicon[]): string { const type = def.type; switch (type) { case "string": // For knownValues, return the type alias name if ( def.knownValues && Array.isArray(def.knownValues) && def.knownValues.length > 0 && propertyName ) { // Reference the generated type alias with namespace const namespace = nsidToNamespace(currentLexicon); return `${namespace}${defNameToPascalCase(propertyName)}`; } return "string"; case "integer": return "number"; case "boolean": return "boolean"; case "bytes": return "string"; // Base64 encoded case "cid-link": return "{ $link: string }"; case "blob": return "BlobRef"; case "unknown": return "unknown"; case "ref": return resolveTypeReference(def.ref!, currentLexicon, lexicons); case "array": if (def.items) { const itemType = convertLexiconTypeToTypeScript( def.items, currentLexicon, undefined, lexicons ); return `${itemType}[]`; } return "any[]"; case "union": if (def.refs && def.refs.length > 0) { const unionTypes = def.refs.map((ref: string) => resolveTypeReference(ref, currentLexicon, lexicons) ); // Add unknown type for open unions if (!def.closed) { unionTypes.push("{ $type: string; [key: string]: unknown }"); } return unionTypes.join(" | "); } return "unknown"; case "object": return "Record<string, unknown>"; default: return "unknown"; }}
// Resolve lexicon type referencesfunction resolveTypeReference( ref: string, currentLexicon: string, lexicons?: Lexicon[]): string { if (ref.startsWith("#")) { // Local reference: #aspectRatio -> CurrentLexiconAspectRatio const defName = ref.slice(1); const namespace = nsidToNamespace(currentLexicon); return `${namespace}["${defNameToPascalCase(defName)}"]`; } else if (ref.includes("#")) { // Cross-lexicon reference: app.bsky.embed.defs#aspectRatio -> AppBskyEmbedDefs["AspectRatio"] const [nsid, defName] = ref.split("#");
if (defName === "main") { // Find the lexicon and check if it has multiple definitions const lexicon = lexicons?.find((lex) => lex.id === nsid); if (lexicon && lexicon.definitions) { const defCount = Object.keys(lexicon.definitions).length; const mainDef = lexicon.definitions.main;
if (defCount === 1 && mainDef) { // Single definition - use clean name if (mainDef.type === "record") { // For records: AppBskyActorProfile return nsidToPascalCase(nsid); } else { // For objects: ComAtprotoRepoStrongRef return nsidToNamespace(nsid); } } else { // Multiple definitions - use namespace pattern const namespace = nsidToNamespace(nsid); return `${namespace}["Main"]`; } } // Fallback return nsidToNamespace(nsid); }
const namespace = nsidToNamespace(nsid); return `${namespace}["${defNameToPascalCase(defName)}"]`; } else { // Direct lexicon reference: check if single or multiple definitions const lexicon = lexicons?.find((lex) => lex.id === ref); if (lexicon && lexicon.definitions) { const defCount = Object.keys(lexicon.definitions).length; const mainDef = lexicon.definitions.main;
if (defCount === 1 && mainDef) { // Single definition - use clean name if (mainDef.type === "record") { // For records: AppBskyActorProfile return nsidToPascalCase(ref); } else { // For objects: ComAtprotoRepoStrongRef return nsidToNamespace(ref); } } else if (mainDef) { // Multiple definitions - use namespace pattern const namespace = nsidToNamespace(ref); return `${namespace}["Main"]`; } } // Fallback return nsidToNamespace(ref); }}
// Generate interface for object definitionsfunction generateObjectInterface( sourceFile: SourceFile, lexicon: Lexicon, defKey: string, defValue: LexiconDefinition, lexicons: Lexicon[]): void { const namespace = nsidToNamespace(lexicon.id);
// For single-definition lexicons with main, use clean name const defCount = Object.keys(lexicon.definitions || {}).length; const interfaceName = defKey === "main" && defCount === 1 ? namespace // Clean name: ComAtprotoRepoStrongRef : `${namespace}${defNameToPascalCase(defKey)}`; // Multi-def: AppBskyRichtextFacetMention
const properties: Array<{ name: string; type: string; hasQuestionToken: boolean; docs?: string[]; }> = [];
if (defValue.properties) { for (const [propName, propDef] of Object.entries(defValue.properties)) { const tsType = convertLexiconTypeToTypeScript( propDef, lexicon.id, propName, lexicons ); const required = defValue.required && defValue.required.includes(propName);
properties.push({ name: propName, type: tsType, hasQuestionToken: !required, docs: (propDef as LexiconProperty).description ? [(propDef as LexiconProperty).description!] : undefined, }); } }
if (properties.length === 0) { sourceFile.addTypeAlias({ name: interfaceName, isExported: true, type: "Record<string, never>", }); } else { sourceFile.addInterface({ name: interfaceName, isExported: true, properties: properties, }); }}
// Generate interface for record definitionsfunction generateRecordInterface( sourceFile: SourceFile, lexicon: Lexicon, defKey: string, defValue: LexiconDefinition, lexicons: Lexicon[]): void { const recordDef = defValue.record; if (!recordDef) return;
const properties: Array<{ name: string; type: string; hasQuestionToken: boolean; docs?: string[]; }> = []; const fieldNames: string[] = [];
if (recordDef.properties) { for (const [propName, propDef] of Object.entries(recordDef.properties)) { const tsType = convertLexiconTypeToTypeScript( propDef, lexicon.id, propName, lexicons ); const required = isPropertyRequired(recordDef, propName);
properties.push({ name: propName, type: tsType, hasQuestionToken: !required, docs: (propDef as LexiconProperty).description ? [(propDef as LexiconProperty).description!] : undefined, });
// Collect sortable field names for sort type if (isFieldSortable(propDef)) { fieldNames.push(propName); } } }
// For main records, use clean naming without Record suffix const interfaceName = defKey === "main" ? nsidToPascalCase(lexicon.id) : `${nsidToNamespace(lexicon.id)}${defNameToPascalCase(defKey)}`;
if (properties.length === 0) { sourceFile.addTypeAlias({ name: interfaceName, isExported: true, type: "Record<string, never>", }); } else { sourceFile.addInterface({ name: interfaceName, isExported: true, properties: properties, }); }
// Generate sort fields type union for records (only if there are sortable fields) if (fieldNames.length > 0) { sourceFile.addTypeAlias({ name: `${interfaceName}SortFields`, isExported: true, type: fieldNames.map((f) => `"${f}"`).join(" | "), }); }}
// Generate type alias for union definitionsfunction generateUnionType( sourceFile: SourceFile, lexicon: Lexicon, defKey: string, defValue: LexiconDefinition, lexicons: Lexicon[]): void { const namespace = nsidToNamespace(lexicon.id); const typeName = `${namespace}${defNameToPascalCase(defKey)}`;
if (defValue.refs && defValue.refs.length > 0) { const unionTypes = defValue.refs.map((ref: string) => resolveTypeReference(ref, lexicon.id, lexicons) );
// Add unknown type for open unions if (!defValue.closed) { unionTypes.push("{ $type: string; [key: string]: unknown }"); }
sourceFile.addTypeAlias({ name: typeName, isExported: true, type: unionTypes.join(" | "), }); }}
// Generate type alias for array definitionsfunction generateArrayType( sourceFile: SourceFile, lexicon: Lexicon, defKey: string, defValue: LexiconDefinition, lexicons: Lexicon[]): void { const namespace = nsidToNamespace(lexicon.id); const typeName = `${namespace}${defNameToPascalCase(defKey)}`;
if (defValue.items) { const itemType = convertLexiconTypeToTypeScript( defValue.items, lexicon.id, undefined, lexicons ); sourceFile.addTypeAlias({ name: typeName, isExported: true, type: `${itemType}[]`, }); }}
// Generate string literal type for token definitionsfunction generateTokenType( sourceFile: SourceFile, lexicon: Lexicon, defKey: string): void { const namespace = nsidToNamespace(lexicon.id); const typeName = `${namespace}${defNameToPascalCase(defKey)}`;
sourceFile.addTypeAlias({ name: typeName, isExported: true, type: `"${lexicon.id}${defKey === "main" ? "" : `#${defKey}`}"`, });}
// Generate type aliases for string fields with knownValuesfunction generateKnownValuesTypes( sourceFile: SourceFile, lexicons: Lexicon[]): void { for (const lexicon of lexicons) { if (lexicon.definitions && typeof lexicon.definitions === "object") { for (const [defKey, defValue] of Object.entries(lexicon.definitions)) { if (defValue.type === "record" && defValue.record?.properties) { for (const [propName, propDef] of Object.entries( defValue.record.properties )) { const prop = propDef as LexiconProperty; if ( prop.type === "string" && prop.knownValues && Array.isArray(prop.knownValues) && prop.knownValues.length > 0 ) { // Generate a type alias for this property, namespaced by lexicon const namespace = nsidToNamespace(lexicon.id); const pascalPropName = defNameToPascalCase(propName); const typeName = `${namespace}${pascalPropName}`;
const knownValueTypes = prop.knownValues .map((value: string) => `'${value}'`) .join("\n | "); const typeDefinition = `${knownValueTypes}\n | (string & Record<string, never>)`;
sourceFile.addTypeAlias({ name: typeName, isExported: true, type: typeDefinition, leadingTrivia: "\n", }); } } } else if (defValue.type === "object" && defValue.properties) { for (const [propName, propDef] of Object.entries( defValue.properties )) { const prop = propDef as LexiconProperty; if ( prop.type === "string" && prop.knownValues && Array.isArray(prop.knownValues) && prop.knownValues.length > 0 ) { // Generate a type alias for this property, namespaced by lexicon const namespace = nsidToNamespace(lexicon.id); const pascalPropName = defNameToPascalCase(propName); const typeName = `${namespace}${pascalPropName}`;
const knownValueTypes = prop.knownValues .map((value: string) => `'${value}'`) .join("\n | "); const typeDefinition = `${knownValueTypes}\n | (string & Record<string, never>)`;
sourceFile.addTypeAlias({ name: typeName, isExported: true, type: typeDefinition, leadingTrivia: "\n", }); } } } else if (defValue.type === "string") { // Handle standalone string definitions with knownValues (like labelValue) const stringDef = defValue as LexiconDefinition; if ( stringDef.knownValues && Array.isArray(stringDef.knownValues) && stringDef.knownValues.length > 0 ) { // Generate a type alias for this definition, namespaced by lexicon const namespace = nsidToNamespace(lexicon.id); const typeName = `${namespace}${defNameToPascalCase(defKey)}`;
const knownValueTypes = stringDef.knownValues .map((value: string) => `'${value}'`) .join("\n | "); const typeDefinition = `${knownValueTypes}\n | (string & Record<string, never>)`;
sourceFile.addTypeAlias({ name: typeName, isExported: true, type: typeDefinition, leadingTrivia: "\n", }); } } } } }}
// Generate namespace interfaces for lexicons with multiple definitionsfunction addLexiconNamespaces( sourceFile: SourceFile, lexicons: Lexicon[]): void { for (const lexicon of lexicons) { if (!lexicon.definitions || typeof lexicon.definitions !== "object") { continue; }
const definitions = Object.keys(lexicon.definitions);
// Skip lexicons with only a main definition (they get traditional Record interfaces) if (definitions.length === 1 && definitions[0] === "main") { continue; }
const namespace = nsidToNamespace(lexicon.id); const namespaceProperties: Array<{ name: string; type: string; isReadonly: boolean; }> = [];
// Add properties for each definition for (const [defKey, defValue] of Object.entries(lexicon.definitions)) { const defName = defNameToPascalCase(defKey);
switch (defValue.type) { case "object": { namespaceProperties.push({ name: defName, type: `${namespace}${defName}`, isReadonly: true, }); break; } case "string": { // Check if this is a string type with knownValues const stringDef = defValue as LexiconDefinition; if ( stringDef.knownValues && Array.isArray(stringDef.knownValues) && stringDef.knownValues.length > 0 ) { // This generates a type alias, reference it in the namespace with full name namespaceProperties.push({ name: defName, type: `${namespace}${defNameToPascalCase(defKey)}`, isReadonly: true, }); } break; } case "union": case "array": case "token": // These generate type aliases, so we reference the type itself namespaceProperties.push({ name: defName, type: `${namespace}${defName}`, isReadonly: true, }); break; case "record": // Records get their own interfaces, reference them if (defKey === "main") { namespaceProperties.push({ name: "Main", type: nsidToPascalCase(lexicon.id), isReadonly: true, }); } else { namespaceProperties.push({ name: defName, type: `${namespace}${defName}`, isReadonly: true, }); } break; } }
// Only create namespace if we have properties if (namespaceProperties.length > 0) { sourceFile.addInterface({ name: namespace, isExported: true, properties: namespaceProperties.map((prop) => ({ name: prop.name, type: prop.type, isReadonly: prop.isReadonly, })), }); } }}
// Base interfaces are imported from @slices/client, only add network.slices specific interfacesfunction addBaseInterfaces( _sourceFile: SourceFile): void { // All interfaces are now generated from lexicons // This function is kept for future extensibility if needed}
// Generate interfaces for query and procedure parameters/input/outputfunction generateQueryProcedureInterfaces( sourceFile: SourceFile, lexicon: Lexicon, defValue: LexiconDefinition, lexicons: Lexicon[], type: "query" | "procedure"): void { const baseName = nsidToPascalCase(lexicon.id);
// Generate parameters interface if present and has properties if (defValue.parameters?.properties && Object.keys(defValue.parameters.properties).length > 0) { const interfaceName = `${baseName}Params`; const properties = Object.entries(defValue.parameters.properties).map( ([propName, propDef]) => ({ name: propName, type: convertLexiconTypeToTypeScript( propDef, lexicon.id, propName, lexicons ), hasQuestionToken: !(defValue.parameters?.required || []).includes(propName), }) );
sourceFile.addInterface({ name: interfaceName, isExported: true, properties, }); }
// Generate input interface for procedures if (type === "procedure" && defValue.input?.schema) { const interfaceName = `${baseName}Input`;
if (defValue.input?.schema?.type === "object" && defValue.input.schema.properties) { const properties = Object.entries(defValue.input.schema.properties).map( ([propName, propDef]) => ({ name: propName, type: convertLexiconTypeToTypeScript( propDef, lexicon.id, propName, lexicons ), hasQuestionToken: !((defValue.input?.schema?.required as string[]) || []).includes(propName), }) );
sourceFile.addInterface({ name: interfaceName, isExported: true, properties, }); } }
// Generate output interface if present if (defValue.output?.schema) { const interfaceName = `${baseName}Output`;
if (defValue.output?.schema?.type === "object" && defValue.output.schema.properties) { const properties = Object.entries(defValue.output.schema.properties).map( ([propName, propDef]) => ({ name: propName, type: convertLexiconTypeToTypeScript( propDef, lexicon.id, propName, lexicons ), hasQuestionToken: !((defValue.output?.schema?.required as string[]) || []).includes(propName), }) );
sourceFile.addInterface({ name: interfaceName, isExported: true, properties, }); } else { // Handle non-object output schemas (like refs) by creating a type alias const outputType = convertLexiconTypeToTypeScript( defValue.output.schema, lexicon.id, undefined, lexicons );
sourceFile.addTypeAlias({ name: interfaceName, isExported: true, type: outputType, }); } }}
export function generateInterfaces( sourceFile: SourceFile, lexicons: Lexicon[]): void { // Base interfaces are imported from @slices/client, only add custom interfaces addBaseInterfaces(sourceFile);
// Generate type aliases for string fields with knownValues generateKnownValuesTypes(sourceFile, lexicons);
// First pass: Generate all individual definition interfaces/types for (const lexicon of lexicons) { if (lexicon.definitions && typeof lexicon.definitions === "object") { for (const [defKey, defValue] of Object.entries(lexicon.definitions)) { switch (defValue.type) { case "record": if (defValue.record) { generateRecordInterface( sourceFile, lexicon, defKey, defValue, lexicons ); } break; case "object": generateObjectInterface( sourceFile, lexicon, defKey, defValue, lexicons ); break; case "union": generateUnionType(sourceFile, lexicon, defKey, defValue, lexicons); break; case "array": generateArrayType(sourceFile, lexicon, defKey, defValue, lexicons); break; case "token": generateTokenType(sourceFile, lexicon, defKey); break; case "query": generateQueryProcedureInterfaces( sourceFile, lexicon, defValue, lexicons, "query" ); break; case "procedure": generateQueryProcedureInterfaces( sourceFile, lexicon, defValue, lexicons, "procedure" ); break; } } } }
// Second pass: Generate namespace interfaces for lexicons with multiple definitions addLexiconNamespaces(sourceFile, lexicons);}