A personal website powered by Astro and ATProto
1#!/usr/bin/env node
2
3import { readdir, readFile, writeFile } from 'fs/promises';
4import { join } from 'path';
5import { loadConfig } from '../src/lib/config/site';
6
7interface LexiconSchema {
8 lexicon: number;
9 id: string;
10 description?: string;
11 defs: Record<string, any>;
12}
13
14function generateTypeScriptTypes(schema: LexiconSchema): string {
15 const nsid = schema.id;
16 const typeName = nsid.split('.').map(part =>
17 part.charAt(0).toUpperCase() + part.slice(1)
18 ).join('');
19
20 const mainDef = schema.defs.main;
21 if (!mainDef || mainDef.type !== 'record') {
22 throw new Error(`Schema ${nsid} must have a 'main' record definition`);
23 }
24
25 const recordSchema = mainDef.record;
26 const properties = recordSchema.properties || {};
27 const required = recordSchema.required || [];
28
29 // Generate property types
30 const propertyTypes: string[] = [];
31
32 for (const [propName, propSchema] of Object.entries(properties)) {
33 const isRequired = required.includes(propName);
34 const optional = isRequired ? '' : '?';
35
36 let type: string;
37
38 switch (propSchema.type) {
39 case 'string':
40 if (propSchema.enum) {
41 type = propSchema.enum.map((v: string) => `'${v}'`).join(' | ');
42 } else {
43 type = 'string';
44 }
45 break;
46 case 'integer':
47 type = 'number';
48 break;
49 case 'boolean':
50 type = 'boolean';
51 break;
52 case 'array':
53 type = 'any[]'; // Could be more specific based on items schema
54 break;
55 case 'object':
56 type = 'Record<string, any>';
57 break;
58 default:
59 type = 'any';
60 }
61
62 propertyTypes.push(` ${propName}${optional}: ${type};`);
63 }
64
65 return `// Generated from lexicon schema: ${nsid}
66// Do not edit manually - regenerate with: npm run gen:types
67
68export interface ${typeName}Record {
69${propertyTypes.join('\n')}
70}
71
72export interface ${typeName} {
73 $type: '${nsid}';
74 value: ${typeName}Record;
75}
76
77// Helper type for discriminated unions
78export type ${typeName}Union = ${typeName};
79`;
80}
81
82async function generateTypes() {
83 const config = loadConfig();
84 const lexiconsDir = join(process.cwd(), 'src/lexicons');
85 const generatedDir = join(process.cwd(), 'src/lib/generated');
86
87 console.log('🔍 Scanning for lexicon schemas...');
88
89 try {
90 const files = await readdir(lexiconsDir);
91 const jsonFiles = files.filter(file => file.endsWith('.json'));
92
93 if (jsonFiles.length === 0) {
94 console.log('No lexicon schema files found in src/lexicons/');
95 return;
96 }
97
98 console.log(`Found ${jsonFiles.length} lexicon schema(s):`);
99
100 const generatedTypes: string[] = [];
101 const unionTypes: string[] = [];
102
103 for (const file of jsonFiles) {
104 const schemaPath = join(lexiconsDir, file);
105 const schemaContent = await readFile(schemaPath, 'utf-8');
106 const schema: LexiconSchema = JSON.parse(schemaContent);
107
108 console.log(` - ${schema.id} (${file})`);
109
110 try {
111 const typesCode = generateTypeScriptTypes(schema);
112 const outputPath = join(generatedDir, `${schema.id.replace(/\./g, '-')}.ts`);
113
114 await writeFile(outputPath, typesCode, 'utf-8');
115 console.log(` ✅ Generated types: ${outputPath}`);
116
117 // Add to union types
118 const typeName = schema.id.split('.').map(part =>
119 part.charAt(0).toUpperCase() + part.slice(1)
120 ).join('');
121 unionTypes.push(typeName);
122 generatedTypes.push(`import type { ${typeName} } from './${schema.id.replace(/\./g, '-')}';`);
123
124 } catch (error) {
125 console.error(` ❌ Failed to generate types for ${schema.id}:`, error);
126 }
127 }
128
129 // Generate index file with union types
130 if (generatedTypes.length > 0) {
131 const indexContent = `// Generated index of all lexicon types
132// Do not edit manually - regenerate with: npm run gen:types
133
134${generatedTypes.join('\n')}
135
136// Union type for all generated lexicon records
137export type GeneratedLexiconUnion = ${unionTypes.join(' | ')};
138
139// Type map for component registry
140export type GeneratedLexiconTypeMap = {
141${unionTypes.map(type => ` '${type}': ${type};`).join('\n')}
142};
143`;
144
145 const indexPath = join(generatedDir, 'lexicon-types.ts');
146 await writeFile(indexPath, indexContent, 'utf-8');
147 console.log(` ✅ Generated index: ${indexPath}`);
148 }
149
150 console.log('\n🎉 Type generation complete!');
151 console.log('\nNext steps:');
152 console.log('1. Import the generated types in your components');
153 console.log('2. Update the component registry with the new types');
154 console.log('3. Create components that use the strongly typed records');
155
156 } catch (error) {
157 console.error('Error generating types:', error);
158 process.exit(1);
159 }
160}
161
162generateTypes();