tz-clean 2.3.2 → 2.4.0

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/index.js CHANGED
@@ -1,428 +1,386 @@
1
- #!/usr/bin/env node
2
- import { execSync } from 'child_process';
3
- import fs from 'fs';
4
- import http from 'http';
5
- import open from 'open';
6
- import path from 'path';
7
- import prompts from 'prompts';
8
- import { fileURLToPath } from 'url';
9
-
10
- const filename = fileURLToPath(import.meta.url);
11
- const dirname = path.dirname(filename);
12
- const configDir = dirname;
13
- const targetDir = process.cwd();
14
-
15
- /**
16
- * Calculates the total number of errors and warnings from the ESLint report data.
17
- * @param {Array<object>} reportData - The report data from ESLint.
18
- * @returns {{totalErrors: number, totalWarnings: number}} The calculated totals.
19
- */
20
- function calculateTotals(reportData) {
21
- let totalErrors = 0;
22
- let totalWarnings = 0;
23
- reportData.forEach((file) => {
24
- totalErrors += file.errorCount;
25
- totalWarnings += file.warningCount;
26
- });
27
- return { totalErrors, totalWarnings };
28
- }
29
-
30
- /**
31
- * Generates an HTML dashboard report.
32
- * @param {Array<object>} reportData - The report data from ESLint.
33
- * @param {string[]} fixedFiles - The list of formatted files.
34
- * @returns {string} The output path of the generated HTML file.
35
- */
36
- function generateHtmlReport(reportData, fixedFiles) {
37
- const templatePath = path.join(configDir, 'report-template.html');
38
- let html = fs.readFileSync(templatePath, 'utf-8');
39
-
40
- html = html.replace('/* REPORT_DATA_PLACEHOLDER */ null', JSON.stringify(reportData));
41
- html = html.replace('/* FIXED_FILES_PLACEHOLDER */ []', JSON.stringify(fixedFiles));
42
-
43
- const outputPath = path.join(targetDir, '.tz-clean-report.html');
44
- fs.writeFileSync(outputPath, html);
45
- return outputPath;
46
- }
47
-
48
- /**
49
- * Parses command line arguments to determine which tools to run.
50
- * @returns {{runEslint: boolean, runPrettier: boolean, runTypecheck: boolean, tools: string[], includePaths: string[], excludePaths: string[]}} The configuration object.
51
- */
52
- function parseArgs() {
53
- const args = process.argv.slice(2);
54
- const runPrettier = !args.includes('--no-prettier');
55
- const runEslint = !args.includes('--no-eslint');
56
- const runTypecheck = !args.includes('--no-typecheck');
57
-
58
- const includePaths = [];
59
- const excludePaths = [];
60
-
61
- for (let i = 0; i < args.length; i++) {
62
- if (args[i] === '--include' && args[i + 1]) {
63
- args[i + 1].split(',').forEach((p) => includePaths.push(p.trim()));
64
- i++;
65
- } else if (args[i] === '--exclude' && args[i + 1]) {
66
- args[i + 1].split(',').forEach((p) => excludePaths.push(p.trim()));
67
- i++;
68
- }
69
- }
70
-
71
- const tools = [];
72
- if (runPrettier) tools.push('Prettier');
73
- if (runEslint) tools.push('ESLint');
74
- if (runTypecheck) tools.push('Typecheck');
75
-
76
- return { excludePaths, includePaths, runEslint, runPrettier, runTypecheck, tools };
77
- }
78
-
79
- /**
80
- * Prints a summary of the analysis report to the terminal.
81
- * @param {Array<object>} reportData - The report data from ESLint.
82
- * @param {string[]} fixedFiles - The list of formatted files.
83
- * @param {number} totalErrors - Total ESLint errors.
84
- * @param {number} totalWarnings - Total ESLint warnings.
85
- * @param {{error: string|null, ran: boolean}} typecheckResult - The typecheck execution result.
86
- * @returns {void}
87
- */
88
- function printTerminalSummary(reportData, fixedFiles, totalErrors, totalWarnings, typecheckResult) {
89
- console.log('\n--- šŸ“Š tz-clean Summary ---');
90
- console.log(`Files Analyzed: ${reportData.length}`);
91
- console.log(`Errors (Critical) : ${totalErrors > 0 ? 'āŒ ' + totalErrors : 'āœ… 0'}`);
92
- console.log(`Warnings : ${totalWarnings > 0 ? 'āš ļø ' + totalWarnings : 'āœ… 0'}`);
93
-
94
- if (!typecheckResult.ran) {
95
- console.log(`Typecheck : āš ļø Skipped`);
96
- } else if (typecheckResult.error) {
97
- console.log(`Typecheck : āŒ ${typecheckResult.error}`);
98
- } else {
99
- console.log(`Typecheck : āœ… Passed`);
100
- }
101
- console.log('---------------------------');
102
-
103
- if (fixedFiles.length > 0) {
104
- console.log('\n✨ Auto-Fixed (Formatted):');
105
- fixedFiles.forEach((file) => console.log(` - ${file}`));
106
- }
107
-
108
- if (totalErrors > 0 || totalWarnings > 0) {
109
- console.log('\nšŸ“ Details:');
110
- reportData.forEach((file) => {
111
- if (file.errorCount > 0 || file.warningCount > 0) {
112
- console.log(`\nšŸ“‚ ${path.relative(targetDir, file.filePath) || file.filePath}`);
113
- file.messages.forEach((msg) => {
114
- const severity = msg.severity === 2 ? 'āŒ Error' : 'āš ļø Warning';
115
- console.log(` ${severity} [${msg.line || 0}:${msg.column || 0}] ${msg.message} (${msg.ruleId || ''})`);
116
- });
117
- }
118
- });
119
- console.log('\n---------------------------\n');
120
- } else {
121
- console.log('\n');
122
- }
123
- }
124
-
125
- /**
126
- * Orchestrates the overall execution of the CLI.
127
- * @returns {Promise<void>}
128
- */
129
- async function run() {
130
- const { runEslint, runPrettier, runTypecheck, tools, includePaths, excludePaths } = parseArgs();
131
-
132
- if (tools.length === 0) {
133
- console.log('No tools selected to run.');
134
- process.exit(0);
135
- }
136
-
137
- console.log(`🧹 Running tz-clean (${tools.join(', ')})...`);
138
- if (includePaths.length > 0) console.log(`šŸ“ Include: ${includePaths.join(', ')}`);
139
- if (excludePaths.length > 0) console.log(`🚫 Exclude: ${excludePaths.join(', ')}`);
140
-
141
- try {
142
- const fixedFiles = runPrettier ? runPrettierTools(includePaths, excludePaths) : [];
143
- const strippedFiles = stripCommentsAndEmptyLines(includePaths, excludePaths);
144
- const allFixedFiles = [...new Set([...fixedFiles, ...strippedFiles])];
145
- const typecheckResult = runTypecheck ? runTypecheckTools() : { error: null, ran: false };
146
- const reportData = runEslint ? runEslintTools(includePaths, excludePaths) : [];
147
- const { totalErrors, totalWarnings } = calculateTotals(reportData);
148
-
149
- console.log('\nāœ… Analysis complete!');
150
- const response = await prompts({
151
- choices: [
152
- { description: 'Quick overview in terminal', title: 'Terminal Summary', value: 'terminal' },
153
- { description: 'Detailed HTML visual report', title: 'UI Dashboard (Browser)', value: 'ui' },
154
- { description: 'Serve on localhost port 8080', title: 'UI Dashboard (Hosted)', value: 'hosted' },
155
- ],
156
- initial: 0,
157
- message: 'How would you like to view the analysis report?',
158
- name: 'reportType',
159
- type: 'select',
160
- });
161
-
162
- if (!response.reportType) {
163
- console.log('Operation cancelled.');
164
- process.exit(0);
165
- }
166
-
167
- if (response.reportType === 'terminal') {
168
- printTerminalSummary(reportData, allFixedFiles, totalErrors, totalWarnings, typecheckResult);
169
- } else if (response.reportType === 'ui') {
170
- console.log('ā³ Generating UI Dashboard...');
171
- const outputPath = generateHtmlReport(reportData, allFixedFiles);
172
- console.log(`šŸš€ Opening report in browser: ${outputPath}`);
173
- await open(outputPath);
174
- } else if (response.reportType === 'hosted') {
175
- console.log('ā³ Generating Hosted UI Dashboard...');
176
- const templatePath = path.join(configDir, 'report-template.html');
177
- let html = fs.readFileSync(templatePath, 'utf-8');
178
- html = html.replace('/* REPORT_DATA_PLACEHOLDER */ null', JSON.stringify(reportData));
179
- html = html.replace('/* FIXED_FILES_PLACEHOLDER */ []', JSON.stringify(allFixedFiles));
180
- serveHostedReport(html);
181
- }
182
- } catch (globalError) {
183
- console.error('\nāŒ tz-clean encountered a fatal error:', globalError.message);
184
- process.exit(1);
185
- }
186
- }
187
-
188
- /**
189
- * Strips all comments from a JS/TS source string without touching string literals or regex.
190
- * Handles single-line (//), block (/* *\/), and preserves URLs inside strings like 'http://...'.
191
- * @param {string} source - The source code string.
192
- * @returns {string} The source with all comments removed.
193
- */
194
- function stripCommentsFromSource(source) {
195
- let result = '';
196
- let i = 0;
197
- const len = source.length;
198
-
199
- while (i < len) {
200
- const ch = source[i];
201
-
202
- // String literals: single, double, or template
203
- if (ch === '"' || ch === "'" || ch === '`') {
204
- const quote = ch;
205
- result += ch;
206
- i++;
207
- while (i < len) {
208
- const sc = source[i];
209
- if (sc === '\\') {
210
- result += sc + (source[i + 1] || '');
211
- i += 2;
212
- continue;
213
- }
214
- result += sc;
215
- i++;
216
- if (sc === quote) break;
217
- }
218
- continue;
219
- }
220
-
221
- // Block comment: /* ... */
222
- if (ch === '/' && source[i + 1] === '*') {
223
- i += 2;
224
- while (i < len) {
225
- if (source[i] === '*' && source[i + 1] === '/') {
226
- i += 2;
227
- break;
228
- }
229
- i++;
230
- }
231
- continue;
232
- }
233
-
234
- // Single-line comment: // ...
235
- if (ch === '/' && source[i + 1] === '/') {
236
- i += 2;
237
- while (i < len && source[i] !== '\n') i++;
238
- continue;
239
- }
240
-
241
- result += ch;
242
- i++;
243
- }
244
-
245
- return result;
246
- }
247
-
248
- /**
249
- * Strips all comments and excess blank lines from JS/TS files in the target paths.
250
- * Removes single-line comments (//), block comments (/* *\/), eslint-disable directives,
251
- * and collapses multiple consecutive blank lines into one.
252
- * @param {string[]} includePaths - Paths to process (defaults to targetDir).
253
- * @param {string[]} excludePaths - Paths to skip.
254
- * @returns {string[]} List of files that were modified.
255
- */
256
- function stripCommentsAndEmptyLines(includePaths = [], excludePaths = []) {
257
- const extensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
258
- const modifiedFiles = [];
259
-
260
- const defaultIgnorePatterns = [
261
- /node_modules/,
262
- /\.min\.(js|cjs|mjs)$/,
263
- /\.bundle\.js$/,
264
- /[/\\](dist|build|vendor|libs)[/\\]/,
265
- ];
266
-
267
- const userIgnorePatterns = excludePaths.map((p) => new RegExp(p.replace(/\//g, '[/\\\\]')));
268
- const allIgnorePatterns = [...defaultIgnorePatterns, ...userIgnorePatterns];
269
-
270
- const scanDir = (dir) => {
271
- let entries;
272
- try {
273
- entries = fs.readdirSync(dir, { withFileTypes: true });
274
- } catch {
275
- return;
276
- }
277
- for (const entry of entries) {
278
- const fullPath = path.join(dir, entry.name);
279
- if (allIgnorePatterns.some((re) => re.test(fullPath))) continue;
280
- if (entry.isDirectory()) {
281
- scanDir(fullPath);
282
- } else if (entry.isFile() && extensions.includes(path.extname(entry.name))) {
283
- const original = fs.readFileSync(fullPath, 'utf-8');
284
- let cleaned = original;
285
-
286
- // Use parser-aware comment stripper (safe for URLs in strings)
287
- cleaned = stripCommentsFromSource(cleaned);
288
-
289
- // Collapse multiple blank lines into one
290
- cleaned = cleaned.replace(/\n{3,}/g, '\n\n');
291
-
292
- // Remove blank lines at start of file
293
- cleaned = cleaned.replace(/^\n+/, '');
294
-
295
- // Ensure single newline at end of file
296
- cleaned = cleaned.trimEnd() + '\n';
297
-
298
- if (cleaned !== original) {
299
- fs.writeFileSync(fullPath, cleaned, 'utf-8');
300
- modifiedFiles.push(path.relative(targetDir, fullPath));
301
- }
302
- }
303
- }
304
- };
305
-
306
- const targets = includePaths.length > 0 ? includePaths.map((p) => path.resolve(targetDir, p)) : [targetDir];
307
- targets.forEach((t) => scanDir(t));
308
-
309
- return modifiedFiles;
310
- }
311
-
312
- /**
313
- * Executes ESLint to analyze code.
314
- * @param {string[]} includePaths - Paths to include in analysis.
315
- * @param {string[]} excludePaths - Paths to exclude from analysis.
316
- * @returns {Array<object>} An array containing the report data from ESLint.
317
- */
318
- function runEslintTools(includePaths = [], excludePaths = []) {
319
- let eslintOutput = '';
320
- let reportData = [];
321
-
322
- const targets = includePaths.length > 0 ? includePaths.map((p) => `"${p}"`).join(' ') : '.';
323
- const ignoreFlags = excludePaths.map((p) => `--ignore-pattern "${p}"`).join(' ');
324
-
325
- try {
326
- eslintOutput = execSync(
327
- `npx eslint ${targets} -c "${path.join(configDir, 'eslint.config.mjs')}" -f json ${ignoreFlags}`,
328
- {
329
- encoding: 'utf-8',
330
- maxBuffer: 1024 * 1024 * 10,
331
- },
332
- );
333
- } catch (err) {
334
- eslintOutput = err.stdout;
335
- }
336
-
337
- if (eslintOutput) {
338
- try {
339
- reportData = JSON.parse(eslintOutput);
340
- } catch (e) {
341
- console.error('āŒ Failed to parse ESLint output.', e);
342
- process.exit(1);
343
- }
344
- }
345
- return reportData;
346
- }
347
-
348
- /**
349
- * Executes Prettier to format files.
350
- * @param {string[]} includePaths - Paths to include in formatting.
351
- * @param {string[]} excludePaths - Paths to exclude from formatting.
352
- * @returns {string[]} An array of files that were fixed.
353
- */
354
- function runPrettierTools(includePaths = [], excludePaths = []) {
355
- let fixedFiles = [];
356
-
357
- const targets = includePaths.length > 0 ? includePaths.map((p) => `"${p}"`).join(' ') : '.';
358
-
359
- // Build a temporary .prettierignore combining the default one + user excludes
360
- const defaultIgnorePath = path.join(configDir, '.prettierignore');
361
- let ignoreContent = fs.readFileSync(defaultIgnorePath, 'utf-8');
362
- if (excludePaths.length > 0) {
363
- ignoreContent += '\n# User-specified excludes\n' + excludePaths.join('\n') + '\n';
364
- }
365
- const tempIgnorePath = path.join(targetDir, '.tz-clean-prettierignore');
366
- fs.writeFileSync(tempIgnorePath, ignoreContent);
367
-
368
- try {
369
- const prettierOutput = execSync(
370
- `npx prettier --write ${targets} --config "${path.join(configDir, '.prettierrc')}" --ignore-path "${tempIgnorePath}" --ignore-unknown`,
371
- { encoding: 'utf-8' },
372
- );
373
- fixedFiles = prettierOutput
374
- .split('\n')
375
- .filter((line) => line.trim() && !line.includes('(unchanged)'))
376
- .map((line) => line.split(' ')[0]);
377
- } catch (err) {
378
- if (err.stdout) {
379
- fixedFiles = err.stdout
380
- .split('\n')
381
- .filter((line) => line.trim() && !line.includes('(unchanged)'))
382
- .map((line) => line.split(' ')[0]);
383
- }
384
- } finally {
385
- if (fs.existsSync(tempIgnorePath)) fs.unlinkSync(tempIgnorePath);
386
- }
387
- return fixedFiles;
388
- }
389
-
390
- /**
391
- * Executes TypeScript type checking.
392
- * @returns {{error: string|null, ran: boolean}} The result of type checking.
393
- */
394
- function runTypecheckTools() {
395
- let typecheckError = null;
396
- let ran = false;
397
- if (fs.existsSync(path.join(targetDir, 'tsconfig.json'))) {
398
- ran = true;
399
- try {
400
- execSync(`npx tsc --noEmit`, { stdio: 'ignore' });
401
- } catch {
402
- typecheckError = 'TypeScript validation failed. Check your types.';
403
- }
404
- }
405
- return { error: typecheckError, ran };
406
- }
407
-
408
- /**
409
- * Serves the HTML report on localhost.
410
- * @param {string} html - The HTML string content.
411
- * @returns {void}
412
- */
413
- function serveHostedReport(html) {
414
- const port = 8080;
415
- const server = http.createServer((req, res) => {
416
- res.writeHead(200, { 'Content-Type': 'text/html' });
417
- res.end(html);
418
- });
419
-
420
- server.listen(port, async () => {
421
- const url = `http://localhost:${port}`;
422
- console.log(`šŸš€ Server running at ${url}`);
423
- console.log('Press Ctrl+C to stop the server.');
424
- await open(url);
425
- });
426
- }
427
-
428
- run();
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'child_process';
3
+ import fs from 'fs';
4
+ import http from 'http';
5
+ import open from 'open';
6
+ import path from 'path';
7
+ import prompts from 'prompts';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ const filename = fileURLToPath(import.meta.url);
11
+ const dirname = path.dirname(filename);
12
+ const configDir = dirname;
13
+ const targetDir = process.cwd();
14
+
15
+ function buildIgnorePatterns(excludePaths) {
16
+ const defaults = [
17
+ new RegExp('node_modules'),
18
+ new RegExp('\\.min\\.(js|cjs|mjs)$'),
19
+ new RegExp('\\.bundle\\.js$'),
20
+ new RegExp('[/\\\\](dist|build|vendor|libs)[/\\\\]'),
21
+ ];
22
+ const user = excludePaths.map((p) => new RegExp(p.replace(new RegExp('/', 'g'), '[/\\\\]')));
23
+ return [...defaults, ...user];
24
+ }
25
+
26
+ function calculateTotals(reportData) {
27
+ let totalErrors = 0;
28
+ let totalWarnings = 0;
29
+ reportData.forEach((file) => {
30
+ totalErrors += file.errorCount;
31
+ totalWarnings += file.warningCount;
32
+ });
33
+ return { totalErrors, totalWarnings };
34
+ }
35
+
36
+ function generateHtmlReport(reportData, fixedFiles) {
37
+ const templatePath = path.join(configDir, 'report-template.html');
38
+ let html = fs.readFileSync(templatePath, 'utf-8');
39
+ html = html.replace('/* REPORT_DATA_PLACEHOLDER */ null', JSON.stringify(reportData));
40
+ html = html.replace('/* FIXED_FILES_PLACEHOLDER */ []', JSON.stringify(fixedFiles));
41
+ const outputPath = path.join(targetDir, '.tz-clean-report.html');
42
+ fs.writeFileSync(outputPath, html);
43
+ return outputPath;
44
+ }
45
+
46
+ function parseArgs() {
47
+ const args = process.argv.slice(2);
48
+ const runPrettier = !args.includes('--no-prettier');
49
+ const runEslint = !args.includes('--no-eslint');
50
+ const runTypecheck = !args.includes('--no-typecheck');
51
+
52
+ const excludePaths = [];
53
+ const includePaths = [];
54
+
55
+ for (let i = 0; i < args.length; i++) {
56
+ if (args[i] === '--include' && args[i + 1]) {
57
+ args[i + 1].split(',').forEach((p) => includePaths.push(p.trim()));
58
+ i++;
59
+ } else if (args[i] === '--exclude' && args[i + 1]) {
60
+ args[i + 1].split(',').forEach((p) => excludePaths.push(p.trim()));
61
+ i++;
62
+ }
63
+ }
64
+
65
+ const tools = [];
66
+ if (runPrettier) tools.push('Prettier');
67
+ if (runEslint) tools.push('ESLint');
68
+ if (runTypecheck) tools.push('Typecheck');
69
+
70
+ return { excludePaths, includePaths, runEslint, runPrettier, runTypecheck, tools };
71
+ }
72
+
73
+ function printTerminalSummary(reportData, fixedFiles, totalErrors, totalWarnings, typecheckResult) {
74
+ console.log('\n--- šŸ“Š tz-clean Summary ---');
75
+ console.log(`Files Analyzed: ${reportData.length}`);
76
+ console.log(`Errors (Critical) : ${totalErrors > 0 ? 'āŒ ' + totalErrors : 'āœ… 0'}`);
77
+ console.log(`Warnings : ${totalWarnings > 0 ? 'āš ļø ' + totalWarnings : 'āœ… 0'}`);
78
+
79
+ if (!typecheckResult.ran) {
80
+ console.log(`Typecheck : āš ļø Skipped`);
81
+ } else if (typecheckResult.error) {
82
+ console.log(`Typecheck : āŒ ${typecheckResult.error}`);
83
+ } else {
84
+ console.log(`Typecheck : āœ… Passed`);
85
+ }
86
+ console.log('---------------------------');
87
+
88
+ if (fixedFiles.length > 0) {
89
+ console.log('\n✨ Auto-Fixed (Formatted):');
90
+ fixedFiles.forEach((file) => console.log(` - ${file}`));
91
+ }
92
+
93
+ if (totalErrors > 0 || totalWarnings > 0) {
94
+ console.log('\nšŸ“ Details:');
95
+ reportData.forEach((file) => {
96
+ if (file.errorCount > 0 || file.warningCount > 0) {
97
+ console.log(`\nšŸ“‚ ${path.relative(targetDir, file.filePath) || file.filePath}`);
98
+ file.messages.forEach((msg) => {
99
+ const severity = msg.severity === 2 ? 'āŒ Error' : 'āš ļø Warning';
100
+ console.log(` ${severity} [${msg.line || 0}:${msg.column || 0}] ${msg.message} (${msg.ruleId || ''})`);
101
+ });
102
+ }
103
+ });
104
+ console.log('\n---------------------------\n');
105
+ } else {
106
+ console.log('\n');
107
+ }
108
+ }
109
+
110
+ async function run() {
111
+ const { excludePaths, includePaths, runEslint, runPrettier, runTypecheck, tools } = parseArgs();
112
+
113
+ if (tools.length === 0) {
114
+ console.log('No tools selected to run.');
115
+ process.exit(0);
116
+ }
117
+
118
+ console.log(`🧹 Running tz-clean (${tools.join(', ')})...`);
119
+ if (includePaths.length > 0) console.log(`šŸ“ Include: ${includePaths.join(', ')}`);
120
+ if (excludePaths.length > 0) console.log(`🚫 Exclude: ${excludePaths.join(', ')}`);
121
+
122
+ try {
123
+ process.stdout.write('ā³ [1/4] Running Prettier...');
124
+ const fixedFiles = runPrettier ? runPrettierTools(includePaths, excludePaths) : [];
125
+ process.stdout.write(`\rāœ… [1/4] Prettier done (${fixedFiles.length} file(s) formatted) \n`);
126
+
127
+ process.stdout.write('ā³ [2/4] Stripping comments...');
128
+ const strippedFiles = stripCommentsAndEmptyLines(includePaths, excludePaths);
129
+ process.stdout.write(`\rāœ… [2/4] Comments stripped (${strippedFiles.length} file(s) modified) \n`);
130
+
131
+ const allFixedFiles = [...new Set([...fixedFiles, ...strippedFiles])];
132
+
133
+ process.stdout.write('ā³ [3/4] Running Typecheck...');
134
+ const typecheckResult = runTypecheck ? runTypecheckTools() : { error: null, ran: false };
135
+ let typecheckStatus = 'skipped';
136
+ if (typecheckResult.ran) {
137
+ typecheckStatus = typecheckResult.error ? 'failed' : 'passed';
138
+ }
139
+ process.stdout.write(`\rāœ… [3/4] Typecheck ${typecheckStatus} \n`);
140
+
141
+ process.stdout.write('ā³ [4/4] Running ESLint...');
142
+ const reportData = runEslint ? runEslintTools(includePaths, excludePaths) : [];
143
+ const { totalErrors, totalWarnings } = calculateTotals(reportData);
144
+ process.stdout.write(`\rāœ… [4/4] ESLint done (${totalErrors} error(s), ${totalWarnings} warning(s)) \n`);
145
+
146
+ console.log('\nāœ… Analysis complete!');
147
+ const response = await prompts({
148
+ choices: [
149
+ { description: 'Quick overview in terminal', title: 'Terminal Summary', value: 'terminal' },
150
+ { description: 'Detailed HTML visual report', title: 'UI Dashboard (Browser)', value: 'ui' },
151
+ { description: 'Serve on localhost port 8080', title: 'UI Dashboard (Hosted)', value: 'hosted' },
152
+ ],
153
+ initial: 0,
154
+ message: 'How would you like to view the analysis report?',
155
+ name: 'reportType',
156
+ type: 'select',
157
+ });
158
+
159
+ if (!response.reportType) {
160
+ console.log('Operation cancelled.');
161
+ process.exit(0);
162
+ }
163
+
164
+ if (response.reportType === 'terminal') {
165
+ printTerminalSummary(reportData, allFixedFiles, totalErrors, totalWarnings, typecheckResult);
166
+ } else if (response.reportType === 'ui') {
167
+ console.log('ā³ Generating UI Dashboard...');
168
+ const outputPath = generateHtmlReport(reportData, allFixedFiles);
169
+ console.log(`šŸš€ Opening report in browser: ${outputPath}`);
170
+ await open(outputPath);
171
+ } else if (response.reportType === 'hosted') {
172
+ console.log('ā³ Generating Hosted UI Dashboard...');
173
+ const templatePath = path.join(configDir, 'report-template.html');
174
+ let html = fs.readFileSync(templatePath, 'utf-8');
175
+ html = html.replace('/* REPORT_DATA_PLACEHOLDER */ null', JSON.stringify(reportData));
176
+ html = html.replace('/* FIXED_FILES_PLACEHOLDER */ []', JSON.stringify(allFixedFiles));
177
+ serveHostedReport(html);
178
+ }
179
+ } catch (globalError) {
180
+ console.error('\nāŒ tz-clean encountered a fatal error:', globalError.message);
181
+ process.exit(1);
182
+ }
183
+ }
184
+
185
+ function runEslintTools(includePaths = [], excludePaths = []) {
186
+ let eslintOutput = '';
187
+ let reportData = [];
188
+
189
+ const targets = includePaths.length > 0 ? includePaths.map((p) => `"${p}"`).join(' ') : '.';
190
+ const ignoreFlags = excludePaths.map((p) => `--ignore-pattern "${p}"`).join(' ');
191
+
192
+ try {
193
+ eslintOutput = execSync(
194
+ `npx eslint ${targets} -c "${path.join(configDir, 'eslint.config.mjs')}" -f json ${ignoreFlags}`,
195
+ {
196
+ encoding: 'utf-8',
197
+ maxBuffer: 1024 * 1024 * 10,
198
+ },
199
+ );
200
+ } catch (err) {
201
+ eslintOutput = err.stdout;
202
+ }
203
+
204
+ if (eslintOutput) {
205
+ try {
206
+ reportData = JSON.parse(eslintOutput);
207
+ } catch (e) {
208
+ console.error('āŒ Failed to parse ESLint output.', e);
209
+ process.exit(1);
210
+ }
211
+ }
212
+ return reportData;
213
+ }
214
+
215
+ function runPrettierTools(includePaths = [], excludePaths = []) {
216
+ let fixedFiles = [];
217
+
218
+ const targets = includePaths.length > 0 ? includePaths.map((p) => `"${p}"`).join(' ') : '.';
219
+ const defaultIgnorePath = path.join(configDir, '.prettierignore');
220
+ let ignoreContent = fs.readFileSync(defaultIgnorePath, 'utf-8');
221
+ if (excludePaths.length > 0) {
222
+ ignoreContent += '\n# User-specified excludes\n' + excludePaths.join('\n') + '\n';
223
+ }
224
+ const tempIgnorePath = path.join(targetDir, '.tz-clean-prettierignore');
225
+ fs.writeFileSync(tempIgnorePath, ignoreContent);
226
+
227
+ try {
228
+ const prettierOutput = execSync(
229
+ `npx prettier --write ${targets} --config "${path.join(configDir, '.prettierrc')}" --ignore-path "${tempIgnorePath}" --ignore-unknown`,
230
+ { encoding: 'utf-8' },
231
+ );
232
+ fixedFiles = prettierOutput
233
+ .split('\n')
234
+ .filter((line) => line.trim() && !line.includes('(unchanged)'))
235
+ .map((line) => line.split(' ')[0]);
236
+ } catch (err) {
237
+ if (err.stdout) {
238
+ fixedFiles = err.stdout
239
+ .split('\n')
240
+ .filter((line) => line.trim() && !line.includes('(unchanged)'))
241
+ .map((line) => line.split(' ')[0]);
242
+ }
243
+ } finally {
244
+ if (fs.existsSync(tempIgnorePath)) fs.unlinkSync(tempIgnorePath);
245
+ }
246
+ return fixedFiles;
247
+ }
248
+
249
+ function runTypecheckTools() {
250
+ let typecheckError = null;
251
+ let ran = false;
252
+ if (fs.existsSync(path.join(targetDir, 'tsconfig.json'))) {
253
+ ran = true;
254
+ try {
255
+ execSync(`npx tsc --noEmit`, { stdio: 'ignore' });
256
+ } catch {
257
+ typecheckError = 'TypeScript validation failed. Check your types.';
258
+ }
259
+ }
260
+ return { error: typecheckError, ran };
261
+ }
262
+
263
+ function serveHostedReport(html) {
264
+ const port = 8080;
265
+ const server = http.createServer((req, res) => {
266
+ res.writeHead(200, { 'Content-Type': 'text/html' });
267
+ res.end(html);
268
+ });
269
+
270
+ server.listen(port, async () => {
271
+ const url = `http://localhost:${port}`;
272
+ console.log(`šŸš€ Server running at ${url}`);
273
+ console.log('Press Ctrl+C to stop the server.');
274
+ await open(url);
275
+ });
276
+ }
277
+
278
+ function skipLineComment(source, startIndex) {
279
+ let i = startIndex + 2;
280
+ const len = source.length;
281
+ while (i < len && source[i] !== '\n') i++;
282
+ return i;
283
+ }
284
+
285
+ function skipStringLiteral(source, startIndex) {
286
+ const quote = source[startIndex];
287
+ let i = startIndex + 1;
288
+ const len = source.length;
289
+ while (i < len) {
290
+ const sc = source[i];
291
+ if (sc === '\\') {
292
+ i += 2;
293
+ continue;
294
+ }
295
+ i++;
296
+ if (sc === quote) break;
297
+ }
298
+ return i;
299
+ }
300
+
301
+ function stripBlockComment(source, startIndex) {
302
+ let i = startIndex + 2;
303
+ const len = source.length;
304
+ while (i < len) {
305
+ if (source[i] === '*' && source[i + 1] === '/') return i + 2;
306
+ i++;
307
+ }
308
+ return i;
309
+ }
310
+
311
+ function stripCommentsAndEmptyLines(includePaths = [], excludePaths = []) {
312
+ const extensions = ['.cjs', '.js', '.jsx', '.mjs', '.ts', '.tsx'];
313
+ const ignorePatterns = buildIgnorePatterns(excludePaths);
314
+ const modifiedFiles = [];
315
+
316
+ const scanDir = (dir) => {
317
+ let entries;
318
+ try {
319
+ entries = fs.readdirSync(dir, { withFileTypes: true });
320
+ } catch {
321
+ return;
322
+ }
323
+ for (const entry of entries) {
324
+ const fullPath = path.join(dir, entry.name);
325
+ if (ignorePatterns.some((re) => re.test(fullPath))) continue;
326
+ if (entry.isDirectory()) {
327
+ scanDir(fullPath);
328
+ } else if (entry.isFile() && extensions.includes(path.extname(entry.name))) {
329
+ const original = fs.readFileSync(fullPath, 'utf-8');
330
+ let cleaned = stripCommentsFromSource(original);
331
+ cleaned = cleaned.replace(new RegExp('\\n{3,}', 'g'), '\n\n');
332
+ cleaned = cleaned.replace(new RegExp('^\\n+'), '');
333
+ cleaned = cleaned.trimEnd() + '\n';
334
+ if (cleaned !== original) {
335
+ fs.writeFileSync(fullPath, cleaned, 'utf-8');
336
+ modifiedFiles.push(path.relative(targetDir, fullPath));
337
+ }
338
+ }
339
+ }
340
+ };
341
+
342
+ const targets = includePaths.length > 0 ? includePaths.map((p) => path.resolve(targetDir, p)) : [targetDir];
343
+ targets.forEach((t) => scanDir(t));
344
+ return modifiedFiles;
345
+ }
346
+
347
+ function stripCommentsFromSource(source) {
348
+ let result = '';
349
+ let i = 0;
350
+ const len = source.length;
351
+
352
+ if (source.startsWith('#!')) {
353
+ const end = source.indexOf('\n');
354
+ const shebanEnd = end === -1 ? len : end + 1;
355
+ result += source.slice(0, shebanEnd);
356
+ i = shebanEnd;
357
+ }
358
+
359
+ while (i < len) {
360
+ const ch = source[i];
361
+
362
+ if (ch === '"' || ch === "'" || ch === '`') {
363
+ const start = i;
364
+ i = skipStringLiteral(source, i);
365
+ result += source.slice(start, i);
366
+ continue;
367
+ }
368
+
369
+ if (ch === '/' && source[i + 1] === '*') {
370
+ i = stripBlockComment(source, i);
371
+ continue;
372
+ }
373
+
374
+ if (ch === '/' && source[i + 1] === '/') {
375
+ i = skipLineComment(source, i);
376
+ continue;
377
+ }
378
+
379
+ result += ch;
380
+ i++;
381
+ }
382
+
383
+ return result;
384
+ }
385
+
386
+ run();