@diegovelasquezweb/a11y-engine 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +190 -152
- package/docs/architecture.md +22 -0
- package/docs/cli-handbook.md +4 -0
- package/docs/outputs.md +37 -23
- package/package.json +1 -1
- package/scripts/engine/analyzer.mjs +40 -14
- package/scripts/engine/dom-scanner.mjs +56 -3
- package/scripts/engine/source-scanner.mjs +1 -1
- package/scripts/index.d.mts +139 -3
- package/scripts/index.mjs +527 -79
|
@@ -949,17 +949,23 @@ export function collectIncompleteFindings(routes) {
|
|
|
949
949
|
}
|
|
950
950
|
|
|
951
951
|
/**
|
|
952
|
-
*
|
|
953
|
-
*
|
|
952
|
+
* Runs the analyzer programmatically on a scan payload.
|
|
953
|
+
* @param {Object} scanPayload - The raw scan output from dom-scanner ({ routes, base_url, projectContext, ... }).
|
|
954
|
+
* @param {{ ignoreFindings?: string[], framework?: string, output?: string }} [options={}]
|
|
955
|
+
* @returns {Object} The enriched findings payload { findings, incomplete_findings, metadata, ... }.
|
|
954
956
|
*/
|
|
955
|
-
function
|
|
956
|
-
|
|
957
|
-
const ignoredRules = new Set(args.ignoreFindings);
|
|
957
|
+
export function runAnalyzer(scanPayload, options = {}) {
|
|
958
|
+
if (!scanPayload) throw new Error("Missing scan payload");
|
|
958
959
|
|
|
959
|
-
const
|
|
960
|
-
|
|
960
|
+
const args = {
|
|
961
|
+
input: null,
|
|
962
|
+
output: options.output || getInternalPath("a11y-findings.json"),
|
|
963
|
+
ignoreFindings: options.ignoreFindings || [],
|
|
964
|
+
framework: options.framework || null,
|
|
965
|
+
};
|
|
961
966
|
|
|
962
|
-
const
|
|
967
|
+
const ignoredRules = new Set(args.ignoreFindings);
|
|
968
|
+
const result = buildFindings(scanPayload, args);
|
|
963
969
|
|
|
964
970
|
if (ignoredRules.size > 0) {
|
|
965
971
|
const knownIds = new Set(
|
|
@@ -989,15 +995,15 @@ function main() {
|
|
|
989
995
|
if (deduplicatedCount > 0) log.info(`Deduplicated ${deduplicatedCount} cross-page finding group(s).`);
|
|
990
996
|
|
|
991
997
|
const overallAssessment = computeOverallAssessment(dedupedFindings);
|
|
992
|
-
const passedCriteria = computePassedCriteria(
|
|
993
|
-
const outOfScope = computeOutOfScope(
|
|
998
|
+
const passedCriteria = computePassedCriteria(scanPayload.routes || [], WCAG_CRITERION_MAP, dedupedFindings);
|
|
999
|
+
const outOfScope = computeOutOfScope(scanPayload.routes || []);
|
|
994
1000
|
const recommendations = computeRecommendations(dedupedFindings);
|
|
995
|
-
const testingMethodology = computeTestingMethodology(
|
|
996
|
-
const incompleteFindings = collectIncompleteFindings(
|
|
1001
|
+
const testingMethodology = computeTestingMethodology(scanPayload);
|
|
1002
|
+
const incompleteFindings = collectIncompleteFindings(scanPayload.routes || []);
|
|
997
1003
|
if (incompleteFindings.length > 0)
|
|
998
1004
|
log.info(`${incompleteFindings.length} incomplete finding(s) require manual review.`);
|
|
999
1005
|
|
|
1000
|
-
|
|
1006
|
+
const outputPayload = {
|
|
1001
1007
|
...result,
|
|
1002
1008
|
findings: dedupedFindings,
|
|
1003
1009
|
incomplete_findings: incompleteFindings,
|
|
@@ -1011,12 +1017,32 @@ function main() {
|
|
|
1011
1017
|
fpFiltered: fpRemovedCount,
|
|
1012
1018
|
deduplicatedCount,
|
|
1013
1019
|
},
|
|
1014
|
-
}
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
// Write to disk for CLI compatibility
|
|
1023
|
+
writeJson(args.output, outputPayload);
|
|
1015
1024
|
|
|
1016
1025
|
if (dedupedFindings.length === 0) {
|
|
1017
1026
|
log.info("Congratulations, no issues found.");
|
|
1018
1027
|
}
|
|
1019
1028
|
log.success(`Findings processed and saved to ${args.output}`);
|
|
1029
|
+
|
|
1030
|
+
return outputPayload;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* CLI entry point — reads from disk, processes, writes to disk.
|
|
1035
|
+
*/
|
|
1036
|
+
function main() {
|
|
1037
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1038
|
+
const payload = readJson(args.input);
|
|
1039
|
+
if (!payload) throw new Error(`Input not found or invalid: ${args.input}`);
|
|
1040
|
+
|
|
1041
|
+
runAnalyzer(payload, {
|
|
1042
|
+
ignoreFindings: args.ignoreFindings,
|
|
1043
|
+
framework: args.framework,
|
|
1044
|
+
output: args.output,
|
|
1045
|
+
});
|
|
1020
1046
|
}
|
|
1021
1047
|
|
|
1022
1048
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
@@ -489,7 +489,16 @@ async function analyzeRoute(
|
|
|
489
489
|
* @param {"pending"|"running"|"done"|"error"} status - Step status.
|
|
490
490
|
* @param {Object} [extra={}] - Additional metadata.
|
|
491
491
|
*/
|
|
492
|
+
/** @type {((step: string, status: string, extra?: object) => void) | null} */
|
|
493
|
+
let _onProgressCallback = null;
|
|
494
|
+
|
|
492
495
|
function writeProgress(step, status, extra = {}) {
|
|
496
|
+
// Notify external callback if set (programmatic API)
|
|
497
|
+
if (_onProgressCallback) {
|
|
498
|
+
_onProgressCallback(step, status, extra);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Always write to disk for CLI consumers
|
|
493
502
|
const progressPath = getInternalPath("progress.json");
|
|
494
503
|
let progress = {};
|
|
495
504
|
try {
|
|
@@ -788,8 +797,45 @@ function mergeViolations(axeViolations, cdpViolations, pa11yViolations) {
|
|
|
788
797
|
* Coordinates browser setup, crawling/discovery, parallel scanning, and result saving.
|
|
789
798
|
* @throws {Error} If navigation to the base URL fails or browser setup issues occur.
|
|
790
799
|
*/
|
|
791
|
-
|
|
792
|
-
|
|
800
|
+
/**
|
|
801
|
+
* Runs the DOM scanner programmatically.
|
|
802
|
+
* @param {Object} options - Scanner configuration (same shape as CLI args object).
|
|
803
|
+
* @param {{ onProgress?: (step: string, status: string, extra?: object) => void }} [callbacks={}]
|
|
804
|
+
* @returns {Promise<Object>} The scan payload { generated_at, base_url, onlyRule, projectContext, routes }.
|
|
805
|
+
*/
|
|
806
|
+
export async function runDomScanner(options = {}, callbacks = {}) {
|
|
807
|
+
const args = {
|
|
808
|
+
baseUrl: options.baseUrl || "",
|
|
809
|
+
routes: options.routes || "",
|
|
810
|
+
output: options.output || getInternalPath("a11y-scan-results.json"),
|
|
811
|
+
maxRoutes: options.maxRoutes ?? DEFAULTS.maxRoutes,
|
|
812
|
+
waitMs: options.waitMs ?? DEFAULTS.waitMs,
|
|
813
|
+
timeoutMs: options.timeoutMs ?? DEFAULTS.timeoutMs,
|
|
814
|
+
headless: options.headless ?? DEFAULTS.headless,
|
|
815
|
+
waitUntil: options.waitUntil ?? DEFAULTS.waitUntil,
|
|
816
|
+
colorScheme: options.colorScheme || null,
|
|
817
|
+
screenshotsDir: options.screenshotsDir || getInternalPath("screenshots"),
|
|
818
|
+
excludeSelectors: options.excludeSelectors || [],
|
|
819
|
+
onlyRule: options.onlyRule || null,
|
|
820
|
+
crawlDepth: Math.min(Math.max(options.crawlDepth ?? DEFAULTS.crawlDepth, 1), 3),
|
|
821
|
+
viewport: options.viewport || null,
|
|
822
|
+
axeTags: options.axeTags || null,
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
if (!args.baseUrl) throw new Error("Missing required option: baseUrl");
|
|
826
|
+
|
|
827
|
+
if (callbacks.onProgress) {
|
|
828
|
+
_onProgressCallback = callbacks.onProgress;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
try {
|
|
832
|
+
return await _runDomScannerInternal(args);
|
|
833
|
+
} finally {
|
|
834
|
+
_onProgressCallback = null;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function _runDomScannerInternal(args) {
|
|
793
839
|
const baseUrl = new URL(args.baseUrl).toString();
|
|
794
840
|
const origin = new URL(baseUrl).origin;
|
|
795
841
|
|
|
@@ -850,7 +896,7 @@ async function main() {
|
|
|
850
896
|
} catch (err) {
|
|
851
897
|
log.error(`Fatal: Could not load base URL ${baseUrl}: ${err.message}`);
|
|
852
898
|
await browser.close();
|
|
853
|
-
|
|
899
|
+
throw new Error(`Could not load base URL ${baseUrl}: ${err.message}`);
|
|
854
900
|
}
|
|
855
901
|
|
|
856
902
|
/**
|
|
@@ -1036,6 +1082,13 @@ async function main() {
|
|
|
1036
1082
|
|
|
1037
1083
|
writeJson(args.output, payload);
|
|
1038
1084
|
log.success(`Routes scan complete. Results saved to ${args.output}`);
|
|
1085
|
+
|
|
1086
|
+
return payload;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async function main() {
|
|
1090
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1091
|
+
await runDomScanner(args);
|
|
1039
1092
|
}
|
|
1040
1093
|
|
|
1041
1094
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
@@ -113,7 +113,7 @@ function walkFiles(dir, extensions, results = []) {
|
|
|
113
113
|
* @param {string} projectDir
|
|
114
114
|
* @returns {string[]}
|
|
115
115
|
*/
|
|
116
|
-
function resolveScanDirs(framework, projectDir) {
|
|
116
|
+
export function resolveScanDirs(framework, projectDir) {
|
|
117
117
|
const boundaries = framework ? SOURCE_BOUNDARIES?.[framework] : null;
|
|
118
118
|
if (!boundaries) return [projectDir];
|
|
119
119
|
|
package/scripts/index.d.mts
CHANGED
|
@@ -63,7 +63,28 @@ export interface EnrichedFinding extends Finding {
|
|
|
63
63
|
fixDifficultyNotes: string | string[] | null;
|
|
64
64
|
screenshotPath: string | null;
|
|
65
65
|
wcagCriterionId: string | null;
|
|
66
|
+
wcagClassification: string | null;
|
|
66
67
|
impactedUsers: string | null;
|
|
68
|
+
primarySelector: string;
|
|
69
|
+
primaryFailureMode: string | null;
|
|
70
|
+
relationshipHint: string | null;
|
|
71
|
+
failureChecks: unknown[];
|
|
72
|
+
relatedContext: unknown[];
|
|
73
|
+
recommendedFix: string;
|
|
74
|
+
totalInstances: number | null;
|
|
75
|
+
relatedRules: string[];
|
|
76
|
+
ownershipStatus: string;
|
|
77
|
+
ownershipReason: string | null;
|
|
78
|
+
primarySourceScope: string[];
|
|
79
|
+
searchStrategy: string;
|
|
80
|
+
managedByLibrary: string | null;
|
|
81
|
+
componentHint: string | null;
|
|
82
|
+
verificationCommand: string | null;
|
|
83
|
+
verificationCommandFallback: string | null;
|
|
84
|
+
checkData: Record<string, unknown> | null;
|
|
85
|
+
pagesAffected: number | null;
|
|
86
|
+
affectedUrls: string[] | null;
|
|
87
|
+
effort: string;
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
export interface SeverityTotals {
|
|
@@ -79,12 +100,22 @@ export interface PersonaGroup {
|
|
|
79
100
|
icon: string;
|
|
80
101
|
}
|
|
81
102
|
|
|
103
|
+
export interface DetectedStack {
|
|
104
|
+
framework: string | null;
|
|
105
|
+
cms: string | null;
|
|
106
|
+
uiLibraries: string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
82
109
|
export interface AuditSummary {
|
|
83
110
|
totals: SeverityTotals;
|
|
84
111
|
score: number;
|
|
85
112
|
label: string;
|
|
86
113
|
wcagStatus: "Pass" | "Conditional Pass" | "Fail";
|
|
87
114
|
personaGroups: Record<string, PersonaGroup>;
|
|
115
|
+
quickWins: EnrichedFinding[];
|
|
116
|
+
targetUrl: string;
|
|
117
|
+
detectedStack: DetectedStack;
|
|
118
|
+
totalFindings: number;
|
|
88
119
|
}
|
|
89
120
|
|
|
90
121
|
// ---------------------------------------------------------------------------
|
|
@@ -106,19 +137,109 @@ export interface PDFReport {
|
|
|
106
137
|
contentType: "application/pdf";
|
|
107
138
|
}
|
|
108
139
|
|
|
140
|
+
export interface HTMLReport {
|
|
141
|
+
html: string;
|
|
142
|
+
contentType: "text/html";
|
|
143
|
+
}
|
|
144
|
+
|
|
109
145
|
export interface ChecklistReport {
|
|
110
146
|
html: string;
|
|
111
147
|
contentType: "text/html";
|
|
112
148
|
}
|
|
113
149
|
|
|
150
|
+
export interface RemediationGuide {
|
|
151
|
+
markdown: string;
|
|
152
|
+
contentType: "text/markdown";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface SourcePatternFinding {
|
|
156
|
+
id: string;
|
|
157
|
+
pattern_id: string;
|
|
158
|
+
title: string;
|
|
159
|
+
severity: string;
|
|
160
|
+
wcag: string;
|
|
161
|
+
wcag_criterion: string;
|
|
162
|
+
wcag_level: string;
|
|
163
|
+
type: string;
|
|
164
|
+
fix_description: string | null;
|
|
165
|
+
status: "confirmed" | "potential";
|
|
166
|
+
file: string;
|
|
167
|
+
line: number;
|
|
168
|
+
match: string;
|
|
169
|
+
context: string;
|
|
170
|
+
source: "code-pattern";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface SourcePatternResult {
|
|
174
|
+
findings: SourcePatternFinding[];
|
|
175
|
+
summary: {
|
|
176
|
+
total: number;
|
|
177
|
+
confirmed: number;
|
|
178
|
+
potential: number;
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface HTMLReportOptions extends ReportOptions {
|
|
183
|
+
screenshotsDir?: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface RemediationOptions extends ReportOptions {
|
|
187
|
+
patternFindings?: Record<string, unknown> | null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface SourcePatternOptions {
|
|
191
|
+
framework?: string;
|
|
192
|
+
onlyPattern?: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Audit options
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
export interface RunAuditOptions {
|
|
200
|
+
baseUrl: string;
|
|
201
|
+
maxRoutes?: number;
|
|
202
|
+
crawlDepth?: number;
|
|
203
|
+
routes?: string;
|
|
204
|
+
waitMs?: number;
|
|
205
|
+
timeoutMs?: number;
|
|
206
|
+
headless?: boolean;
|
|
207
|
+
waitUntil?: string;
|
|
208
|
+
colorScheme?: string;
|
|
209
|
+
viewport?: { width: number; height: number };
|
|
210
|
+
axeTags?: string[];
|
|
211
|
+
onlyRule?: string;
|
|
212
|
+
excludeSelectors?: string[];
|
|
213
|
+
ignoreFindings?: string[];
|
|
214
|
+
framework?: string;
|
|
215
|
+
projectDir?: string;
|
|
216
|
+
skipPatterns?: boolean;
|
|
217
|
+
onProgress?: (step: string, status: string, extra?: Record<string, unknown>) => void;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Enrichment options
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
export interface EnrichmentOptions {
|
|
225
|
+
screenshotUrlBuilder?: (rawPath: string) => string;
|
|
226
|
+
}
|
|
227
|
+
|
|
114
228
|
// ---------------------------------------------------------------------------
|
|
115
229
|
// Public API
|
|
116
230
|
// ---------------------------------------------------------------------------
|
|
117
231
|
|
|
118
|
-
export function
|
|
119
|
-
|
|
232
|
+
export function runAudit(options: RunAuditOptions): Promise<ScanPayload>;
|
|
233
|
+
|
|
234
|
+
export function getEnrichedFindings(
|
|
235
|
+
input: ScanPayload | Finding[] | Record<string, unknown>[],
|
|
236
|
+
options?: EnrichmentOptions
|
|
237
|
+
): EnrichedFinding[];
|
|
120
238
|
|
|
121
|
-
export function getAuditSummary(
|
|
239
|
+
export function getAuditSummary(
|
|
240
|
+
findings: EnrichedFinding[],
|
|
241
|
+
payload?: ScanPayload | null
|
|
242
|
+
): AuditSummary;
|
|
122
243
|
|
|
123
244
|
export function getPDFReport(
|
|
124
245
|
payload: ScanPayload,
|
|
@@ -128,3 +249,18 @@ export function getPDFReport(
|
|
|
128
249
|
export function getChecklist(
|
|
129
250
|
options?: Pick<ReportOptions, "baseUrl">
|
|
130
251
|
): Promise<ChecklistReport>;
|
|
252
|
+
|
|
253
|
+
export function getHTMLReport(
|
|
254
|
+
payload: ScanPayload,
|
|
255
|
+
options?: HTMLReportOptions
|
|
256
|
+
): Promise<HTMLReport>;
|
|
257
|
+
|
|
258
|
+
export function getRemediationGuide(
|
|
259
|
+
payload: ScanPayload & { incomplete_findings?: unknown[] },
|
|
260
|
+
options?: RemediationOptions
|
|
261
|
+
): Promise<RemediationGuide>;
|
|
262
|
+
|
|
263
|
+
export function getSourcePatterns(
|
|
264
|
+
projectDir: string,
|
|
265
|
+
options?: SourcePatternOptions
|
|
266
|
+
): Promise<SourcePatternResult>;
|