import 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 type function 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"; default: return "unknown"; } } // Resolve lexicon type references function 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 definitions function 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", }); } else { sourceFile.addInterface({ name: interfaceName, isExported: true, properties: properties, }); } } // Generate interface for record definitions function 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", }); } 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 definitions function 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 definitions function 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 definitions function 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 knownValues function 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)`; 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)`; 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)`; sourceFile.addTypeAlias({ name: typeName, isExported: true, type: typeDefinition, leadingTrivia: "\n", }); } } } } } } // Generate namespace interfaces for lexicons with multiple definitions function 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 interfaces function 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/output function 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); }