@diegovelasquezweb/a11y-engine 0.1.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/LICENSE +21 -0
- package/README.md +20 -0
- package/assets/discovery/crawler-config.json +11 -0
- package/assets/discovery/stack-detection.json +33 -0
- package/assets/remediation/axe-check-maps.json +31 -0
- package/assets/remediation/code-patterns.json +109 -0
- package/assets/remediation/guardrails.json +24 -0
- package/assets/remediation/intelligence.json +4166 -0
- package/assets/remediation/source-boundaries.json +46 -0
- package/assets/reporting/compliance-config.json +173 -0
- package/assets/reporting/manual-checks.json +944 -0
- package/assets/reporting/wcag-reference.json +588 -0
- package/package.json +37 -0
- package/scripts/audit.mjs +326 -0
- package/scripts/core/asset-loader.mjs +54 -0
- package/scripts/core/toolchain.mjs +102 -0
- package/scripts/core/utils.mjs +105 -0
- package/scripts/engine/analyzer.mjs +1022 -0
- package/scripts/engine/dom-scanner.mjs +685 -0
- package/scripts/engine/source-scanner.mjs +300 -0
- package/scripts/reports/builders/checklist.mjs +307 -0
- package/scripts/reports/builders/html.mjs +766 -0
- package/scripts/reports/builders/md.mjs +96 -0
- package/scripts/reports/builders/pdf.mjs +259 -0
- package/scripts/reports/renderers/findings.mjs +188 -0
- package/scripts/reports/renderers/html.mjs +452 -0
- package/scripts/reports/renderers/md.mjs +595 -0
- package/scripts/reports/renderers/pdf.mjs +551 -0
- package/scripts/reports/renderers/utils.mjs +42 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@diegovelasquezweb/a11y-engine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WCAG 2.2 AA accessibility audit engine — scanner, analyzer, and report builders",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/diegovelasquezweb/a11y-engine.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/diegovelasquezweb/a11y-engine#readme",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"a11y-audit": "./scripts/audit.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"scripts/**",
|
|
20
|
+
"assets/**",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"prepublishOnly": "node -e \"console.log('Publishing @diegovelasquezweb/a11y-engine...')\""
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@axe-core/playwright": "^4.11.1",
|
|
30
|
+
"axe-core": "^4.11.1",
|
|
31
|
+
"pa11y": "^9.1.1",
|
|
32
|
+
"playwright": "^1.58.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"vitest": "^4.0.18"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file audit.mjs
|
|
3
|
+
* @description Orchestrator for the accessibility audit pipeline.
|
|
4
|
+
* Coordinates the multi-stage process including dependency verification,
|
|
5
|
+
* site discovery/crawling, automated analysis, and final report generation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn, execSync } from "node:child_process";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import { log, DEFAULTS, SKILL_ROOT, getInternalPath } from "./core/utils.mjs";
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Prints the CLI usage instructions and available command-line options.
|
|
18
|
+
* This is displayed when the user runs the script with --help or provides invalid arguments.
|
|
19
|
+
*/
|
|
20
|
+
function printUsage() {
|
|
21
|
+
log.info(`Usage:
|
|
22
|
+
node scripts/audit.mjs --base-url <url> [options]
|
|
23
|
+
|
|
24
|
+
Targeting & Scope:
|
|
25
|
+
--base-url <url> (Required) The target website to audit.
|
|
26
|
+
--max-routes <num> Max routes to discover and scan (default: 10).
|
|
27
|
+
--crawl-depth <num> How deep to follow links during discovery (1-3, default: 2).
|
|
28
|
+
--routes <csv> Custom list of paths to scan.
|
|
29
|
+
--project-dir <path> Path to the audited project (for stack auto-detection).
|
|
30
|
+
|
|
31
|
+
Audit Intelligence:
|
|
32
|
+
--target <text> Compliance target label (default: "WCAG 2.2 AA").
|
|
33
|
+
--only-rule <id> Only check for this specific rule ID.
|
|
34
|
+
--ignore-findings <csv> Ignore specific rule IDs.
|
|
35
|
+
--exclude-selectors <csv> Exclude CSS selectors from scan.
|
|
36
|
+
|
|
37
|
+
Execution & Emulation:
|
|
38
|
+
--color-scheme <val> Emulate color scheme: "light" or "dark".
|
|
39
|
+
--wait-until <val> Page load strategy: domcontentloaded|load|networkidle (default: domcontentloaded).
|
|
40
|
+
--framework <val> Override auto-detected stack (nextjs|gatsby|react|nuxt|vue|angular|astro|svelte|shopify|wordpress|drupal).
|
|
41
|
+
--viewport <WxH> Viewport dimensions as WIDTHxHEIGHT (e.g., 375x812 for mobile).
|
|
42
|
+
--headed Run browser in visible mode (overrides headless).
|
|
43
|
+
--with-reports Generate HTML and PDF reports (requires --output).
|
|
44
|
+
--skip-reports Omit HTML and PDF report generation (default).
|
|
45
|
+
--skip-patterns Skip source code pattern scanning even if --project-dir is set.
|
|
46
|
+
--affected-only Re-scan only routes that had violations in the previous scan (faster re-audits).
|
|
47
|
+
--wait-ms <num> Time to wait after page load (default: 2000).
|
|
48
|
+
--timeout-ms <num> Network timeout (default: 30000).
|
|
49
|
+
-h, --help Show this help.
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @const {number} Execution timeout for child processes (15 minutes). */
|
|
54
|
+
const SCRIPT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Helper function to run a Node.js script as a child process.
|
|
58
|
+
* @param {string} scriptName - File name of the script located in the same directory.
|
|
59
|
+
* @param {string[]} [args=[]] - Command line arguments to pass to the script.
|
|
60
|
+
* @param {Object} [env={}] - Optional environment variables for the child process.
|
|
61
|
+
* @returns {Promise<void>} Resolves when the script finishes successfully.
|
|
62
|
+
* @throws {Error} If the process exits with a non-zero code or times out.
|
|
63
|
+
*/
|
|
64
|
+
async function runScript(scriptName, args = [], env = {}) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const scriptPath = path.join(__dirname, scriptName);
|
|
67
|
+
log.info(`Running: ${scriptName} ${args.join(" ")}`);
|
|
68
|
+
|
|
69
|
+
const proc = spawn("node", [scriptPath, ...args], {
|
|
70
|
+
stdio: "inherit",
|
|
71
|
+
cwd: SKILL_ROOT,
|
|
72
|
+
env: { ...process.env, ...env },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
proc.kill("SIGTERM");
|
|
77
|
+
reject(
|
|
78
|
+
new Error(
|
|
79
|
+
`Script ${scriptName} timed out after ${SCRIPT_TIMEOUT_MS / 1000}s`,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
}, SCRIPT_TIMEOUT_MS);
|
|
83
|
+
|
|
84
|
+
proc.on("error", (err) => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
reject(new Error(`Failed to start ${scriptName}: ${err.message}`));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
proc.on("close", (code) => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
if (code === 0) {
|
|
92
|
+
resolve();
|
|
93
|
+
} else {
|
|
94
|
+
reject(new Error(`Script ${scriptName} failed with exit code ${code}`));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The main application entry point for the accessibility audit orchestrator.
|
|
102
|
+
* Orchestrates the entire audit flow:
|
|
103
|
+
* 1. Validates inputs and environment
|
|
104
|
+
* 2. Ensures dependencies and toolchain are ready
|
|
105
|
+
* 3. Executes site discovery and scanning
|
|
106
|
+
* 4. Runs findings analysis and enrichment
|
|
107
|
+
* 5. Generates the final audit reports (Markdown, HTML, PDF)
|
|
108
|
+
* @throws {Error} If any stage of the audit pipeline fails.
|
|
109
|
+
*/
|
|
110
|
+
async function main() {
|
|
111
|
+
const argv = process.argv.slice(2);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Internal helper to extract values from command line flags.
|
|
115
|
+
* Supports both --flag=value and --flag value formats.
|
|
116
|
+
* @param {string} name - The flag name without the leading dashes.
|
|
117
|
+
* @returns {string|null} The value of the flag.
|
|
118
|
+
*/
|
|
119
|
+
function getArgValue(name) {
|
|
120
|
+
const entry = argv.find((a) => a.startsWith(`--${name}=`));
|
|
121
|
+
if (entry) return entry.split("=")[1];
|
|
122
|
+
const index = argv.indexOf(`--${name}`);
|
|
123
|
+
if (index !== -1 && argv[index + 1]) return argv[index + 1];
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
128
|
+
printUsage();
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const baseUrl = getArgValue("base-url");
|
|
133
|
+
const maxRoutes = getArgValue("max-routes") || DEFAULTS.maxRoutes;
|
|
134
|
+
const crawlDepth = getArgValue("crawl-depth") || DEFAULTS.crawlDepth;
|
|
135
|
+
const routes = getArgValue("routes");
|
|
136
|
+
const waitMs = getArgValue("wait-ms") || DEFAULTS.waitMs;
|
|
137
|
+
const timeoutMs = getArgValue("timeout-ms") || DEFAULTS.timeoutMs;
|
|
138
|
+
|
|
139
|
+
const sessionFile = getInternalPath("a11y-session.json");
|
|
140
|
+
let projectDir = getArgValue("project-dir");
|
|
141
|
+
if (projectDir) {
|
|
142
|
+
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
|
143
|
+
fs.writeFileSync(sessionFile, JSON.stringify({ project_dir: path.resolve(projectDir) }), "utf-8");
|
|
144
|
+
} else if (fs.existsSync(sessionFile)) {
|
|
145
|
+
try {
|
|
146
|
+
const session = JSON.parse(fs.readFileSync(sessionFile, "utf-8"));
|
|
147
|
+
if (session.project_dir) projectDir = session.project_dir;
|
|
148
|
+
} catch { /* ignore malformed session file */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const colorScheme = getArgValue("color-scheme");
|
|
152
|
+
const target = getArgValue("target");
|
|
153
|
+
const headless = !argv.includes("--headed");
|
|
154
|
+
|
|
155
|
+
const onlyRule = getArgValue("only-rule");
|
|
156
|
+
const skipReports = argv.includes("--skip-reports") || !argv.includes("--with-reports");
|
|
157
|
+
const skipPatterns = argv.includes("--skip-patterns");
|
|
158
|
+
const affectedOnly = argv.includes("--affected-only");
|
|
159
|
+
const ignoreFindings = getArgValue("ignore-findings");
|
|
160
|
+
const excludeSelectors = getArgValue("exclude-selectors");
|
|
161
|
+
|
|
162
|
+
const waitUntil = getArgValue("wait-until");
|
|
163
|
+
const framework = getArgValue("framework");
|
|
164
|
+
const viewportArg = getArgValue("viewport");
|
|
165
|
+
let viewport = null;
|
|
166
|
+
if (viewportArg) {
|
|
167
|
+
const [w, h] = viewportArg.split("x").map(Number);
|
|
168
|
+
if (w && h) viewport = { width: w, height: h };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!baseUrl) {
|
|
172
|
+
log.error("Missing required argument: --base-url");
|
|
173
|
+
log.info("Usage: node scripts/audit.mjs --base-url <url> [options]");
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
new URL(baseUrl);
|
|
179
|
+
} catch {
|
|
180
|
+
log.error(
|
|
181
|
+
`Invalid URL: "${baseUrl}". Provide a full URL including protocol (e.g., https://example.com).`,
|
|
182
|
+
);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const childEnv = {};
|
|
187
|
+
if (projectDir) childEnv.A11Y_PROJECT_DIR = path.resolve(projectDir);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
log.info("Starting accessibility audit pipeline...");
|
|
191
|
+
|
|
192
|
+
const nodeModulesPath = path.join(SKILL_ROOT, "node_modules");
|
|
193
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
194
|
+
log.info(
|
|
195
|
+
"First run detected — installing skill dependencies (one-time setup)...",
|
|
196
|
+
);
|
|
197
|
+
try {
|
|
198
|
+
execSync("pnpm install", { cwd: SKILL_ROOT, stdio: "ignore" });
|
|
199
|
+
} catch {
|
|
200
|
+
execSync("npm install", { cwd: SKILL_ROOT, stdio: "ignore" });
|
|
201
|
+
}
|
|
202
|
+
log.success("Dependencies ready.");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await runScript("core/toolchain.mjs");
|
|
206
|
+
|
|
207
|
+
const screenshotsDir = getInternalPath("screenshots");
|
|
208
|
+
fs.rmSync(screenshotsDir, { recursive: true, force: true });
|
|
209
|
+
|
|
210
|
+
let effectiveRoutes = routes;
|
|
211
|
+
if (affectedOnly && !routes) {
|
|
212
|
+
const prevScanPath = getInternalPath("a11y-scan-results.json");
|
|
213
|
+
if (fs.existsSync(prevScanPath)) {
|
|
214
|
+
try {
|
|
215
|
+
const prevScan = JSON.parse(fs.readFileSync(prevScanPath, "utf-8"));
|
|
216
|
+
const affected = (prevScan.routes ?? [])
|
|
217
|
+
.filter((r) => Array.isArray(r.violations) && r.violations.length > 0)
|
|
218
|
+
.map((r) => r.path);
|
|
219
|
+
if (affected.length > 0) {
|
|
220
|
+
effectiveRoutes = affected.join(",");
|
|
221
|
+
log.info(`--affected-only: re-scanning ${affected.length} route(s) with previous violations.`);
|
|
222
|
+
} else {
|
|
223
|
+
log.info("--affected-only: no previous violations found — running full scan.");
|
|
224
|
+
}
|
|
225
|
+
} catch { /* fallback to full scan */ }
|
|
226
|
+
} else {
|
|
227
|
+
log.info("--affected-only: no previous scan found — running full scan.");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const scanArgs = [
|
|
232
|
+
"--base-url",
|
|
233
|
+
baseUrl,
|
|
234
|
+
"--max-routes",
|
|
235
|
+
maxRoutes.toString(),
|
|
236
|
+
"--wait-ms",
|
|
237
|
+
waitMs.toString(),
|
|
238
|
+
"--timeout-ms",
|
|
239
|
+
timeoutMs.toString(),
|
|
240
|
+
"--headless",
|
|
241
|
+
headless.toString(),
|
|
242
|
+
"--screenshots-dir",
|
|
243
|
+
screenshotsDir,
|
|
244
|
+
"--crawl-depth",
|
|
245
|
+
crawlDepth.toString(),
|
|
246
|
+
];
|
|
247
|
+
if (onlyRule) scanArgs.push("--only-rule", onlyRule);
|
|
248
|
+
if (excludeSelectors)
|
|
249
|
+
scanArgs.push("--exclude-selectors", excludeSelectors);
|
|
250
|
+
if (effectiveRoutes) scanArgs.push("--routes", effectiveRoutes);
|
|
251
|
+
if (colorScheme) scanArgs.push("--color-scheme", colorScheme);
|
|
252
|
+
if (waitUntil) scanArgs.push("--wait-until", waitUntil);
|
|
253
|
+
if (viewport) {
|
|
254
|
+
scanArgs.push("--viewport", `${viewport.width}x${viewport.height}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
await runScript("engine/dom-scanner.mjs", scanArgs, childEnv);
|
|
258
|
+
|
|
259
|
+
const analyzerArgs = [];
|
|
260
|
+
if (ignoreFindings) analyzerArgs.push("--ignore-findings", ignoreFindings);
|
|
261
|
+
if (framework) analyzerArgs.push("--framework", framework);
|
|
262
|
+
await runScript("engine/analyzer.mjs", analyzerArgs);
|
|
263
|
+
|
|
264
|
+
if (projectDir && !skipPatterns) {
|
|
265
|
+
const patternArgs = ["--project-dir", path.resolve(projectDir)];
|
|
266
|
+
let resolvedFramework = framework;
|
|
267
|
+
if (!resolvedFramework) {
|
|
268
|
+
try {
|
|
269
|
+
const findings = JSON.parse(fs.readFileSync(getInternalPath("a11y-findings.json"), "utf-8"));
|
|
270
|
+
resolvedFramework = findings?.metadata?.projectContext?.framework ?? null;
|
|
271
|
+
} catch { /* ignore */ }
|
|
272
|
+
}
|
|
273
|
+
if (resolvedFramework) patternArgs.push("--framework", resolvedFramework);
|
|
274
|
+
await runScript("engine/source-scanner.mjs", patternArgs);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const mdOutput = getInternalPath("remediation.md");
|
|
278
|
+
const mdArgs = ["--output", mdOutput, "--base-url", baseUrl];
|
|
279
|
+
if (target) mdArgs.push("--target", target);
|
|
280
|
+
|
|
281
|
+
if (skipReports) {
|
|
282
|
+
await runScript("reports/builders/md.mjs", mdArgs);
|
|
283
|
+
} else {
|
|
284
|
+
const output = getArgValue("output");
|
|
285
|
+
if (!output) {
|
|
286
|
+
log.error(
|
|
287
|
+
"When using --with-reports, you must specify --output <path> for the HTML report location.",
|
|
288
|
+
);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
const absoluteOutputPath = path.isAbsolute(output)
|
|
292
|
+
? output
|
|
293
|
+
: path.resolve(output);
|
|
294
|
+
|
|
295
|
+
const buildArgs = ["--output", absoluteOutputPath, "--base-url", baseUrl];
|
|
296
|
+
if (target) buildArgs.push("--target", target);
|
|
297
|
+
|
|
298
|
+
const pdfOutput = absoluteOutputPath.replace(/\.html$/, ".pdf");
|
|
299
|
+
const pdfArgs = ["--output", pdfOutput, "--base-url", baseUrl];
|
|
300
|
+
if (target) pdfArgs.push("--target", target);
|
|
301
|
+
|
|
302
|
+
const checklistOutput = path.join(path.dirname(absoluteOutputPath), "checklist.html");
|
|
303
|
+
const checklistArgs = ["--output", checklistOutput, "--base-url", baseUrl];
|
|
304
|
+
|
|
305
|
+
await Promise.all([
|
|
306
|
+
runScript("reports/builders/html.mjs", buildArgs),
|
|
307
|
+
runScript("reports/builders/checklist.mjs", checklistArgs),
|
|
308
|
+
runScript("reports/builders/md.mjs", mdArgs),
|
|
309
|
+
runScript("reports/builders/pdf.mjs", pdfArgs),
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
console.log(`REPORT_PATH=${absoluteOutputPath}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
log.success("Audit complete! Remediation roadmap ready.");
|
|
316
|
+
console.log(`REMEDIATION_PATH=${mdOutput}`);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
log.error(error.message);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
main().catch((error) => {
|
|
324
|
+
log.error(`Critical Audit Failure: ${error.message}`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file assets.mjs
|
|
3
|
+
* @description Centralized asset paths and JSON loaders for the a11y skill.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const ASSET_ROOT = path.join(__dirname, "..", "..", "assets");
|
|
12
|
+
|
|
13
|
+
export const ASSET_PATHS = {
|
|
14
|
+
discovery: {
|
|
15
|
+
crawlerConfig: path.join(ASSET_ROOT, "discovery", "crawler-config.json"),
|
|
16
|
+
stackDetection: path.join(
|
|
17
|
+
ASSET_ROOT,
|
|
18
|
+
"discovery",
|
|
19
|
+
"stack-detection.json",
|
|
20
|
+
),
|
|
21
|
+
},
|
|
22
|
+
remediation: {
|
|
23
|
+
intelligence: path.join(ASSET_ROOT, "remediation", "intelligence.json"),
|
|
24
|
+
axeCheckMaps: path.join(
|
|
25
|
+
ASSET_ROOT,
|
|
26
|
+
"remediation",
|
|
27
|
+
"axe-check-maps.json",
|
|
28
|
+
),
|
|
29
|
+
guardrails: path.join(ASSET_ROOT, "remediation", "guardrails.json"),
|
|
30
|
+
sourceBoundaries: path.join(
|
|
31
|
+
ASSET_ROOT,
|
|
32
|
+
"remediation",
|
|
33
|
+
"source-boundaries.json",
|
|
34
|
+
),
|
|
35
|
+
codePatterns: path.join(ASSET_ROOT, "remediation", "code-patterns.json"),
|
|
36
|
+
},
|
|
37
|
+
reporting: {
|
|
38
|
+
wcagReference: path.join(ASSET_ROOT, "reporting", "wcag-reference.json"),
|
|
39
|
+
complianceConfig: path.join(
|
|
40
|
+
ASSET_ROOT,
|
|
41
|
+
"reporting",
|
|
42
|
+
"compliance-config.json",
|
|
43
|
+
),
|
|
44
|
+
manualChecks: path.join(ASSET_ROOT, "reporting", "manual-checks.json"),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function loadAssetJson(filePath, label) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error(`Missing or invalid ${label} — reinstall the skill.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file toolchain.mjs
|
|
3
|
+
* @description Validates the development environment and required dependencies for the a11y skill.
|
|
4
|
+
* Checks for the presence of node_modules and installed Playwright browsers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { SKILL_ROOT, log } from "./utils.mjs";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Prints the CLI usage instructions and available options for the toolchain checker.
|
|
13
|
+
*/
|
|
14
|
+
function printUsage() {
|
|
15
|
+
log.info(`Usage:
|
|
16
|
+
node toolchain.mjs [options]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
-h, --help Show this help
|
|
20
|
+
`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parses command-line arguments to handle the help flag.
|
|
25
|
+
* @param {string[]} argv - Array of command-line arguments.
|
|
26
|
+
*/
|
|
27
|
+
function parseArgs(argv) {
|
|
28
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
29
|
+
printUsage();
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Verifies if the local node_modules directory exists.
|
|
36
|
+
* @returns {boolean} True if the directory exists, false otherwise.
|
|
37
|
+
*/
|
|
38
|
+
function checkNodeModules() {
|
|
39
|
+
const nodeModulesPath = path.join(SKILL_ROOT, "node_modules");
|
|
40
|
+
return fs.existsSync(nodeModulesPath);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Verifies if Playwright browsers are properly installed and accessible.
|
|
45
|
+
* @returns {Promise<boolean>} True if the Chromium executable exists, false otherwise.
|
|
46
|
+
*/
|
|
47
|
+
async function checkPlaywrightBrowsers() {
|
|
48
|
+
try {
|
|
49
|
+
const { chromium } = await import("playwright");
|
|
50
|
+
const executablePath = chromium.executablePath();
|
|
51
|
+
return fs.existsSync(executablePath);
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The main execution function for the toolchain validation.
|
|
59
|
+
* Performs all checks and logs results to the console.
|
|
60
|
+
* @throws {Error} If any critical dependency is missing.
|
|
61
|
+
*/
|
|
62
|
+
async function main() {
|
|
63
|
+
parseArgs(process.argv.slice(2));
|
|
64
|
+
const checks = [];
|
|
65
|
+
|
|
66
|
+
const modulesOk = checkNodeModules();
|
|
67
|
+
checks.push({
|
|
68
|
+
tool: "Local dependencies (node_modules)",
|
|
69
|
+
required: true,
|
|
70
|
+
ok: modulesOk,
|
|
71
|
+
fix: "Run 'pnpm install' to initialize the skill dependencies.",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const pwOk = await checkPlaywrightBrowsers();
|
|
75
|
+
checks.push({
|
|
76
|
+
tool: "Playwright installed",
|
|
77
|
+
required: true,
|
|
78
|
+
ok: pwOk,
|
|
79
|
+
fix: "Run 'pnpm install' (browsers should install automatically via postinstall or verify with 'npx playwright install').",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const blockers = checks.filter((c) => c.required && !c.ok);
|
|
83
|
+
const result = {
|
|
84
|
+
checked_at: new Date().toISOString(),
|
|
85
|
+
checks,
|
|
86
|
+
blockers: blockers.map((b) => ({ tool: b.tool, fix: b.fix })),
|
|
87
|
+
ok: blockers.length === 0,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (result.ok) {
|
|
91
|
+
log.success("Toolchain is ready.");
|
|
92
|
+
} else {
|
|
93
|
+
log.error(`Toolchain has ${blockers.length} blockers.`);
|
|
94
|
+
blockers.forEach((b) => log.warn(`Missing: ${b.tool}. Fix: ${b.fix}`));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main().catch((error) => {
|
|
100
|
+
log.error(`Toolchain Check Failure: ${error.message}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file utils.mjs
|
|
3
|
+
* @description Core utility functions and shared configuration for the a11y skill.
|
|
4
|
+
* Provides logging, file I/O operations, and path management used throughout the audit pipeline.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The absolute root directory of the a11y skill project.
|
|
16
|
+
* @type {string}
|
|
17
|
+
*/
|
|
18
|
+
export const SKILL_ROOT = path.join(__dirname, "..", "..");
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default configuration values for the accessibility audit scanner.
|
|
22
|
+
* Used when specific options are not provided via CLI or config file.
|
|
23
|
+
* @type {Object}
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULTS = {
|
|
26
|
+
maxRoutes: 10,
|
|
27
|
+
crawlDepth: 2,
|
|
28
|
+
complianceTarget: "WCAG 2.2 AA",
|
|
29
|
+
colorScheme: "light",
|
|
30
|
+
viewports: [{ width: 1280, height: 800, name: "Desktop" }],
|
|
31
|
+
headless: true,
|
|
32
|
+
waitMs: 2000,
|
|
33
|
+
timeoutMs: 30000,
|
|
34
|
+
waitUntil: "domcontentloaded",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Standardized logging utility for consistent terminal output across scripts.
|
|
39
|
+
* Supports info, success, warning, and error levels with ANSI color coding.
|
|
40
|
+
*/
|
|
41
|
+
export const log = {
|
|
42
|
+
/**
|
|
43
|
+
* Logs an informational message in cyan.
|
|
44
|
+
* @param {string} msg - The message to log.
|
|
45
|
+
*/
|
|
46
|
+
info: (msg) => console.log(`\x1b[36m[INFO]\x1b[0m ${msg}`),
|
|
47
|
+
/**
|
|
48
|
+
* Logs a success message in green.
|
|
49
|
+
* @param {string} msg - The message to log.
|
|
50
|
+
*/
|
|
51
|
+
success: (msg) => console.log(`\x1b[32m[SUCCESS]\x1b[0m ${msg}`),
|
|
52
|
+
/**
|
|
53
|
+
* Logs a warning message in yellow.
|
|
54
|
+
* @param {string} msg - The message to log.
|
|
55
|
+
*/
|
|
56
|
+
warn: (msg) => console.log(`\x1b[33m[WARN]\x1b[0m ${msg}`),
|
|
57
|
+
/**
|
|
58
|
+
* Logs an error message in red, optionally including a troubleshooting hint.
|
|
59
|
+
* @param {string} msg - The error message.
|
|
60
|
+
* @param {string} [hint=""] - An optional hint or troubleshooting suggestion.
|
|
61
|
+
*/
|
|
62
|
+
error: (msg, hint = "") => {
|
|
63
|
+
console.error(`\x1b[31m[ERROR]\x1b[0m ${msg}`);
|
|
64
|
+
if (hint) {
|
|
65
|
+
console.log(`\x1b[35m[TROUBLESHOOTING]\x1b[0m ${hint}`);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Writes data to a JSON file, creating parent directories if they don't exist.
|
|
72
|
+
* @param {string} filePath - Absolute path to the destination file.
|
|
73
|
+
* @param {any} data - The data to serialize (will be formatted with 2-space indentation).
|
|
74
|
+
*/
|
|
75
|
+
export function writeJson(filePath, data) {
|
|
76
|
+
const dir = path.dirname(filePath);
|
|
77
|
+
if (!fs.existsSync(dir)) {
|
|
78
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Reads and parses a JSON file.
|
|
85
|
+
* @param {string} filePath - Absolute path to the JSON file.
|
|
86
|
+
* @returns {any|null} The parsed data or null if the file doesn't exist or is invalid.
|
|
87
|
+
*/
|
|
88
|
+
export function readJson(filePath) {
|
|
89
|
+
if (!fs.existsSync(filePath)) return null;
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
log.error(`Failed to read JSON from ${filePath}: ${e.message}`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Constructs an absolute path to a file within the internal audit directory.
|
|
100
|
+
* @param {string} filename - The name of the file.
|
|
101
|
+
* @returns {string} The resolved absolute path.
|
|
102
|
+
*/
|
|
103
|
+
export function getInternalPath(filename) {
|
|
104
|
+
return path.join(SKILL_ROOT, ".audit", filename);
|
|
105
|
+
}
|