equall-cli 0.1.2 → 0.1.4

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/dist/cli.js CHANGED
@@ -5,8 +5,9 @@ import {
5
5
  __export,
6
6
  __require,
7
7
  __toESM,
8
+ globby,
8
9
  runScan
9
- } from "./chunk-JMFHJMBB.js";
10
+ } from "./chunk-CMP4ST3J.js";
10
11
 
11
12
  // node_modules/commander/lib/error.js
12
13
  var require_error = __commonJS({
@@ -3009,7 +3010,9 @@ var require_commander = __commonJS({
3009
3010
  });
3010
3011
 
3011
3012
  // src/cli.ts
3012
- import { resolve, basename } from "path";
3013
+ import { resolve as resolve2, basename } from "path";
3014
+ import { readFileSync, existsSync, statSync } from "fs";
3015
+ import { fileURLToPath } from "url";
3013
3016
 
3014
3017
  // node_modules/commander/esm.mjs
3015
3018
  var import_index = __toESM(require_commander(), 1);
@@ -6512,7 +6515,7 @@ function bar(value, width = 20) {
6512
6515
  const color = scoreColor(value);
6513
6516
  return `${color}${"\u2588".repeat(filled)}${DIM}${"\u2591".repeat(empty)}${RESET} ${color}${value}${RESET}`;
6514
6517
  }
6515
- function printResult(result) {
6518
+ function printResult(result, options = {}) {
6516
6519
  const { score, conformance_level, pour_scores, summary, scanners_used, duration_ms } = result;
6517
6520
  console.log();
6518
6521
  console.log(`${BOLD} \u25C6 EQUALL \u2014 Accessibility Score${RESET}`);
@@ -6531,6 +6534,9 @@ function printResult(result) {
6531
6534
  const bpIssuesCount = result.issues.length - wcagIssuesCount;
6532
6535
  console.log(` ${summary.files_scanned} files scanned \xB7 ${wcagIssuesCount} WCAG violations \xB7 ${bpIssuesCount} best-practice issues`);
6533
6536
  console.log(` ${RED}${summary.by_severity.critical} critical${RESET} ${YELLOW}${summary.by_severity.serious} serious${RESET} ${CYAN}${summary.by_severity.moderate} moderate${RESET} ${DIM}${summary.by_severity.minor} minor${RESET}`);
6537
+ if (summary.ignored_count > 0) {
6538
+ console.log(` ${DIM}${summary.ignored_count} issue${summary.ignored_count > 1 ? "s" : ""} ignored via equall-ignore${RESET}`);
6539
+ }
6534
6540
  if (result.criteria_total > 0) {
6535
6541
  const covered = result.criteria_covered.length;
6536
6542
  const total = result.criteria_total;
@@ -6559,8 +6565,9 @@ function printResult(result) {
6559
6565
  console.log();
6560
6566
  printCoaching(result);
6561
6567
  console.log();
6562
- const wcagIssues = result.issues.filter((i) => i.wcag_criteria.length > 0);
6563
- const bpIssues = result.issues.filter((i) => i.wcag_criteria.length === 0);
6568
+ const visibleIssues = result.issues.filter((i) => !i.ignored);
6569
+ const wcagIssues = visibleIssues.filter((i) => i.wcag_criteria.length > 0);
6570
+ const bpIssues = visibleIssues.filter((i) => i.wcag_criteria.length === 0);
6564
6571
  if (wcagIssues.length > 0) {
6565
6572
  console.log(` ${BOLD}WCAG Violations${RESET}`);
6566
6573
  const grouped = groupByCriterion(wcagIssues);
@@ -6597,6 +6604,17 @@ function printResult(result) {
6597
6604
  }
6598
6605
  console.log();
6599
6606
  }
6607
+ if (options.showIgnored) {
6608
+ const ignoredIssues = result.issues.filter((i) => i.ignored);
6609
+ if (ignoredIssues.length > 0) {
6610
+ console.log(` ${BOLD}Ignored${RESET}`);
6611
+ for (const issue of ignoredIssues) {
6612
+ const location = issue.line ? `:${issue.line}` : "";
6613
+ console.log(` ${DIM}\u2298${RESET} ${DIM}${issue.file_path}${location}${RESET} ${issue.scanner_rule_id}`);
6614
+ }
6615
+ console.log();
6616
+ }
6617
+ }
6600
6618
  console.log(` ${DIM}Scanners: ${scanners_used.map((s) => `${s.name}@${s.version} (${s.issues_found} issues)`).join(", ")}${RESET}`);
6601
6619
  console.log(` ${DIM}Completed in ${(duration_ms / 1e3).toFixed(1)}s${RESET}`);
6602
6620
  console.log();
@@ -6660,16 +6678,172 @@ function printJson(result) {
6660
6678
  console.log(JSON.stringify(result, null, 2));
6661
6679
  }
6662
6680
 
6681
+ // src/ignores.ts
6682
+ import { readFile, writeFile } from "fs/promises";
6683
+ import { resolve } from "path";
6684
+ var IGNORE_PATTERN = /equall-ignore-(next-line|file)(?:\s+(\S+))?/;
6685
+ async function findIgnores(rootPath) {
6686
+ const paths = await globby(
6687
+ ["**/*.{html,htm,jsx,tsx,vue,svelte,astro}"],
6688
+ {
6689
+ cwd: rootPath,
6690
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**"],
6691
+ absolute: false,
6692
+ gitignore: true
6693
+ }
6694
+ );
6695
+ const entries = [];
6696
+ for (const relativePath of paths) {
6697
+ const absolutePath = resolve(rootPath, relativePath);
6698
+ let content;
6699
+ try {
6700
+ content = await readFile(absolutePath, "utf-8");
6701
+ } catch {
6702
+ continue;
6703
+ }
6704
+ const lines = content.split("\n");
6705
+ for (let i = 0; i < lines.length; i++) {
6706
+ const match = lines[i].match(IGNORE_PATTERN);
6707
+ if (!match) continue;
6708
+ const type = match[1];
6709
+ if (type === "file" && i >= 5) continue;
6710
+ entries.push({
6711
+ file_path: relativePath,
6712
+ line: i + 1,
6713
+ type,
6714
+ rule_id: match[2] ?? null,
6715
+ raw: lines[i].trim()
6716
+ });
6717
+ }
6718
+ }
6719
+ return entries.sort((a, b) => a.file_path.localeCompare(b.file_path) || a.line - b.line);
6720
+ }
6721
+ async function addIgnoreFile(rootPath, filePath) {
6722
+ const absolutePath = resolve(rootPath, filePath);
6723
+ let content;
6724
+ try {
6725
+ content = await readFile(absolutePath, "utf-8");
6726
+ } catch {
6727
+ return null;
6728
+ }
6729
+ const lines = content.split("\n");
6730
+ if (lines.slice(0, 5).some((l) => l.includes("equall-ignore-file"))) {
6731
+ return null;
6732
+ }
6733
+ const ext = filePath.split(".").pop()?.toLowerCase();
6734
+ let comment;
6735
+ if (ext === "html" || ext === "htm") {
6736
+ comment = "<!-- equall-ignore-file -->";
6737
+ } else {
6738
+ comment = "// equall-ignore-file";
6739
+ }
6740
+ await writeFile(absolutePath, comment + "\n" + content, "utf-8");
6741
+ return { file: filePath, comment };
6742
+ }
6743
+ async function removeIgnore(rootPath, target) {
6744
+ const ignores = await findIgnores(rootPath);
6745
+ const colonIdx = target.lastIndexOf(":");
6746
+ let targetFile;
6747
+ let targetLine = null;
6748
+ if (colonIdx > 0) {
6749
+ const maybeLine = parseInt(target.slice(colonIdx + 1), 10);
6750
+ if (!isNaN(maybeLine)) {
6751
+ targetFile = target.slice(0, colonIdx);
6752
+ targetLine = maybeLine;
6753
+ } else {
6754
+ targetFile = target;
6755
+ }
6756
+ } else {
6757
+ targetFile = target;
6758
+ }
6759
+ const matches = ignores.filter((e) => {
6760
+ if (e.file_path !== targetFile) return false;
6761
+ if (targetLine !== null) return e.line === targetLine;
6762
+ return true;
6763
+ });
6764
+ if (matches.length === 0) {
6765
+ return { removed: [], notFound: true };
6766
+ }
6767
+ const byFile = /* @__PURE__ */ new Map();
6768
+ for (const m of matches) {
6769
+ const lines = byFile.get(m.file_path) ?? [];
6770
+ lines.push(m.line);
6771
+ byFile.set(m.file_path, lines);
6772
+ }
6773
+ for (const [filePath, lineNumbers] of byFile) {
6774
+ const absolutePath = resolve(rootPath, filePath);
6775
+ const content = await readFile(absolutePath, "utf-8");
6776
+ const lines = content.split("\n");
6777
+ const toRemove = new Set(lineNumbers.map((l) => l - 1));
6778
+ const newLines = lines.filter((_, i) => !toRemove.has(i));
6779
+ await writeFile(absolutePath, newLines.join("\n"), "utf-8");
6780
+ }
6781
+ return { removed: matches, notFound: false };
6782
+ }
6783
+ async function addIgnore(rootPath, target, ruleId) {
6784
+ const colonIdx = target.lastIndexOf(":");
6785
+ if (colonIdx <= 0) return null;
6786
+ const targetFile = target.slice(0, colonIdx);
6787
+ const targetLine = parseInt(target.slice(colonIdx + 1), 10);
6788
+ if (isNaN(targetLine) || targetLine < 1) return null;
6789
+ const absolutePath = resolve(rootPath, targetFile);
6790
+ let content;
6791
+ try {
6792
+ content = await readFile(absolutePath, "utf-8");
6793
+ } catch {
6794
+ return null;
6795
+ }
6796
+ const lines = content.split("\n");
6797
+ if (targetLine > lines.length) return null;
6798
+ const targetContent = lines[targetLine - 1];
6799
+ const indent = targetContent.match(/^(\s*)/)?.[1] ?? "";
6800
+ const ext = targetFile.split(".").pop()?.toLowerCase();
6801
+ const ruleSuffix = ruleId ? ` ${ruleId}` : "";
6802
+ let comment;
6803
+ if (ext === "html" || ext === "htm") {
6804
+ comment = `${indent}<!-- equall-ignore-next-line${ruleSuffix} -->`;
6805
+ } else if (ext === "jsx" || ext === "tsx") {
6806
+ const inJsx = targetContent.trimStart().startsWith("<") || targetContent.includes(">");
6807
+ comment = inJsx ? `${indent}{/* equall-ignore-next-line${ruleSuffix} */}` : `${indent}// equall-ignore-next-line${ruleSuffix}`;
6808
+ } else {
6809
+ comment = `${indent}// equall-ignore-next-line${ruleSuffix}`;
6810
+ }
6811
+ lines.splice(targetLine - 1, 0, comment);
6812
+ await writeFile(absolutePath, lines.join("\n"), "utf-8");
6813
+ return { file: targetFile, line: targetLine, comment };
6814
+ }
6815
+ async function clearAllIgnores(rootPath) {
6816
+ const ignores = await findIgnores(rootPath);
6817
+ if (ignores.length === 0) return [];
6818
+ const byFile = /* @__PURE__ */ new Map();
6819
+ for (const entry of ignores) {
6820
+ const lines = byFile.get(entry.file_path) ?? [];
6821
+ lines.push(entry.line);
6822
+ byFile.set(entry.file_path, lines);
6823
+ }
6824
+ for (const [filePath, lineNumbers] of byFile) {
6825
+ const absolutePath = resolve(rootPath, filePath);
6826
+ const content = await readFile(absolutePath, "utf-8");
6827
+ const lines = content.split("\n");
6828
+ const toRemove = new Set(lineNumbers.map((l) => l - 1));
6829
+ const newLines = lines.filter((_, i) => !toRemove.has(i));
6830
+ await writeFile(absolutePath, newLines.join("\n"), "utf-8");
6831
+ }
6832
+ return ignores;
6833
+ }
6834
+
6663
6835
  // src/cli.ts
6836
+ var __dir = resolve2(fileURLToPath(import.meta.url), "..");
6837
+ var pkg = JSON.parse(readFileSync(resolve2(__dir, "..", "package.json"), "utf-8"));
6664
6838
  var program2 = new Command();
6665
- program2.name("equall").description("Open-source accessibility scoring \u2014 aggregates axe-core, eslint-plugin-jsx-a11y, and more.").version("0.1.0");
6666
- program2.command("scan").description("Scan a project for accessibility issues").argument("[path]", "Path to project root", ".").option("-l, --level <level>", "WCAG conformance target: A, AA, or AAA", "AA").option("--include <patterns...>", "Glob patterns to include").option("--exclude <patterns...>", "Glob patterns to exclude").option("--json", "Output results as JSON").option("--no-color", "Disable colored output").action(async (path, opts) => {
6839
+ program2.name("equall").description("Open-source accessibility scoring \u2014 aggregates axe-core, eslint-plugin-jsx-a11y, and more.").version(pkg.version);
6840
+ program2.command("scan").description("Scan a project for accessibility issues").argument("[path]", "Path to project root", ".").option("-l, --level <level>", "WCAG conformance target: A, AA, or AAA", "AA").option("--include <patterns...>", "Glob patterns to include").option("--exclude <patterns...>", "Glob patterns to exclude").option("--json", "Output results as JSON").option("-i, --show-ignored", "Show ignored issues in output").option("--no-color", "Disable colored output").action(async (path, opts) => {
6667
6841
  const level = opts.level.toUpperCase();
6668
6842
  if (!["A", "AA", "AAA"].includes(level)) {
6669
6843
  console.error(`Invalid level "${opts.level}". Use A, AA, or AAA.`);
6670
6844
  process.exit(1);
6671
6845
  }
6672
- const displayName = basename(resolve(path));
6846
+ const displayName = basename(resolve2(path));
6673
6847
  const spinner = opts.json ? null : ora({ text: `Scanning ${displayName}`, indent: 2 }).start();
6674
6848
  try {
6675
6849
  const result = await runScan({
@@ -6691,7 +6865,7 @@ program2.command("scan").description("Scan a project for accessibility issues").
6691
6865
  if (opts.json) {
6692
6866
  printJson(result);
6693
6867
  } else {
6694
- printResult(result);
6868
+ printResult(result, { showIgnored: opts.showIgnored });
6695
6869
  }
6696
6870
  if (result.score < 50) process.exit(1);
6697
6871
  } catch (error2) {
@@ -6703,4 +6877,83 @@ program2.command("scan").description("Scan a project for accessibility issues").
6703
6877
  process.exit(2);
6704
6878
  }
6705
6879
  });
6880
+ var DIM2 = "\x1B[2m";
6881
+ var BOLD2 = "\x1B[1m";
6882
+ var RESET2 = "\x1B[0m";
6883
+ var GREEN2 = "\x1B[32m";
6884
+ var YELLOW2 = "\x1B[33m";
6885
+ program2.command("ignore").description("Add, list, or remove equall-ignore comments").argument("[target]", "File:line to ignore (e.g. src/Modal.tsx:89)").argument("[rule-id]", "Optional rule ID (e.g. jsx-a11y/alt-text)").option("-p, --path <path>", "Path to project root", ".").option("--remove <target>", "Remove ignore at file:line or all ignores in a file").option("--clear", "Remove all equall-ignore comments from the project").option("--list", "List all equall-ignore comments").action(async (target, ruleId, opts) => {
6886
+ const rootPath = resolve2(opts.path);
6887
+ if (opts.clear) {
6888
+ const removed = await clearAllIgnores(rootPath);
6889
+ if (removed.length === 0) {
6890
+ console.log("\n No equall-ignore comments found.\n");
6891
+ } else {
6892
+ console.log(`
6893
+ ${GREEN2}Removed ${removed.length} ignore comment${removed.length > 1 ? "s" : ""}${RESET2}
6894
+ `);
6895
+ }
6896
+ return;
6897
+ }
6898
+ if (opts.remove) {
6899
+ const { removed, notFound } = await removeIgnore(rootPath, opts.remove);
6900
+ if (notFound) {
6901
+ console.error(`
6902
+ No equall-ignore found at ${opts.remove}
6903
+ `);
6904
+ process.exit(1);
6905
+ }
6906
+ for (const entry of removed) {
6907
+ const location = entry.type === "file" ? "" : `:${entry.line}`;
6908
+ console.log(` ${GREEN2}Removed${RESET2} ${entry.file_path}${location} ${DIM2}${entry.raw}${RESET2}`);
6909
+ }
6910
+ console.log();
6911
+ return;
6912
+ }
6913
+ if (target && target.includes(":")) {
6914
+ const result = await addIgnore(rootPath, target, ruleId);
6915
+ if (!result) {
6916
+ console.error(`
6917
+ Could not add ignore at ${target}. Check that the file and line exist.
6918
+ `);
6919
+ process.exit(1);
6920
+ }
6921
+ console.log(`
6922
+ ${GREEN2}Added${RESET2} ${result.file}:${result.line}`);
6923
+ console.log(` ${DIM2}${result.comment.trim()}${RESET2}
6924
+ `);
6925
+ return;
6926
+ }
6927
+ const isDirectory = target && existsSync(resolve2(rootPath, target)) && statSync(resolve2(rootPath, target)).isDirectory();
6928
+ if (target && !isDirectory && (target.includes("/") || target.match(/\.\w+$/))) {
6929
+ const result = await addIgnoreFile(rootPath, target);
6930
+ if (!result) {
6931
+ console.error(`
6932
+ Could not ignore ${target}. File not found or already ignored.
6933
+ `);
6934
+ process.exit(1);
6935
+ }
6936
+ console.log(`
6937
+ ${GREEN2}Added${RESET2} ${result.file}`);
6938
+ console.log(` ${DIM2}${result.comment}${RESET2}
6939
+ `);
6940
+ return;
6941
+ }
6942
+ const listPath = target ? resolve2(target) : rootPath;
6943
+ const ignores = await findIgnores(listPath);
6944
+ if (ignores.length === 0) {
6945
+ console.log("\n No equall-ignore comments found.\n");
6946
+ return;
6947
+ }
6948
+ console.log(`
6949
+ ${BOLD2}${ignores.length} ignore${ignores.length > 1 ? "s" : ""}${RESET2}
6950
+ `);
6951
+ for (const entry of ignores) {
6952
+ const location = `:${entry.line}`;
6953
+ const type = entry.type === "file" ? `${YELLOW2}equall-ignore-file${RESET2}` : `equall-ignore-next-line`;
6954
+ const rule = entry.rule_id ? ` ${DIM2}${entry.rule_id}${RESET2}` : ` ${DIM2}(all rules)${RESET2}`;
6955
+ console.log(` ${entry.file_path}${location}${" ".repeat(Math.max(1, 40 - entry.file_path.length - location.length))}${type}${rule}`);
6956
+ }
6957
+ console.log();
6958
+ });
6706
6959
  program2.parse();
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ interface GladosIssue {
12
12
  message: string;
13
13
  help_url: string | null;
14
14
  suggestion: string | null;
15
+ ignored?: boolean;
15
16
  }
16
17
  type WcagLevel = 'A' | 'AA' | 'AAA';
17
18
  type PourPrinciple = 'perceivable' | 'operable' | 'understandable' | 'robust';
@@ -66,6 +67,7 @@ interface ScanSummary {
66
67
  by_scanner: Record<string, number>;
67
68
  criteria_tested: string[];
68
69
  criteria_failed: string[];
70
+ ignored_count: number;
69
71
  }
70
72
  interface ScannerInfo {
71
73
  name: string;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from 'module'; import { fileURLToPat
2
2
  import {
3
3
  computeScanResult,
4
4
  runScan
5
- } from "./chunk-JMFHJMBB.js";
5
+ } from "./chunk-CMP4ST3J.js";
6
6
  export {
7
7
  computeScanResult,
8
8
  runScan
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "equall-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Open-source accessibility scoring CLI — aggregates axe-core, eslint-plugin-jsx-a11y, and more into a unified score.",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -51,12 +51,14 @@
51
51
  "@types/jsdom": "^21.1.7",
52
52
  "@types/node": "^22.0.0",
53
53
  "axe-core": "^4.10.0",
54
+ "cheerio": "^1.2.0",
54
55
  "commander": "^12.1.0",
55
56
  "eslint": "^9.0.0",
56
57
  "eslint-plugin-jsx-a11y": "^6.10.0",
57
58
  "globby": "^14.0.0",
58
59
  "jsdom": "^25.0.0",
59
60
  "ora": "^9.3.0",
61
+ "text-readability": "^1.1.1",
60
62
  "tsup": "^8.0.0",
61
63
  "tsx": "^4.0.0",
62
64
  "typescript": "^5.9.3",
@@ -65,4 +67,4 @@
65
67
  "engines": {
66
68
  "node": ">=18.0.0"
67
69
  }
68
- }
70
+ }