gbu-accessibility-package 3.5.0 โ 3.8.1
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/CHANGELOG.md +63 -0
- package/README-vi.md +51 -7
- package/README.md +50 -7
- package/bin/fix.js +140 -0
- package/bin/test.js +71 -0
- package/cli.js +97 -5
- package/lib/fixer.js +1494 -122
- package/package.json +16 -5
- package/ENHANCED_ALT_FEATURES.md +0 -230
- package/demo/advanced-test.html +0 -44
- package/demo/aria-label-test.html +0 -32
- package/demo/broken-links-test.html +0 -41
- package/demo/comprehensive-test.html +0 -21
- package/demo/demo.js +0 -73
- package/demo/duplicate-roles.html +0 -45
- package/demo/enhanced-alt-test.html +0 -150
- package/demo/form-labels-test.html +0 -87
- package/demo/heading-structure-test.html +0 -60
- package/demo/nested-controls-test.html +0 -92
- package/demo/sample.html +0 -47
- package/example.js +0 -121
package/lib/fixer.js
CHANGED
|
@@ -2274,10 +2274,13 @@ class AccessibilityFixer {
|
|
|
2274
2274
|
console.log(chalk.yellow('\n๐ Step 8: Heading analysis...'));
|
|
2275
2275
|
results.headings = await this.analyzeHeadings(directory);
|
|
2276
2276
|
|
|
2277
|
-
// Step 9: Check broken links (no auto-fix)
|
|
2278
|
-
console.log(chalk.yellow('\n๐ Step 9:
|
|
2277
|
+
// Step 9: Check broken links and missing resources (no auto-fix)
|
|
2278
|
+
console.log(chalk.yellow('\n๐ Step 9: External links check...'));
|
|
2279
2279
|
results.brokenLinks = await this.checkBrokenLinks(directory);
|
|
2280
2280
|
|
|
2281
|
+
console.log(chalk.yellow('\n๐ Step 9b: Missing resources check...'));
|
|
2282
|
+
results.missingResources = await this.check404Resources(directory);
|
|
2283
|
+
|
|
2281
2284
|
// Step 10: Cleanup duplicate roles
|
|
2282
2285
|
console.log(chalk.yellow('\n๐งน Step 10: Cleanup duplicate roles...'));
|
|
2283
2286
|
results.cleanup = await this.cleanupDuplicateRoles(directory);
|
|
@@ -2866,9 +2869,9 @@ class AccessibilityFixer {
|
|
|
2866
2869
|
return fixed;
|
|
2867
2870
|
}
|
|
2868
2871
|
|
|
2869
|
-
// Check for broken links
|
|
2872
|
+
// Check for broken external links only
|
|
2870
2873
|
async checkBrokenLinks(directory = '.') {
|
|
2871
|
-
console.log(chalk.blue('๐ Checking for broken links
|
|
2874
|
+
console.log(chalk.blue('๐ Checking for broken external links...'));
|
|
2872
2875
|
|
|
2873
2876
|
const htmlFiles = await this.findHtmlFiles(directory);
|
|
2874
2877
|
const results = [];
|
|
@@ -2876,7 +2879,7 @@ class AccessibilityFixer {
|
|
|
2876
2879
|
for (const file of htmlFiles) {
|
|
2877
2880
|
try {
|
|
2878
2881
|
const content = await fs.readFile(file, 'utf8');
|
|
2879
|
-
const issues = await this.analyzeBrokenLinks(content, file);
|
|
2882
|
+
const issues = await this.analyzeBrokenLinks(content, file, 'external-only');
|
|
2880
2883
|
|
|
2881
2884
|
if (issues.length > 0) {
|
|
2882
2885
|
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
@@ -2895,12 +2898,46 @@ class AccessibilityFixer {
|
|
|
2895
2898
|
}
|
|
2896
2899
|
}
|
|
2897
2900
|
|
|
2898
|
-
console.log(chalk.blue(`\n๐ Summary: Analyzed links in ${results.length} files`));
|
|
2901
|
+
console.log(chalk.blue(`\n๐ Summary: Analyzed external links in ${results.length} files`));
|
|
2899
2902
|
console.log(chalk.gray('๐ก Broken link issues require manual review and cannot be auto-fixed'));
|
|
2900
2903
|
return results;
|
|
2901
2904
|
}
|
|
2902
2905
|
|
|
2903
|
-
|
|
2906
|
+
// Check for 404 resources (local files) only
|
|
2907
|
+
async check404Resources(directory = '.') {
|
|
2908
|
+
console.log(chalk.blue('๐ Checking for 404 resources (missing local files)...'));
|
|
2909
|
+
|
|
2910
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
2911
|
+
const results = [];
|
|
2912
|
+
|
|
2913
|
+
for (const file of htmlFiles) {
|
|
2914
|
+
try {
|
|
2915
|
+
const content = await fs.readFile(file, 'utf8');
|
|
2916
|
+
const issues = await this.analyzeBrokenLinks(content, file, 'local-only');
|
|
2917
|
+
|
|
2918
|
+
if (issues.length > 0) {
|
|
2919
|
+
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
2920
|
+
issues.forEach(issue => {
|
|
2921
|
+
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
2922
|
+
if (issue.suggestion) {
|
|
2923
|
+
console.log(chalk.gray(` ๐ก ${issue.suggestion}`));
|
|
2924
|
+
}
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
results.push({ file, status: 'analyzed', issues: issues.length, missingResources: issues });
|
|
2929
|
+
} catch (error) {
|
|
2930
|
+
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
2931
|
+
results.push({ file, status: 'error', error: error.message });
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
console.log(chalk.blue(`\n๐ Summary: Analyzed local resources in ${results.length} files`));
|
|
2936
|
+
console.log(chalk.gray('๐ก Missing resource issues require manual review and cannot be auto-fixed'));
|
|
2937
|
+
return results;
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
async analyzeBrokenLinks(content, filePath, mode = 'all') {
|
|
2904
2941
|
const issues = [];
|
|
2905
2942
|
const path = require('path');
|
|
2906
2943
|
const http = require('http');
|
|
@@ -2908,20 +2945,33 @@ class AccessibilityFixer {
|
|
|
2908
2945
|
const { URL } = require('url');
|
|
2909
2946
|
|
|
2910
2947
|
// Extract all links and resources
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
//
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2948
|
+
let linkPatterns = [];
|
|
2949
|
+
|
|
2950
|
+
if (mode === 'external-only') {
|
|
2951
|
+
// Only anchor links for external link checking
|
|
2952
|
+
linkPatterns = [
|
|
2953
|
+
{ pattern: /<a[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Link', element: 'a' }
|
|
2954
|
+
];
|
|
2955
|
+
} else if (mode === 'local-only') {
|
|
2956
|
+
// Only resources (not anchor links) for 404 resource checking
|
|
2957
|
+
linkPatterns = [
|
|
2958
|
+
{ pattern: /<img[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Image', element: 'img' },
|
|
2959
|
+
{ pattern: /<link[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'CSS', element: 'link' },
|
|
2960
|
+
{ pattern: /<script[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Script', element: 'script' },
|
|
2961
|
+
{ pattern: /<video[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Video', element: 'video' },
|
|
2962
|
+
{ pattern: /<audio[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Audio', element: 'audio' }
|
|
2963
|
+
];
|
|
2964
|
+
} else {
|
|
2965
|
+
// All patterns for comprehensive checking
|
|
2966
|
+
linkPatterns = [
|
|
2967
|
+
{ pattern: /<a[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Link', element: 'a' },
|
|
2968
|
+
{ pattern: /<img[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Image', element: 'img' },
|
|
2969
|
+
{ pattern: /<link[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'CSS', element: 'link' },
|
|
2970
|
+
{ pattern: /<script[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Script', element: 'script' },
|
|
2971
|
+
{ pattern: /<video[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Video', element: 'video' },
|
|
2972
|
+
{ pattern: /<audio[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi, type: 'Audio', element: 'audio' }
|
|
2973
|
+
];
|
|
2974
|
+
}
|
|
2925
2975
|
|
|
2926
2976
|
const baseDir = path.dirname(filePath);
|
|
2927
2977
|
|
|
@@ -2929,7 +2979,7 @@ class AccessibilityFixer {
|
|
|
2929
2979
|
let match;
|
|
2930
2980
|
while ((match = linkPattern.pattern.exec(content)) !== null) {
|
|
2931
2981
|
const url = match[1];
|
|
2932
|
-
const issue = await this.checkSingleLink(url, baseDir, linkPattern.type, linkPattern.element);
|
|
2982
|
+
const issue = await this.checkSingleLink(url, baseDir, linkPattern.type, linkPattern.element, mode);
|
|
2933
2983
|
if (issue) {
|
|
2934
2984
|
issues.push(issue);
|
|
2935
2985
|
}
|
|
@@ -2939,7 +2989,7 @@ class AccessibilityFixer {
|
|
|
2939
2989
|
return issues;
|
|
2940
2990
|
}
|
|
2941
2991
|
|
|
2942
|
-
async checkSingleLink(url, baseDir, resourceType, elementType) {
|
|
2992
|
+
async checkSingleLink(url, baseDir, resourceType, elementType, mode = 'all') {
|
|
2943
2993
|
// Skip certain URLs
|
|
2944
2994
|
if (this.shouldSkipUrl(url)) {
|
|
2945
2995
|
return null;
|
|
@@ -2947,10 +2997,16 @@ class AccessibilityFixer {
|
|
|
2947
2997
|
|
|
2948
2998
|
try {
|
|
2949
2999
|
if (this.isExternalUrl(url)) {
|
|
2950
|
-
// Check external URLs
|
|
3000
|
+
// Check external URLs only if mode allows
|
|
3001
|
+
if (mode === 'local-only') {
|
|
3002
|
+
return null; // Skip external URLs in local-only mode
|
|
3003
|
+
}
|
|
2951
3004
|
return await this.checkExternalUrl(url, resourceType, elementType);
|
|
2952
3005
|
} else {
|
|
2953
|
-
// Check local files
|
|
3006
|
+
// Check local files only if mode allows
|
|
3007
|
+
if (mode === 'external-only') {
|
|
3008
|
+
return null; // Skip local files in external-only mode
|
|
3009
|
+
}
|
|
2954
3010
|
return await this.checkLocalFile(url, baseDir, resourceType, elementType);
|
|
2955
3011
|
}
|
|
2956
3012
|
} catch (error) {
|
|
@@ -3050,13 +3106,14 @@ class AccessibilityFixer {
|
|
|
3050
3106
|
async checkLocalFile(url, baseDir, resourceType, elementType) {
|
|
3051
3107
|
const path = require('path');
|
|
3052
3108
|
|
|
3053
|
-
// Handle
|
|
3109
|
+
// Handle different URL types
|
|
3054
3110
|
let filePath;
|
|
3055
3111
|
if (url.startsWith('/')) {
|
|
3056
|
-
// Absolute path from web root -
|
|
3057
|
-
|
|
3112
|
+
// Absolute path from web root - find project root and resolve from there
|
|
3113
|
+
const projectRoot = this.findProjectRoot(baseDir);
|
|
3114
|
+
filePath = path.join(projectRoot, url.substring(1));
|
|
3058
3115
|
} else {
|
|
3059
|
-
// Relative path
|
|
3116
|
+
// Relative path - resolve relative to current HTML file directory
|
|
3060
3117
|
filePath = path.resolve(baseDir, url);
|
|
3061
3118
|
}
|
|
3062
3119
|
|
|
@@ -3070,11 +3127,41 @@ class AccessibilityFixer {
|
|
|
3070
3127
|
suggestion: `Create the missing file or update the ${resourceType.toLowerCase()} path`,
|
|
3071
3128
|
url: url,
|
|
3072
3129
|
filePath: filePath,
|
|
3130
|
+
resolvedPath: filePath,
|
|
3073
3131
|
resourceType: resourceType
|
|
3074
3132
|
};
|
|
3075
3133
|
}
|
|
3076
3134
|
}
|
|
3077
3135
|
|
|
3136
|
+
// Helper method to find project root directory
|
|
3137
|
+
findProjectRoot(startDir) {
|
|
3138
|
+
const path = require('path');
|
|
3139
|
+
const fs = require('fs');
|
|
3140
|
+
|
|
3141
|
+
let currentDir = startDir;
|
|
3142
|
+
const root = path.parse(currentDir).root;
|
|
3143
|
+
|
|
3144
|
+
// Look for common project root indicators
|
|
3145
|
+
while (currentDir !== root) {
|
|
3146
|
+
// Check for package.json, .git, or other project indicators
|
|
3147
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
3148
|
+
const gitPath = path.join(currentDir, '.git');
|
|
3149
|
+
const nodeModulesPath = path.join(currentDir, 'node_modules');
|
|
3150
|
+
|
|
3151
|
+
if (fs.existsSync(packageJsonPath) || fs.existsSync(gitPath) || fs.existsSync(nodeModulesPath)) {
|
|
3152
|
+
return currentDir;
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
// Move up one directory
|
|
3156
|
+
const parentDir = path.dirname(currentDir);
|
|
3157
|
+
if (parentDir === currentDir) break; // Reached filesystem root
|
|
3158
|
+
currentDir = parentDir;
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
// If no project root found, use the original baseDir (fallback)
|
|
3162
|
+
return startDir;
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3078
3165
|
// Analyze headings (no auto-fix, only suggestions)
|
|
3079
3166
|
async analyzeHeadings(directory = '.') {
|
|
3080
3167
|
console.log(chalk.blue('๐ Analyzing heading structure...'));
|
|
@@ -3612,12 +3699,18 @@ class AccessibilityFixer {
|
|
|
3612
3699
|
results.steps.push({ step: 8, name: 'Heading analysis', suggestions: totalHeadingSuggestions });
|
|
3613
3700
|
console.log(chalk.gray('๐ก Heading issues require manual review and cannot be auto-fixed'));
|
|
3614
3701
|
|
|
3615
|
-
// Step 9: Broken links check
|
|
3616
|
-
console.log(chalk.blue('๐ Step
|
|
3702
|
+
// Step 9: Broken links and missing resources check
|
|
3703
|
+
console.log(chalk.blue('๐ Step 9a: External links check...'));
|
|
3617
3704
|
const brokenLinksResults = await this.checkBrokenLinks(directory);
|
|
3618
3705
|
const totalBrokenLinks = brokenLinksResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
3619
|
-
results.steps.push({ step:
|
|
3620
|
-
|
|
3706
|
+
results.steps.push({ step: '9a', name: 'External links check', issues: totalBrokenLinks });
|
|
3707
|
+
|
|
3708
|
+
console.log(chalk.blue('๐ Step 9b: Missing resources check...'));
|
|
3709
|
+
const missingResourcesResults = await this.check404Resources(directory);
|
|
3710
|
+
const totalMissingResources = missingResourcesResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
3711
|
+
results.steps.push({ step: '9b', name: 'Missing resources check', issues: totalMissingResources });
|
|
3712
|
+
|
|
3713
|
+
console.log(chalk.gray('๐ก Link and resource issues require manual review and cannot be auto-fixed'));
|
|
3621
3714
|
|
|
3622
3715
|
// Step 10: Cleanup duplicate roles
|
|
3623
3716
|
console.log(chalk.blue('๐งน Step 10: Cleanup duplicate roles...'));
|
|
@@ -3984,71 +4077,7 @@ class AccessibilityFixer {
|
|
|
3984
4077
|
return results;
|
|
3985
4078
|
}
|
|
3986
4079
|
|
|
3987
|
-
async checkBrokenLinks(directory = '.') {
|
|
3988
|
-
console.log(chalk.blue('๐ Checking for broken links and 404 resources...'));
|
|
3989
|
-
|
|
3990
|
-
const htmlFiles = await this.findHtmlFiles(directory);
|
|
3991
|
-
const results = [];
|
|
3992
|
-
let totalIssuesFound = 0;
|
|
3993
|
-
|
|
3994
|
-
for (const file of htmlFiles) {
|
|
3995
|
-
try {
|
|
3996
|
-
const content = await fs.readFile(file, 'utf8');
|
|
3997
|
-
const issues = this.analyzeBrokenLinks(content, file);
|
|
3998
|
-
|
|
3999
|
-
if (issues.length > 0) {
|
|
4000
|
-
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
4001
|
-
issues.forEach(issue => {
|
|
4002
|
-
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
4003
|
-
if (issue.suggestion) {
|
|
4004
|
-
console.log(chalk.gray(` ๐ก ${issue.suggestion}`));
|
|
4005
|
-
}
|
|
4006
|
-
totalIssuesFound++;
|
|
4007
|
-
});
|
|
4008
|
-
}
|
|
4009
|
-
|
|
4010
|
-
results.push({ file, status: 'analyzed', issues: issues.length });
|
|
4011
|
-
} catch (error) {
|
|
4012
|
-
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
4013
|
-
results.push({ file, status: 'error', error: error.message });
|
|
4014
|
-
}
|
|
4015
|
-
}
|
|
4016
|
-
|
|
4017
|
-
console.log(chalk.blue(`\n๐ Summary: Analyzed links in ${results.length} files`));
|
|
4018
|
-
console.log(chalk.gray('๐ก Broken link issues require manual review and cannot be auto-fixed'));
|
|
4019
|
-
return results;
|
|
4020
|
-
}
|
|
4021
4080
|
|
|
4022
|
-
analyzeBrokenLinks(content, filePath) {
|
|
4023
|
-
const issues = [];
|
|
4024
|
-
|
|
4025
|
-
// Check for local image files
|
|
4026
|
-
const imgPattern = /<img[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi;
|
|
4027
|
-
let match;
|
|
4028
|
-
|
|
4029
|
-
while ((match = imgPattern.exec(content)) !== null) {
|
|
4030
|
-
const src = match[1];
|
|
4031
|
-
|
|
4032
|
-
// Skip external URLs and data URLs
|
|
4033
|
-
if (src.startsWith('http') || src.startsWith('data:') || src.startsWith('//')) {
|
|
4034
|
-
continue;
|
|
4035
|
-
}
|
|
4036
|
-
|
|
4037
|
-
// Check if local file exists
|
|
4038
|
-
const fullPath = path.resolve(path.dirname(filePath), src);
|
|
4039
|
-
try {
|
|
4040
|
-
require('fs').statSync(fullPath);
|
|
4041
|
-
} catch (error) {
|
|
4042
|
-
issues.push({
|
|
4043
|
-
type: '๐ Image not found',
|
|
4044
|
-
description: `img file does not exist: ${src}`,
|
|
4045
|
-
suggestion: 'Create the missing file or update the image path'
|
|
4046
|
-
});
|
|
4047
|
-
}
|
|
4048
|
-
}
|
|
4049
|
-
|
|
4050
|
-
return issues;
|
|
4051
|
-
}
|
|
4052
4081
|
|
|
4053
4082
|
async cleanupDuplicateRoles(directory = '.') {
|
|
4054
4083
|
console.log(chalk.blue('๐งน Cleaning up duplicate role attributes...'));
|
|
@@ -4403,12 +4432,18 @@ class AccessibilityFixer {
|
|
|
4403
4432
|
results.steps.push({ step: 9, name: 'Heading analysis', suggestions: totalHeadingSuggestions });
|
|
4404
4433
|
console.log(chalk.gray('๐ก Heading issues require manual review and cannot be auto-fixed'));
|
|
4405
4434
|
|
|
4406
|
-
// Step 10: Broken links check
|
|
4407
|
-
console.log(chalk.blue('๐ Step
|
|
4435
|
+
// Step 10: Broken links and missing resources check
|
|
4436
|
+
console.log(chalk.blue('๐ Step 10a: External links check...'));
|
|
4408
4437
|
const brokenLinksResults = await this.checkBrokenLinks(directory);
|
|
4409
4438
|
const totalBrokenLinks = brokenLinksResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4410
|
-
results.steps.push({ step:
|
|
4411
|
-
|
|
4439
|
+
results.steps.push({ step: '10a', name: 'External links check', issues: totalBrokenLinks });
|
|
4440
|
+
|
|
4441
|
+
console.log(chalk.blue('๐ Step 10b: Missing resources check...'));
|
|
4442
|
+
const missingResourcesResults = await this.check404Resources(directory);
|
|
4443
|
+
const totalMissingResources = missingResourcesResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4444
|
+
results.steps.push({ step: '10b', name: 'Missing resources check', issues: totalMissingResources });
|
|
4445
|
+
|
|
4446
|
+
console.log(chalk.gray('๐ก Link and resource issues require manual review and cannot be auto-fixed'));
|
|
4412
4447
|
|
|
4413
4448
|
// Step 11: Cleanup duplicate roles
|
|
4414
4449
|
console.log(chalk.blue('๐งน Step 11: Cleanup duplicate roles...'));
|
|
@@ -5088,37 +5123,1374 @@ class AccessibilityFixer {
|
|
|
5088
5123
|
return fallbacks[lang] || fallbacks.en;
|
|
5089
5124
|
}
|
|
5090
5125
|
|
|
5091
|
-
|
|
5092
|
-
|
|
5126
|
+
/**
|
|
5127
|
+
* Fix heading structure issues
|
|
5128
|
+
* Ensures proper heading hierarchy: only one h1, proper nesting, no duplicates in same section
|
|
5129
|
+
*/
|
|
5130
|
+
async fixHeadingStructure(directory = '.') {
|
|
5131
|
+
console.log(chalk.blue('๐ Fixing heading structure...'));
|
|
5093
5132
|
|
|
5094
|
-
|
|
5095
|
-
const
|
|
5133
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
5134
|
+
const results = [];
|
|
5135
|
+
let totalIssuesFound = 0;
|
|
5136
|
+
let totalFixesApplied = 0;
|
|
5096
5137
|
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5138
|
+
for (const file of htmlFiles) {
|
|
5139
|
+
try {
|
|
5140
|
+
const content = await fs.readFile(file, 'utf8');
|
|
5141
|
+
const analysis = this.analyzeHeadingStructure(content);
|
|
5142
|
+
|
|
5143
|
+
if (analysis.issues.length > 0) {
|
|
5144
|
+
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
5145
|
+
analysis.issues.forEach(issue => {
|
|
5146
|
+
console.log(chalk.yellow(` ๐ ${issue.type}: ${issue.description}`));
|
|
5147
|
+
console.log(chalk.gray(` ๐ก ${issue.suggestion}`));
|
|
5148
|
+
totalIssuesFound++;
|
|
5149
|
+
});
|
|
5150
|
+
}
|
|
5151
|
+
|
|
5152
|
+
let fixed = content;
|
|
5153
|
+
let fixesApplied = 0;
|
|
5154
|
+
|
|
5155
|
+
if (this.config.autoFixHeadings) {
|
|
5156
|
+
const fixResult = this.fixHeadingStructureInContent(content, analysis);
|
|
5157
|
+
fixed = fixResult.content;
|
|
5158
|
+
fixesApplied = fixResult.fixes;
|
|
5159
|
+
totalFixesApplied += fixesApplied;
|
|
5160
|
+
|
|
5161
|
+
if (fixesApplied > 0) {
|
|
5162
|
+
console.log(chalk.green(`โ
Fixed heading structure in: ${file} (${fixesApplied} fixes)`));
|
|
5163
|
+
}
|
|
5164
|
+
}
|
|
5165
|
+
|
|
5166
|
+
if (fixed !== content) {
|
|
5167
|
+
if (this.config.backupFiles) {
|
|
5168
|
+
await fs.writeFile(`${file}.backup`, content);
|
|
5169
|
+
}
|
|
5170
|
+
|
|
5171
|
+
if (!this.config.dryRun) {
|
|
5172
|
+
await fs.writeFile(file, fixed);
|
|
5173
|
+
}
|
|
5174
|
+
|
|
5175
|
+
results.push({
|
|
5176
|
+
file,
|
|
5177
|
+
status: 'fixed',
|
|
5178
|
+
issues: analysis.issues.length,
|
|
5179
|
+
fixes: fixesApplied
|
|
5180
|
+
});
|
|
5181
|
+
} else {
|
|
5182
|
+
results.push({
|
|
5183
|
+
file,
|
|
5184
|
+
status: 'no-change',
|
|
5185
|
+
issues: analysis.issues.length,
|
|
5186
|
+
fixes: 0
|
|
5187
|
+
});
|
|
5188
|
+
}
|
|
5189
|
+
} catch (error) {
|
|
5190
|
+
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
5191
|
+
results.push({ file, status: 'error', error: error.message });
|
|
5101
5192
|
}
|
|
5102
|
-
return files;
|
|
5103
5193
|
}
|
|
5104
5194
|
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5195
|
+
console.log(chalk.blue(`\n๐ Summary: Found ${totalIssuesFound} heading issues across ${results.length} files`));
|
|
5196
|
+
if (this.config.autoFixHeadings) {
|
|
5197
|
+
console.log(chalk.blue(` Fixed ${totalFixesApplied} heading issues automatically`));
|
|
5198
|
+
} else {
|
|
5199
|
+
console.log(chalk.gray('๐ก Use --auto-fix-headings option to enable automatic fixes'));
|
|
5200
|
+
}
|
|
5201
|
+
|
|
5202
|
+
return results;
|
|
5203
|
+
}
|
|
5204
|
+
|
|
5205
|
+
/**
|
|
5206
|
+
* Analyze heading structure for issues
|
|
5207
|
+
*/
|
|
5208
|
+
analyzeHeadingStructure(content) {
|
|
5209
|
+
const issues = [];
|
|
5210
|
+
const headings = [];
|
|
5211
|
+
|
|
5212
|
+
// Extract all headings with their positions and context
|
|
5213
|
+
const headingRegex = /<h([1-6])([^>]*)>(.*?)<\/h[1-6]>/gi;
|
|
5214
|
+
let match;
|
|
5215
|
+
let headingIndex = 0;
|
|
5216
|
+
|
|
5217
|
+
while ((match = headingRegex.exec(content)) !== null) {
|
|
5218
|
+
headingIndex++;
|
|
5219
|
+
const level = parseInt(match[1]);
|
|
5220
|
+
const attributes = match[2];
|
|
5221
|
+
const text = match[3].replace(/<[^>]*>/g, '').trim();
|
|
5222
|
+
const fullMatch = match[0];
|
|
5223
|
+
const position = match.index;
|
|
5108
5224
|
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5225
|
+
// Find the section context (look for parent elements)
|
|
5226
|
+
const beforeContent = content.substring(0, position);
|
|
5227
|
+
const sectionContext = this.findSectionContext(beforeContent);
|
|
5228
|
+
|
|
5229
|
+
headings.push({
|
|
5230
|
+
level,
|
|
5231
|
+
text,
|
|
5232
|
+
attributes,
|
|
5233
|
+
fullMatch,
|
|
5234
|
+
position,
|
|
5235
|
+
index: headingIndex,
|
|
5236
|
+
sectionContext
|
|
5237
|
+
});
|
|
5238
|
+
}
|
|
5239
|
+
|
|
5240
|
+
if (headings.length === 0) {
|
|
5241
|
+
return { issues, headings };
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5244
|
+
// Check for multiple h1 elements
|
|
5245
|
+
const h1Elements = headings.filter(h => h.level === 1);
|
|
5246
|
+
if (h1Elements.length > 1) {
|
|
5247
|
+
issues.push({
|
|
5248
|
+
type: 'Multiple h1 elements',
|
|
5249
|
+
description: `Found ${h1Elements.length} h1 elements, should have only one`,
|
|
5250
|
+
suggestion: 'Convert extra h1 elements to h2-h6 as appropriate',
|
|
5251
|
+
severity: 'error',
|
|
5252
|
+
headings: h1Elements,
|
|
5253
|
+
fix: 'convert-extra-h1'
|
|
5254
|
+
});
|
|
5255
|
+
} else if (h1Elements.length === 0) {
|
|
5256
|
+
issues.push({
|
|
5257
|
+
type: 'Missing h1 element',
|
|
5258
|
+
description: 'Page should have exactly one h1 element',
|
|
5259
|
+
suggestion: 'Convert the first heading to h1 or add an h1 element',
|
|
5260
|
+
severity: 'error',
|
|
5261
|
+
fix: 'add-missing-h1'
|
|
5262
|
+
});
|
|
5263
|
+
}
|
|
5264
|
+
|
|
5265
|
+
// Check for heading level skipping
|
|
5266
|
+
for (let i = 1; i < headings.length; i++) {
|
|
5267
|
+
const current = headings[i];
|
|
5268
|
+
const previous = headings[i - 1];
|
|
5269
|
+
|
|
5270
|
+
if (current.level > previous.level + 1) {
|
|
5271
|
+
issues.push({
|
|
5272
|
+
type: 'Heading level skip',
|
|
5273
|
+
description: `Heading level jumps from h${previous.level} to h${current.level}`,
|
|
5274
|
+
suggestion: `Use h${previous.level + 1} instead of h${current.level}`,
|
|
5275
|
+
severity: 'warning',
|
|
5276
|
+
heading: current,
|
|
5277
|
+
previousHeading: previous,
|
|
5278
|
+
fix: 'fix-level-skip'
|
|
5279
|
+
});
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5282
|
+
|
|
5283
|
+
// Check for empty headings
|
|
5284
|
+
headings.forEach(heading => {
|
|
5285
|
+
if (!heading.text || heading.text.length === 0) {
|
|
5286
|
+
issues.push({
|
|
5287
|
+
type: 'Empty heading',
|
|
5288
|
+
description: `Heading ${heading.index} (h${heading.level}) is empty`,
|
|
5289
|
+
suggestion: 'Add descriptive text to the heading or remove it',
|
|
5290
|
+
severity: 'warning',
|
|
5291
|
+
heading,
|
|
5292
|
+
fix: 'fix-empty-heading'
|
|
5293
|
+
});
|
|
5294
|
+
}
|
|
5295
|
+
});
|
|
5296
|
+
|
|
5297
|
+
// Check for duplicate headings in the same section
|
|
5298
|
+
const sectionGroups = {};
|
|
5299
|
+
headings.forEach(heading => {
|
|
5300
|
+
const sectionKey = heading.sectionContext || 'root';
|
|
5301
|
+
if (!sectionGroups[sectionKey]) {
|
|
5302
|
+
sectionGroups[sectionKey] = [];
|
|
5303
|
+
}
|
|
5304
|
+
sectionGroups[sectionKey].push(heading);
|
|
5305
|
+
});
|
|
5306
|
+
|
|
5307
|
+
Object.entries(sectionGroups).forEach(([section, sectionHeadings]) => {
|
|
5308
|
+
const textGroups = {};
|
|
5309
|
+
sectionHeadings.forEach(heading => {
|
|
5310
|
+
if (heading.text) {
|
|
5311
|
+
const normalizedText = heading.text.toLowerCase().trim();
|
|
5312
|
+
if (!textGroups[normalizedText]) {
|
|
5313
|
+
textGroups[normalizedText] = [];
|
|
5314
|
+
}
|
|
5315
|
+
textGroups[normalizedText].push(heading);
|
|
5316
|
+
}
|
|
5317
|
+
});
|
|
5318
|
+
|
|
5319
|
+
Object.entries(textGroups).forEach(([text, duplicates]) => {
|
|
5320
|
+
if (duplicates.length > 1 && duplicates[0].level === duplicates[1].level) {
|
|
5321
|
+
issues.push({
|
|
5322
|
+
type: 'Duplicate heading',
|
|
5323
|
+
description: `Duplicate h${duplicates[0].level} heading: "${text}"`,
|
|
5324
|
+
suggestion: 'Make heading text unique or merge content',
|
|
5325
|
+
severity: 'warning',
|
|
5326
|
+
headings: duplicates,
|
|
5327
|
+
section,
|
|
5328
|
+
fix: 'fix-duplicate-heading'
|
|
5329
|
+
});
|
|
5330
|
+
}
|
|
5331
|
+
});
|
|
5332
|
+
});
|
|
5333
|
+
|
|
5334
|
+
return { issues, headings };
|
|
5335
|
+
}
|
|
5336
|
+
|
|
5337
|
+
/**
|
|
5338
|
+
* Find section context for a heading
|
|
5339
|
+
*/
|
|
5340
|
+
findSectionContext(beforeContent) {
|
|
5341
|
+
// Look for common section elements
|
|
5342
|
+
const sectionPatterns = [
|
|
5343
|
+
/<section[^>]*>/gi,
|
|
5344
|
+
/<article[^>]*>/gi,
|
|
5345
|
+
/<main[^>]*>/gi,
|
|
5346
|
+
/<div[^>]*class[^>]*section[^>]*>/gi,
|
|
5347
|
+
/<div[^>]*id[^>]*section[^>]*>/gi
|
|
5348
|
+
];
|
|
5349
|
+
|
|
5350
|
+
let lastSectionMatch = null;
|
|
5351
|
+
let lastSectionPosition = -1;
|
|
5352
|
+
|
|
5353
|
+
sectionPatterns.forEach(pattern => {
|
|
5354
|
+
let match;
|
|
5355
|
+
pattern.lastIndex = 0; // Reset regex
|
|
5356
|
+
while ((match = pattern.exec(beforeContent)) !== null) {
|
|
5357
|
+
if (match.index > lastSectionPosition) {
|
|
5358
|
+
lastSectionPosition = match.index;
|
|
5359
|
+
lastSectionMatch = match[0];
|
|
5116
5360
|
}
|
|
5117
5361
|
}
|
|
5362
|
+
});
|
|
5363
|
+
|
|
5364
|
+
return lastSectionMatch ? `section_${lastSectionPosition}` : 'root';
|
|
5365
|
+
}
|
|
5366
|
+
|
|
5367
|
+
/**
|
|
5368
|
+
* Fix heading structure issues in content
|
|
5369
|
+
*/
|
|
5370
|
+
fixHeadingStructureInContent(content, analysis) {
|
|
5371
|
+
let fixed = content;
|
|
5372
|
+
let fixesApplied = 0;
|
|
5373
|
+
|
|
5374
|
+
if (!analysis || !analysis.issues) {
|
|
5375
|
+
return { content: fixed, fixes: fixesApplied };
|
|
5118
5376
|
}
|
|
5119
5377
|
|
|
5120
|
-
|
|
5121
|
-
|
|
5378
|
+
// Sort issues by position (from end to start to avoid position shifts)
|
|
5379
|
+
const sortedIssues = analysis.issues
|
|
5380
|
+
.filter(issue => issue.fix)
|
|
5381
|
+
.sort((a, b) => {
|
|
5382
|
+
const posA = a.heading?.position || a.headings?.[0]?.position || 0;
|
|
5383
|
+
const posB = b.heading?.position || b.headings?.[0]?.position || 0;
|
|
5384
|
+
return posB - posA;
|
|
5385
|
+
});
|
|
5386
|
+
|
|
5387
|
+
sortedIssues.forEach(issue => {
|
|
5388
|
+
switch (issue.fix) {
|
|
5389
|
+
case 'convert-extra-h1':
|
|
5390
|
+
// Convert extra h1 elements to h2
|
|
5391
|
+
if (issue.headings && issue.headings.length > 1) {
|
|
5392
|
+
// Keep the first h1, convert others to h2
|
|
5393
|
+
for (let i = 1; i < issue.headings.length; i++) {
|
|
5394
|
+
const heading = issue.headings[i];
|
|
5395
|
+
const newHeading = heading.fullMatch.replace(/h1/gi, 'h2');
|
|
5396
|
+
fixed = fixed.replace(heading.fullMatch, newHeading);
|
|
5397
|
+
console.log(chalk.yellow(` ๐ Converted extra h1 to h2: "${heading.text}"`));
|
|
5398
|
+
fixesApplied++;
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
break;
|
|
5402
|
+
|
|
5403
|
+
case 'add-missing-h1':
|
|
5404
|
+
// Convert the first heading to h1
|
|
5405
|
+
if (analysis.headings && analysis.headings.length > 0) {
|
|
5406
|
+
const firstHeading = analysis.headings[0];
|
|
5407
|
+
const newHeading = firstHeading.fullMatch.replace(
|
|
5408
|
+
new RegExp(`h${firstHeading.level}`, 'gi'),
|
|
5409
|
+
'h1'
|
|
5410
|
+
);
|
|
5411
|
+
fixed = fixed.replace(firstHeading.fullMatch, newHeading);
|
|
5412
|
+
console.log(chalk.yellow(` ๐ Converted first heading to h1: "${firstHeading.text}"`));
|
|
5413
|
+
fixesApplied++;
|
|
5414
|
+
}
|
|
5415
|
+
break;
|
|
5416
|
+
|
|
5417
|
+
case 'fix-level-skip':
|
|
5418
|
+
// Fix heading level skipping
|
|
5419
|
+
if (issue.heading && issue.previousHeading) {
|
|
5420
|
+
const correctLevel = issue.previousHeading.level + 1;
|
|
5421
|
+
const newHeading = issue.heading.fullMatch.replace(
|
|
5422
|
+
new RegExp(`h${issue.heading.level}`, 'gi'),
|
|
5423
|
+
`h${correctLevel}`
|
|
5424
|
+
);
|
|
5425
|
+
fixed = fixed.replace(issue.heading.fullMatch, newHeading);
|
|
5426
|
+
console.log(chalk.yellow(` ๐ Fixed level skip: h${issue.heading.level} โ h${correctLevel} for "${issue.heading.text}"`));
|
|
5427
|
+
fixesApplied++;
|
|
5428
|
+
}
|
|
5429
|
+
break;
|
|
5430
|
+
|
|
5431
|
+
case 'fix-empty-heading':
|
|
5432
|
+
// Add text to empty headings based on context
|
|
5433
|
+
if (issue.heading) {
|
|
5434
|
+
const contextText = this.generateHeadingText(issue.heading, content);
|
|
5435
|
+
const newHeading = issue.heading.fullMatch.replace(
|
|
5436
|
+
/(<h[1-6][^>]*>)(\s*)(.*?)(\s*)(<\/h[1-6]>)/i,
|
|
5437
|
+
`$1${contextText}$5`
|
|
5438
|
+
);
|
|
5439
|
+
fixed = fixed.replace(issue.heading.fullMatch, newHeading);
|
|
5440
|
+
console.log(chalk.yellow(` ๐ Added text to empty heading: "${contextText}"`));
|
|
5441
|
+
fixesApplied++;
|
|
5442
|
+
}
|
|
5443
|
+
break;
|
|
5444
|
+
|
|
5445
|
+
case 'fix-duplicate-heading':
|
|
5446
|
+
// Make duplicate headings unique by adding numbers
|
|
5447
|
+
if (issue.headings && issue.headings.length > 1) {
|
|
5448
|
+
for (let i = 1; i < issue.headings.length; i++) {
|
|
5449
|
+
const heading = issue.headings[i];
|
|
5450
|
+
const newText = `${heading.text} (${i + 1})`;
|
|
5451
|
+
const newHeading = heading.fullMatch.replace(
|
|
5452
|
+
heading.text,
|
|
5453
|
+
newText
|
|
5454
|
+
);
|
|
5455
|
+
fixed = fixed.replace(heading.fullMatch, newHeading);
|
|
5456
|
+
console.log(chalk.yellow(` ๐ Made duplicate heading unique: "${newText}"`));
|
|
5457
|
+
fixesApplied++;
|
|
5458
|
+
}
|
|
5459
|
+
}
|
|
5460
|
+
break;
|
|
5461
|
+
}
|
|
5462
|
+
});
|
|
5463
|
+
|
|
5464
|
+
return { content: fixed, fixes: fixesApplied };
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5467
|
+
/**
|
|
5468
|
+
* Generate appropriate text for empty headings based on context
|
|
5469
|
+
*/
|
|
5470
|
+
generateHeadingText(heading, content) {
|
|
5471
|
+
// Look for context around the heading
|
|
5472
|
+
const position = heading.position;
|
|
5473
|
+
const beforeText = content.substring(Math.max(0, position - 200), position);
|
|
5474
|
+
const afterText = content.substring(position + heading.fullMatch.length, position + heading.fullMatch.length + 200);
|
|
5475
|
+
|
|
5476
|
+
// Extract meaningful words from surrounding content
|
|
5477
|
+
const contextWords = (beforeText + ' ' + afterText)
|
|
5478
|
+
.replace(/<[^>]*>/g, ' ')
|
|
5479
|
+
.replace(/[^\w\s]/g, ' ')
|
|
5480
|
+
.split(/\s+/)
|
|
5481
|
+
.filter(word => word.length > 3 && !/^\d+$/.test(word))
|
|
5482
|
+
.slice(0, 3);
|
|
5483
|
+
|
|
5484
|
+
if (contextWords.length > 0) {
|
|
5485
|
+
return contextWords.join(' ');
|
|
5486
|
+
}
|
|
5487
|
+
|
|
5488
|
+
// Fallback based on heading level
|
|
5489
|
+
const levelNames = {
|
|
5490
|
+
1: 'Main Content',
|
|
5491
|
+
2: 'Section',
|
|
5492
|
+
3: 'Subsection',
|
|
5493
|
+
4: 'Details',
|
|
5494
|
+
5: 'Information',
|
|
5495
|
+
6: 'Notes'
|
|
5496
|
+
};
|
|
5497
|
+
|
|
5498
|
+
return levelNames[heading.level] || 'Content';
|
|
5499
|
+
}
|
|
5500
|
+
|
|
5501
|
+
async findHtmlFiles(directory) {
|
|
5502
|
+
const files = [];
|
|
5503
|
+
|
|
5504
|
+
// Check if the path is a file or directory
|
|
5505
|
+
const stat = await fs.stat(directory);
|
|
5506
|
+
|
|
5507
|
+
if (stat.isFile()) {
|
|
5508
|
+
// If it's a file, check if it's HTML
|
|
5509
|
+
if (directory.endsWith('.html')) {
|
|
5510
|
+
files.push(directory);
|
|
5511
|
+
}
|
|
5512
|
+
return files;
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5515
|
+
// If it's a directory, scan recursively
|
|
5516
|
+
async function scan(dir) {
|
|
5517
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
5518
|
+
|
|
5519
|
+
for (const entry of entries) {
|
|
5520
|
+
const fullPath = path.join(dir, entry.name);
|
|
5521
|
+
|
|
5522
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
5523
|
+
await scan(fullPath);
|
|
5524
|
+
} else if (entry.isFile() && entry.name.endsWith('.html')) {
|
|
5525
|
+
files.push(fullPath);
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
|
|
5530
|
+
await scan(directory);
|
|
5531
|
+
return files;
|
|
5532
|
+
}
|
|
5533
|
+
|
|
5534
|
+
// Check for unused files in the project
|
|
5535
|
+
async checkUnusedFiles(directory = '.') {
|
|
5536
|
+
console.log(chalk.blue('๐๏ธ Checking for unused files...'));
|
|
5537
|
+
|
|
5538
|
+
const results = [];
|
|
5539
|
+
const allFiles = await this.findAllProjectFiles(directory);
|
|
5540
|
+
const referencedFiles = await this.findReferencedFiles(directory);
|
|
5541
|
+
|
|
5542
|
+
// Normalize paths for comparison
|
|
5543
|
+
const normalizedReferenced = new Set();
|
|
5544
|
+
referencedFiles.forEach(file => {
|
|
5545
|
+
normalizedReferenced.add(path.resolve(file));
|
|
5546
|
+
});
|
|
5547
|
+
|
|
5548
|
+
for (const file of allFiles) {
|
|
5549
|
+
const absolutePath = path.resolve(file);
|
|
5550
|
+
|
|
5551
|
+
// Skip certain files that are typically not referenced directly
|
|
5552
|
+
if (this.shouldSkipUnusedCheck(file)) {
|
|
5553
|
+
continue;
|
|
5554
|
+
}
|
|
5555
|
+
|
|
5556
|
+
if (!normalizedReferenced.has(absolutePath)) {
|
|
5557
|
+
const relativePath = path.relative(directory, file);
|
|
5558
|
+
const fileType = this.getFileType(file);
|
|
5559
|
+
|
|
5560
|
+
console.log(chalk.yellow(` ๐๏ธ Unused ${fileType}: ${relativePath}`));
|
|
5561
|
+
|
|
5562
|
+
results.push({
|
|
5563
|
+
type: `๐๏ธ Unused ${fileType}`,
|
|
5564
|
+
description: `File not referenced anywhere: ${relativePath}`,
|
|
5565
|
+
suggestion: `Consider removing if truly unused: ${relativePath}`,
|
|
5566
|
+
filePath: file,
|
|
5567
|
+
fileType: fileType,
|
|
5568
|
+
relativePath: relativePath
|
|
5569
|
+
});
|
|
5570
|
+
}
|
|
5571
|
+
}
|
|
5572
|
+
|
|
5573
|
+
console.log(chalk.blue(`\n๐ Summary: Found ${results.length} potentially unused files`));
|
|
5574
|
+
console.log(chalk.gray('๐ก Review carefully before removing - some files may be referenced dynamically'));
|
|
5575
|
+
|
|
5576
|
+
return results;
|
|
5577
|
+
}
|
|
5578
|
+
|
|
5579
|
+
async findAllProjectFiles(directory) {
|
|
5580
|
+
const files = [];
|
|
5581
|
+
const extensions = ['.html', '.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.ico', '.pdf', '.mp4', '.webm', '.mp3', '.wav'];
|
|
5582
|
+
|
|
5583
|
+
async function scan(dir) {
|
|
5584
|
+
try {
|
|
5585
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
5586
|
+
|
|
5587
|
+
for (const entry of entries) {
|
|
5588
|
+
const fullPath = path.join(dir, entry.name);
|
|
5589
|
+
|
|
5590
|
+
if (entry.isDirectory()) {
|
|
5591
|
+
// Skip common directories that shouldn't be analyzed
|
|
5592
|
+
if (!this.shouldSkipDirectory(entry.name)) {
|
|
5593
|
+
await scan(fullPath);
|
|
5594
|
+
}
|
|
5595
|
+
} else if (entry.isFile()) {
|
|
5596
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
5597
|
+
if (extensions.includes(ext)) {
|
|
5598
|
+
files.push(fullPath);
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
}
|
|
5602
|
+
} catch (error) {
|
|
5603
|
+
// Skip directories we can't read
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
|
|
5607
|
+
await scan.call(this, directory);
|
|
5608
|
+
return files;
|
|
5609
|
+
}
|
|
5610
|
+
|
|
5611
|
+
async findReferencedFiles(directory) {
|
|
5612
|
+
const referenced = new Set();
|
|
5613
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
5614
|
+
const cssFiles = await this.findCssFiles(directory);
|
|
5615
|
+
const jsFiles = await this.findJsFiles(directory);
|
|
5616
|
+
|
|
5617
|
+
// Find references in HTML files
|
|
5618
|
+
for (const htmlFile of htmlFiles) {
|
|
5619
|
+
try {
|
|
5620
|
+
const content = await fs.readFile(htmlFile, 'utf8');
|
|
5621
|
+
const refs = this.extractFileReferences(content, path.dirname(htmlFile));
|
|
5622
|
+
refs.forEach(ref => referenced.add(ref));
|
|
5623
|
+
} catch (error) {
|
|
5624
|
+
// Skip files we can't read
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
|
|
5628
|
+
// Find references in CSS files
|
|
5629
|
+
for (const cssFile of cssFiles) {
|
|
5630
|
+
try {
|
|
5631
|
+
const content = await fs.readFile(cssFile, 'utf8');
|
|
5632
|
+
const refs = this.extractCssReferences(content, path.dirname(cssFile));
|
|
5633
|
+
refs.forEach(ref => referenced.add(ref));
|
|
5634
|
+
} catch (error) {
|
|
5635
|
+
// Skip files we can't read
|
|
5636
|
+
}
|
|
5637
|
+
}
|
|
5638
|
+
|
|
5639
|
+
// Find references in JS files
|
|
5640
|
+
for (const jsFile of jsFiles) {
|
|
5641
|
+
try {
|
|
5642
|
+
const content = await fs.readFile(jsFile, 'utf8');
|
|
5643
|
+
const refs = this.extractJsReferences(content, path.dirname(jsFile));
|
|
5644
|
+
refs.forEach(ref => referenced.add(ref));
|
|
5645
|
+
} catch (error) {
|
|
5646
|
+
// Skip files we can't read
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5649
|
+
|
|
5650
|
+
return Array.from(referenced);
|
|
5651
|
+
}
|
|
5652
|
+
|
|
5653
|
+
extractFileReferences(content, baseDir) {
|
|
5654
|
+
const references = [];
|
|
5655
|
+
|
|
5656
|
+
// HTML patterns for file references
|
|
5657
|
+
const patterns = [
|
|
5658
|
+
// Images
|
|
5659
|
+
/<img[^>]*src\s*=\s*["']([^"']+)["']/gi,
|
|
5660
|
+
// Links (CSS, other files)
|
|
5661
|
+
/<link[^>]*href\s*=\s*["']([^"']+)["']/gi,
|
|
5662
|
+
// Scripts
|
|
5663
|
+
/<script[^>]*src\s*=\s*["']([^"']+)["']/gi,
|
|
5664
|
+
// Anchors
|
|
5665
|
+
/<a[^>]*href\s*=\s*["']([^"']+)["']/gi,
|
|
5666
|
+
// Video/Audio
|
|
5667
|
+
/<(?:video|audio)[^>]*src\s*=\s*["']([^"']+)["']/gi,
|
|
5668
|
+
// Object/Embed
|
|
5669
|
+
/<(?:object|embed)[^>]*src\s*=\s*["']([^"']+)["']/gi,
|
|
5670
|
+
// Iframe
|
|
5671
|
+
/<iframe[^>]*src\s*=\s*["']([^"']+)["']/gi,
|
|
5672
|
+
// Meta (for icons)
|
|
5673
|
+
/<meta[^>]*content\s*=\s*["']([^"']+\.(ico|png|jpg|jpeg|svg))["']/gi
|
|
5674
|
+
];
|
|
5675
|
+
|
|
5676
|
+
for (const pattern of patterns) {
|
|
5677
|
+
let match;
|
|
5678
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
5679
|
+
const url = match[1];
|
|
5680
|
+
if (this.isLocalFile(url)) {
|
|
5681
|
+
const resolvedPath = this.resolveFilePath(url, baseDir);
|
|
5682
|
+
if (resolvedPath) {
|
|
5683
|
+
references.push(resolvedPath);
|
|
5684
|
+
}
|
|
5685
|
+
}
|
|
5686
|
+
}
|
|
5687
|
+
}
|
|
5688
|
+
|
|
5689
|
+
return references;
|
|
5690
|
+
}
|
|
5691
|
+
|
|
5692
|
+
extractCssReferences(content, baseDir) {
|
|
5693
|
+
const references = [];
|
|
5694
|
+
|
|
5695
|
+
// CSS patterns for file references
|
|
5696
|
+
const patterns = [
|
|
5697
|
+
// url() function
|
|
5698
|
+
/url\s*\(\s*["']?([^"')]+)["']?\s*\)/gi,
|
|
5699
|
+
// @import
|
|
5700
|
+
/@import\s+["']([^"']+)["']/gi
|
|
5701
|
+
];
|
|
5702
|
+
|
|
5703
|
+
for (const pattern of patterns) {
|
|
5704
|
+
let match;
|
|
5705
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
5706
|
+
const url = match[1];
|
|
5707
|
+
if (this.isLocalFile(url)) {
|
|
5708
|
+
const resolvedPath = this.resolveFilePath(url, baseDir);
|
|
5709
|
+
if (resolvedPath) {
|
|
5710
|
+
references.push(resolvedPath);
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
}
|
|
5715
|
+
|
|
5716
|
+
return references;
|
|
5717
|
+
}
|
|
5718
|
+
|
|
5719
|
+
extractJsReferences(content, baseDir) {
|
|
5720
|
+
const references = [];
|
|
5721
|
+
|
|
5722
|
+
// JavaScript patterns for file references
|
|
5723
|
+
const patterns = [
|
|
5724
|
+
// require() calls
|
|
5725
|
+
/require\s*\(\s*["']([^"']+)["']\s*\)/gi,
|
|
5726
|
+
// import statements
|
|
5727
|
+
/import\s+.*?from\s+["']([^"']+)["']/gi,
|
|
5728
|
+
// fetch() calls with local files
|
|
5729
|
+
/fetch\s*\(\s*["']([^"']*\.(html|css|js|json|xml))["']\s*\)/gi,
|
|
5730
|
+
// XMLHttpRequest
|
|
5731
|
+
/\.open\s*\(\s*["'][^"']*["']\s*,\s*["']([^"']*\.(html|css|js|json|xml))["']/gi
|
|
5732
|
+
];
|
|
5733
|
+
|
|
5734
|
+
for (const pattern of patterns) {
|
|
5735
|
+
let match;
|
|
5736
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
5737
|
+
const url = match[1];
|
|
5738
|
+
if (this.isLocalFile(url)) {
|
|
5739
|
+
const resolvedPath = this.resolveFilePath(url, baseDir);
|
|
5740
|
+
if (resolvedPath) {
|
|
5741
|
+
references.push(resolvedPath);
|
|
5742
|
+
}
|
|
5743
|
+
}
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5746
|
+
|
|
5747
|
+
return references;
|
|
5748
|
+
}
|
|
5749
|
+
|
|
5750
|
+
async findCssFiles(directory) {
|
|
5751
|
+
const files = [];
|
|
5752
|
+
|
|
5753
|
+
async function scan(dir) {
|
|
5754
|
+
try {
|
|
5755
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
5756
|
+
|
|
5757
|
+
for (const entry of entries) {
|
|
5758
|
+
const fullPath = path.join(dir, entry.name);
|
|
5759
|
+
|
|
5760
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
5761
|
+
await scan(fullPath);
|
|
5762
|
+
} else if (entry.isFile() && entry.name.endsWith('.css')) {
|
|
5763
|
+
files.push(fullPath);
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5766
|
+
} catch (error) {
|
|
5767
|
+
// Skip directories we can't read
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
|
|
5771
|
+
await scan(directory);
|
|
5772
|
+
return files;
|
|
5773
|
+
}
|
|
5774
|
+
|
|
5775
|
+
async findJsFiles(directory) {
|
|
5776
|
+
const files = [];
|
|
5777
|
+
|
|
5778
|
+
async function scan(dir) {
|
|
5779
|
+
try {
|
|
5780
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
5781
|
+
|
|
5782
|
+
for (const entry of entries) {
|
|
5783
|
+
const fullPath = path.join(dir, entry.name);
|
|
5784
|
+
|
|
5785
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
5786
|
+
await scan(fullPath);
|
|
5787
|
+
} else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.mjs'))) {
|
|
5788
|
+
files.push(fullPath);
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
} catch (error) {
|
|
5792
|
+
// Skip directories we can't read
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
|
|
5796
|
+
await scan(directory);
|
|
5797
|
+
return files;
|
|
5798
|
+
}
|
|
5799
|
+
|
|
5800
|
+
shouldSkipDirectory(dirName) {
|
|
5801
|
+
const skipDirs = [
|
|
5802
|
+
'node_modules', '.git', '.svn', '.hg', 'bower_components',
|
|
5803
|
+
'vendor', 'tmp', 'temp', '.cache', 'dist', 'build', 'coverage',
|
|
5804
|
+
'.nyc_output', '.vscode', '.idea', '__pycache__', '.DS_Store'
|
|
5805
|
+
];
|
|
5806
|
+
return skipDirs.includes(dirName) || dirName.startsWith('.');
|
|
5807
|
+
}
|
|
5808
|
+
|
|
5809
|
+
shouldSkipUnusedCheck(filePath) {
|
|
5810
|
+
const fileName = path.basename(filePath);
|
|
5811
|
+
const dirName = path.basename(path.dirname(filePath));
|
|
5812
|
+
|
|
5813
|
+
// Skip certain file types and patterns
|
|
5814
|
+
const skipPatterns = [
|
|
5815
|
+
// Common files that might not be directly referenced
|
|
5816
|
+
/^(index|main|app)\.(html|js|css)$/i,
|
|
5817
|
+
/^(readme|license|changelog)/i,
|
|
5818
|
+
/^package\.json$/i,
|
|
5819
|
+
/^\.gitignore$/i,
|
|
5820
|
+
/^favicon\.(ico|png)$/i,
|
|
5821
|
+
/^robots\.txt$/i,
|
|
5822
|
+
/^sitemap\.xml$/i,
|
|
5823
|
+
// Backup files
|
|
5824
|
+
/\.backup$/i,
|
|
5825
|
+
// Test directories
|
|
5826
|
+
/test|spec|__tests__/i
|
|
5827
|
+
];
|
|
5828
|
+
|
|
5829
|
+
return skipPatterns.some(pattern =>
|
|
5830
|
+
pattern.test(fileName) || pattern.test(dirName)
|
|
5831
|
+
);
|
|
5832
|
+
}
|
|
5833
|
+
|
|
5834
|
+
getFileType(filePath) {
|
|
5835
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
5836
|
+
const typeMap = {
|
|
5837
|
+
'.html': 'HTML',
|
|
5838
|
+
'.css': 'CSS',
|
|
5839
|
+
'.js': 'JavaScript',
|
|
5840
|
+
'.mjs': 'JavaScript Module',
|
|
5841
|
+
'.jpg': 'Image',
|
|
5842
|
+
'.jpeg': 'Image',
|
|
5843
|
+
'.png': 'Image',
|
|
5844
|
+
'.gif': 'Image',
|
|
5845
|
+
'.svg': 'SVG',
|
|
5846
|
+
'.webp': 'Image',
|
|
5847
|
+
'.ico': 'Icon',
|
|
5848
|
+
'.pdf': 'PDF',
|
|
5849
|
+
'.mp4': 'Video',
|
|
5850
|
+
'.webm': 'Video',
|
|
5851
|
+
'.mp3': 'Audio',
|
|
5852
|
+
'.wav': 'Audio'
|
|
5853
|
+
};
|
|
5854
|
+
|
|
5855
|
+
return typeMap[ext] || 'File';
|
|
5856
|
+
}
|
|
5857
|
+
|
|
5858
|
+
isLocalFile(url) {
|
|
5859
|
+
return !url.startsWith('http://') &&
|
|
5860
|
+
!url.startsWith('https://') &&
|
|
5861
|
+
!url.startsWith('//') &&
|
|
5862
|
+
!url.startsWith('data:') &&
|
|
5863
|
+
!url.startsWith('mailto:') &&
|
|
5864
|
+
!url.startsWith('tel:') &&
|
|
5865
|
+
!url.startsWith('#');
|
|
5866
|
+
}
|
|
5867
|
+
|
|
5868
|
+
resolveFilePath(url, baseDir) {
|
|
5869
|
+
try {
|
|
5870
|
+
let filePath;
|
|
5871
|
+
|
|
5872
|
+
if (url.startsWith('/')) {
|
|
5873
|
+
// Absolute path from web root
|
|
5874
|
+
filePath = path.join(baseDir, url.substring(1));
|
|
5875
|
+
} else {
|
|
5876
|
+
// Relative path
|
|
5877
|
+
filePath = path.resolve(baseDir, url);
|
|
5878
|
+
}
|
|
5879
|
+
|
|
5880
|
+
return filePath;
|
|
5881
|
+
} catch (error) {
|
|
5882
|
+
return null;
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
|
|
5886
|
+
// Check for dead code in CSS and JavaScript files
|
|
5887
|
+
async checkDeadCode(directory = '.') {
|
|
5888
|
+
console.log(chalk.blue('โ ๏ธ Checking for dead code...'));
|
|
5889
|
+
|
|
5890
|
+
const results = [];
|
|
5891
|
+
|
|
5892
|
+
// Check CSS dead code
|
|
5893
|
+
const cssDeadCode = await this.checkDeadCss(directory);
|
|
5894
|
+
results.push(...cssDeadCode);
|
|
5895
|
+
|
|
5896
|
+
// Check JavaScript dead code
|
|
5897
|
+
const jsDeadCode = await this.checkDeadJs(directory);
|
|
5898
|
+
results.push(...jsDeadCode);
|
|
5899
|
+
|
|
5900
|
+
console.log(chalk.blue(`\n๐ Summary: Found ${results.length} potential dead code issues`));
|
|
5901
|
+
console.log(chalk.gray('๐ก Dead code analysis is heuristic - manual review recommended'));
|
|
5902
|
+
|
|
5903
|
+
return results;
|
|
5904
|
+
}
|
|
5905
|
+
|
|
5906
|
+
async checkDeadCss(directory) {
|
|
5907
|
+
console.log(chalk.cyan('๐จ Analyzing CSS dead code...'));
|
|
5908
|
+
|
|
5909
|
+
const results = [];
|
|
5910
|
+
const cssFiles = await this.findCssFiles(directory);
|
|
5911
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
5912
|
+
|
|
5913
|
+
// Get all HTML content to check against
|
|
5914
|
+
let allHtmlContent = '';
|
|
5915
|
+
for (const htmlFile of htmlFiles) {
|
|
5916
|
+
try {
|
|
5917
|
+
const content = await fs.readFile(htmlFile, 'utf8');
|
|
5918
|
+
allHtmlContent += content + '\n';
|
|
5919
|
+
} catch (error) {
|
|
5920
|
+
// Skip files we can't read
|
|
5921
|
+
}
|
|
5922
|
+
}
|
|
5923
|
+
|
|
5924
|
+
for (const cssFile of cssFiles) {
|
|
5925
|
+
try {
|
|
5926
|
+
const content = await fs.readFile(cssFile, 'utf8');
|
|
5927
|
+
const deadRules = this.findDeadCssRules(content, allHtmlContent);
|
|
5928
|
+
|
|
5929
|
+
if (deadRules.length > 0) {
|
|
5930
|
+
const relativePath = path.relative(directory, cssFile);
|
|
5931
|
+
console.log(chalk.cyan(`\n๐ ${relativePath}:`));
|
|
5932
|
+
|
|
5933
|
+
deadRules.forEach(rule => {
|
|
5934
|
+
console.log(chalk.yellow(` โ ๏ธ Potentially dead CSS: ${rule.selector}`));
|
|
5935
|
+
results.push({
|
|
5936
|
+
type: 'โ ๏ธ Dead CSS rule',
|
|
5937
|
+
description: `CSS selector not found in HTML: ${rule.selector}`,
|
|
5938
|
+
suggestion: `Consider removing unused CSS rule: ${rule.selector}`,
|
|
5939
|
+
filePath: cssFile,
|
|
5940
|
+
relativePath: relativePath,
|
|
5941
|
+
selector: rule.selector,
|
|
5942
|
+
lineNumber: rule.lineNumber
|
|
5943
|
+
});
|
|
5944
|
+
});
|
|
5945
|
+
}
|
|
5946
|
+
|
|
5947
|
+
} catch (error) {
|
|
5948
|
+
console.error(chalk.red(`โ Error analyzing CSS ${cssFile}: ${error.message}`));
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5951
|
+
|
|
5952
|
+
return results;
|
|
5953
|
+
}
|
|
5954
|
+
|
|
5955
|
+
findDeadCssRules(cssContent, htmlContent) {
|
|
5956
|
+
const deadRules = [];
|
|
5957
|
+
|
|
5958
|
+
// Simple CSS parser to extract selectors
|
|
5959
|
+
const cssRules = this.parseCssSelectors(cssContent);
|
|
5960
|
+
|
|
5961
|
+
for (const rule of cssRules) {
|
|
5962
|
+
if (this.isCssSelectorUsed(rule.selector, htmlContent)) {
|
|
5963
|
+
continue; // Selector is used
|
|
5964
|
+
}
|
|
5965
|
+
|
|
5966
|
+
// Skip certain selectors that are commonly dynamic
|
|
5967
|
+
if (this.shouldSkipCssSelector(rule.selector)) {
|
|
5968
|
+
continue;
|
|
5969
|
+
}
|
|
5970
|
+
|
|
5971
|
+
deadRules.push(rule);
|
|
5972
|
+
}
|
|
5973
|
+
|
|
5974
|
+
return deadRules;
|
|
5975
|
+
}
|
|
5976
|
+
|
|
5977
|
+
parseCssSelectors(cssContent) {
|
|
5978
|
+
const rules = [];
|
|
5979
|
+
const lines = cssContent.split('\n');
|
|
5980
|
+
let inRule = false;
|
|
5981
|
+
|
|
5982
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5983
|
+
const line = lines[i].trim();
|
|
5984
|
+
|
|
5985
|
+
// Skip comments and empty lines
|
|
5986
|
+
if (line.startsWith('/*') || line.includes('*/') || !line) {
|
|
5987
|
+
continue;
|
|
5988
|
+
}
|
|
5989
|
+
|
|
5990
|
+
// Skip @rules like @media, @keyframes
|
|
5991
|
+
if (line.startsWith('@')) {
|
|
5992
|
+
continue;
|
|
5993
|
+
}
|
|
5994
|
+
|
|
5995
|
+
// Check if we're entering a rule block
|
|
5996
|
+
if (line.includes('{')) {
|
|
5997
|
+
inRule = true;
|
|
5998
|
+
// Extract selector part before the {
|
|
5999
|
+
const selectorPart = line.split('{')[0].trim();
|
|
6000
|
+
if (selectorPart && !selectorPart.includes(':') && !selectorPart.includes(';')) {
|
|
6001
|
+
rules.push({
|
|
6002
|
+
selector: selectorPart,
|
|
6003
|
+
lineNumber: i + 1
|
|
6004
|
+
});
|
|
6005
|
+
}
|
|
6006
|
+
continue;
|
|
6007
|
+
}
|
|
6008
|
+
|
|
6009
|
+
// Check if we're exiting a rule block
|
|
6010
|
+
if (line.includes('}')) {
|
|
6011
|
+
inRule = false;
|
|
6012
|
+
continue;
|
|
6013
|
+
}
|
|
6014
|
+
|
|
6015
|
+
// If we're not in a rule and line doesn't contain CSS properties, it might be a selector
|
|
6016
|
+
if (!inRule && !line.includes(':') && !line.includes(';') && line.length > 0) {
|
|
6017
|
+
rules.push({
|
|
6018
|
+
selector: line,
|
|
6019
|
+
lineNumber: i + 1
|
|
6020
|
+
});
|
|
6021
|
+
}
|
|
6022
|
+
}
|
|
6023
|
+
|
|
6024
|
+
return rules;
|
|
6025
|
+
}
|
|
6026
|
+
|
|
6027
|
+
isCssSelectorUsed(selector, htmlContent) {
|
|
6028
|
+
// Simple heuristic checks
|
|
6029
|
+
|
|
6030
|
+
// Check for class selectors
|
|
6031
|
+
if (selector.startsWith('.')) {
|
|
6032
|
+
const className = selector.substring(1).split(/[:\s>+~]/)[0];
|
|
6033
|
+
return htmlContent.includes(`class="${className}"`) ||
|
|
6034
|
+
htmlContent.includes(`class='${className}'`) ||
|
|
6035
|
+
htmlContent.includes(`class=".*${className}.*"`) ||
|
|
6036
|
+
htmlContent.includes(`class='.*${className}.*'`);
|
|
6037
|
+
}
|
|
6038
|
+
|
|
6039
|
+
// Check for ID selectors
|
|
6040
|
+
if (selector.startsWith('#')) {
|
|
6041
|
+
const idName = selector.substring(1).split(/[:\s>+~]/)[0];
|
|
6042
|
+
return htmlContent.includes(`id="${idName}"`) ||
|
|
6043
|
+
htmlContent.includes(`id='${idName}'`);
|
|
6044
|
+
}
|
|
6045
|
+
|
|
6046
|
+
// Check for tag selectors
|
|
6047
|
+
const tagMatch = selector.match(/^([a-zA-Z]+)/);
|
|
6048
|
+
if (tagMatch) {
|
|
6049
|
+
const tagName = tagMatch[1].toLowerCase();
|
|
6050
|
+
return htmlContent.toLowerCase().includes(`<${tagName}`);
|
|
6051
|
+
}
|
|
6052
|
+
|
|
6053
|
+
return true; // Conservative approach - assume complex selectors are used
|
|
6054
|
+
}
|
|
6055
|
+
|
|
6056
|
+
shouldSkipCssSelector(selector) {
|
|
6057
|
+
const skipPatterns = [
|
|
6058
|
+
// Pseudo-classes and pseudo-elements
|
|
6059
|
+
/:hover|:focus|:active|:visited|:before|:after/,
|
|
6060
|
+
// Media queries and complex selectors
|
|
6061
|
+
/@media|@keyframes|@font-face/,
|
|
6062
|
+
// Dynamic classes that might be added by JavaScript
|
|
6063
|
+
/\.active|\.selected|\.hidden|\.show|\.hide/,
|
|
6064
|
+
// Common framework classes
|
|
6065
|
+
/\.btn|\.form|\.nav|\.modal|\.tooltip/
|
|
6066
|
+
];
|
|
6067
|
+
|
|
6068
|
+
return skipPatterns.some(pattern => pattern.test(selector));
|
|
6069
|
+
}
|
|
6070
|
+
|
|
6071
|
+
async checkDeadJs(directory) {
|
|
6072
|
+
console.log(chalk.cyan('๐ Analyzing JavaScript dead code...'));
|
|
6073
|
+
|
|
6074
|
+
const results = [];
|
|
6075
|
+
const jsFiles = await this.findJsFiles(directory);
|
|
6076
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
6077
|
+
|
|
6078
|
+
// Get all HTML content to check against
|
|
6079
|
+
let allHtmlContent = '';
|
|
6080
|
+
for (const htmlFile of htmlFiles) {
|
|
6081
|
+
try {
|
|
6082
|
+
const content = await fs.readFile(htmlFile, 'utf8');
|
|
6083
|
+
allHtmlContent += content + '\n';
|
|
6084
|
+
} catch (error) {
|
|
6085
|
+
// Skip files we can't read
|
|
6086
|
+
}
|
|
6087
|
+
}
|
|
6088
|
+
|
|
6089
|
+
for (const jsFile of jsFiles) {
|
|
6090
|
+
try {
|
|
6091
|
+
const content = await fs.readFile(jsFile, 'utf8');
|
|
6092
|
+
const deadCode = this.findDeadJsCode(content, allHtmlContent, jsFile);
|
|
6093
|
+
|
|
6094
|
+
if (deadCode.length > 0) {
|
|
6095
|
+
const relativePath = path.relative(directory, jsFile);
|
|
6096
|
+
console.log(chalk.cyan(`\n๐ ${relativePath}:`));
|
|
6097
|
+
|
|
6098
|
+
deadCode.forEach(code => {
|
|
6099
|
+
console.log(chalk.yellow(` โ ๏ธ Potentially dead JS: ${code.name}`));
|
|
6100
|
+
results.push({
|
|
6101
|
+
type: 'โ ๏ธ Dead JavaScript',
|
|
6102
|
+
description: `${code.type} not referenced: ${code.name}`,
|
|
6103
|
+
suggestion: `Consider removing unused ${code.type.toLowerCase()}: ${code.name}`,
|
|
6104
|
+
filePath: jsFile,
|
|
6105
|
+
relativePath: relativePath,
|
|
6106
|
+
name: code.name,
|
|
6107
|
+
codeType: code.type,
|
|
6108
|
+
lineNumber: code.lineNumber
|
|
6109
|
+
});
|
|
6110
|
+
});
|
|
6111
|
+
}
|
|
6112
|
+
|
|
6113
|
+
} catch (error) {
|
|
6114
|
+
console.error(chalk.red(`โ Error analyzing JS ${jsFile}: ${error.message}`));
|
|
6115
|
+
}
|
|
6116
|
+
}
|
|
6117
|
+
|
|
6118
|
+
return results;
|
|
6119
|
+
}
|
|
6120
|
+
|
|
6121
|
+
findDeadJsCode(jsContent, htmlContent, jsFilePath) {
|
|
6122
|
+
const deadCode = [];
|
|
6123
|
+
|
|
6124
|
+
// Find function declarations
|
|
6125
|
+
const functions = this.parseJsFunctions(jsContent);
|
|
6126
|
+
const variables = this.parseJsVariables(jsContent);
|
|
6127
|
+
|
|
6128
|
+
// Check if functions are used
|
|
6129
|
+
for (const func of functions) {
|
|
6130
|
+
if (!this.isJsFunctionUsed(func.name, jsContent, htmlContent)) {
|
|
6131
|
+
deadCode.push({
|
|
6132
|
+
type: 'Function',
|
|
6133
|
+
name: func.name,
|
|
6134
|
+
lineNumber: func.lineNumber
|
|
6135
|
+
});
|
|
6136
|
+
}
|
|
6137
|
+
}
|
|
6138
|
+
|
|
6139
|
+
// Check if variables are used
|
|
6140
|
+
for (const variable of variables) {
|
|
6141
|
+
if (!this.isJsVariableUsed(variable.name, jsContent, htmlContent)) {
|
|
6142
|
+
deadCode.push({
|
|
6143
|
+
type: 'Variable',
|
|
6144
|
+
name: variable.name,
|
|
6145
|
+
lineNumber: variable.lineNumber
|
|
6146
|
+
});
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
|
|
6150
|
+
return deadCode;
|
|
6151
|
+
}
|
|
6152
|
+
|
|
6153
|
+
parseJsFunctions(jsContent) {
|
|
6154
|
+
const functions = [];
|
|
6155
|
+
const lines = jsContent.split('\n');
|
|
6156
|
+
|
|
6157
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6158
|
+
const line = lines[i];
|
|
6159
|
+
|
|
6160
|
+
// Function declarations
|
|
6161
|
+
const funcMatch = line.match(/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/);
|
|
6162
|
+
if (funcMatch) {
|
|
6163
|
+
functions.push({
|
|
6164
|
+
name: funcMatch[1],
|
|
6165
|
+
lineNumber: i + 1
|
|
6166
|
+
});
|
|
6167
|
+
}
|
|
6168
|
+
|
|
6169
|
+
// Arrow functions assigned to variables
|
|
6170
|
+
const arrowMatch = line.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*\([^)]*\)\s*=>/);
|
|
6171
|
+
if (arrowMatch) {
|
|
6172
|
+
functions.push({
|
|
6173
|
+
name: arrowMatch[1],
|
|
6174
|
+
lineNumber: i + 1
|
|
6175
|
+
});
|
|
6176
|
+
}
|
|
6177
|
+
}
|
|
6178
|
+
|
|
6179
|
+
return functions;
|
|
6180
|
+
}
|
|
6181
|
+
|
|
6182
|
+
parseJsVariables(jsContent) {
|
|
6183
|
+
const variables = [];
|
|
6184
|
+
const lines = jsContent.split('\n');
|
|
6185
|
+
|
|
6186
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6187
|
+
const line = lines[i];
|
|
6188
|
+
|
|
6189
|
+
// Variable declarations
|
|
6190
|
+
const varMatch = line.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
6191
|
+
if (varMatch && !line.includes('=') && !line.includes('function')) {
|
|
6192
|
+
variables.push({
|
|
6193
|
+
name: varMatch[1],
|
|
6194
|
+
lineNumber: i + 1
|
|
6195
|
+
});
|
|
6196
|
+
}
|
|
6197
|
+
}
|
|
6198
|
+
|
|
6199
|
+
return variables;
|
|
6200
|
+
}
|
|
6201
|
+
|
|
6202
|
+
isJsFunctionUsed(functionName, jsContent, htmlContent) {
|
|
6203
|
+
// Check if function is called in JS
|
|
6204
|
+
const jsCallPattern = new RegExp(`${functionName}\\s*\\(`, 'g');
|
|
6205
|
+
if (jsCallPattern.test(jsContent)) {
|
|
6206
|
+
return true;
|
|
6207
|
+
}
|
|
6208
|
+
|
|
6209
|
+
// Check if function is referenced in HTML (onclick, etc.)
|
|
6210
|
+
const htmlCallPattern = new RegExp(`${functionName}\\s*\\(`, 'g');
|
|
6211
|
+
if (htmlCallPattern.test(htmlContent)) {
|
|
6212
|
+
return true;
|
|
6213
|
+
}
|
|
6214
|
+
|
|
6215
|
+
// Check for event handlers in HTML
|
|
6216
|
+
const eventPattern = new RegExp(`on\\w+\\s*=\\s*["'][^"']*${functionName}`, 'g');
|
|
6217
|
+
if (eventPattern.test(htmlContent)) {
|
|
6218
|
+
return true;
|
|
6219
|
+
}
|
|
6220
|
+
|
|
6221
|
+
return false;
|
|
6222
|
+
}
|
|
6223
|
+
|
|
6224
|
+
isJsVariableUsed(variableName, jsContent, htmlContent) {
|
|
6225
|
+
// Create pattern that excludes the declaration line
|
|
6226
|
+
const lines = jsContent.split('\n');
|
|
6227
|
+
let usageFound = false;
|
|
6228
|
+
|
|
6229
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6230
|
+
const line = lines[i];
|
|
6231
|
+
|
|
6232
|
+
// Skip the declaration line
|
|
6233
|
+
if (line.includes(`${variableName}`) &&
|
|
6234
|
+
(line.includes('const ') || line.includes('let ') || line.includes('var '))) {
|
|
6235
|
+
continue;
|
|
6236
|
+
}
|
|
6237
|
+
|
|
6238
|
+
// Check for usage
|
|
6239
|
+
const usagePattern = new RegExp(`\\b${variableName}\\b`);
|
|
6240
|
+
if (usagePattern.test(line)) {
|
|
6241
|
+
usageFound = true;
|
|
6242
|
+
break;
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
|
|
6246
|
+
return usageFound;
|
|
6247
|
+
}
|
|
6248
|
+
|
|
6249
|
+
// Check file sizes and suggest optimizations
|
|
6250
|
+
async checkFileSizes(directory = '.') {
|
|
6251
|
+
console.log(chalk.blue('๐ Analyzing file sizes and suggesting optimizations...'));
|
|
6252
|
+
|
|
6253
|
+
const results = [];
|
|
6254
|
+
const allFiles = await this.findAllProjectFiles(directory);
|
|
6255
|
+
const sizeThresholds = {
|
|
6256
|
+
image: 500 * 1024, // 500KB for images
|
|
6257
|
+
// css: 100 * 1024, // 100KB for CSS
|
|
6258
|
+
js: 200 * 1024, // 200KB for JavaScript
|
|
6259
|
+
// html: 50 * 1024, // 50KB for HTML
|
|
6260
|
+
video: 10 * 1024 * 1024, // 10MB for videos
|
|
6261
|
+
audio: 5 * 1024 * 1024, // 5MB for audio
|
|
6262
|
+
other: 1 * 1024 * 1024 // 1MB for other files
|
|
6263
|
+
};
|
|
6264
|
+
|
|
6265
|
+
let totalSize = 0;
|
|
6266
|
+
const fileSizes = [];
|
|
6267
|
+
|
|
6268
|
+
for (const file of allFiles) {
|
|
6269
|
+
try {
|
|
6270
|
+
const stats = await require('fs').promises.stat(file);
|
|
6271
|
+
const fileSize = stats.size;
|
|
6272
|
+
const fileType = this.getFileCategory(file);
|
|
6273
|
+
const relativePath = path.relative(directory, file);
|
|
6274
|
+
|
|
6275
|
+
totalSize += fileSize;
|
|
6276
|
+
fileSizes.push({
|
|
6277
|
+
path: file,
|
|
6278
|
+
relativePath: relativePath,
|
|
6279
|
+
size: fileSize,
|
|
6280
|
+
type: fileType,
|
|
6281
|
+
sizeFormatted: this.formatFileSize(fileSize)
|
|
6282
|
+
});
|
|
6283
|
+
|
|
6284
|
+
// Check if file exceeds threshold
|
|
6285
|
+
const threshold = sizeThresholds[fileType] || sizeThresholds.other;
|
|
6286
|
+
if (fileSize > threshold) {
|
|
6287
|
+
const suggestions = this.getSizeOptimizationSuggestions(file, fileSize, fileType);
|
|
6288
|
+
|
|
6289
|
+
console.log(chalk.yellow(` ๐ Large ${fileType}: ${relativePath} (${this.formatFileSize(fileSize)})`));
|
|
6290
|
+
suggestions.forEach(suggestion => {
|
|
6291
|
+
console.log(chalk.gray(` ๐ก ${suggestion}`));
|
|
6292
|
+
});
|
|
6293
|
+
|
|
6294
|
+
results.push({
|
|
6295
|
+
type: `๐ Large ${fileType}`,
|
|
6296
|
+
description: `File size ${this.formatFileSize(fileSize)} exceeds recommended ${this.formatFileSize(threshold)}`,
|
|
6297
|
+
filePath: file,
|
|
6298
|
+
relativePath: relativePath,
|
|
6299
|
+
size: fileSize,
|
|
6300
|
+
sizeFormatted: this.formatFileSize(fileSize),
|
|
6301
|
+
threshold: threshold,
|
|
6302
|
+
thresholdFormatted: this.formatFileSize(threshold),
|
|
6303
|
+
suggestions: suggestions,
|
|
6304
|
+
fileType: fileType
|
|
6305
|
+
});
|
|
6306
|
+
}
|
|
6307
|
+
} catch (error) {
|
|
6308
|
+
// Skip files we can't read
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
6311
|
+
|
|
6312
|
+
// Sort files by size (largest first)
|
|
6313
|
+
fileSizes.sort((a, b) => b.size - a.size);
|
|
6314
|
+
|
|
6315
|
+
// Get type breakdown for return data only
|
|
6316
|
+
const typeBreakdown = this.getFileSizeBreakdown(fileSizes);
|
|
6317
|
+
|
|
6318
|
+
console.log(chalk.blue(`\n๐ Summary: Found ${results.length} files that could be optimized`));
|
|
6319
|
+
console.log(chalk.gray('๐ก File size analysis is based on common best practices'));
|
|
6320
|
+
|
|
6321
|
+
return {
|
|
6322
|
+
largeFiles: results,
|
|
6323
|
+
allFiles: fileSizes,
|
|
6324
|
+
totalSize: totalSize,
|
|
6325
|
+
totalFiles: fileSizes.length,
|
|
6326
|
+
typeBreakdown: typeBreakdown
|
|
6327
|
+
};
|
|
6328
|
+
}
|
|
6329
|
+
|
|
6330
|
+
getFileCategory(filePath) {
|
|
6331
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
6332
|
+
|
|
6333
|
+
if (['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.ico', '.bmp'].includes(ext)) {
|
|
6334
|
+
return 'image';
|
|
6335
|
+
} else if (['.css', '.scss', '.sass', '.less'].includes(ext)) {
|
|
6336
|
+
return 'css';
|
|
6337
|
+
} else if (['.js', '.mjs', '.ts', '.jsx', '.tsx'].includes(ext)) {
|
|
6338
|
+
return 'js';
|
|
6339
|
+
} else if (['.html', '.htm', '.xhtml'].includes(ext)) {
|
|
6340
|
+
return 'html';
|
|
6341
|
+
} else if (['.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv'].includes(ext)) {
|
|
6342
|
+
return 'video';
|
|
6343
|
+
} else if (['.mp3', '.wav', '.ogg', '.aac', '.flac'].includes(ext)) {
|
|
6344
|
+
return 'audio';
|
|
6345
|
+
} else if (['.pdf', '.doc', '.docx', '.txt'].includes(ext)) {
|
|
6346
|
+
return 'document';
|
|
6347
|
+
} else if (['.zip', '.rar', '.tar', '.gz', '.7z'].includes(ext)) {
|
|
6348
|
+
return 'archive';
|
|
6349
|
+
} else if (['.ttf', '.woff', '.woff2', '.otf', '.eot'].includes(ext)) {
|
|
6350
|
+
return 'font';
|
|
6351
|
+
} else {
|
|
6352
|
+
return 'other';
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
|
|
6356
|
+
getFileIcon(fileType) {
|
|
6357
|
+
const icons = {
|
|
6358
|
+
image: '๐ผ๏ธ',
|
|
6359
|
+
css: '๐จ',
|
|
6360
|
+
js: '๐',
|
|
6361
|
+
html: '๐',
|
|
6362
|
+
video: '๐ฅ',
|
|
6363
|
+
audio: '๐ต',
|
|
6364
|
+
document: '๐',
|
|
6365
|
+
archive: '๐ฆ',
|
|
6366
|
+
font: '๐ค',
|
|
6367
|
+
other: '๐'
|
|
6368
|
+
};
|
|
6369
|
+
return icons[fileType] || '๐';
|
|
6370
|
+
}
|
|
6371
|
+
|
|
6372
|
+
formatFileSize(bytes) {
|
|
6373
|
+
if (bytes === 0) return '0 B';
|
|
6374
|
+
|
|
6375
|
+
const k = 1024;
|
|
6376
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
6377
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
6378
|
+
|
|
6379
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
6380
|
+
}
|
|
6381
|
+
|
|
6382
|
+
getSizeOptimizationSuggestions(filePath, fileSize, fileType) {
|
|
6383
|
+
const suggestions = [];
|
|
6384
|
+
const fileName = path.basename(filePath);
|
|
6385
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
6386
|
+
|
|
6387
|
+
switch (fileType) {
|
|
6388
|
+
case 'image':
|
|
6389
|
+
if (['.jpg', '.jpeg'].includes(ext)) {
|
|
6390
|
+
suggestions.push('Compress JPEG with tools like ImageOptim, TinyPNG, or jpegoptim');
|
|
6391
|
+
suggestions.push('Consider converting to WebP format for better compression');
|
|
6392
|
+
if (fileSize > 1024 * 1024) {
|
|
6393
|
+
suggestions.push('Resize image dimensions if currently larger than needed');
|
|
6394
|
+
}
|
|
6395
|
+
} else if (ext === '.png') {
|
|
6396
|
+
suggestions.push('Compress PNG with tools like OptiPNG, PNGCrush, or TinyPNG');
|
|
6397
|
+
suggestions.push('Consider converting to WebP for smaller size');
|
|
6398
|
+
suggestions.push('Use JPEG for photos if transparency not needed');
|
|
6399
|
+
} else if (ext === '.svg') {
|
|
6400
|
+
suggestions.push('Minify SVG code with SVGO or similar tools');
|
|
6401
|
+
suggestions.push('Remove unnecessary metadata and comments');
|
|
6402
|
+
} else if (ext === '.gif') {
|
|
6403
|
+
suggestions.push('Consider converting to WebP or MP4 for animations');
|
|
6404
|
+
suggestions.push('Reduce color palette if possible');
|
|
6405
|
+
}
|
|
6406
|
+
break;
|
|
6407
|
+
|
|
6408
|
+
case 'js':
|
|
6409
|
+
suggestions.push('Minify JavaScript code with tools like UglifyJS or Terser');
|
|
6410
|
+
suggestions.push('Enable gzip/brotli compression on your web server');
|
|
6411
|
+
suggestions.push('Consider code splitting for large bundles');
|
|
6412
|
+
suggestions.push('Remove unused code and dead code elimination');
|
|
6413
|
+
if (fileSize > 500 * 1024) {
|
|
6414
|
+
suggestions.push('Consider breaking into smaller modules');
|
|
6415
|
+
}
|
|
6416
|
+
break;
|
|
6417
|
+
|
|
6418
|
+
case 'css':
|
|
6419
|
+
suggestions.push('Minify CSS with tools like CleanCSS or cssnano');
|
|
6420
|
+
suggestions.push('Remove unused CSS rules (run --dead-code analysis)');
|
|
6421
|
+
suggestions.push('Enable gzip/brotli compression on your web server');
|
|
6422
|
+
suggestions.push('Consider using CSS-in-JS or CSS modules for better tree-shaking');
|
|
6423
|
+
break;
|
|
6424
|
+
|
|
6425
|
+
case 'html':
|
|
6426
|
+
suggestions.push('Minify HTML by removing whitespace and comments');
|
|
6427
|
+
suggestions.push('Enable gzip/brotli compression on your web server');
|
|
6428
|
+
suggestions.push('Inline critical CSS for above-the-fold content');
|
|
6429
|
+
break;
|
|
6430
|
+
|
|
6431
|
+
case 'video':
|
|
6432
|
+
suggestions.push('Compress video with H.264 or H.265 codecs');
|
|
6433
|
+
suggestions.push('Consider multiple quality versions (720p, 1080p)');
|
|
6434
|
+
suggestions.push('Use streaming formats like HLS or DASH for large videos');
|
|
6435
|
+
suggestions.push('Consider hosting on CDN or video platforms');
|
|
6436
|
+
break;
|
|
6437
|
+
|
|
6438
|
+
case 'audio':
|
|
6439
|
+
suggestions.push('Use compressed formats like MP3 or AAC instead of WAV');
|
|
6440
|
+
suggestions.push('Reduce bitrate if quality allows (128-192 kbps often sufficient)');
|
|
6441
|
+
suggestions.push('Consider streaming for long audio files');
|
|
6442
|
+
break;
|
|
6443
|
+
|
|
6444
|
+
case 'font':
|
|
6445
|
+
suggestions.push('Use WOFF2 format for better compression');
|
|
6446
|
+
suggestions.push('Subset fonts to include only needed characters');
|
|
6447
|
+
suggestions.push('Consider using system fonts or web font alternatives');
|
|
6448
|
+
break;
|
|
6449
|
+
|
|
6450
|
+
case 'document':
|
|
6451
|
+
if (ext === '.pdf') {
|
|
6452
|
+
suggestions.push('Compress PDF with tools like Ghostscript or online compressors');
|
|
6453
|
+
suggestions.push('Reduce image quality in PDF if acceptable');
|
|
6454
|
+
}
|
|
6455
|
+
break;
|
|
6456
|
+
|
|
6457
|
+
default:
|
|
6458
|
+
suggestions.push('Enable compression on your web server');
|
|
6459
|
+
suggestions.push('Consider if this file is necessary in production');
|
|
6460
|
+
}
|
|
6461
|
+
|
|
6462
|
+
// General suggestions for all large files
|
|
6463
|
+
if (fileSize > 1024 * 1024) {
|
|
6464
|
+
suggestions.push('Consider lazy loading if not needed immediately');
|
|
6465
|
+
suggestions.push('Use CDN for better delivery performance');
|
|
6466
|
+
}
|
|
6467
|
+
|
|
6468
|
+
return suggestions;
|
|
6469
|
+
}
|
|
6470
|
+
|
|
6471
|
+
getFileSizeBreakdown(fileSizes) {
|
|
6472
|
+
const breakdown = {};
|
|
6473
|
+
|
|
6474
|
+
fileSizes.forEach(file => {
|
|
6475
|
+
if (!breakdown[file.type]) {
|
|
6476
|
+
breakdown[file.type] = {
|
|
6477
|
+
count: 0,
|
|
6478
|
+
totalSize: 0
|
|
6479
|
+
};
|
|
6480
|
+
}
|
|
6481
|
+
breakdown[file.type].count++;
|
|
6482
|
+
breakdown[file.type].totalSize += file.size;
|
|
6483
|
+
});
|
|
6484
|
+
|
|
6485
|
+
// Sort by total size
|
|
6486
|
+
const sortedBreakdown = {};
|
|
6487
|
+
Object.entries(breakdown)
|
|
6488
|
+
.sort(([,a], [,b]) => b.totalSize - a.totalSize)
|
|
6489
|
+
.forEach(([key, value]) => {
|
|
6490
|
+
sortedBreakdown[key] = value;
|
|
6491
|
+
});
|
|
6492
|
+
|
|
6493
|
+
return sortedBreakdown;
|
|
5122
6494
|
}
|
|
5123
6495
|
}
|
|
5124
6496
|
|