forked from
slices.network/slices
fork
Configure Feed
Select the types of activity you want to include in your feed.
Highly ambitious ATProtocol AppView service and sdks
fork
Configure Feed
Select the types of activity you want to include in your feed.
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}