Skip to content

Commit 13664bb

Browse files
hyperpolymathclaude
andcommitted
feat(rescript): port core/cli/github-action packages from TS to ReScript
- @accessibility-everywhere/core (Arangodb.res): full port of arangodb.ts (409 LOC) including all type interfaces, DB initialization, index creation, WCAG 2.1 criteria data, and 7 query methods (getSiteByUrl, getRecentScansForSite, getViolationsForScan, getTopSites, getCommonViolations, getSiteViolationTrend, getOrganizationSites). Tagged-template aql queries converted to bind-vars-form db.query() for cleaner FFI. - @accessibility-everywhere/github-action (Action.res): full port of src/index.ts. FFI for @actions/core (getInput, info, warning, setFailed, setOutput, summary) and @actions/github (context, getOctokit, octokit.rest.issues.createComment). - @accessibility-everywhere/cli (Cli.res): full port of src/cli.ts. FFI for commander, chalk, ora, cli-table3, fs-extra, path. All three subcommands (scan, ci, batch) ported with the same UX as the TS original. Workspace dep "@accessibility-everywhere/scanner" referenced via @module externals; package.json runtime dep dropped pending workspace setup. Tests deferred: action_test.ts, cli_test.ts (Deno.test rewrite separate session). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 272bd1b commit 13664bb

21 files changed

Lines changed: 3444 additions & 847 deletions

tools/cli/package-lock.json

Lines changed: 487 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/cli/package.json

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,26 @@
22
"name": "@accessibility-everywhere/cli",
33
"version": "1.0.0",
44
"description": "Command-line tool for accessibility scanning",
5-
"main": "dist/index.js",
5+
"main": "src/Cli.mjs",
6+
"type": "module",
67
"bin": {
7-
"a11y-scan": "dist/cli.js",
8-
"accessibility-scan": "dist/cli.js"
8+
"a11y-scan": "src/Cli.mjs",
9+
"accessibility-scan": "src/Cli.mjs"
910
},
1011
"scripts": {
11-
"build": "tsc",
12-
"dev": "ts-node src/cli.ts",
13-
"test": "jest"
12+
"build": "rescript build",
13+
"dev": "rescript build -w",
14+
"clean": "rescript clean"
1415
},
1516
"dependencies": {
16-
"@accessibility-everywhere/scanner": "^1.0.0",
1717
"commander": "^11.1.0",
18-
"chalk": "^4.1.2",
19-
"ora": "^5.4.1",
18+
"chalk": "^5.3.0",
19+
"ora": "^7.0.1",
2020
"cli-table3": "^0.6.3",
2121
"fs-extra": "^11.2.0"
2222
},
2323
"devDependencies": {
24-
"@types/node": "^20.10.0",
25-
"@types/fs-extra": "^11.0.4",
26-
"typescript": "^5.3.2",
27-
"ts-node": "^10.9.2"
24+
"rescript": "^11.1.0",
25+
"@rescript/core": "^1.5.0"
2826
}
2927
}

tools/cli/rescript.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "@accessibility-everywhere/cli",
3+
"sources": [
4+
{
5+
"dir": "src",
6+
"subdirs": true
7+
}
8+
],
9+
"package-specs": {
10+
"module": "esmodule",
11+
"in-source": true
12+
},
13+
"suffix": ".mjs",
14+
"bs-dependencies": ["@rescript/core"],
15+
"bsc-flags": ["-open RescriptCore"]
16+
}

tools/cli/src/Cli.mjs

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
// Generated by ReScript, PLEASE EDIT WITH CARE
2+
3+
import Ora from "ora";
4+
import * as Path from "path";
5+
import Chalk from "chalk";
6+
import * as Js_exn from "rescript/lib/es6/js_exn.js";
7+
import * as FsExtra from "fs-extra";
8+
import * as Core__Int from "@rescript/core/src/Core__Int.mjs";
9+
import * as Commander from "commander";
10+
import CliTable3 from "cli-table3";
11+
import * as Core__Option from "@rescript/core/src/Core__Option.mjs";
12+
import * as Caml_js_exceptions from "rescript/lib/es6/caml_js_exceptions.js";
13+
import * as Scanner from "@accessibility-everywhere/scanner";
14+
15+
var Scanner$1 = {};
16+
17+
var Commander$1 = {};
18+
19+
var Chalk$1 = {};
20+
21+
var Ora$1 = {};
22+
23+
var Table = {};
24+
25+
var Fs = {};
26+
27+
var Path$1 = {};
28+
29+
var Process = {};
30+
31+
var chalk = Chalk;
32+
33+
function getGrade(score) {
34+
if (score >= 90) {
35+
return "A";
36+
} else if (score >= 80) {
37+
return "B";
38+
} else if (score >= 70) {
39+
return "C";
40+
} else if (score >= 60) {
41+
return "D";
42+
} else {
43+
return "F";
44+
}
45+
}
46+
47+
var bar = "=".repeat(70);
48+
49+
function parseWcagLevel(s) {
50+
switch (s) {
51+
case "A" :
52+
return "A";
53+
case "AAA" :
54+
return "AAA";
55+
default:
56+
return "AA";
57+
}
58+
}
59+
60+
function wcagLabel(w) {
61+
if (w === "AA") {
62+
return "AA";
63+
} else if (w === "AAA") {
64+
return "AAA";
65+
} else {
66+
return "A";
67+
}
68+
}
69+
70+
function impactColored(impact) {
71+
var label = impact === "serious" ? "serious" : (
72+
impact === "minor" ? "minor" : (
73+
impact === "critical" ? "critical" : "moderate"
74+
)
75+
);
76+
if (impact === "serious") {
77+
return chalk.red(label);
78+
} else if (impact === "minor") {
79+
return chalk.blue(label);
80+
} else if (impact === "critical") {
81+
return chalk.bold.red(label);
82+
} else {
83+
return chalk.yellow(label);
84+
}
85+
}
86+
87+
function scoreColored(score) {
88+
if (score >= 90) {
89+
return chalk.bold.green(score.toString());
90+
} else if (score >= 70) {
91+
return chalk.bold.yellow(score.toString());
92+
} else {
93+
return chalk.bold.red(score.toString());
94+
}
95+
}
96+
97+
function generateMarkdown(result, url) {
98+
var header = "# Accessibility Report\n\n**URL:** " + url + "\n**Score:** " + result.score.toString() + "/100\n\n## Violations\n\n";
99+
var body = result.violations.map(function (v, i) {
100+
return "### " + (i + 1 | 0).toString() + ". " + v.help + "\n\n" + ("- **Impact:** " + v.impact + "\n") + ("- **Instances:** " + v.nodes.length.toString() + "\n") + ("- **Help:** " + v.helpUrl + "\n\n");
101+
}).join("");
102+
return header + body;
103+
}
104+
105+
async function scanAction(url, options) {
106+
var spinner = Ora("Scanning for accessibility issues...").start();
107+
try {
108+
var scanner = Scanner.createScanner();
109+
var result = await scanner.scan({
110+
url: url,
111+
wcagLevel: parseWcagLevel(options.level),
112+
screenshot: options.screenshot
113+
});
114+
spinner.succeed("Scan complete!");
115+
console.log("\n" + bar);
116+
console.log(chalk.bold.blue("Accessibility Report"));
117+
console.log(bar);
118+
console.log("URL: " + url);
119+
console.log("WCAG Level: " + options.level);
120+
console.log("Score: " + scoreColored(result.score) + "/100 (Grade " + getGrade(result.score) + ")");
121+
console.log(bar + "\n");
122+
var summaryTable = new CliTable3({
123+
head: [
124+
"Metric",
125+
"Count"
126+
],
127+
colWidths: [
128+
30,
129+
10
130+
]
131+
});
132+
summaryTable.push([
133+
"✅ Passes",
134+
result.passes.length.toString()
135+
]);
136+
summaryTable.push([
137+
"❌ Violations",
138+
result.violations.length.toString()
139+
]);
140+
summaryTable.push([
141+
"⚠️ Needs Review",
142+
result.incomplete.length.toString()
143+
]);
144+
console.log(summaryTable.toString() + "\n");
145+
if (result.violations.length > 0) {
146+
console.log(chalk.bold.red("Found " + result.violations.length.toString() + " violations:\n"));
147+
var format = options.format;
148+
if (format === "table") {
149+
var violationsTable = new CliTable3({
150+
head: [
151+
"Impact",
152+
"Description",
153+
"Instances",
154+
"WCAG"
155+
],
156+
colWidths: [
157+
12,
158+
50,
159+
10,
160+
15
161+
]
162+
});
163+
result.violations.forEach(function (v) {
164+
violationsTable.push([
165+
impactColored(v.impact),
166+
v.description,
167+
v.nodes.length.toString(),
168+
v.tags.join(", ")
169+
]);
170+
});
171+
console.log(violationsTable.toString());
172+
} else if (format === "markdown") {
173+
console.log(generateMarkdown(result, url));
174+
} else {
175+
console.log(JSON.stringify(result, null, 2));
176+
}
177+
} else {
178+
console.log(chalk.bold.green("🎉 No violations found! Great job!"));
179+
}
180+
var out = options.output;
181+
if (out !== undefined) {
182+
await FsExtra.writeJson(out, result, {
183+
spaces: 2
184+
});
185+
console.log(chalk.gray("\n✓ Results saved to " + out));
186+
}
187+
return process.exit(result.violations.length > 0 ? 1 : 0);
188+
}
189+
catch (raw_err){
190+
var err = Caml_js_exceptions.internalToOCamlException(raw_err);
191+
if (err.RE_EXN_ID === Js_exn.$$Error) {
192+
spinner.fail("Scan failed");
193+
console.error(chalk.red(Core__Option.getOr(err._1.message, "unknown error")));
194+
return process.exit(1);
195+
}
196+
throw err;
197+
}
198+
}
199+
200+
async function ciAction(url, options) {
201+
var spinner = Ora("Running CI scan...").start();
202+
try {
203+
var scanner = Scanner.createScanner();
204+
var result = await scanner.scan({
205+
url: url,
206+
wcagLevel: parseWcagLevel(options.level)
207+
});
208+
spinner.succeed("CI scan complete");
209+
var minScore = Core__Option.getOr(Core__Int.fromString(options.minScore, undefined), 70);
210+
var failOnViolations = Core__Option.getOr(options.failOnViolations, false);
211+
console.log("Score: " + result.score.toString() + "/100");
212+
console.log("Violations: " + result.violations.length.toString());
213+
if (failOnViolations && result.violations.length > 0) {
214+
console.error(chalk.red("✗ Failed: Found " + result.violations.length.toString() + " violations"));
215+
process.exit(1);
216+
}
217+
if (result.score < minScore) {
218+
console.error(chalk.red("✗ Failed: Score " + result.score.toString() + " below minimum " + minScore.toString()));
219+
process.exit(1);
220+
}
221+
console.log(chalk.green("✓ Passed all checks"));
222+
return process.exit(0);
223+
}
224+
catch (raw_err){
225+
var err = Caml_js_exceptions.internalToOCamlException(raw_err);
226+
if (err.RE_EXN_ID === Js_exn.$$Error) {
227+
spinner.fail("CI scan failed");
228+
console.error(chalk.red(Core__Option.getOr(err._1.message, "unknown error")));
229+
return process.exit(1);
230+
}
231+
throw err;
232+
}
233+
}
234+
235+
async function batchAction(file, options) {
236+
try {
237+
var raw = await FsExtra.readFile(file, "utf-8");
238+
var urls = raw.split("\n").map(function (prim) {
239+
return prim.trim();
240+
}).filter(function (s) {
241+
if (s !== "") {
242+
return !s.startsWith("#");
243+
} else {
244+
return false;
245+
}
246+
});
247+
console.log(chalk.blue("Scanning " + urls.length.toString() + " URLs..."));
248+
var outDir = Core__Option.getOr(options.output, "./scan-results");
249+
await FsExtra.ensureDir(outDir);
250+
var scanner = Scanner.createScanner();
251+
var completed = 0;
252+
var failed = 0;
253+
var total = urls.length;
254+
for(var i = 0; i < total; ++i){
255+
var url = urls[i];
256+
var spinner = Ora("[" + (i + 1 | 0).toString() + "/" + total.toString() + "] " + url).start();
257+
try {
258+
var result = await scanner.scan({
259+
url: url,
260+
wcagLevel: parseWcagLevel(options.level)
261+
});
262+
var safeName = url.replace(/[^a-z0-9]/gi, "_") + ".json";
263+
var filepath = Path.join(outDir, safeName);
264+
await FsExtra.writeJson(filepath, result, {
265+
spaces: 2
266+
});
267+
spinner.succeed(url + " - Score: " + result.score.toString());
268+
completed = completed + 1 | 0;
269+
}
270+
catch (raw_err){
271+
var err = Caml_js_exceptions.internalToOCamlException(raw_err);
272+
if (err.RE_EXN_ID === Js_exn.$$Error) {
273+
spinner.fail(url + " - " + Core__Option.getOr(err._1.message, "unknown error"));
274+
failed = failed + 1 | 0;
275+
} else {
276+
throw err;
277+
}
278+
}
279+
}
280+
console.log(chalk.green("\n✓ Completed: " + completed.toString()));
281+
if (failed > 0) {
282+
console.log(chalk.red("✗ Failed: " + failed.toString()));
283+
return ;
284+
} else {
285+
return ;
286+
}
287+
}
288+
catch (raw_err$1){
289+
var err$1 = Caml_js_exceptions.internalToOCamlException(raw_err$1);
290+
if (err$1.RE_EXN_ID === Js_exn.$$Error) {
291+
console.error(chalk.red(Core__Option.getOr(err$1._1.message, "unknown error")));
292+
return process.exit(1);
293+
}
294+
throw err$1;
295+
}
296+
}
297+
298+
var program = new Commander.Command().name("accessibility-scan").description("Command-line tool for accessibility scanning").version("1.0.0");
299+
300+
program.command("scan").description("Scan a URL for accessibility issues").argument("<url>", "URL to scan").option("-l, --level <level>", "WCAG level (A, AA, AAA)", "AA").option("-o, --output <file>", "Output file for results (JSON)").option("-f, --format <format>", "Output format (json, table, markdown)", "table").option("--screenshot", "Take screenshot").action(scanAction);
301+
302+
program.command("ci").description("Run accessibility scan for CI/CD").argument("<url>", "URL to scan").option("-l, --level <level>", "WCAG level (A, AA, AAA)", "AA").option("--min-score <score>", "Minimum required score", "70").option("--fail-on-violations", "Fail if any violations found").action(ciAction);
303+
304+
program.command("batch").description("Scan multiple URLs from a file").argument("<file>", "File containing URLs (one per line)").option("-l, --level <level>", "WCAG level (A, AA, AAA)", "AA").option("-o, --output <dir>", "Output directory for results", "./scan-results").action(batchAction);
305+
306+
program.parse();
307+
308+
export {
309+
Scanner$1 as Scanner,
310+
Commander$1 as Commander,
311+
Chalk$1 as Chalk,
312+
Ora$1 as Ora,
313+
Table ,
314+
Fs ,
315+
Path$1 as Path,
316+
Process ,
317+
chalk ,
318+
getGrade ,
319+
bar ,
320+
parseWcagLevel ,
321+
wcagLabel ,
322+
impactColored ,
323+
scoreColored ,
324+
generateMarkdown ,
325+
scanAction ,
326+
ciAction ,
327+
batchAction ,
328+
program ,
329+
}
330+
/* chalk Not a pure module */

0 commit comments

Comments
 (0)