v0.3.0 d226234

generate-types.js RawHistoryFolder

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();

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close