@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/README.md +707 -209
- package/bin/fez +16 -6
- package/bin/fez-compile +347 -0
- package/bin/fez-debug +25 -0
- package/bin/fez-index +16 -4
- package/bin/refactor +699 -0
- package/dist/fez.js +142 -33
- package/dist/fez.js.map +4 -4
- package/fez.d.ts +533 -0
- package/package.json +25 -15
- package/src/fez/compile.js +396 -164
- package/src/fez/connect.js +249 -146
- package/src/fez/defaults.js +272 -92
- package/src/fez/instance.js +673 -514
- package/src/fez/lib/await-helper.js +64 -0
- package/src/fez/lib/global-state.js +22 -4
- package/src/fez/lib/index.js +140 -0
- package/src/fez/lib/localstorage.js +44 -0
- package/src/fez/lib/n.js +38 -23
- package/src/fez/lib/pubsub.js +208 -0
- package/src/fez/lib/svelte-template-lib.js +339 -0
- package/src/fez/lib/svelte-template.js +472 -0
- package/src/fez/lib/template.js +114 -119
- package/src/fez/morph.js +384 -0
- package/src/fez/root.js +279 -209
- package/src/fez/utility.js +319 -149
- package/src/fez/utils/dump.js +114 -84
- package/src/fez/utils/highlight_all.js +1 -1
- package/src/fez.js +65 -43
- package/src/svelte-cde-adapter.coffee +10 -2
- package/src/fez/vendor/idiomorph.js +0 -860
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()
|