uiaudit.js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/auditor.d.ts +11 -0
  2. package/dist/auditor.d.ts.map +1 -0
  3. package/dist/auditor.js +70 -0
  4. package/dist/auditor.js.map +1 -0
  5. package/dist/auditors/accessibility.d.ts +8 -0
  6. package/dist/auditors/accessibility.d.ts.map +1 -0
  7. package/dist/auditors/accessibility.js +1769 -0
  8. package/dist/auditors/accessibility.js.map +1 -0
  9. package/dist/auditors/performance.d.ts +8 -0
  10. package/dist/auditors/performance.d.ts.map +1 -0
  11. package/dist/auditors/performance.js +168 -0
  12. package/dist/auditors/performance.js.map +1 -0
  13. package/dist/auditors/seo.d.ts +8 -0
  14. package/dist/auditors/seo.d.ts.map +1 -0
  15. package/dist/auditors/seo.js +171 -0
  16. package/dist/auditors/seo.js.map +1 -0
  17. package/dist/cli.d.ts +10 -0
  18. package/dist/cli.d.ts.map +1 -0
  19. package/dist/cli.js +115 -0
  20. package/dist/cli.js.map +1 -0
  21. package/dist/index.d.ts +13 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +12 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/parser/index.d.ts +18 -0
  26. package/dist/parser/index.d.ts.map +1 -0
  27. package/dist/parser/index.js +87 -0
  28. package/dist/parser/index.js.map +1 -0
  29. package/dist/parser/traverse.d.ts +10 -0
  30. package/dist/parser/traverse.d.ts.map +1 -0
  31. package/dist/parser/traverse.js +13 -0
  32. package/dist/parser/traverse.js.map +1 -0
  33. package/dist/reporter/terminal.d.ts +3 -0
  34. package/dist/reporter/terminal.d.ts.map +1 -0
  35. package/dist/reporter/terminal.js +200 -0
  36. package/dist/reporter/terminal.js.map +1 -0
  37. package/dist/types.d.ts +38 -0
  38. package/dist/types.d.ts.map +1 -0
  39. package/dist/types.js +3 -0
  40. package/dist/types.js.map +1 -0
  41. package/package.json +39 -0
  42. package/src/auditor.ts +100 -0
  43. package/src/auditors/accessibility.ts +2125 -0
  44. package/src/auditors/performance.ts +212 -0
  45. package/src/auditors/seo.ts +212 -0
  46. package/src/cli.ts +162 -0
  47. package/src/index.ts +22 -0
  48. package/src/parser/index.ts +106 -0
  49. package/src/parser/traverse.ts +14 -0
  50. package/src/reporter/terminal.ts +247 -0
  51. package/src/types.ts +51 -0
  52. package/tsconfig.json +47 -0
@@ -0,0 +1,106 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as parser from '@babel/parser';
4
+ import type { File } from '@babel/types';
5
+
6
+ // ─── Types ───────────────────────────────────────────────────────────────────
7
+
8
+ export interface ParsedFile {
9
+ filePath: string;
10
+ ast: File;
11
+ source: string;
12
+ }
13
+
14
+ // ─── Constants ───────────────────────────────────────────────────────────────
15
+
16
+ const SUPPORTED_EXT = new Set(['.tsx', '.ts', '.jsx', '.js']);
17
+
18
+ // Directories we never descend into
19
+ const IGNORED_DIRS = new Set([
20
+ 'node_modules', '.next', 'dist', 'build', '.git',
21
+ '.cache', 'coverage', 'out', '.turbo', 'storybook-static',
22
+ ]);
23
+
24
+ // ─── Public API ──────────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Parse a single file with Babel.
28
+ * Returns null for unsupported extensions or parse errors (we never crash on
29
+ * a single bad file — we just skip it and continue).
30
+ */
31
+ export function parseFile(filePath: string): ParsedFile | null {
32
+ if (!SUPPORTED_EXT.has(path.extname(filePath))) return null;
33
+
34
+ try {
35
+ const source = fs.readFileSync(filePath, 'utf-8');
36
+ const ast = parser.parse(source, {
37
+ sourceType: 'module',
38
+ // Enable all syntax that appears in real React/Next.js projects
39
+ plugins: [
40
+ 'jsx',
41
+ 'typescript',
42
+ 'decorators-legacy',
43
+ 'classProperties',
44
+ 'classStaticBlock',
45
+ 'optionalChaining',
46
+ 'nullishCoalescingOperator',
47
+ 'importAssertions',
48
+ ],
49
+ });
50
+ return { filePath, ast, source };
51
+ } catch {
52
+ // Silently skip files that fail to parse.
53
+ // This happens with binary files, unusual encodings, or unsupported syntax.
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Recursively collect all supported source files under a target path.
60
+ * Target can be either a single file or a directory.
61
+ */
62
+ export function collectFiles(target: string): string[] {
63
+ const absTarget = path.resolve(target);
64
+
65
+ let stat: fs.Stats;
66
+ try {
67
+ stat = fs.statSync(absTarget);
68
+ } catch {
69
+ throw new Error(`Path not found: ${absTarget}`);
70
+ }
71
+
72
+ if (stat.isFile()) {
73
+ return SUPPORTED_EXT.has(path.extname(absTarget)) ? [absTarget] : [];
74
+ }
75
+
76
+ if (stat.isDirectory()) {
77
+ return walkDir(absTarget);
78
+ }
79
+
80
+ return [];
81
+ }
82
+
83
+ // ─── Private helpers ─────────────────────────────────────────────────────────
84
+
85
+ function walkDir(dir: string): string[] {
86
+ const results: string[] = [];
87
+
88
+ for (const entry of fs.readdirSync(dir)) {
89
+ if (IGNORED_DIRS.has(entry)) continue;
90
+
91
+ const fullPath = path.join(dir, entry);
92
+
93
+ try {
94
+ const entryStat = fs.statSync(fullPath);
95
+ if (entryStat.isDirectory()) {
96
+ results.push(...walkDir(fullPath));
97
+ } else if (SUPPORTED_EXT.has(path.extname(entry))) {
98
+ results.push(fullPath);
99
+ }
100
+ } catch {
101
+ // Skip files we can't stat (permission issues, broken symlinks, etc.)
102
+ }
103
+ }
104
+
105
+ return results;
106
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @babel/traverse has a well-known CommonJS interop quirk.
3
+ * Depending on the Node version and moduleResolution settings,
4
+ * the default export may land on `.default` or on the module itself.
5
+ * This file normalises that into a single reliable `traverse` export.
6
+ *
7
+ * All auditors import traverse from HERE, not directly from @babel/traverse.
8
+ */
9
+
10
+ import _traverse from '@babel/traverse';
11
+
12
+ export const traverse = ((typeof (_traverse as any).default === 'function'
13
+ ? (_traverse as any).default
14
+ : _traverse) as any) as (ast: any, visitors: any) => any;
@@ -0,0 +1,247 @@
1
+ import chalk from 'chalk';
2
+ import type { AuditReport, AuditResult, Issue, IssueImpact } from '../types.js';
3
+
4
+ // ─── Impact colour map ────────────────────────────────────────────────────────
5
+
6
+ const IMPACT_BADGE: Record<IssueImpact, string> = {
7
+ critical: chalk.bgRed.white.bold(' CRITICAL '),
8
+ major: chalk.bgYellow.black.bold(' MAJOR '),
9
+ minor: chalk.bgGray.white( ' MINOR '),
10
+ };
11
+
12
+ // ─── Public API ───────────────────────────────────────────────────────────────
13
+
14
+ export function renderTerminal(report: AuditReport): void {
15
+ console.log('');
16
+ printHeader(report);
17
+ printScoreSummary(report);
18
+
19
+ const { results } = report;
20
+ if (results.performance) printCategorySection(results.performance);
21
+ if (results.seo) printCategorySection(results.seo);
22
+ if (results.accessibility) printCategorySection(results.accessibility);
23
+
24
+ printCriticalSummary(report);
25
+ printFooter(report);
26
+ }
27
+
28
+ // ─── Header ──────────────────────────────────────────────────────────────────
29
+
30
+ function printHeader(report: AuditReport): void {
31
+ const W = 66;
32
+ const rule = '─'.repeat(W);
33
+
34
+ const centre = (text: string) => {
35
+ const pad = Math.max(0, Math.floor((W - text.length) / 2));
36
+ return ' '.repeat(pad) + text + ' '.repeat(Math.max(0, W - pad - text.length));
37
+ };
38
+
39
+ console.log(chalk.cyan(`┌${rule}┐`));
40
+ console.log(chalk.cyan('│') + chalk.bold(centre('🔍 UIAudit')) + chalk.cyan('│'));
41
+ console.log(chalk.cyan('│') + chalk.cyan(centre(truncate(report.target, W - 2))) + chalk.cyan('│'));
42
+ console.log(chalk.cyan('│') + chalk.dim(centre(new Date(report.timestamp).toLocaleString())) + chalk.cyan('│'));
43
+ console.log(chalk.cyan(`└${rule}┘`));
44
+ console.log('');
45
+ }
46
+
47
+ // ─── Score summary table ──────────────────────────────────────────────────────
48
+
49
+ function printScoreSummary(report: AuditReport): void {
50
+ const { overallScore, totalFiles, results } = report;
51
+
52
+ console.log(
53
+ `${chalk.bold('Overall Score:')} ${colorScore(overallScore)(`${overallScore}/100`)} ${scoreLabel(overallScore)}` +
54
+ chalk.dim(` (${totalFiles} file${totalFiles !== 1 ? 's' : ''} scanned)\n`)
55
+ );
56
+
57
+ const rows: [string, AuditResult | undefined][] = [
58
+ ['Performance', results.performance],
59
+ ['SEO', results.seo],
60
+ ['Accessibility', results.accessibility],
61
+ ];
62
+
63
+ for (const [label, result] of rows) {
64
+ if (!result) continue;
65
+ const { score, counts } = result;
66
+
67
+ const bar = scoreBar(score);
68
+ const scoreStr = colorScore(score)(`${score}/100`.padEnd(8));
69
+ const summary =
70
+ counts.total === 0
71
+ ? chalk.green('No issues ✓')
72
+ : [
73
+ counts.critical ? chalk.red.bold(`${counts.critical} critical`) : '',
74
+ counts.major ? chalk.yellow(`${counts.major} major`) : '',
75
+ counts.minor ? chalk.gray(`${counts.minor} minor`) : '',
76
+ ]
77
+ .filter(Boolean)
78
+ .join(chalk.dim(' '));
79
+
80
+ console.log(` ${chalk.bold(label.padEnd(15))} ${scoreStr} ${bar} ${summary}`);
81
+ }
82
+
83
+ console.log('');
84
+ }
85
+
86
+ // ─── Category section ─────────────────────────────────────────────────────────
87
+
88
+ function printCategorySection(result: AuditResult): void {
89
+ const CATEGORY_COLORS: Record<string, chalk.ChalkFunction> = {
90
+ performance: chalk.blue.bold,
91
+ seo: chalk.green.bold,
92
+ accessibility: chalk.magenta.bold,
93
+ };
94
+
95
+ const color = CATEGORY_COLORS[result.category] ?? chalk.bold;
96
+ const title = result.category.toUpperCase();
97
+
98
+ console.log(color('═'.repeat(62)));
99
+ console.log(
100
+ color(` ${title}`) +
101
+ color(' · ') +
102
+ colorScore(result.score)(`${result.score}/100`)
103
+ );
104
+ console.log(color('═'.repeat(62)));
105
+ console.log('');
106
+
107
+ if (result.issues.length === 0) {
108
+ console.log(` ${chalk.green('✅')} No issues found.\n`);
109
+ return;
110
+ }
111
+
112
+ // Print issues grouped by impact level (critical → major → minor)
113
+ const groups: IssueImpact[] = ['critical', 'major', 'minor'];
114
+ for (const impact of groups) {
115
+ const group = result.issues.filter((i) => i.impact === impact);
116
+ if (group.length === 0) continue;
117
+
118
+ for (const issue of group) {
119
+ printIssue(issue);
120
+ }
121
+ }
122
+
123
+ console.log('');
124
+ }
125
+
126
+ // ─── Single issue block ───────────────────────────────────────────────────────
127
+
128
+ function printIssue(issue: Issue): void {
129
+ const statusIcon = issue.status === 'fail' ? '❌' : '⚠️ ';
130
+ const badge = IMPACT_BADGE[issue.impact];
131
+
132
+ // Location string (file:line)
133
+ const location = issue.file && issue.line
134
+ ? chalk.dim(` ${shortPath(issue.file)}:${issue.line}`)
135
+ : '';
136
+
137
+ console.log(` ${statusIcon} ${badge} ${chalk.bold(issue.title)}${location}`);
138
+ console.log(` ${chalk.dim(issue.description)}`);
139
+ console.log('');
140
+
141
+ // Code snippet (what's wrong)
142
+ if (issue.codeSnippet) {
143
+ console.log(` ${chalk.red.dim('✗')} ${chalk.red.dim(issue.codeSnippet)}`);
144
+ }
145
+
146
+ // Fix snippet (what to write instead)
147
+ if (issue.fixSnippet) {
148
+ console.log(` ${chalk.green.dim('✓')} ${chalk.green(issue.fixSnippet)}`);
149
+ }
150
+
151
+ // Multi-line suggestion
152
+ console.log('');
153
+ const suggestionLines = issue.suggestion.split('\n');
154
+ console.log(` ${chalk.cyan('💡 Fix:')} ${suggestionLines[0]}`);
155
+ for (const line of suggestionLines.slice(1)) {
156
+ console.log(` ${chalk.dim(line)}`);
157
+ }
158
+
159
+ console.log('');
160
+ console.log(' ' + chalk.dim('─'.repeat(54)));
161
+ console.log('');
162
+ }
163
+
164
+ // ─── Critical summary (always shown at the end if any critical exist) ─────────
165
+
166
+ function printCriticalSummary(report: AuditReport): void {
167
+ const criticals: Issue[] = [];
168
+
169
+ for (const result of Object.values(report.results)) {
170
+ if (!result) continue;
171
+ criticals.push(...result.issues.filter((i) => i.impact === 'critical'));
172
+ }
173
+
174
+ if (criticals.length === 0) return;
175
+
176
+ console.log(chalk.red.bold(`┌─ 🚨 Fix These First (${criticals.length} critical issue${criticals.length !== 1 ? 's' : ''}) ${'─'.repeat(25)}┐`));
177
+ console.log('');
178
+
179
+ criticals.slice(0, 6).forEach((issue, idx) => {
180
+ const cat = issue.category.padEnd(13);
181
+ console.log(
182
+ ` ${chalk.red.bold(String(idx + 1) + '.')} ` +
183
+ chalk.dim(`[${cat}]`) + ' ' +
184
+ chalk.bold(issue.title)
185
+ );
186
+ if (issue.file) {
187
+ console.log(` ${chalk.dim(shortPath(issue.file))}${issue.line ? chalk.dim(`:${issue.line}`) : ''}`);
188
+ }
189
+ console.log('');
190
+ });
191
+
192
+ console.log(chalk.red.bold('└' + '─'.repeat(58) + '┘'));
193
+ console.log('');
194
+ }
195
+
196
+ // ─── Footer ───────────────────────────────────────────────────────────────────
197
+
198
+ function printFooter(report: AuditReport): void {
199
+ const totalIssues = Object.values(report.results)
200
+ .filter(Boolean)
201
+ .reduce((sum, r) => sum + r!.counts.total, 0);
202
+
203
+ if (totalIssues === 0) {
204
+ console.log(chalk.green.bold(' ✅ All checks passed. Your code looks good!\n'));
205
+ } else {
206
+ console.log(
207
+ chalk.dim(
208
+ ` Found ${totalIssues} issue${totalIssues !== 1 ? 's' : ''} across ` +
209
+ `${report.totalFiles} file${report.totalFiles !== 1 ? 's' : ''}. ` +
210
+ `Run with --output json to export the full report.\n`
211
+ )
212
+ );
213
+ }
214
+ }
215
+
216
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
217
+
218
+ function scoreBar(score: number): string {
219
+ const filled = Math.round(score / 10);
220
+ const empty = 10 - filled;
221
+ const color = score >= 90 ? chalk.green : score >= 70 ? chalk.yellow : chalk.red;
222
+ return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
223
+ }
224
+
225
+ function colorScore(score: number): chalk.ChalkFunction {
226
+ if (score >= 90) return chalk.green.bold;
227
+ if (score >= 70) return chalk.yellow.bold;
228
+ if (score >= 50) return chalk.yellow;
229
+ return chalk.red.bold;
230
+ }
231
+
232
+ function scoreLabel(score: number): string {
233
+ if (score >= 90) return chalk.green('Excellent ✓');
234
+ if (score >= 70) return chalk.yellow('Good');
235
+ if (score >= 50) return chalk.yellow('Needs work');
236
+ return chalk.red.bold('Critical issues found');
237
+ }
238
+
239
+ /** Show only the last 3 path segments to keep output readable. */
240
+ function shortPath(filePath: string): string {
241
+ const parts = filePath.replace(/\\/g, '/').split('/');
242
+ return (parts.length > 3 ? '…/' : '') + parts.slice(-3).join('/');
243
+ }
244
+
245
+ function truncate(str: string, maxLen: number): string {
246
+ return str.length > maxLen ? '…' + str.slice(-(maxLen - 1)) : str;
247
+ }
package/src/types.ts ADDED
@@ -0,0 +1,51 @@
1
+ // ─── Audit Categories ────────────────────────────────────────────────────────
2
+
3
+ export type AuditCategory = 'performance' | 'seo' | 'accessibility';
4
+ export type IssueImpact = 'critical' | 'major' | 'minor';
5
+ export type IssueStatus = 'fail' | 'warning';
6
+
7
+ // ─── A single issue found in a file ─────────────────────────────────────────
8
+
9
+ export interface Issue {
10
+ id: string;
11
+ category: AuditCategory;
12
+ title: string;
13
+ description: string;
14
+ impact: IssueImpact;
15
+ status: IssueStatus;
16
+ suggestion: string; // Plain-text fix explanation
17
+ codeSnippet?: string; // The problematic pattern
18
+ fixSnippet?: string; // The corrected version
19
+ file?: string; // Absolute path
20
+ line?: number; // Line number in the source file
21
+ }
22
+
23
+ // ─── Result for a single audit category ─────────────────────────────────────
24
+
25
+ export interface AuditResult {
26
+ category: AuditCategory;
27
+ score: number; // 0–100
28
+ issues: Issue[];
29
+ counts: {
30
+ critical: number;
31
+ major: number;
32
+ minor: number;
33
+ total: number;
34
+ };
35
+ }
36
+
37
+ // ─── The full report returned by runAudit() ──────────────────────────────────
38
+
39
+ export interface AuditReport {
40
+ target: string; // The path that was audited
41
+ timestamp: string; // ISO 8601
42
+ overallScore: number; // Weighted average of category scores
43
+ totalFiles: number; // How many .tsx/.ts/.jsx/.js files were scanned
44
+ results: Partial<Record<AuditCategory, AuditResult>>;
45
+ }
46
+
47
+ // ─── Options passed in from CLI or programmatic API ─────────────────────────
48
+
49
+ export interface AuditOptions {
50
+ types: AuditCategory[];
51
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ // File Layout
5
+ "rootDir": "./src",
6
+ "outDir": "./dist",
7
+ "esModuleInterop": true,
8
+ // Environment Settings
9
+ // See also https://aka.ms/tsconfig/module
10
+
11
+ "module": "Node16",
12
+ "moduleResolution": "node16",
13
+ "target": "es2020",
14
+ "types": ["node"],
15
+ // For nodejs:
16
+ // "lib": ["esnext"],
17
+ // "types": ["node"],
18
+ // and npm install -D @types/node
19
+
20
+ // Other Outputs
21
+ "sourceMap": true,
22
+ "declaration": true,
23
+ "declarationMap": true,
24
+ "emitDeclarationOnly": false,
25
+
26
+ // Stricter Typechecking Options
27
+ "noUncheckedIndexedAccess": true,
28
+ "exactOptionalPropertyTypes": true,
29
+
30
+ // Style Options
31
+ // "noImplicitReturns": true,
32
+ // "noImplicitOverride": true,
33
+ // "noUnusedLocals": true,
34
+ // "noUnusedParameters": true,
35
+ // "noFallthroughCasesInSwitch": true,
36
+ // "noPropertyAccessFromIndexSignature": true,
37
+
38
+ // Recommended Options
39
+ "strict": true,
40
+ "jsx": "react-jsx",
41
+ "verbatimModuleSyntax": true,
42
+ "isolatedModules": true,
43
+ "noUncheckedSideEffectImports": true,
44
+ "moduleDetection": "force",
45
+ "skipLibCheck": true,
46
+ }
47
+ }