1#!/usr/bin/env node
2
3import { execSync } from 'child_process';
4import { writeFileSync, readFileSync, existsSync, statSync, mkdirSync } from 'fs';
5import { join, dirname } from 'path';
6
7/**
8 * Generate TypeScript types from Go DTOs by parsing Go struct definitions
9 * Watches for changes in Go files and regenerates automatically
10 */
11
12const GO_MODELS_PATH = 'internal/db/';
13const GO_PARSER_PATH = 'internal/parser/';
14const TS_OUTPUT_PATH = 'frontend/src/types/generated.ts';
15
16// Helper function to convert Go types to TypeScript
17function convertGoTypeToTS(goType) {
18 const cleanType = goType.trim().replace(/^\*/, ''); // Remove pointer indicator
19
20 // Handle Go slices: []Type -> Type[]
21 if (cleanType.startsWith('[]')) {
22 const elementType = cleanType.slice(2); // Remove []
23 return convertGoTypeToTS(elementType) + '[]';
24 }
25
26 // Handle sql.Null types
27 if (cleanType.includes('sql.NullString')) return 'string | null';
28 if (cleanType.includes('sql.NullInt64')) return 'number | null';
29 if (cleanType.includes('sql.NullTime')) return 'string | null';
30 if (cleanType.includes('sql.NullBool')) return 'boolean | null';
31 if (cleanType.includes('sql.NullFloat64')) return 'number | null';
32
33 // Handle Go package prefixes
34 if (cleanType.includes('*db.History')) return 'History | null';
35 if (cleanType.includes('db.History')) return 'History';
36
37 if (cleanType.includes('time.Time')) return 'string'; // ISO date strings
38 if (cleanType.includes('time.Duration')) return 'number'; // Nanoseconds
39 if (cleanType.includes('map[string]interface{}')) return 'Record<string, unknown>';
40 if (cleanType.includes('map[string]string')) return 'Record<string, string>';
41 if (cleanType.includes('map[string]any')) return 'Record<string, unknown>';
42 if (cleanType.includes('string')) return 'string';
43 if (cleanType.includes('int') || cleanType.includes('float')) return 'number';
44 if (cleanType.includes('bool')) return 'boolean';
45 if (cleanType.match(/^[A-Z][a-zA-Z0-9]*$/)) return cleanType; // Custom struct types
46
47 return 'unknown'; // Fallback
48}
49
50// Parse Go struct to extract JSON field names and types
51function parseGoStruct(goCode, structName) {
52 // More sophisticated regex to handle nested braces in type definitions
53 const structStart = new RegExp(`type\\s+${structName}\\s+struct\\s*{`, 's');
54 const startMatch = goCode.match(structStart);
55
56 if (!startMatch) return null;
57
58 // Find the matching closing brace by counting braces
59 const startIndex = startMatch.index + startMatch[0].length;
60 let braceCount = 1;
61 let endIndex = startIndex;
62
63 for (let i = startIndex; i < goCode.length && braceCount > 0; i++) {
64 if (goCode[i] === '{') braceCount++;
65 if (goCode[i] === '}') braceCount--;
66 endIndex = i;
67 }
68
69 if (braceCount !== 0) return null; // Unmatched braces
70
71 const structContent = goCode.substring(startIndex, endIndex);
72 const match = [null, structContent]; // Simulate regex match format
73
74 if (!match) return null;
75
76 // First, reconstruct the struct content by merging lines that belong together
77 const rawContent = match[1];
78 const lines = rawContent.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('//'));
79
80 // Merge multi-line field definitions
81 const mergedFields = [];
82 let currentField = '';
83
84 for (const line of lines) {
85 // If line starts with a field name (capital letter), it's a new field
86 if (/^\w+\s+/.test(line)) {
87 // Save previous field if exists
88 if (currentField) {
89 mergedFields.push(currentField);
90 }
91 currentField = line;
92 } else {
93 // This line continues the previous field
94 currentField += ' ' + line;
95 }
96 }
97
98 // Don't forget the last field
99 if (currentField) {
100 mergedFields.push(currentField);
101 }
102
103 const fields = mergedFields.map(fieldLine => {
104 // Check for embedded struct (no field name, just type)
105 const embeddedMatch = fieldLine.match(/^([A-Z][a-zA-Z0-9]*)\s*$/);
106 if (embeddedMatch) {
107 const embeddedType = embeddedMatch[1];
108 // Return a special marker for embedded structs
109 return {
110 embedded: true,
111 type: embeddedType
112 };
113 }
114
115 // Parse: FieldName type `json:"field_name,omitempty"`
116 const fieldMatch = fieldLine.match(/^(\w+)\s+([^`]+?)\s*`json:"([^"]+)".*?`/);
117 if (!fieldMatch) return null;
118
119 const [, fieldName, goType, jsonTag] = fieldMatch;
120 const jsonField = jsonTag.split(',')[0]; // Remove omitempty, etc.
121 const isOptional = jsonTag.includes('omitempty') || jsonTag.includes(',omitempty') || goType.trim().startsWith('*');
122
123 // Convert Go types to TypeScript using helper function
124 const tsType = convertGoTypeToTS(goType.trim());
125
126 return {
127 name: jsonField,
128 type: tsType,
129 optional: isOptional
130 };
131 })
132 .filter(Boolean);
133
134 // Handle embedded structs by resolving their fields
135 const resolvedFields = [];
136 for (const field of fields) {
137 if (field.embedded) {
138 // Find the embedded struct and get its fields
139 const embeddedFields = parseGoStruct(goCode, field.type);
140 if (embeddedFields) {
141 resolvedFields.push(...embeddedFields);
142 }
143 } else {
144 resolvedFields.push(field);
145 }
146 }
147
148 return resolvedFields;
149}
150
151function generateTSInterface(structName, fields) {
152 const interfaceFields = fields.map(field =>
153 ` ${field.name}${field.optional ? '?' : ''}: ${field.type};`
154 ).join('\n');
155
156 return `export interface ${structName} {\n${interfaceFields}\n}`;
157}
158
159// Function to discover all struct names in Go code
160function findAllStructs(goCode) {
161 const structRegex = /type\s+([A-Z][a-zA-Z0-9]*)\s+struct\s*{/g;
162 const structs = [];
163 let match;
164
165 while ((match = structRegex.exec(goCode)) !== null) {
166 structs.push(match[1]);
167 }
168
169 return structs;
170}
171
172// Function to find Go enums (const declarations)
173function findGoEnums(goCode) {
174 const enums = [];
175
176 // Look for InputType specifically
177 if (goCode.includes('type InputType int')) {
178 // Find the const block that contains InputType values
179 const constRegex = /const\s*\(\s*([\s\S]*?)\)/;
180 const matches = goCode.match(constRegex);
181
182 if (matches) {
183 const constBody = matches[1];
184 // Look for any identifier that starts with InputType
185 const lines = constBody.split('\n');
186 const values = [];
187 let foundInputTypeBlock = false;
188
189 for (const line of lines) {
190 const trimmedLine = line.trim();
191
192 // Skip comment lines
193 if (trimmedLine.startsWith('//')) continue;
194
195 // Check if this line contains an InputType identifier
196 const identifierMatch = trimmedLine.match(/^\s*(InputType\w+)/);
197 if (identifierMatch) {
198 values.push(identifierMatch[1]);
199 foundInputTypeBlock = true;
200 } else if (foundInputTypeBlock && trimmedLine === ')') {
201 // End of const block
202 break;
203 }
204 }
205
206 if (values.length > 0) {
207 enums.push({ name: 'InputType', values });
208 }
209 }
210 }
211
212 return enums;
213}
214
215function generateTSEnum(enumName, values) {
216 const enumValues = values.map((value, index) => ` ${value} = ${index}`).join(',\n');
217 return `export enum ${enumName} {\n${enumValues}\n}`;
218}
219
220function generateTypes() {
221 console.log('🔧 Parsing Go structs and generating TypeScript types...');
222
223 const sourceFiles = [
224 { path: GO_MODELS_PATH, files: ['models.go'] },
225 { path: GO_PARSER_PATH, files: ['types.go'] }
226 ];
227
228 let combinedTypes = `// GENERATED FROM GO FILES: SEE THE GENERATOR SCRIPT
229// Auto-generated TypeScript types from Go structs
230// Generated on: ${new Date().toISOString()}
231// Generator script: scripts/generate-types.js
232// Source files: internal/db/models.go, internal/parser/types.go
233// Do not edit manually - run 'node scripts/generate-types.js' to regenerate
234
235`;
236
237 for (const { path, files } of sourceFiles) {
238 for (const filename of files) {
239 const filePath = join(path, filename);
240 if (!existsSync(filePath)) continue;
241
242 const goCode = readFileSync(filePath, 'utf-8');
243 console.log(` 📄 Processing ${filename}...`);
244
245 // Automatically discover all structs in this file
246 const structsInFile = findAllStructs(goCode);
247 const enumsInFile = findGoEnums(goCode);
248
249 // Generate enums first
250 console.log(` Found ${enumsInFile.length} enums in ${filename}`);
251 for (const enumDef of enumsInFile) {
252 const tsEnum = generateTSEnum(enumDef.name, enumDef.values);
253 combinedTypes += tsEnum + '\n\n';
254 console.log(` ✓ Generated enum ${enumDef.name} with values: ${enumDef.values.join(', ')}`);
255 }
256
257 for (const structName of structsInFile) {
258 const fields = parseGoStruct(goCode, structName);
259 if (fields && fields.length > 0) {
260 const tsInterface = generateTSInterface(structName, fields);
261 combinedTypes += tsInterface + '\n\n';
262 console.log(` ✓ Generated ${structName}`);
263 }
264 }
265 }
266 }
267
268 // Add manual types that are hard to auto-generate
269 combinedTypes += `// Manual types and enums
270export enum InputType {
271 InputTypeDirectURL = 0,
272 InputTypeSearchShortcut = 1,
273 InputTypeHistorySearch = 2,
274 InputTypeFallbackSearch = 3
275}
276
277// Type aliases for frontend usage
278export type HistoryEntry = History;
279export type SearchShortcut = Shortcut;
280export type ZoomEntry = ZoomSetting;
281`;
282
283 // Ensure output directory exists
284 mkdirSync(dirname(TS_OUTPUT_PATH), { recursive: true });
285
286 writeFileSync(TS_OUTPUT_PATH, combinedTypes);
287 console.log(`✅ TypeScript types generated at: ${TS_OUTPUT_PATH}`);
288}
289
290// One-time generation
291generateTypes();