@dinoreic/fez 0.4.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/refactor ADDED
@@ -0,0 +1,699 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Fez Refactor Tool
4
+ *
5
+ * Comprehensive code quality and modernization checks for Fez components.
6
+ *
7
+ * Usage:
8
+ * bun run refactor # Report issues only
9
+ * bun run refactor --fix # Auto-fix issues
10
+ * bun run refactor --check # Same as default (report only)
11
+ * bun run refactor --help # Show help
12
+ *
13
+ * Features:
14
+ * 1. Legacy syntax migration ({{ }} → { })
15
+ * 2. Code modernization (var → const/let, etc.)
16
+ * 3. Performance audit (FAST flags, state nesting, etc.)
17
+ * 4. Import cleanup (unused imports, sorting, etc.)
18
+ */
19
+
20
+ import { readFileSync, writeFileSync, statSync } from 'fs'
21
+ import { join, relative } from 'path'
22
+
23
+ // =============================================================================
24
+ // CONFIGURATION
25
+ // =============================================================================
26
+
27
+ const ROOT_DIR = process.cwd()
28
+ const FIX_MODE = process.argv.includes('--fix')
29
+ const CHECK_MODE = process.argv.includes('--check') || !FIX_MODE
30
+ const SHOW_HELP = process.argv.includes('--help') || process.argv.includes('-h')
31
+
32
+ const SCAN_DIRS = [
33
+ 'demo/fez',
34
+ 'src',
35
+ 'test'
36
+ ]
37
+
38
+ const FEZ_GLOB = '**/*.fez'
39
+ const JS_GLOB = '**/*.{js,ts}'
40
+
41
+ // Legacy syntax patterns
42
+ const LEGACY_PATTERNS = [
43
+ { regex: /\{\{\s*(.*?)\s*\}\}/g, replacement: '{$1}', name: 'expression' },
44
+ { regex: /\{\{#?if\s+(.*?)\}\}/g, replacement: '{#if $1}', name: 'conditional' },
45
+ { regex: /\{\{\/if\}\}/g, replacement: '{/if}', name: 'conditional close' },
46
+ { regex: /\{\{#?unless\s+(.*?)\}\}/g, replacement: '{#unless $1}', name: 'unless' },
47
+ { regex: /\{\{\/unless\}\}/g, replacement: '{/unless}', name: 'unless close' },
48
+ { regex: /\{\{:?else\s+if\s+(.*?)\}\}/g, replacement: '{:else if $1}', name: 'else if' },
49
+ { regex: /\{\{:?elsif\s+(.*?)\}\}/g, replacement: '{:else if $1}', name: 'elsif' },
50
+ { regex: /\{\{:?elseif\s+(.*?)\}\}/g, replacement: '{:else if $1}', name: 'elseif' },
51
+ { regex: /\{\{:?else\}\}/g, replacement: '{:else}', name: 'else' },
52
+ { regex: /\{\{#?for\s+(.*?)\}\}/g, replacement: '{#for $1}', name: 'for loop' },
53
+ { regex: /\{\{\/for\}\}/g, replacement: '{/for}', name: 'for close' },
54
+ { regex: /\{\{#?each\s+(.*?)\}\}/g, replacement: '{#each $1}', name: 'each loop' },
55
+ { regex: /\{\{\/each\}\}/g, replacement: '{/each}', name: 'each close' },
56
+ { regex: /\{\{#?(?:raw|html)\s+(.*?)\}\}/g, replacement: '{@html $1}', name: 'raw html' },
57
+ { regex: /\{\{json\s+(.*?)\}\}/g, replacement: '{@json $1}', name: 'json' },
58
+ { regex: /\{\{block\s+(\w+)\s*\}\}/g, replacement: '{@block $1}', name: 'block' },
59
+ { regex: /\{\{\/block\}\}/g, replacement: '{/block}', name: 'block close' },
60
+ ]
61
+
62
+ // =============================================================================
63
+ // UTILITY FUNCTIONS
64
+ // =============================================================================
65
+
66
+ function glob(pattern, dirs) {
67
+ const { globSync } = require('glob')
68
+ const files = []
69
+ for (const dir of dirs) {
70
+ const fullPattern = join(ROOT_DIR, dir, pattern)
71
+ try {
72
+ files.push(...globSync(fullPattern))
73
+ } catch (e) {
74
+ // Directory doesn't exist or no matches
75
+ }
76
+ }
77
+ return files
78
+ }
79
+
80
+ function relativePath(filepath) {
81
+ return relative(ROOT_DIR, filepath)
82
+ }
83
+
84
+ function read(filepath) {
85
+ return readFileSync(filepath, 'utf-8')
86
+ }
87
+
88
+ function write(filepath, content) {
89
+ writeFileSync(filepath, content, 'utf-8')
90
+ }
91
+
92
+ function fileExists(filepath) {
93
+ try {
94
+ statSync(filepath)
95
+ return true
96
+ } catch {
97
+ return false
98
+ }
99
+ }
100
+
101
+ // =============================================================================
102
+ // REFACTOR 1: LEGACY SYNTAX MIGRATION
103
+ // =============================================================================
104
+
105
+ function checkLegacySyntax(files) {
106
+ const issues = []
107
+
108
+ for (const file of files.filter(f => f.endsWith('.fez'))) {
109
+ const content = read(file)
110
+ const lines = content.split('\n')
111
+
112
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
113
+ const line = lines[lineNum]
114
+
115
+ for (const pattern of LEGACY_PATTERNS) {
116
+ pattern.regex.lastIndex = 0 // Reset regex state
117
+ const match = pattern.regex.exec(line)
118
+
119
+ if (match) {
120
+ issues.push({
121
+ file,
122
+ line: lineNum + 1,
123
+ type: 'legacy-syntax',
124
+ category: '1. Legacy Syntax Migration',
125
+ message: `Old {{ }} syntax for ${pattern.name}`,
126
+ suggestion: `Use ${pattern.replacement.replace('$1', '...')} instead`,
127
+ current: match[0],
128
+ fixable: true,
129
+ })
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ return issues
136
+ }
137
+
138
+ function fixLegacySyntax(content) {
139
+ let fixed = content
140
+ let changed = false
141
+
142
+ for (const pattern of LEGACY_PATTERNS) {
143
+ if (pattern.regex.test(fixed)) {
144
+ fixed = fixed.replace(pattern.regex, pattern.replacement)
145
+ changed = true
146
+ }
147
+ pattern.regex.lastIndex = 0 // Reset for next iteration
148
+ }
149
+
150
+ return { content: fixed, changed }
151
+ }
152
+
153
+ // =============================================================================
154
+ // REFACTOR 2: CODE MODERNIZATION
155
+ // =============================================================================
156
+
157
+ function checkCodeModernization(files) {
158
+ const issues = []
159
+
160
+ for (const file of files.filter(f => /\.(js|fez)$/.test(f))) {
161
+ const content = read(file)
162
+ const lines = content.split('\n')
163
+
164
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
165
+ const line = lines[lineNum]
166
+ const trimmed = line.trim()
167
+
168
+ // Skip comments and strings
169
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue
170
+
171
+ // Check for var usage
172
+ if (/\bvar\s+\w+/.test(line)) {
173
+ issues.push({
174
+ file,
175
+ line: lineNum + 1,
176
+ type: 'var-declaration',
177
+ category: '2. Code Modernization',
178
+ message: 'Using var instead of const/let',
179
+ suggestion: 'Replace with const (or let if reassigned)',
180
+ current: line.match(/\bvar\s+\w+/)?.[0] || 'var',
181
+ fixable: true,
182
+ })
183
+ }
184
+
185
+ // Check for function() {} that could be arrow functions
186
+ if (/\w+\s*=\s*function\s*\(/.test(line) && !line.includes('this')) {
187
+ // Only flag if 'this' isn't used in the function body
188
+ const funcMatch = line.match(/function\s*\(([^)]*)\)\s*\{?/)
189
+ if (funcMatch && !content.slice(lineNum).slice(0, 50).includes('this.')) {
190
+ issues.push({
191
+ file,
192
+ line: lineNum + 1,
193
+ type: 'function-expression',
194
+ category: '2. Code Modernization',
195
+ message: 'Using function() instead of arrow function',
196
+ suggestion: 'Convert to arrow function: (params) => {}',
197
+ current: 'function() {}',
198
+ fixable: false, // Manual review needed
199
+ })
200
+ }
201
+ }
202
+
203
+ // Check for string concatenation that could be template literals
204
+ if (/\w+\s*=\s*['"`].*\+\s*\w+/.test(line) && !line.includes('`')) {
205
+ issues.push({
206
+ file,
207
+ line: lineNum + 1,
208
+ type: 'string-concat',
209
+ category: '2. Code Modernization',
210
+ message: 'String concatenation instead of template literal',
211
+ suggestion: 'Use template literal: `string ${var}`',
212
+ current: '"..." + var',
213
+ fixable: false, // Complex to auto-fix safely
214
+ })
215
+ }
216
+
217
+ // Check for .then() chains that could be async/await
218
+ if (/\.then\(/.test(line) && !line.includes('async') && !lines.slice(Math.max(0, lineNum - 3), lineNum).some(l => l.includes('async'))) {
219
+ // Don't flag if it's a simple one-liner
220
+ if (line.split('.then(').length > 2) {
221
+ issues.push({
222
+ file,
223
+ line: lineNum + 1,
224
+ type: 'promise-chain',
225
+ category: '2. Code Modernization',
226
+ message: 'Promise chain instead of async/await',
227
+ suggestion: 'Consider using async/await for readability',
228
+ current: '.then().then()',
229
+ fixable: false, // Complex transformation
230
+ })
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ return issues
237
+ }
238
+
239
+ function fixVarToConstLet(content) {
240
+ const lines = content.split('\n')
241
+ let changed = false
242
+
243
+ for (let i = 0; i < lines.length; i++) {
244
+ const line = lines[i]
245
+
246
+ // Skip comments
247
+ if (line.trim().startsWith('//') || line.trim().startsWith('*')) continue
248
+
249
+ // Replace var with const (conservative - don't try to detect reassignment)
250
+ if (/\bvar\s+/.test(line)) {
251
+ lines[i] = line.replace(/\bvar\s+/, 'const ')
252
+ changed = true
253
+ }
254
+ }
255
+
256
+ return { content: lines.join('\n'), changed }
257
+ }
258
+
259
+ // =============================================================================
260
+ // REFACTOR 3: PERFORMANCE AUDIT
261
+ // =============================================================================
262
+
263
+ function checkPerformance(files) {
264
+ const issues = []
265
+
266
+ // Only check .fez files
267
+ const fezFiles = files.filter(f => f.endsWith('.fez'))
268
+
269
+ for (const file of fezFiles) {
270
+ const content = read(file)
271
+ const lines = content.split('\n')
272
+
273
+ // Extract component name from filename
274
+ const componentName = file.split('/').pop().replace('.fez', '')
275
+
276
+ // Check for missing FAST flag
277
+ let hasFAST = /^ FAST\s*=\s*(true|false)/m.test(content)
278
+ let hasSlot = /<slot\s*\/?>/i.test(content) || /this\.root\.childNodes/.test(content)
279
+
280
+ if (!hasFAST && !hasSlot) {
281
+ // Component has no slots and no FAST flag - could benefit from FAST = true
282
+ issues.push({
283
+ file,
284
+ line: 1,
285
+ type: 'missing-fast-flag',
286
+ category: '3. Performance Audit',
287
+ message: `Component "${componentName}" missing FAST = true flag`,
288
+ suggestion: 'Add FAST = true to prevent flicker during render',
289
+ current: 'No FAST flag',
290
+ fixable: true,
291
+ })
292
+ }
293
+
294
+ // Check for deep state nesting (more than 3 levels)
295
+ const stateMatches = content.match(/this\.state\.\w+\.\w+\.\w+\.\w+/g)
296
+ if (stateMatches) {
297
+ for (const match of stateMatches) {
298
+ const depth = match.split('.').length - 1
299
+ if (depth > 3) {
300
+ issues.push({
301
+ file,
302
+ line: lines.findIndex(l => l.includes(match)) + 1,
303
+ type: 'deep-state-nesting',
304
+ category: '3. Performance Audit',
305
+ message: `Deep state nesting (${depth} levels): ${match}`,
306
+ suggestion: 'Flatten state structure for better performance',
307
+ current: match,
308
+ fixable: false,
309
+ })
310
+ }
311
+ }
312
+ }
313
+
314
+ // Check for large inline HTML templates (>100 lines)
315
+ const htmlMatch = content.match(/HTML\s*=\s*`([\s\S]*?)`/)
316
+ if (htmlMatch) {
317
+ const htmlLines = htmlMatch[1].split('\n').length
318
+ if (htmlLines > 100) {
319
+ issues.push({
320
+ file,
321
+ line: lines.findIndex(l => l.includes('HTML = `')) + 1,
322
+ type: 'large-template',
323
+ category: '3. Performance Audit',
324
+ message: `Large inline HTML template (${htmlLines} lines)`,
325
+ suggestion: 'Consider splitting into smaller child components',
326
+ current: `${htmlLines} line template`,
327
+ fixable: false,
328
+ })
329
+ }
330
+ }
331
+
332
+ // Check for missing onDestroy cleanup for intervals/timeouts
333
+ const hasSetInterval = /this\.setInterval\(/.test(content)
334
+ const hasSetTimeout = /this\.setTimeout\(/.test(content)
335
+ const hasOnDestroy = /onDestroy\s*\(\s*\)/.test(content)
336
+
337
+ if ((hasSetInterval || hasSetTimeout) && !hasOnDestroy) {
338
+ // Note: FezBase auto-cleans these, but explicit onDestroy is still good practice
339
+ issues.push({
340
+ file,
341
+ line: lines.findIndex(l => l.includes('setInterval') || l.includes('setTimeout')) + 1,
342
+ type: 'missing-explicit-cleanup',
343
+ category: '3. Performance Audit',
344
+ message: 'Using setInterval/setTimeout without explicit onDestroy',
345
+ suggestion: 'Add onDestroy() for explicit cleanup (though auto-cleanup exists)',
346
+ current: 'No onDestroy method',
347
+ fixable: false,
348
+ })
349
+ }
350
+ }
351
+
352
+ return issues
353
+ }
354
+
355
+ function fixFASTFlag(content) {
356
+ // Only add FAST = true if no slot usage
357
+ if (/<slot/i.test(content) || /this\.root\.childNodes/.test(content)) {
358
+ return { content, changed: false }
359
+ }
360
+
361
+ // Check if FAST already exists
362
+ if (/^ FAST\s*=/m.test(content)) {
363
+ return { content, changed: false }
364
+ }
365
+
366
+ // Find the right place to insert FAST (after class { or after init)
367
+ const classMatch = content.match(/^class\s*\{/m)
368
+ if (classMatch) {
369
+ const insertPos = classMatch.index + classMatch[0].length
370
+ const newContent = content.slice(0, insertPos) + '\n FAST = true' + content.slice(insertPos)
371
+ return { content: newContent, changed: true }
372
+ }
373
+
374
+ return { content, changed: false }
375
+ }
376
+
377
+ // =============================================================================
378
+ // REFACTOR 4: IMPORT CLEANUP
379
+ // =============================================================================
380
+
381
+ function checkImports(files) {
382
+ const issues = []
383
+
384
+ for (const file of files.filter(f => /\.(js|fez)$/.test(f))) {
385
+ const content = read(file)
386
+ const lines = content.split('\n')
387
+
388
+ let inScript = true // Assume JS file, for .fez we'll detect script block
389
+
390
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
391
+ const line = lines[lineNum]
392
+
393
+ // For .fez files, only check inside <script> blocks
394
+ if (file.endsWith('.fez')) {
395
+ if (line.trim() === '<script>') {
396
+ inScript = true
397
+ continue
398
+ } else if (line.trim() === '</script>') {
399
+ inScript = false
400
+ continue
401
+ }
402
+ if (!inScript) continue
403
+ }
404
+
405
+ // Check for unused imports
406
+ const importMatch = line.match(/import\s+(\{[^}]+\}|[\w]+)\s+from\s+['"]([^'"]+)['"]/)
407
+ if (importMatch) {
408
+ const imported = importMatch[1]
409
+ const source = importMatch[2]
410
+
411
+ // Parse imported names
412
+ const names = imported.startsWith('{')
413
+ ? imported.replace(/[\{\}]/g, '').split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim())
414
+ : [imported]
415
+
416
+ // Check if each name is used in the rest of the file
417
+ const restOfFile = lines.slice(lineNum + 1).join('\n')
418
+
419
+ for (const name of names) {
420
+ if (name && !new RegExp(`\\b${name}\\b`).test(restOfFile)) {
421
+ issues.push({
422
+ file,
423
+ line: lineNum + 1,
424
+ type: 'unused-import',
425
+ category: '4. Import Cleanup',
426
+ message: `Import "${name}" from "${source}" is unused`,
427
+ suggestion: `Remove "${name}" from import`,
428
+ current: name,
429
+ fixable: true,
430
+ })
431
+ }
432
+ }
433
+
434
+ // Check if imports are sorted
435
+ if (imported.startsWith('{')) {
436
+ const namesList = imported.replace(/[\{\}]/g, '').split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean)
437
+ const sorted = [...namesList].sort()
438
+
439
+ if (JSON.stringify(namesList) !== JSON.stringify(sorted)) {
440
+ issues.push({
441
+ file,
442
+ line: lineNum + 1,
443
+ type: 'unsorted-imports',
444
+ category: '4. Import Cleanup',
445
+ message: `Imports from "${source}" are not sorted`,
446
+ suggestion: `Sort alphabetically: ${sorted.join(', ')}`,
447
+ current: namesList.join(', '),
448
+ fixable: true,
449
+ })
450
+ }
451
+ }
452
+ }
453
+
454
+ // Check for duplicate imports
455
+ const allImports = lines.filter(l => /^\s*import\s+/.test(l))
456
+ const importSources = allImports.map(l => l.match(/from\s+['"]([^'"]+)['"]/)?.[1]).filter(Boolean)
457
+
458
+ const duplicates = importSources.filter((src, idx) => importSources.indexOf(src) !== idx)
459
+ if (duplicates.length > 0 && lineNum === lines.findIndex(l => l.includes(`from '${duplicates[0]}'`) || l.includes(`from "${duplicates[0]}"`))) {
460
+ issues.push({
461
+ file,
462
+ line: lineNum + 1,
463
+ type: 'duplicate-imports',
464
+ category: '4. Import Cleanup',
465
+ message: `Duplicate imports from "${duplicates[0]}"`,
466
+ suggestion: 'Consolidate into single import statement',
467
+ current: `Multiple imports from ${duplicates[0]}`,
468
+ fixable: false, // Complex to auto-fix
469
+ })
470
+ }
471
+ }
472
+ }
473
+
474
+ return issues
475
+ }
476
+
477
+ function fixUnusedImport(content, unusedName) {
478
+ const lines = content.split('\n')
479
+ let changed = false
480
+
481
+ for (let i = 0; i < lines.length; i++) {
482
+ const line = lines[i]
483
+ const importMatch = line.match(/import\s+(\{[^}]+\})\s+from\s+['"]([^'"]+)['"]/)
484
+
485
+ if (importMatch) {
486
+ const imported = importMatch[1]
487
+ const names = imported.replace(/[\{\}]/g, '').split(',').map(n => n.trim())
488
+
489
+ if (names.includes(unusedName)) {
490
+ const newNames = names.filter(n => n !== unusedName)
491
+
492
+ if (newNames.length === 0) {
493
+ // Remove entire import line
494
+ lines[i] = ''
495
+ } else {
496
+ // Remove unused name
497
+ lines[i] = line.replace(imported, `{ ${newNames.join(', ')} }`)
498
+ }
499
+ changed = true
500
+ }
501
+ }
502
+ }
503
+
504
+ return { content: lines.join('\n'), changed }
505
+ }
506
+
507
+ function fixUnsortedImports(content) {
508
+ const lines = content.split('\n')
509
+ let changed = false
510
+
511
+ for (let i = 0; i < lines.length; i++) {
512
+ const importMatch = lines[i].match(/import\s+(\{[^}]+\})\s+from\s+['"]([^'"]+)['"]/)
513
+
514
+ if (importMatch) {
515
+ const imported = importMatch[1]
516
+ const names = imported.replace(/[\{\}]/g, '').split(',').map(n => n.trim())
517
+ const sorted = [...names].sort()
518
+
519
+ if (JSON.stringify(names) !== JSON.stringify(sorted)) {
520
+ lines[i] = lines[i].replace(imported, `{ ${sorted.join(', ')} }`)
521
+ changed = true
522
+ }
523
+ }
524
+ }
525
+
526
+ return { content: lines.join('\n'), changed }
527
+ }
528
+
529
+ // =============================================================================
530
+ // MAIN EXECUTION
531
+ // =============================================================================
532
+
533
+ function main() {
534
+ if (SHOW_HELP) {
535
+ console.log(`
536
+ Fez Refactor Tool
537
+ =================
538
+
539
+ Usage:
540
+ bun run refactor # Report issues only
541
+ bun run refactor --fix # Auto-fix issues
542
+ bun run refactor --check # Same as default (report only)
543
+ bun run refactor --help # Show this help
544
+
545
+ Checks:
546
+ 1. Legacy Syntax Migration Convert {{ }} to { }
547
+ 2. Code Modernization var → const/let, modern patterns
548
+ 3. Performance Audit FAST flags, state depth, template size
549
+ 4. Import Cleanup Unused imports, sorting, duplicates
550
+
551
+ Fixable Issues (with --fix):
552
+ - Legacy {{ }} syntax → { }
553
+ - var → const
554
+ - Missing FAST = true flag
555
+ - Unused imports (removes them)
556
+ - Unsorted imports (sorts them)
557
+ `)
558
+ process.exit(0)
559
+ }
560
+
561
+ console.log('Fez Refactor Tool')
562
+ console.log('=================')
563
+ console.log(`Mode: ${FIX_MODE ? 'Auto-fix' : 'Report only'}`)
564
+ console.log()
565
+
566
+ // Collect all files
567
+ const allFiles = []
568
+ for (const dir of SCAN_DIRS) {
569
+ try {
570
+ const { globSync } = require('glob')
571
+ const fezFiles = globSync(join(ROOT_DIR, dir, '**/*.fez'))
572
+ const jsFiles = globSync(join(ROOT_DIR, dir, '**/*.{js,ts}'))
573
+ allFiles.push(...fezFiles, ...jsFiles)
574
+ } catch (e) {
575
+ // Directory doesn't exist
576
+ }
577
+ }
578
+
579
+ // Remove duplicates
580
+ const uniqueFiles = [...new Set(allFiles)]
581
+
582
+ console.log(`Scanning ${uniqueFiles.length} files...`)
583
+ console.log()
584
+
585
+ // Run all checks
586
+ const allIssues = []
587
+
588
+ const checks = [
589
+ { name: 'Legacy Syntax Migration', fn: checkLegacySyntax },
590
+ { name: 'Code Modernization', fn: checkCodeModernization },
591
+ { name: 'Performance Audit', fn: checkPerformance },
592
+ { name: 'Import Cleanup', fn: checkImports },
593
+ ]
594
+
595
+ for (const check of checks) {
596
+ console.log(`Checking: ${check.name}...`)
597
+ const issues = check.fn(uniqueFiles)
598
+ allIssues.push(...issues)
599
+ console.log(` Found ${issues.length} issues`)
600
+ }
601
+
602
+ console.log()
603
+
604
+ // Apply fixes if in fix mode
605
+ if (FIX_MODE) {
606
+ const fixableIssues = allIssues.filter(i => i.fixable)
607
+ console.log(`Applying ${fixableIssues.length} auto-fixes...\n`)
608
+
609
+ // Group issues by file
610
+ const issuesByFile = {}
611
+ for (const issue of fixableIssues) {
612
+ if (!issuesByFile[issue.file]) issuesByFile[issue.file] = []
613
+ issuesByFile[issue.file].push(issue)
614
+ }
615
+
616
+ let fixedCount = 0
617
+ for (const [file, issues] of Object.entries(issuesByFile)) {
618
+ let content = read(file)
619
+ let changed = false
620
+
621
+ for (const issue of issues) {
622
+ if (issue.type === 'legacy-syntax') {
623
+ const result = fixLegacySyntax(content)
624
+ content = result.content
625
+ changed = changed || result.changed
626
+ } else if (issue.type === 'var-declaration') {
627
+ const result = fixVarToConstLet(content)
628
+ content = result.content
629
+ changed = changed || result.changed
630
+ } else if (issue.type === 'missing-fast-flag') {
631
+ const result = fixFASTFlag(content)
632
+ content = result.content
633
+ changed = changed || result.changed
634
+ } else if (issue.type === 'unused-import') {
635
+ const result = fixUnusedImport(content, issue.current)
636
+ content = result.content
637
+ changed = changed || result.changed
638
+ } else if (issue.type === 'unsorted-imports') {
639
+ const result = fixUnsortedImports(content)
640
+ content = result.content
641
+ changed = changed || result.changed
642
+ }
643
+
644
+ if (changed) fixedCount++
645
+ }
646
+
647
+ if (changed) {
648
+ write(file, content)
649
+ console.log(` Fixed: ${relativePath(file)}`)
650
+ }
651
+ }
652
+
653
+ console.log(`\nApplied ${fixedCount} fixes`)
654
+ }
655
+
656
+ // Report issues
657
+ if (allIssues.length > 0) {
658
+ console.log()
659
+ console.log('Issues found:')
660
+ console.log()
661
+
662
+ // Group by category
663
+ const byCategory = {}
664
+ for (const issue of allIssues) {
665
+ if (!byCategory[issue.category]) byCategory[issue.category] = []
666
+ byCategory[issue.category].push(issue)
667
+ }
668
+
669
+ for (const [category, issues] of Object.entries(byCategory)) {
670
+ console.log(`${category}`)
671
+ console.log('-'.repeat(category.length))
672
+
673
+ for (const issue of issues) {
674
+ const fixable = issue.fixable ? ' (auto-fixable)' : ''
675
+ console.log(` ${relativePath(issue.file)}:${issue.line}`)
676
+ console.log(` ${issue.message}${fixable}`)
677
+ if (!FIX_MODE) {
678
+ console.log(` → ${issue.suggestion}`)
679
+ }
680
+ }
681
+ console.log()
682
+ }
683
+
684
+ console.log(`Total: ${allIssues.length} issues (${allIssues.filter(i => i.fixable).length} auto-fixable)`)
685
+
686
+ if (!FIX_MODE) {
687
+ console.log()
688
+ console.log('Run with --fix to auto-fix issues')
689
+ }
690
+
691
+ process.exit(allIssues.length > 0 ? 1 : 0)
692
+ } else {
693
+ console.log('✅ No issues found! Code is clean.')
694
+ process.exit(0)
695
+ }
696
+ }
697
+
698
+ // Run
699
+ main()