gbu-accessibility-package 3.8.0 ā 3.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README-vi.md +16 -10
- package/README.md +3 -0
- package/bin/fix.js +140 -0
- package/bin/test.js +71 -0
- package/cli.js +23 -1
- package/lib/fixer.js +504 -104
- package/package.json +4 -5
- package/ENHANCED_ALT_FEATURES.md +0 -230
- package/PACKAGE_SUMMARY.md +0 -191
- package/demo/1mb-jpg-example-file.jpg +0 -0
- 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/dead-code-test.css +0 -68
- package/demo/dead-code-test.html +0 -36
- package/demo/dead-code-test.js +0 -77
- 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/heading-structure-test.html.backup +0 -60
- package/demo/large-file-demo.css +0 -213
- package/demo/nested-controls-test.html +0 -92
- package/demo/sample.html +0 -47
- package/demo/test-external-links.html +0 -26
- package/demo/unused-files-test.html +0 -31
- package/demo/unused-image.png +0 -1
- package/demo/unused-page.html +0 -11
- package/demo/unused-script.js +0 -12
- package/demo/unused-style.css +0 -10
- package/demo/very-large-file.js +0 -2
- package/example.js +0 -121
package/lib/fixer.js
CHANGED
|
@@ -1451,6 +1451,136 @@ class AccessibilityFixer {
|
|
|
1451
1451
|
return results;
|
|
1452
1452
|
}
|
|
1453
1453
|
|
|
1454
|
+
// Fix aria-labels for images and other elements
|
|
1455
|
+
async fixAriaLabels(directory = '.') {
|
|
1456
|
+
console.log(chalk.blue('š·ļø Fixing aria-label attributes...'));
|
|
1457
|
+
|
|
1458
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
1459
|
+
const results = [];
|
|
1460
|
+
let totalIssuesFound = 0;
|
|
1461
|
+
|
|
1462
|
+
for (const file of htmlFiles) {
|
|
1463
|
+
try {
|
|
1464
|
+
const content = await fs.readFile(file, 'utf8');
|
|
1465
|
+
const fixed = this.fixAriaLabelsInContent(content);
|
|
1466
|
+
|
|
1467
|
+
if (fixed.content !== content) {
|
|
1468
|
+
if (!this.config.dryRun) {
|
|
1469
|
+
if (this.config.backupFiles) {
|
|
1470
|
+
await fs.writeFile(`${file}.backup`, content);
|
|
1471
|
+
}
|
|
1472
|
+
await fs.writeFile(file, fixed.content);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
console.log(chalk.green(`ā
Fixed aria-label attributes in: ${file}`));
|
|
1476
|
+
totalIssuesFound += fixed.changes;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
results.push({ file, status: 'processed', changes: fixed.changes });
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
console.error(chalk.red(`ā Error processing ${file}: ${error.message}`));
|
|
1482
|
+
results.push({ file, status: 'error', error: error.message });
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
console.log(chalk.blue(`\nš Summary: Found ${totalIssuesFound} aria-label issues across ${results.length} files`));
|
|
1487
|
+
return results;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
fixAriaLabelsInContent(content) {
|
|
1491
|
+
let fixed = content;
|
|
1492
|
+
let changes = 0;
|
|
1493
|
+
|
|
1494
|
+
// Fix images - add aria-label from alt text
|
|
1495
|
+
fixed = fixed.replace(
|
|
1496
|
+
/<img([^>]*>)/gi,
|
|
1497
|
+
(match) => {
|
|
1498
|
+
// Check if aria-label already exists
|
|
1499
|
+
if (/aria-label\s*=/i.test(match)) {
|
|
1500
|
+
return match; // Return unchanged if aria-label already exists
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// Extract alt text to use for aria-label
|
|
1504
|
+
const altMatch = match.match(/alt\s*=\s*["']([^"']*)["']/i);
|
|
1505
|
+
if (altMatch && altMatch[1].trim()) {
|
|
1506
|
+
const altText = altMatch[1].trim();
|
|
1507
|
+
const updatedImg = match.replace(/(<img[^>]*?)(\s*>)/i, `$1 aria-label="${altText}"$2`);
|
|
1508
|
+
console.log(chalk.yellow(` š·ļø Added aria-label="${altText}" to image element`));
|
|
1509
|
+
changes++;
|
|
1510
|
+
return updatedImg;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
return match;
|
|
1514
|
+
}
|
|
1515
|
+
);
|
|
1516
|
+
|
|
1517
|
+
// Fix buttons without aria-label but with text content
|
|
1518
|
+
fixed = fixed.replace(
|
|
1519
|
+
/<button([^>]*>)(.*?)<\/button>/gi,
|
|
1520
|
+
(match, attributes, content) => {
|
|
1521
|
+
// Skip if aria-label already exists
|
|
1522
|
+
if (/aria-label\s*=/i.test(attributes)) {
|
|
1523
|
+
return match;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Extract text content for aria-label
|
|
1527
|
+
const textContent = content.replace(/<[^>]*>/g, '').trim();
|
|
1528
|
+
if (textContent) {
|
|
1529
|
+
const updatedButton = match.replace(/(<button[^>]*?)(\s*>)/i, `$1 aria-label="${textContent}"$2`);
|
|
1530
|
+
console.log(chalk.yellow(` š·ļø Added aria-label="${textContent}" to button element`));
|
|
1531
|
+
changes++;
|
|
1532
|
+
return updatedButton;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
return match;
|
|
1536
|
+
}
|
|
1537
|
+
);
|
|
1538
|
+
|
|
1539
|
+
// Fix links without aria-label but with text content
|
|
1540
|
+
fixed = fixed.replace(
|
|
1541
|
+
/<a([^>]*href[^>]*>)(.*?)<\/a>/gi,
|
|
1542
|
+
(match, attributes, content) => {
|
|
1543
|
+
// Skip if aria-label already exists
|
|
1544
|
+
if (/aria-label\s*=/i.test(attributes)) {
|
|
1545
|
+
return match;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// Skip if it's just text content (not generic)
|
|
1549
|
+
const textContent = content.replace(/<[^>]*>/g, '').trim();
|
|
1550
|
+
const genericTexts = ['link', 'click here', 'read more', 'more info', 'here', 'this'];
|
|
1551
|
+
|
|
1552
|
+
if (textContent && genericTexts.some(generic =>
|
|
1553
|
+
textContent.toLowerCase().includes(generic.toLowerCase()))) {
|
|
1554
|
+
// Extract meaningful context or use href for generic links
|
|
1555
|
+
const hrefMatch = attributes.match(/href\s*=\s*["']([^"']*)["']/i);
|
|
1556
|
+
if (hrefMatch && hrefMatch[1]) {
|
|
1557
|
+
const href = hrefMatch[1];
|
|
1558
|
+
let ariaLabel = textContent;
|
|
1559
|
+
|
|
1560
|
+
// Improve generic link text
|
|
1561
|
+
if (href.includes('mailto:')) {
|
|
1562
|
+
ariaLabel = `Email: ${href.replace('mailto:', '')}`;
|
|
1563
|
+
} else if (href.includes('tel:')) {
|
|
1564
|
+
ariaLabel = `Phone: ${href.replace('tel:', '')}`;
|
|
1565
|
+
} else if (href.startsWith('http')) {
|
|
1566
|
+
const domain = new URL(href).hostname;
|
|
1567
|
+
ariaLabel = `Link to ${domain}`;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
const updatedLink = match.replace(/(<a[^>]*?)(\s*>)/i, `$1 aria-label="${ariaLabel}"$2`);
|
|
1571
|
+
console.log(chalk.yellow(` š·ļø Added aria-label="${ariaLabel}" to link element`));
|
|
1572
|
+
changes++;
|
|
1573
|
+
return updatedLink;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
return match;
|
|
1578
|
+
}
|
|
1579
|
+
);
|
|
1580
|
+
|
|
1581
|
+
return { content: fixed, changes };
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1454
1584
|
async addMainLandmarks(directory = '.') {
|
|
1455
1585
|
console.log(chalk.yellow('šļø Main landmark detection (manual review required)...'));
|
|
1456
1586
|
|
|
@@ -2014,33 +2144,18 @@ class AccessibilityFixer {
|
|
|
2014
2144
|
// Fix picture elements with img children - move role from picture to img
|
|
2015
2145
|
fixed = this.fixPictureImgRoles(fixed);
|
|
2016
2146
|
|
|
2017
|
-
// Fix all images - add role="img"
|
|
2147
|
+
// Fix all images - add role="img" only
|
|
2018
2148
|
fixed = fixed.replace(
|
|
2019
2149
|
/<img([^>]*>)/gi,
|
|
2020
2150
|
(match) => {
|
|
2021
|
-
let updatedImg = match;
|
|
2022
|
-
let hasChanges = false;
|
|
2023
|
-
|
|
2024
2151
|
// Check if role attribute already exists
|
|
2025
2152
|
if (!/role\s*=/i.test(match)) {
|
|
2026
|
-
updatedImg =
|
|
2153
|
+
const updatedImg = match.replace(/(<img[^>]*?)(\s*>)/i, '$1 role="img"$2');
|
|
2027
2154
|
console.log(chalk.yellow(` š¼ļø Added role="img" to image element`));
|
|
2028
|
-
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
// Check if aria-label already exists
|
|
2032
|
-
if (!/aria-label\s*=/i.test(match)) {
|
|
2033
|
-
// Extract alt text to use for aria-label
|
|
2034
|
-
const altMatch = match.match(/alt\s*=\s*["']([^"']*)["']/i);
|
|
2035
|
-
if (altMatch && altMatch[1].trim()) {
|
|
2036
|
-
const altText = altMatch[1].trim();
|
|
2037
|
-
updatedImg = updatedImg.replace(/(<img[^>]*?)(\s*>)/i, `$1 aria-label="${altText}"$2`);
|
|
2038
|
-
console.log(chalk.yellow(` š·ļø Added aria-label="${altText}" to image element`));
|
|
2039
|
-
hasChanges = true;
|
|
2040
|
-
}
|
|
2155
|
+
return updatedImg;
|
|
2041
2156
|
}
|
|
2042
2157
|
|
|
2043
|
-
return
|
|
2158
|
+
return match;
|
|
2044
2159
|
}
|
|
2045
2160
|
);
|
|
2046
2161
|
|
|
@@ -3106,13 +3221,14 @@ class AccessibilityFixer {
|
|
|
3106
3221
|
async checkLocalFile(url, baseDir, resourceType, elementType) {
|
|
3107
3222
|
const path = require('path');
|
|
3108
3223
|
|
|
3109
|
-
// Handle
|
|
3224
|
+
// Handle different URL types
|
|
3110
3225
|
let filePath;
|
|
3111
3226
|
if (url.startsWith('/')) {
|
|
3112
|
-
// Absolute path from web root -
|
|
3113
|
-
|
|
3227
|
+
// Absolute path from web root - find project root and resolve from there
|
|
3228
|
+
const projectRoot = this.findProjectRoot(baseDir);
|
|
3229
|
+
filePath = path.join(projectRoot, url.substring(1));
|
|
3114
3230
|
} else {
|
|
3115
|
-
// Relative path
|
|
3231
|
+
// Relative path - resolve relative to current HTML file directory
|
|
3116
3232
|
filePath = path.resolve(baseDir, url);
|
|
3117
3233
|
}
|
|
3118
3234
|
|
|
@@ -3126,11 +3242,41 @@ class AccessibilityFixer {
|
|
|
3126
3242
|
suggestion: `Create the missing file or update the ${resourceType.toLowerCase()} path`,
|
|
3127
3243
|
url: url,
|
|
3128
3244
|
filePath: filePath,
|
|
3245
|
+
resolvedPath: filePath,
|
|
3129
3246
|
resourceType: resourceType
|
|
3130
3247
|
};
|
|
3131
3248
|
}
|
|
3132
3249
|
}
|
|
3133
3250
|
|
|
3251
|
+
// Helper method to find project root directory
|
|
3252
|
+
findProjectRoot(startDir) {
|
|
3253
|
+
const path = require('path');
|
|
3254
|
+
const fs = require('fs');
|
|
3255
|
+
|
|
3256
|
+
let currentDir = startDir;
|
|
3257
|
+
const root = path.parse(currentDir).root;
|
|
3258
|
+
|
|
3259
|
+
// Look for common project root indicators
|
|
3260
|
+
while (currentDir !== root) {
|
|
3261
|
+
// Check for package.json, .git, or other project indicators
|
|
3262
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
3263
|
+
const gitPath = path.join(currentDir, '.git');
|
|
3264
|
+
const nodeModulesPath = path.join(currentDir, 'node_modules');
|
|
3265
|
+
|
|
3266
|
+
if (fs.existsSync(packageJsonPath) || fs.existsSync(gitPath) || fs.existsSync(nodeModulesPath)) {
|
|
3267
|
+
return currentDir;
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
// Move up one directory
|
|
3271
|
+
const parentDir = path.dirname(currentDir);
|
|
3272
|
+
if (parentDir === currentDir) break; // Reached filesystem root
|
|
3273
|
+
currentDir = parentDir;
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
// If no project root found, use the original baseDir (fallback)
|
|
3277
|
+
return startDir;
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3134
3280
|
// Analyze headings (no auto-fix, only suggestions)
|
|
3135
3281
|
async analyzeHeadings(directory = '.') {
|
|
3136
3282
|
console.log(chalk.blue('š Analyzing heading structure...'));
|
|
@@ -4359,70 +4505,77 @@ class AccessibilityFixer {
|
|
|
4359
4505
|
const totalRoleIssues = roleResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4360
4506
|
results.steps.push({ step: 3, name: 'Role attributes', fixed: roleFixed, issues: totalRoleIssues });
|
|
4361
4507
|
|
|
4362
|
-
// Step 4:
|
|
4363
|
-
console.log(chalk.blue('
|
|
4508
|
+
// Step 4: Aria-label attributes
|
|
4509
|
+
console.log(chalk.blue('š·ļø Step 4: Aria-label attributes...'));
|
|
4510
|
+
const ariaResults = await this.fixAriaLabels(directory);
|
|
4511
|
+
const ariaFixed = ariaResults.filter(r => r.status === 'processed' && r.changes > 0).length;
|
|
4512
|
+
const totalAriaIssues = ariaResults.reduce((sum, r) => sum + (r.changes || 0), 0);
|
|
4513
|
+
results.steps.push({ step: 4, name: 'Aria-label attributes', fixed: ariaFixed, issues: totalAriaIssues });
|
|
4514
|
+
|
|
4515
|
+
// Step 5: Form labels
|
|
4516
|
+
console.log(chalk.blue('š Step 5: Form labels...'));
|
|
4364
4517
|
const formResults = await this.fixFormLabels(directory);
|
|
4365
4518
|
const formFixed = formResults.filter(r => r.status === 'fixed').length;
|
|
4366
4519
|
const totalFormIssues = formResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4367
|
-
results.steps.push({ step:
|
|
4520
|
+
results.steps.push({ step: 5, name: 'Form labels', fixed: formFixed, issues: totalFormIssues });
|
|
4368
4521
|
|
|
4369
|
-
// Step
|
|
4370
|
-
console.log(chalk.blue('šÆ Step
|
|
4522
|
+
// Step 6: Nested interactive controls (NEW!)
|
|
4523
|
+
console.log(chalk.blue('šÆ Step 6: Nested interactive controls...'));
|
|
4371
4524
|
const nestedResults = await this.fixNestedInteractiveControls(directory);
|
|
4372
4525
|
const nestedFixed = nestedResults.filter(r => r.status === 'fixed').length;
|
|
4373
4526
|
const totalNestedIssues = nestedResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4374
|
-
results.steps.push({ step:
|
|
4527
|
+
results.steps.push({ step: 6, name: 'Nested interactive controls', fixed: nestedFixed, issues: totalNestedIssues });
|
|
4375
4528
|
|
|
4376
|
-
// Step
|
|
4377
|
-
console.log(chalk.blue('š Step
|
|
4529
|
+
// Step 7: Button names
|
|
4530
|
+
console.log(chalk.blue('š Step 7: Button names...'));
|
|
4378
4531
|
const buttonResults = await this.fixButtonNames(directory);
|
|
4379
4532
|
const buttonFixed = buttonResults.filter(r => r.status === 'fixed').length;
|
|
4380
4533
|
const totalButtonIssues = buttonResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4381
|
-
results.steps.push({ step:
|
|
4534
|
+
results.steps.push({ step: 7, name: 'Button names', fixed: buttonFixed, issues: totalButtonIssues });
|
|
4382
4535
|
|
|
4383
|
-
// Step
|
|
4384
|
-
console.log(chalk.blue('š Step
|
|
4536
|
+
// Step 8: Link names
|
|
4537
|
+
console.log(chalk.blue('š Step 8: Link names...'));
|
|
4385
4538
|
const linkResults = await this.fixLinkNames(directory);
|
|
4386
4539
|
const linkFixed = linkResults.filter(r => r.status === 'fixed').length;
|
|
4387
4540
|
const totalLinkIssues = linkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4388
|
-
results.steps.push({ step:
|
|
4541
|
+
results.steps.push({ step: 8, name: 'Link names', fixed: linkFixed, issues: totalLinkIssues });
|
|
4389
4542
|
|
|
4390
|
-
// Step
|
|
4391
|
-
console.log(chalk.blue('šļø Step
|
|
4543
|
+
// Step 9: Landmarks
|
|
4544
|
+
console.log(chalk.blue('šļø Step 9: Landmarks...'));
|
|
4392
4545
|
const landmarkResults = await this.fixLandmarks(directory);
|
|
4393
4546
|
const landmarkFixed = landmarkResults.filter(r => r.status === 'fixed').length;
|
|
4394
4547
|
const totalLandmarkIssues = landmarkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4395
|
-
results.steps.push({ step:
|
|
4548
|
+
results.steps.push({ step: 9, name: 'Landmarks', fixed: landmarkFixed, issues: totalLandmarkIssues });
|
|
4396
4549
|
|
|
4397
|
-
// Step
|
|
4398
|
-
console.log(chalk.blue('š Step
|
|
4550
|
+
// Step 10: Heading analysis
|
|
4551
|
+
console.log(chalk.blue('š Step 10: Heading analysis...'));
|
|
4399
4552
|
const headingResults = await this.analyzeHeadings(directory);
|
|
4400
4553
|
const totalHeadingSuggestions = headingResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4401
|
-
results.steps.push({ step:
|
|
4554
|
+
results.steps.push({ step: 10, name: 'Heading analysis', suggestions: totalHeadingSuggestions });
|
|
4402
4555
|
console.log(chalk.gray('š” Heading issues require manual review and cannot be auto-fixed'));
|
|
4403
4556
|
|
|
4404
|
-
// Step
|
|
4405
|
-
console.log(chalk.blue('š Step
|
|
4557
|
+
// Step 11: Broken links and missing resources check
|
|
4558
|
+
console.log(chalk.blue('š Step 11a: External links check...'));
|
|
4406
4559
|
const brokenLinksResults = await this.checkBrokenLinks(directory);
|
|
4407
4560
|
const totalBrokenLinks = brokenLinksResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4408
|
-
results.steps.push({ step: '
|
|
4561
|
+
results.steps.push({ step: '11a', name: 'External links check', issues: totalBrokenLinks });
|
|
4409
4562
|
|
|
4410
|
-
console.log(chalk.blue('š Step
|
|
4563
|
+
console.log(chalk.blue('š Step 11b: Missing resources check...'));
|
|
4411
4564
|
const missingResourcesResults = await this.check404Resources(directory);
|
|
4412
4565
|
const totalMissingResources = missingResourcesResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4413
|
-
results.steps.push({ step: '
|
|
4566
|
+
results.steps.push({ step: '11b', name: 'Missing resources check', issues: totalMissingResources });
|
|
4414
4567
|
|
|
4415
4568
|
console.log(chalk.gray('š” Link and resource issues require manual review and cannot be auto-fixed'));
|
|
4416
4569
|
|
|
4417
|
-
// Step
|
|
4418
|
-
console.log(chalk.blue('š§¹ Step
|
|
4570
|
+
// Step 12: Cleanup duplicate roles
|
|
4571
|
+
console.log(chalk.blue('š§¹ Step 12: Cleanup duplicate roles...'));
|
|
4419
4572
|
const cleanupResults = await this.cleanupDuplicateRoles(directory);
|
|
4420
4573
|
const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
|
|
4421
|
-
results.steps.push({ step:
|
|
4574
|
+
results.steps.push({ step: 12, name: 'Cleanup duplicate roles', fixed: cleanupFixed });
|
|
4422
4575
|
|
|
4423
4576
|
// Calculate totals
|
|
4424
4577
|
results.totalFiles = Math.max(
|
|
4425
|
-
langResults.length, altResults.length, roleResults.length, formResults.length,
|
|
4578
|
+
langResults.length, altResults.length, roleResults.length, ariaResults.length, formResults.length,
|
|
4426
4579
|
nestedResults.length, buttonResults.length, linkResults.length, landmarkResults.length,
|
|
4427
4580
|
headingResults.length, brokenLinksResults.length, cleanupResults.length
|
|
4428
4581
|
);
|
|
@@ -5500,49 +5653,117 @@ class AccessibilityFixer {
|
|
|
5500
5653
|
return files;
|
|
5501
5654
|
}
|
|
5502
5655
|
|
|
5503
|
-
// Check for unused files in the project
|
|
5656
|
+
// Check for unused files in the project - enhanced for comprehensive project-wide scanning
|
|
5504
5657
|
async checkUnusedFiles(directory = '.') {
|
|
5505
|
-
console.log(chalk.blue('šļø
|
|
5658
|
+
console.log(chalk.blue('šļø Analyzing unused files across entire project...'));
|
|
5506
5659
|
|
|
5507
|
-
const
|
|
5508
|
-
const allFiles = await this.findAllProjectFiles(directory);
|
|
5509
|
-
const referencedFiles = await this.findReferencedFiles(directory);
|
|
5660
|
+
const startTime = Date.now();
|
|
5510
5661
|
|
|
5511
|
-
//
|
|
5512
|
-
const
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5662
|
+
// Find project root for comprehensive scanning
|
|
5663
|
+
const projectRoot = this.findProjectRoot(directory);
|
|
5664
|
+
console.log(chalk.gray(`š Project root: ${path.relative(process.cwd(), projectRoot) || '.'}`));
|
|
5665
|
+
|
|
5666
|
+
// Get all project files from root (comprehensive scan)
|
|
5667
|
+
const allFiles = await this.findAllProjectFiles(projectRoot);
|
|
5668
|
+
console.log(chalk.gray(`š Found ${allFiles.length} total files in project`));
|
|
5669
|
+
|
|
5670
|
+
// Get referenced files from entire project (not just target directory)
|
|
5671
|
+
const referencedFiles = await this.findReferencedFiles(projectRoot);
|
|
5672
|
+
console.log(chalk.gray(`š Found ${referencedFiles.size} referenced files`));
|
|
5673
|
+
|
|
5674
|
+
// Find unused files
|
|
5675
|
+
const unusedFiles = [];
|
|
5516
5676
|
|
|
5517
5677
|
for (const file of allFiles) {
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
// Skip certain files that are typically not referenced directly
|
|
5678
|
+
// Skip certain directories and files that shouldn't be considered "unused"
|
|
5521
5679
|
if (this.shouldSkipUnusedCheck(file)) {
|
|
5522
5680
|
continue;
|
|
5523
5681
|
}
|
|
5524
5682
|
|
|
5525
|
-
if
|
|
5526
|
-
|
|
5683
|
+
// Check if file is referenced anywhere in the project
|
|
5684
|
+
const relativePath = path.relative(projectRoot, file);
|
|
5685
|
+
const isReferenced = this.isFileReferenced(file, relativePath, referencedFiles, projectRoot);
|
|
5686
|
+
|
|
5687
|
+
if (!isReferenced) {
|
|
5688
|
+
const stats = await require('fs').promises.stat(file);
|
|
5689
|
+
const fileSize = this.formatFileSize(stats.size);
|
|
5527
5690
|
const fileType = this.getFileType(file);
|
|
5528
5691
|
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
relativePath: relativePath
|
|
5692
|
+
unusedFiles.push({
|
|
5693
|
+
path: file,
|
|
5694
|
+
relativePath: relativePath,
|
|
5695
|
+
size: stats.size,
|
|
5696
|
+
formattedSize: fileSize,
|
|
5697
|
+
type: fileType,
|
|
5698
|
+
description: `File not referenced anywhere in project: ${relativePath}`,
|
|
5699
|
+
suggestion: `Consider removing if truly unused: ${relativePath}`
|
|
5538
5700
|
});
|
|
5539
5701
|
}
|
|
5540
5702
|
}
|
|
5541
5703
|
|
|
5542
|
-
|
|
5543
|
-
|
|
5704
|
+
// Sort by size (largest first)
|
|
5705
|
+
unusedFiles.sort((a, b) => b.size - a.size);
|
|
5544
5706
|
|
|
5545
|
-
|
|
5707
|
+
// Display results
|
|
5708
|
+
if (unusedFiles.length === 0) {
|
|
5709
|
+
console.log(chalk.green('ā
No unused files found! All files are properly referenced.'));
|
|
5710
|
+
} else {
|
|
5711
|
+
console.log(chalk.yellow(`\nš Found ${unusedFiles.length} potentially unused files:`));
|
|
5712
|
+
|
|
5713
|
+
unusedFiles.forEach((file, index) => {
|
|
5714
|
+
const icon = this.getFileIcon(file.type);
|
|
5715
|
+
console.log(chalk.yellow(` ${index + 1}. ${icon} ${file.relativePath} (${file.formattedSize})`));
|
|
5716
|
+
});
|
|
5717
|
+
|
|
5718
|
+
const totalSize = unusedFiles.reduce((sum, file) => sum + file.size, 0);
|
|
5719
|
+
console.log(chalk.blue(`\nš Total unused file size: ${this.formatFileSize(totalSize)}`));
|
|
5720
|
+
console.log(chalk.gray('š” Review these files before deleting - some may be used dynamically or required for deployment'));
|
|
5721
|
+
}
|
|
5722
|
+
|
|
5723
|
+
const endTime = Date.now();
|
|
5724
|
+
console.log(chalk.gray(`ā±ļø Analysis completed in ${endTime - startTime}ms`));
|
|
5725
|
+
|
|
5726
|
+
return {
|
|
5727
|
+
unusedFiles: unusedFiles,
|
|
5728
|
+
totalFiles: allFiles.length,
|
|
5729
|
+
referencedFiles: referencedFiles.size,
|
|
5730
|
+
unusedCount: unusedFiles.length,
|
|
5731
|
+
totalUnusedSize: unusedFiles.reduce((sum, file) => sum + file.size, 0)
|
|
5732
|
+
};
|
|
5733
|
+
}
|
|
5734
|
+
|
|
5735
|
+
// Enhanced file reference checking
|
|
5736
|
+
isFileReferenced(filePath, relativePath, referencedFiles, projectRoot) {
|
|
5737
|
+
// Check various possible reference formats
|
|
5738
|
+
const possibleRefs = [
|
|
5739
|
+
relativePath, // relative/to/file.ext
|
|
5740
|
+
'/' + relativePath, // /relative/to/file.ext
|
|
5741
|
+
'./' + relativePath, // ./relative/to/file.ext
|
|
5742
|
+
'../' + relativePath, // ../relative/to/file.ext
|
|
5743
|
+
path.basename(filePath), // file.ext
|
|
5744
|
+
'/' + path.basename(filePath), // /file.ext
|
|
5745
|
+
relativePath.replace(/\\/g, '/'), // normalize windows paths
|
|
5746
|
+
'/' + relativePath.replace(/\\/g, '/'), // /normalized/path
|
|
5747
|
+
relativePath.replace(/^\.\//, ''), // remove leading ./
|
|
5748
|
+
relativePath.replace(/^\//, ''), // remove leading /
|
|
5749
|
+
];
|
|
5750
|
+
|
|
5751
|
+
// Check if any reference format exists
|
|
5752
|
+
for (const ref of possibleRefs) {
|
|
5753
|
+
if (referencedFiles.has(ref)) {
|
|
5754
|
+
return true;
|
|
5755
|
+
}
|
|
5756
|
+
}
|
|
5757
|
+
|
|
5758
|
+
// Check for partial matches (for dynamic imports)
|
|
5759
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
5760
|
+
for (const ref of referencedFiles) {
|
|
5761
|
+
if (ref.includes(fileName) || ref.includes(relativePath)) {
|
|
5762
|
+
return true;
|
|
5763
|
+
}
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5766
|
+
return false;
|
|
5546
5767
|
}
|
|
5547
5768
|
|
|
5548
5769
|
async findAllProjectFiles(directory) {
|
|
@@ -5577,46 +5798,103 @@ class AccessibilityFixer {
|
|
|
5577
5798
|
return files;
|
|
5578
5799
|
}
|
|
5579
5800
|
|
|
5801
|
+
// Enhanced reference finding across entire project
|
|
5580
5802
|
async findReferencedFiles(directory) {
|
|
5581
|
-
const
|
|
5582
|
-
const htmlFiles = await this.findHtmlFiles(directory);
|
|
5583
|
-
const cssFiles = await this.findCssFiles(directory);
|
|
5584
|
-
const jsFiles = await this.findJsFiles(directory);
|
|
5803
|
+
const referencedFiles = new Set();
|
|
5585
5804
|
|
|
5586
|
-
// Find
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
const content = await fs.readFile(htmlFile, 'utf8');
|
|
5590
|
-
const refs = this.extractFileReferences(content, path.dirname(htmlFile));
|
|
5591
|
-
refs.forEach(ref => referenced.add(ref));
|
|
5592
|
-
} catch (error) {
|
|
5593
|
-
// Skip files we can't read
|
|
5594
|
-
}
|
|
5595
|
-
}
|
|
5805
|
+
// Find all source files that could contain references
|
|
5806
|
+
const sourceFiles = await this.findSourceFiles(directory);
|
|
5807
|
+
console.log(chalk.gray(`š Scanning ${sourceFiles.length} source files for references...`));
|
|
5596
5808
|
|
|
5597
|
-
|
|
5598
|
-
for (const cssFile of cssFiles) {
|
|
5809
|
+
for (const sourceFile of sourceFiles) {
|
|
5599
5810
|
try {
|
|
5600
|
-
const content = await fs.readFile(
|
|
5601
|
-
const
|
|
5602
|
-
|
|
5811
|
+
const content = await fs.readFile(sourceFile, 'utf-8');
|
|
5812
|
+
const baseDir = path.dirname(sourceFile);
|
|
5813
|
+
|
|
5814
|
+
// Extract references based on file type
|
|
5815
|
+
const refs = this.extractAllReferences(content, baseDir, sourceFile);
|
|
5816
|
+
refs.forEach(ref => referencedFiles.add(ref));
|
|
5817
|
+
|
|
5603
5818
|
} catch (error) {
|
|
5604
|
-
|
|
5819
|
+
console.log(chalk.gray(`ā ļø Could not read ${sourceFile}: ${error.message}`));
|
|
5605
5820
|
}
|
|
5606
5821
|
}
|
|
5607
5822
|
|
|
5608
|
-
|
|
5609
|
-
|
|
5823
|
+
return referencedFiles;
|
|
5824
|
+
}
|
|
5825
|
+
|
|
5826
|
+
// Find all source files that could contain references
|
|
5827
|
+
async findSourceFiles(directory) {
|
|
5828
|
+
const sourceFiles = [];
|
|
5829
|
+
|
|
5830
|
+
const walk = async (dir) => {
|
|
5610
5831
|
try {
|
|
5611
|
-
const
|
|
5612
|
-
|
|
5613
|
-
|
|
5832
|
+
const files = await fs.readdir(dir);
|
|
5833
|
+
|
|
5834
|
+
for (const file of files) {
|
|
5835
|
+
const filePath = path.join(dir, file);
|
|
5836
|
+
const stat = await fs.stat(filePath);
|
|
5837
|
+
|
|
5838
|
+
if (stat.isDirectory()) {
|
|
5839
|
+
if (!this.shouldSkipDirectory(file)) {
|
|
5840
|
+
await walk(filePath);
|
|
5841
|
+
}
|
|
5842
|
+
} else {
|
|
5843
|
+
// Include files that could contain references
|
|
5844
|
+
const ext = path.extname(file).toLowerCase();
|
|
5845
|
+
if (['.html', '.htm', '.css', '.js', '.jsx', '.ts', '.tsx',
|
|
5846
|
+
'.vue', '.php', '.json', '.md', '.xml', '.svg'].includes(ext)) {
|
|
5847
|
+
sourceFiles.push(filePath);
|
|
5848
|
+
}
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5614
5851
|
} catch (error) {
|
|
5615
|
-
|
|
5852
|
+
console.log(chalk.gray(`ā ļø Could not read directory ${dir}: ${error.message}`));
|
|
5616
5853
|
}
|
|
5854
|
+
};
|
|
5855
|
+
|
|
5856
|
+
await walk(directory);
|
|
5857
|
+
return sourceFiles;
|
|
5858
|
+
}
|
|
5859
|
+
|
|
5860
|
+
// Enhanced reference extraction
|
|
5861
|
+
extractAllReferences(content, baseDir, sourceFile) {
|
|
5862
|
+
const references = new Set();
|
|
5863
|
+
|
|
5864
|
+
// Get file extension to determine extraction method
|
|
5865
|
+
const ext = path.extname(sourceFile).toLowerCase();
|
|
5866
|
+
|
|
5867
|
+
if (['.html', '.htm'].includes(ext)) {
|
|
5868
|
+
// HTML references
|
|
5869
|
+
const htmlRefs = this.extractFileReferences(content, baseDir);
|
|
5870
|
+
htmlRefs.forEach(ref => references.add(ref));
|
|
5871
|
+
|
|
5872
|
+
} else if (ext === '.css') {
|
|
5873
|
+
// CSS references
|
|
5874
|
+
const cssRefs = this.extractCssReferences(content, baseDir);
|
|
5875
|
+
cssRefs.forEach(ref => references.add(ref));
|
|
5876
|
+
|
|
5877
|
+
} else if (['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
|
|
5878
|
+
// JavaScript/TypeScript references
|
|
5879
|
+
const jsRefs = this.extractJsReferences(content, baseDir);
|
|
5880
|
+
jsRefs.forEach(ref => references.add(ref));
|
|
5881
|
+
|
|
5882
|
+
// Also extract import statements
|
|
5883
|
+
const importRefs = this.extractImportReferences(content, baseDir);
|
|
5884
|
+
importRefs.forEach(ref => references.add(ref));
|
|
5885
|
+
|
|
5886
|
+
} else if (ext === '.json') {
|
|
5887
|
+
// JSON references (like package.json, config files)
|
|
5888
|
+
const jsonRefs = this.extractJsonReferences(content, baseDir);
|
|
5889
|
+
jsonRefs.forEach(ref => references.add(ref));
|
|
5890
|
+
|
|
5891
|
+
} else {
|
|
5892
|
+
// Generic file references (for .md, .xml, etc.)
|
|
5893
|
+
const genericRefs = this.extractGenericReferences(content, baseDir);
|
|
5894
|
+
genericRefs.forEach(ref => references.add(ref));
|
|
5617
5895
|
}
|
|
5618
5896
|
|
|
5619
|
-
return Array.from(
|
|
5897
|
+
return Array.from(references);
|
|
5620
5898
|
}
|
|
5621
5899
|
|
|
5622
5900
|
extractFileReferences(content, baseDir) {
|
|
@@ -6461,6 +6739,128 @@ class AccessibilityFixer {
|
|
|
6461
6739
|
|
|
6462
6740
|
return sortedBreakdown;
|
|
6463
6741
|
}
|
|
6742
|
+
|
|
6743
|
+
// Extract import/require statements
|
|
6744
|
+
extractImportReferences(content, baseDir) {
|
|
6745
|
+
const references = [];
|
|
6746
|
+
|
|
6747
|
+
// ES6 imports and CommonJS requires
|
|
6748
|
+
const importPatterns = [
|
|
6749
|
+
/import\s+.*?\s+from\s+['"]([^'"]+)['"]/g,
|
|
6750
|
+
/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
6751
|
+
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
6752
|
+
/import\s+['"]([^'"]+)['"]/g
|
|
6753
|
+
];
|
|
6754
|
+
|
|
6755
|
+
importPatterns.forEach(pattern => {
|
|
6756
|
+
let match;
|
|
6757
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
6758
|
+
const importPath = match[1];
|
|
6759
|
+
|
|
6760
|
+
// Skip npm packages (don't start with . or /)
|
|
6761
|
+
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
|
6762
|
+
continue;
|
|
6763
|
+
}
|
|
6764
|
+
|
|
6765
|
+
// Resolve the import path
|
|
6766
|
+
const resolved = this.resolveImportPath(importPath, baseDir);
|
|
6767
|
+
if (resolved) {
|
|
6768
|
+
references.push(resolved);
|
|
6769
|
+
}
|
|
6770
|
+
}
|
|
6771
|
+
});
|
|
6772
|
+
|
|
6773
|
+
return references;
|
|
6774
|
+
}
|
|
6775
|
+
|
|
6776
|
+
// Extract JSON references
|
|
6777
|
+
extractJsonReferences(content, baseDir) {
|
|
6778
|
+
const references = [];
|
|
6779
|
+
|
|
6780
|
+
try {
|
|
6781
|
+
const json = JSON.parse(content);
|
|
6782
|
+
|
|
6783
|
+
// Extract file references from JSON values
|
|
6784
|
+
const extractFromObject = (obj) => {
|
|
6785
|
+
if (typeof obj === 'string') {
|
|
6786
|
+
// Check if it looks like a file path
|
|
6787
|
+
if (obj.includes('.') && (obj.includes('/') || obj.includes('\\'))) {
|
|
6788
|
+
const resolved = this.resolveFilePath(obj, baseDir);
|
|
6789
|
+
if (resolved) {
|
|
6790
|
+
references.push(resolved);
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
6793
|
+
} else if (typeof obj === 'object' && obj !== null) {
|
|
6794
|
+
Object.values(obj).forEach(value => {
|
|
6795
|
+
if (Array.isArray(value)) {
|
|
6796
|
+
value.forEach(extractFromObject);
|
|
6797
|
+
} else {
|
|
6798
|
+
extractFromObject(value);
|
|
6799
|
+
}
|
|
6800
|
+
});
|
|
6801
|
+
}
|
|
6802
|
+
};
|
|
6803
|
+
|
|
6804
|
+
extractFromObject(json);
|
|
6805
|
+
} catch (error) {
|
|
6806
|
+
// Invalid JSON, skip
|
|
6807
|
+
}
|
|
6808
|
+
|
|
6809
|
+
return references;
|
|
6810
|
+
}
|
|
6811
|
+
|
|
6812
|
+
// Extract generic file references
|
|
6813
|
+
extractGenericReferences(content, baseDir) {
|
|
6814
|
+
const references = [];
|
|
6815
|
+
|
|
6816
|
+
// Look for file-like patterns
|
|
6817
|
+
const patterns = [
|
|
6818
|
+
/['"]((?:\.{1,2}\/)?[^'"]*\.[a-zA-Z0-9]{1,5})['"]/g, // Quoted file paths
|
|
6819
|
+
/src\s*=\s*['"']([^'"']+)['"']/g, // src attributes
|
|
6820
|
+
/href\s*=\s*['"']([^'"']+)['"']/g, // href attributes
|
|
6821
|
+
/url\s*\(\s*['"']?([^'"')]+)['"']?\s*\)/g // CSS url()
|
|
6822
|
+
];
|
|
6823
|
+
|
|
6824
|
+
patterns.forEach(pattern => {
|
|
6825
|
+
let match;
|
|
6826
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
6827
|
+
const filePath = match[1];
|
|
6828
|
+
|
|
6829
|
+
if (this.isLocalFile(filePath)) {
|
|
6830
|
+
const resolved = this.resolveFilePath(filePath, baseDir);
|
|
6831
|
+
if (resolved) {
|
|
6832
|
+
references.push(resolved);
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
});
|
|
6837
|
+
|
|
6838
|
+
return references;
|
|
6839
|
+
}
|
|
6840
|
+
|
|
6841
|
+
// Resolve import paths (handle extensions and index files)
|
|
6842
|
+
resolveImportPath(importPath, baseDir) {
|
|
6843
|
+
const possiblePaths = [
|
|
6844
|
+
importPath,
|
|
6845
|
+
importPath + '.js',
|
|
6846
|
+
importPath + '.jsx',
|
|
6847
|
+
importPath + '.ts',
|
|
6848
|
+
importPath + '.tsx',
|
|
6849
|
+
importPath + '/index.js',
|
|
6850
|
+
importPath + '/index.jsx',
|
|
6851
|
+
importPath + '/index.ts',
|
|
6852
|
+
importPath + '/index.tsx'
|
|
6853
|
+
];
|
|
6854
|
+
|
|
6855
|
+
for (const possiblePath of possiblePaths) {
|
|
6856
|
+
const resolved = this.resolveFilePath(possiblePath, baseDir);
|
|
6857
|
+
if (resolved) {
|
|
6858
|
+
return resolved;
|
|
6859
|
+
}
|
|
6860
|
+
}
|
|
6861
|
+
|
|
6862
|
+
return null;
|
|
6863
|
+
}
|
|
6464
6864
|
}
|
|
6465
6865
|
|
|
6466
6866
|
module.exports = AccessibilityFixer;
|