@surea11y/core 1.3.0 → 1.4.1
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 +87 -2
- package/README.md +109 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +26 -0
- package/docs/ARIA_DEPRECATION.md +95 -0
- package/docs/CI_INTEGRATIONS.md +7 -7
- package/docs/ENGINE_OPTIONS.md +1 -1
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +1 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +6 -6
- package/package.json +52 -16
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +4 -6
- package/src/checks/automatic/aria-allowed-attr.js +663 -134
- package/src/checks/automatic/aria-allowed-role.js +2 -0
- package/src/checks/automatic/aria-braille-equivalent.js +2 -0
- package/src/checks/automatic/aria-conditional-attr.js +8 -7
- package/src/checks/automatic/aria-deprecated-role.js +107 -38
- package/src/checks/automatic/aria-hidden-body.js +6 -4
- package/src/checks/automatic/aria-hidden-focus.js +12 -13
- package/src/checks/automatic/aria-prohibited-attr.js +98 -105
- package/src/checks/automatic/aria-prohibited-children.js +56 -87
- package/src/checks/automatic/aria-required-attr.js +6 -7
- package/src/checks/automatic/aria-required-children.js +7 -10
- package/src/checks/automatic/aria-required-parent.js +20 -25
- package/src/checks/automatic/aria-role-name-present.js +2 -0
- package/src/checks/automatic/aria-roles-valid.js +33 -6
- package/src/checks/automatic/aria-valid-attr-value.js +18 -15
- package/src/checks/automatic/aria-valid-attr.js +2 -0
- package/src/checks/automatic/autocomplete-valid.js +39 -1
- package/src/checks/automatic/avoid-inline-spacing.js +181 -20
- package/src/checks/automatic/binary-control-name-present.js +13 -3
- package/src/checks/automatic/button-name-present.js +54 -21
- package/src/checks/automatic/canvas-text-alternative-present.js +17 -8
- package/src/checks/automatic/combobox-name-present.js +10 -1
- package/src/checks/automatic/contrast-computable.js +2 -0
- package/src/checks/automatic/contrast-enhanced.js +2 -0
- package/src/checks/automatic/contrast-minimum.js +2 -0
- package/src/checks/automatic/css-orientation-lock.js +21 -27
- package/src/checks/automatic/definition-list-children-valid.js +6 -6
- package/src/checks/automatic/deprecated-elements-not-used.js +4 -2
- package/src/checks/automatic/dialog-name-present.js +18 -11
- package/src/checks/automatic/dlitem-parent-valid.js +2 -0
- package/src/checks/automatic/duplicate-id-aria.js +4 -3
- package/src/checks/automatic/embed-text-alternative-present.js +2 -0
- package/src/checks/automatic/form-control-programmatic-label-present.js +50 -4
- package/src/checks/automatic/form-control-single-label.js +110 -43
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -0
- package/src/checks/automatic/iframe-focusable-content.js +246 -18
- package/src/checks/automatic/iframe-name-present.js +2 -0
- package/src/checks/automatic/iframe-title-unique.js +3 -1
- package/src/checks/automatic/img-alt-present.js +23 -18
- package/src/checks/automatic/input-image-alt-present.js +101 -52
- package/src/checks/automatic/label-in-name.js +97 -27
- package/src/checks/automatic/language-page-present.js +7 -1
- package/src/checks/automatic/link-in-text-block.js +2 -0
- package/src/checks/automatic/link-name-present.js +52 -17
- package/src/checks/automatic/list-children-valid.js +14 -24
- package/src/checks/automatic/listbox-name-present.js +10 -1
- package/src/checks/automatic/listitem-parent-valid.js +30 -7
- package/src/checks/automatic/menuitem-name-present.js +10 -1
- package/src/checks/automatic/meta-refresh-no-exceptions.js +33 -4
- package/src/checks/automatic/meta-refresh-timing-absent.js +32 -4
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +39 -15
- package/src/checks/automatic/meter-name-present.js +12 -4
- package/src/checks/automatic/nested-interactive-controls-absent.js +177 -25
- package/src/checks/automatic/object-text-alternative-present.js +16 -7
- package/src/checks/automatic/option-name-present.js +10 -1
- package/src/checks/automatic/page-title-present.js +2 -0
- package/src/checks/automatic/progressbar-name-present.js +16 -11
- package/src/checks/automatic/role-img-alt-present.js +4 -4
- package/src/checks/automatic/searchbox-name-present.js +10 -1
- package/src/checks/automatic/server-side-image-map-absent.js +4 -3
- package/src/checks/automatic/slider-name-present.js +13 -2
- package/src/checks/automatic/spinbutton-name-present.js +10 -1
- package/src/checks/automatic/summary-name-present.js +10 -1
- package/src/checks/automatic/svg-image-text-alternative-present.js +2 -0
- package/src/checks/automatic/svg-text-alternative-present.js +17 -5
- package/src/checks/automatic/tab-name-present.js +10 -1
- package/src/checks/automatic/table-headers-attr-valid.js +3 -2
- package/src/checks/automatic/table-th-has-data-cells.js +69 -6
- package/src/checks/automatic/target-size-minimum.js +5 -0
- package/src/checks/automatic/td-has-header.js +24 -1
- package/src/checks/automatic/textbox-name-present.js +10 -1
- package/src/checks/automatic/tooltip-name-present.js +10 -1
- package/src/checks/automatic/treeitem-name-present.js +10 -1
- package/src/checks/automatic/valid-lang.js +18 -3
- package/src/checks/automatic/video-poster-text-alternative-present.js +2 -0
- package/src/checks/manual/accesskeys-manual.js +3 -1
- package/src/checks/manual/area-alt-decorative-manual.js +2 -0
- package/src/checks/manual/area-alt-quality-manual.js +2 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +14 -23
- package/src/checks/manual/aria-text-manual.js +6 -5
- package/src/checks/manual/bypass-blocks-present-manual.js +279 -0
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/css-hidden-focus.js +184 -9
- package/src/checks/manual/embed-text-alternative-quality-manual.js +18 -13
- package/src/checks/manual/empty-heading-manual.js +17 -17
- package/src/checks/manual/empty-table-header-manual.js +52 -25
- package/src/checks/manual/focus-order-semantics-manual.js +16 -4
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -0
- package/src/checks/manual/heading-order-manual.js +28 -1
- package/src/checks/manual/identical-links-same-purpose-manual.js +2 -0
- package/src/checks/manual/image-redundant-alt-manual.js +21 -1
- package/src/checks/manual/img-alt-decorative-manual.js +2 -0
- package/src/checks/manual/img-alt-quality-manual.js +2 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +26 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +2 -0
- package/src/checks/manual/label-title-only-manual.js +29 -22
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +51 -55
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +44 -31
- package/src/checks/manual/landmark-main-is-top-level-manual.js +41 -24
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +16 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +14 -21
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +10 -13
- package/src/checks/manual/landmark-one-main-manual.js +12 -23
- package/src/checks/manual/landmark-unique-manual.js +37 -52
- package/src/checks/manual/link-name-quality-manual.js +2 -0
- package/src/checks/manual/media-transcript-present-manual.js +2 -0
- package/src/checks/manual/meta-viewport-large-manual.js +3 -1
- package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -0
- package/src/checks/manual/no-autoplay-audio-manual.js +2 -0
- package/src/checks/manual/object-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/p-as-heading-manual.js +2 -0
- package/src/checks/manual/page-has-heading-one-manual.js +12 -11
- package/src/checks/manual/page-title-patterns-manual.js +2 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +51 -37
- package/src/checks/manual/region-manual.js +27 -36
- package/src/checks/manual/scope-attr-valid-manual.js +3 -1
- package/src/checks/manual/scrollable-region-focusable-manual.js +2 -0
- package/src/checks/manual/skip-link-manual.js +7 -6
- package/src/checks/manual/svg-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/tabindex-manual.js +3 -1
- package/src/checks/manual/table-duplicate-name-manual.js +5 -4
- package/src/checks/manual/table-fake-caption-manual.js +24 -3
- package/src/checks/manual/video-caption-manual.js +2 -0
- package/src/checks/manual-review.js +2 -0
- package/src/core.js +11820 -3317
- package/src/index.js +2 -0
- package/src/report.js +51 -9
- package/src/sarif.js +20 -5
- package/surea11y.browser.js +4943 -1388
- package/bin/core.js +0 -473
- package/docs/CLI.md +0 -128
- package/src/catalogs/composites.wcag.js +0 -454
- package/src/checks/automatic/bypass-blocks-present.js +0 -215
- package/src/checks/rules-and-tags.full.csv +0 -19
- package/src/checks/rules-and-tags.full.json +0 -259
- package/src/core/aria-helpers.js +0 -1211
- package/src/core/contrast-helpers.js +0 -1302
- package/src/core/dom-helpers.js +0 -4493
- package/src/core/dom-runner.js +0 -787
- package/src/core/frame-messaging.js +0 -261
- package/src/core/frame-scan.js +0 -190
- package/src/core/rollup-composites.js +0 -127
- package/src/core/rule-meta.js +0 -176
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -1228
- package/src/i18n/fr.js +0 -1185
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -59
- package/src/policy/schemas/engine-options.schema.json +0 -103
- package/src/policy/schemas/policy-contract.schema.json +0 -40
package/bin/core.js
DELETED
|
@@ -1,473 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* surea11y CLI — a thin wrapper around the library for ad hoc/CI use.
|
|
6
|
-
*
|
|
7
|
-
* This is the one part of the package that depends on jsdom (see
|
|
8
|
-
* package.json's "dependencies" vs the library itself, which has none --
|
|
9
|
-
* `require('surea11y')` never loads jsdom; only running this CLI does).
|
|
10
|
-
* Static-HTML only: it does not execute page JavaScript, so client-rendered
|
|
11
|
-
* content won't be scanned. For that, use a real browser via Puppeteer/
|
|
12
|
-
* Playwright — see docs/INTEGRATION.md Pattern 2.
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* surea11y scan <file-or-url> [options]
|
|
16
|
-
*
|
|
17
|
-
* See docs/CLI.md for the full option reference.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const fs = require('fs');
|
|
21
|
-
const path = require('path');
|
|
22
|
-
|
|
23
|
-
const pkg = require('../package.json');
|
|
24
|
-
const { buildBaselineEntries, matchBaseline } = require('../src/baseline.js');
|
|
25
|
-
const { renderHtmlReport } = require('../src/report.js');
|
|
26
|
-
const { renderSarifReport } = require('../src/sarif.js');
|
|
27
|
-
|
|
28
|
-
// Piping output to `head`/`less`/etc. closes stdout early — without this,
|
|
29
|
-
// the next write throws an unhandled EPIPE and crashes with a raw stack
|
|
30
|
-
// trace instead of just stopping quietly, like well-behaved CLI tools do.
|
|
31
|
-
process.stdout.on('error', (err) => {
|
|
32
|
-
if (err && err.code === 'EPIPE') process.exit(0);
|
|
33
|
-
throw err;
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
function printHelp() {
|
|
37
|
-
process.stdout.write(`surea11y v${pkg.version}
|
|
38
|
-
|
|
39
|
-
Usage:
|
|
40
|
-
surea11y scan <file-or-url> [options]
|
|
41
|
-
|
|
42
|
-
Options:
|
|
43
|
-
--json Print the raw result object as JSON instead of a summary
|
|
44
|
-
--locale <locale> Locale for output text (default: en)
|
|
45
|
-
--rules <ids> Comma-separated rule IDs to run (only these)
|
|
46
|
-
--exclude-rules <ids> Comma-separated rule IDs to exclude
|
|
47
|
-
--tags <tags> Comma-separated tags to run (e.g. wcag2a,wcag2aa)
|
|
48
|
-
--context <selector> CSS selector to scope the scan to a subtree
|
|
49
|
-
--custom-rules <path> Load runtime custom rules from a JS file (repeatable)
|
|
50
|
-
--write-baseline <path> Write every current "fail" occurrence to <path>; never fails the build
|
|
51
|
-
--baseline <path> Gate only on occurrences not already recorded in <path>
|
|
52
|
-
--html <path> Write a self-contained, browsable HTML report to <path>
|
|
53
|
-
--sarif <path> Write a SARIF 2.1.0 report to <path> (for GitHub Code Scanning etc.)
|
|
54
|
-
-h, --help Show this help
|
|
55
|
-
-v, --version Show the installed version
|
|
56
|
-
|
|
57
|
-
Exit codes:
|
|
58
|
-
0 scan completed, no "fail" outcomes (or no *new* ones, with --baseline)
|
|
59
|
-
1 scan completed, at least one "fail" outcome (or *new* one, with --baseline)
|
|
60
|
-
2 usage error or the scan itself could not run (bad path/URL, network failure, etc.)
|
|
61
|
-
|
|
62
|
-
Examples:
|
|
63
|
-
surea11y scan ./index.html
|
|
64
|
-
surea11y scan https://example.com/ --tags wcag2a,wcag2aa
|
|
65
|
-
surea11y scan ./index.html --json > result.json
|
|
66
|
-
surea11y scan ./index.html --write-baseline baseline.json
|
|
67
|
-
surea11y scan ./index.html --baseline baseline.json
|
|
68
|
-
surea11y scan ./index.html --html report.html
|
|
69
|
-
surea11y scan ./index.html --baseline baseline.json --sarif results.sarif
|
|
70
|
-
surea11y scan ./index.html --custom-rules ./a11y-rules.js
|
|
71
|
-
|
|
72
|
-
See docs/BASELINE.md for the baseline/allowlist mechanism, docs/REPORT.md for the HTML report,
|
|
73
|
-
docs/SARIF.md for the SARIF report, docs/CLI.md#custom-rules for custom rules.
|
|
74
|
-
`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function parseArgs(argv) {
|
|
78
|
-
const out = { _: [], json: false };
|
|
79
|
-
for (let i = 0; i < argv.length; i++) {
|
|
80
|
-
const a = argv[i];
|
|
81
|
-
switch (a) {
|
|
82
|
-
case '--json':
|
|
83
|
-
out.json = true;
|
|
84
|
-
break;
|
|
85
|
-
case '--locale':
|
|
86
|
-
out.locale = argv[++i];
|
|
87
|
-
break;
|
|
88
|
-
case '--rules':
|
|
89
|
-
out.rules = argv[++i];
|
|
90
|
-
break;
|
|
91
|
-
case '--exclude-rules':
|
|
92
|
-
out.excludeRules = argv[++i];
|
|
93
|
-
break;
|
|
94
|
-
case '--tags':
|
|
95
|
-
out.tags = argv[++i];
|
|
96
|
-
break;
|
|
97
|
-
case '--context':
|
|
98
|
-
out.context = argv[++i];
|
|
99
|
-
break;
|
|
100
|
-
case '--custom-rules':
|
|
101
|
-
(out.customRules = out.customRules || []).push(argv[++i]);
|
|
102
|
-
break;
|
|
103
|
-
case '--baseline':
|
|
104
|
-
out.baseline = argv[++i];
|
|
105
|
-
break;
|
|
106
|
-
case '--write-baseline':
|
|
107
|
-
out.writeBaseline = argv[++i];
|
|
108
|
-
break;
|
|
109
|
-
case '--html':
|
|
110
|
-
out.html = argv[++i];
|
|
111
|
-
break;
|
|
112
|
-
case '--sarif':
|
|
113
|
-
out.sarif = argv[++i];
|
|
114
|
-
break;
|
|
115
|
-
case '-h':
|
|
116
|
-
case '--help':
|
|
117
|
-
out.help = true;
|
|
118
|
-
break;
|
|
119
|
-
case '-v':
|
|
120
|
-
case '--version':
|
|
121
|
-
out.version = true;
|
|
122
|
-
break;
|
|
123
|
-
default:
|
|
124
|
-
out._.push(a);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return out;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function isUrl(s) {
|
|
131
|
-
return /^https?:\/\//i.test(s);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function formatError(err) {
|
|
135
|
-
const base = err && err.message ? err.message : String(err);
|
|
136
|
-
const cause =
|
|
137
|
-
err && err.cause && err.cause.message
|
|
138
|
-
? err.cause.message
|
|
139
|
-
: err && err.cause
|
|
140
|
-
? String(err.cause)
|
|
141
|
-
: '';
|
|
142
|
-
return cause ? `${base}: ${cause}` : base;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function loadHtml(target) {
|
|
146
|
-
if (isUrl(target)) {
|
|
147
|
-
const res = await fetch(target);
|
|
148
|
-
if (!res.ok) {
|
|
149
|
-
throw new Error(`Fetching ${target} failed: HTTP ${res.status} ${res.statusText}`);
|
|
150
|
-
}
|
|
151
|
-
return { html: await res.text(), url: target };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const resolved = path.resolve(process.cwd(), target);
|
|
155
|
-
if (!fs.existsSync(resolved)) {
|
|
156
|
-
throw new Error(`No such file: ${resolved}`);
|
|
157
|
-
}
|
|
158
|
-
return { html: fs.readFileSync(resolved, 'utf8'), url: `file://${resolved}` };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function buildEngineOptions(args, customRules) {
|
|
162
|
-
const engineOptions = {};
|
|
163
|
-
if (args.locale) engineOptions.locale = args.locale;
|
|
164
|
-
if (args.rules || args.excludeRules) {
|
|
165
|
-
engineOptions.rules = {};
|
|
166
|
-
if (args.rules) engineOptions.rules.include = args.rules;
|
|
167
|
-
if (args.excludeRules) engineOptions.rules.exclude = args.excludeRules;
|
|
168
|
-
}
|
|
169
|
-
if (args.tags) engineOptions.tags = { include: args.tags };
|
|
170
|
-
if (customRules && customRules.length) engineOptions.customRules = customRules;
|
|
171
|
-
return engineOptions;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Loads one --custom-rules file. The CLI runs custom rules in the same
|
|
175
|
-
// process/JS realm as the scan itself (unlike a binding whose engineOptions
|
|
176
|
-
// crosses page.evaluate()'s serialization boundary), so a descriptor's
|
|
177
|
-
// runInPage/applicability may be a real function -- see the customRules
|
|
178
|
-
// section of docs/ENGINE_OPTIONS.md for the full descriptor contract this
|
|
179
|
-
// mirrors. Validated up front (not left to the engine's own silent-skip of
|
|
180
|
-
// invalid entries) so a typo in a hand-authored file fails loudly as a usage
|
|
181
|
-
// error instead of quietly producing a scan with one fewer rule than expected.
|
|
182
|
-
function loadCustomRulesFile(customRulesPath) {
|
|
183
|
-
const resolved = path.resolve(process.cwd(), customRulesPath);
|
|
184
|
-
|
|
185
|
-
let loaded;
|
|
186
|
-
try {
|
|
187
|
-
loaded = require(resolved);
|
|
188
|
-
} catch (err) {
|
|
189
|
-
throw new Error(`Could not load custom rules file "${customRulesPath}": ${formatError(err)}`, {
|
|
190
|
-
cause: err
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const descriptors = Array.isArray(loaded) ? loaded : [loaded];
|
|
195
|
-
|
|
196
|
-
for (const d of descriptors) {
|
|
197
|
-
const hasId = d && typeof d === 'object' && typeof d.id === 'string' && d.id.trim();
|
|
198
|
-
const hasRunInPage =
|
|
199
|
-
d &&
|
|
200
|
-
(typeof d.runInPage === 'function' ||
|
|
201
|
-
(typeof d.runInPage === 'string' && d.runInPage.trim()));
|
|
202
|
-
if (!hasId || !hasRunInPage) {
|
|
203
|
-
throw new Error(
|
|
204
|
-
`Custom rules file "${customRulesPath}" must export a rule descriptor ({ id, runInPage, ... }) or an array of them. See docs/CLI.md#custom-rules.`
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return descriptors;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function printSummary(result, baselineMatch) {
|
|
213
|
-
const byOutcome = { pass: 0, fail: 0, cantTell: 0, notApplicable: 0 };
|
|
214
|
-
for (const r of result.checksResults) {
|
|
215
|
-
if (Object.prototype.hasOwnProperty.call(byOutcome, r.outcome)) byOutcome[r.outcome] += 1;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
process.stdout.write(`\nsurea11y scan: ${result.url || '(no url)'}\n`);
|
|
219
|
-
process.stdout.write(
|
|
220
|
-
` pass: ${byOutcome.pass} fail: ${byOutcome.fail} cantTell: ${byOutcome.cantTell} notApplicable: ${byOutcome.notApplicable}\n\n`
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
const fails = result.checksResults.filter((r) => r.outcome === 'fail');
|
|
224
|
-
if (fails.length) {
|
|
225
|
-
process.stdout.write(`FAIL (${fails.length} rule(s)):\n`);
|
|
226
|
-
for (const r of fails) {
|
|
227
|
-
process.stdout.write(
|
|
228
|
-
`\n ${r.ruleId} (${r.severity}, ${r.occurrences.length} occurrence(s))\n`
|
|
229
|
-
);
|
|
230
|
-
for (const occ of r.occurrences.slice(0, 5)) {
|
|
231
|
-
process.stdout.write(` - ${occ.selector || '(no selector)'}\n ${occ.summary}\n`);
|
|
232
|
-
if (occ.hint) process.stdout.write(` hint: ${occ.hint}\n`);
|
|
233
|
-
}
|
|
234
|
-
if (r.occurrences.length > 5) {
|
|
235
|
-
process.stdout.write(` ... and ${r.occurrences.length - 5} more\n`);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
process.stdout.write('\n');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const cantTells = result.checksResults.filter((r) => r.outcome === 'cantTell');
|
|
242
|
-
if (cantTells.length) {
|
|
243
|
-
process.stdout.write(
|
|
244
|
-
`cantTell — needs human review (${cantTells.length} rule(s)): ${cantTells.map((r) => r.ruleId).join(', ')}\n\n`
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (baselineMatch) {
|
|
249
|
-
process.stdout.write(
|
|
250
|
-
`baseline: ${baselineMatch.knownCount} known, ${baselineMatch.newCount} new, ${baselineMatch.staleCount} stale (no longer detected)\n`
|
|
251
|
-
);
|
|
252
|
-
if (baselineMatch.newCount) {
|
|
253
|
-
process.stdout.write(`\nNEW (not in baseline, ${baselineMatch.newCount} occurrence(s)):\n`);
|
|
254
|
-
for (const occ of baselineMatch.newOccurrences.slice(0, 5)) {
|
|
255
|
-
process.stdout.write(
|
|
256
|
-
` - ${occ.ruleId}: ${occ.selector || '(no selector)'}\n ${occ.summary}\n`
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
if (baselineMatch.newOccurrences.length > 5) {
|
|
260
|
-
process.stdout.write(` ... and ${baselineMatch.newOccurrences.length - 5} more\n`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
process.stdout.write('\n');
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function loadBaselineFile(baselinePath) {
|
|
268
|
-
let raw;
|
|
269
|
-
try {
|
|
270
|
-
raw = fs.readFileSync(baselinePath, 'utf8');
|
|
271
|
-
} catch (err) {
|
|
272
|
-
throw new Error(
|
|
273
|
-
`Could not read baseline file "${baselinePath}": ${formatError(err)}. Run with --write-baseline ${baselinePath} first to create one.`,
|
|
274
|
-
{ cause: err }
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
let parsed;
|
|
279
|
-
try {
|
|
280
|
-
parsed = JSON.parse(raw);
|
|
281
|
-
} catch (err) {
|
|
282
|
-
throw new Error(`Baseline file "${baselinePath}" is not valid JSON: ${formatError(err)}`, {
|
|
283
|
-
cause: err
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.entries)) {
|
|
288
|
-
throw new Error(
|
|
289
|
-
`Baseline file "${baselinePath}" is not a supported baseline (expected { version: 1, entries: [...] }). Regenerate it with --write-baseline.`
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return parsed;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async function runScan(args) {
|
|
297
|
-
const target = args._[0];
|
|
298
|
-
if (!target) {
|
|
299
|
-
process.stderr.write('Error: scan requires a file path or URL. See --help.\n');
|
|
300
|
-
process.exitCode = 2;
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (args.baseline && args.writeBaseline) {
|
|
305
|
-
process.stderr.write(
|
|
306
|
-
'Error: --baseline and --write-baseline cannot be used together in the same run. See --help.\n'
|
|
307
|
-
);
|
|
308
|
-
process.exitCode = 2;
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
let baselineFile = null;
|
|
313
|
-
if (args.baseline) {
|
|
314
|
-
try {
|
|
315
|
-
baselineFile = loadBaselineFile(args.baseline);
|
|
316
|
-
} catch (err) {
|
|
317
|
-
process.stderr.write(`Error: ${formatError(err)}\n`);
|
|
318
|
-
process.exitCode = 2;
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
let customRules = [];
|
|
324
|
-
if (args.customRules && args.customRules.length) {
|
|
325
|
-
try {
|
|
326
|
-
for (const customRulesPath of args.customRules) {
|
|
327
|
-
customRules = customRules.concat(loadCustomRulesFile(customRulesPath));
|
|
328
|
-
}
|
|
329
|
-
} catch (err) {
|
|
330
|
-
process.stderr.write(`Error: ${formatError(err)}\n`);
|
|
331
|
-
process.exitCode = 2;
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
let html, url;
|
|
337
|
-
try {
|
|
338
|
-
({ html, url } = await loadHtml(target));
|
|
339
|
-
} catch (err) {
|
|
340
|
-
process.stderr.write(`Error: ${formatError(err)}\n`);
|
|
341
|
-
process.exitCode = 2;
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
let JSDOM;
|
|
346
|
-
try {
|
|
347
|
-
({ JSDOM } = require('jsdom'));
|
|
348
|
-
} catch {
|
|
349
|
-
process.stderr.write(
|
|
350
|
-
'Error: the surea11y CLI requires jsdom. Run `npm install jsdom` (it should already be a dependency of this package — this likely means a broken install).\n'
|
|
351
|
-
);
|
|
352
|
-
process.exitCode = 2;
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const { runDomRulesInPage } = require('../src/index.js');
|
|
357
|
-
|
|
358
|
-
const dom = new JSDOM(html, { url, pretendToBeVisual: true });
|
|
359
|
-
global.window = dom.window;
|
|
360
|
-
global.document = dom.window.document;
|
|
361
|
-
|
|
362
|
-
let result;
|
|
363
|
-
try {
|
|
364
|
-
result = runDomRulesInPage(
|
|
365
|
-
url,
|
|
366
|
-
args.context || null,
|
|
367
|
-
buildEngineOptions(args, customRules),
|
|
368
|
-
null
|
|
369
|
-
);
|
|
370
|
-
} finally {
|
|
371
|
-
dom.window.close();
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (args.html) {
|
|
375
|
-
fs.writeFileSync(
|
|
376
|
-
args.html,
|
|
377
|
-
renderHtmlReport(result, { title: `surea11y scan report — ${target}` })
|
|
378
|
-
);
|
|
379
|
-
process.stderr.write(`Wrote HTML report to: ${args.html}\n`);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (args.sarif) {
|
|
383
|
-
fs.writeFileSync(
|
|
384
|
-
args.sarif,
|
|
385
|
-
renderSarifReport(result, {
|
|
386
|
-
toolVersion: pkg.version,
|
|
387
|
-
informationUri: (pkg.homepage || '').replace(/#.*$/, ''),
|
|
388
|
-
baselineEntries: baselineFile ? baselineFile.entries : undefined
|
|
389
|
-
})
|
|
390
|
-
);
|
|
391
|
-
process.stderr.write(`Wrote SARIF report to: ${args.sarif}\n`);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (args.writeBaseline) {
|
|
395
|
-
const entries = buildBaselineEntries(result);
|
|
396
|
-
const payload = { version: 1, generatedAt: new Date().toISOString(), entries };
|
|
397
|
-
fs.writeFileSync(args.writeBaseline, JSON.stringify(payload, null, 2) + '\n');
|
|
398
|
-
|
|
399
|
-
if (args.json) {
|
|
400
|
-
process.stdout.write(
|
|
401
|
-
JSON.stringify(
|
|
402
|
-
{
|
|
403
|
-
...result,
|
|
404
|
-
baseline: { mode: 'write', path: args.writeBaseline, entries: entries.length }
|
|
405
|
-
},
|
|
406
|
-
null,
|
|
407
|
-
2
|
|
408
|
-
) + '\n'
|
|
409
|
-
);
|
|
410
|
-
} else {
|
|
411
|
-
printSummary(result);
|
|
412
|
-
}
|
|
413
|
-
process.stderr.write(
|
|
414
|
-
`Wrote ${entries.length} occurrence(s) to baseline: ${args.writeBaseline}\n`
|
|
415
|
-
);
|
|
416
|
-
process.exitCode = 0;
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (baselineFile) {
|
|
421
|
-
const match = matchBaseline(result, baselineFile.entries);
|
|
422
|
-
|
|
423
|
-
if (args.json) {
|
|
424
|
-
process.stdout.write(
|
|
425
|
-
JSON.stringify({ ...result, baseline: { mode: 'check', ...match } }, null, 2) + '\n'
|
|
426
|
-
);
|
|
427
|
-
} else {
|
|
428
|
-
printSummary(result, match);
|
|
429
|
-
}
|
|
430
|
-
process.exitCode = match.newCount > 0 ? 1 : 0;
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if (args.json) {
|
|
435
|
-
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
436
|
-
} else {
|
|
437
|
-
printSummary(result);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const hasFail = result.checksResults.some((r) => r.outcome === 'fail');
|
|
441
|
-
process.exitCode = hasFail ? 1 : 0;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
async function main() {
|
|
445
|
-
const args = parseArgs(process.argv.slice(2));
|
|
446
|
-
|
|
447
|
-
if (args.version) {
|
|
448
|
-
process.stdout.write(`${pkg.version}\n`);
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
if (args.help || args._.length === 0) {
|
|
452
|
-
printHelp();
|
|
453
|
-
process.exitCode = args._.length === 0 && !args.help ? 2 : 0;
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const [command, ...rest] = args._;
|
|
458
|
-
if (command !== 'scan') {
|
|
459
|
-
process.stderr.write(
|
|
460
|
-
`Error: unknown command "${command}". Only "scan" is supported. See --help.\n`
|
|
461
|
-
);
|
|
462
|
-
process.exitCode = 2;
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
args._ = rest;
|
|
467
|
-
await runScan(args);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
main().catch((err) => {
|
|
471
|
-
process.stderr.write(`Error: ${formatError(err)}\n`);
|
|
472
|
-
process.exitCode = 2;
|
|
473
|
-
});
|
package/docs/CLI.md
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
# CLI
|
|
2
|
-
|
|
3
|
-
`surea11y` ships a small CLI (`bin/core.js`) for ad hoc scans and CI, on top of the library API described in [`INTEGRATION.md`](./INTEGRATION.md).
|
|
4
|
-
|
|
5
|
-
```sh
|
|
6
|
-
npx @surea11y/core scan ./index.html
|
|
7
|
-
npx @surea11y/core scan https://example.com/
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
## What it can and can't scan
|
|
11
|
-
|
|
12
|
-
The CLI reads **static HTML only** — a local file, or the raw response of an HTTP(S) GET request — and never executes page JavaScript. That means client-rendered content (anything your framework injects after page load) won't be captured, and geometry-dependent rules like `target-size-minimum` will report `notApplicable` (no real CSS layout — see [`LIMITATIONS.md`](./LIMITATIONS.md)). If you need either of those, drive a real browser yourself and use `runa11yCoreInPage` directly — see [`INTEGRATION.md`](./INTEGRATION.md) Pattern 2. The CLI is the fast path for static/server-rendered pages and CI; the library is what you reach for beyond that.
|
|
13
|
-
|
|
14
|
-
## Options
|
|
15
|
-
|
|
16
|
-
| Flag | Meaning |
|
|
17
|
-
|---|---|
|
|
18
|
-
| `--json` | Print the raw result object (see [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md)) instead of a human-readable summary. |
|
|
19
|
-
| `--locale <locale>` | Output text locale (default `en`) — see [`I18N.md`](./I18N.md). |
|
|
20
|
-
| `--rules <ids>` | Comma-separated rule IDs — only run these. |
|
|
21
|
-
| `--exclude-rules <ids>` | Comma-separated rule IDs — never run these. |
|
|
22
|
-
| `--tags <tags>` | Comma-separated tags — e.g. `--tags wcag2a,wcag2aa` to target a conformance level (see [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md)). |
|
|
23
|
-
| `--context <selector>` | Scope the scan to one CSS-selected subtree. |
|
|
24
|
-
| `--custom-rules <path>` | Load runtime custom rules from a local JS file. Repeatable. See [Custom rules](#custom-rules) below. |
|
|
25
|
-
| `--write-baseline <path>` | Write every current `fail` occurrence to `<path>`; never fails the build. See [`BASELINE.md`](./BASELINE.md). |
|
|
26
|
-
| `--baseline <path>` | Gate only on occurrences not already recorded in `<path>`. See [`BASELINE.md`](./BASELINE.md). |
|
|
27
|
-
| `--html <path>` | Write a self-contained, browsable HTML report to `<path>`. See [`REPORT.md`](./REPORT.md). |
|
|
28
|
-
| `--sarif <path>` | Write a SARIF 2.1.0 report to `<path>` (e.g. for GitHub Code Scanning). See [`SARIF.md`](./SARIF.md). |
|
|
29
|
-
| `-h`, `--help` | Show usage. |
|
|
30
|
-
| `-v`, `--version` | Show the installed version. |
|
|
31
|
-
|
|
32
|
-
These map directly onto `engineOptions`/`runOnly` (see [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md)) — `--rules`/`--exclude-rules` become `engineOptions.rules.include`/`.exclude`, `--tags` becomes `engineOptions.tags.include`.
|
|
33
|
-
|
|
34
|
-
## Exit codes
|
|
35
|
-
|
|
36
|
-
| Code | Meaning |
|
|
37
|
-
|---|---|
|
|
38
|
-
| `0` | Scan completed, no `fail` outcomes. |
|
|
39
|
-
| `1` | Scan completed, at least one `fail` outcome — the CI-gating case. |
|
|
40
|
-
| `2` | Usage error, or the scan itself couldn't run (bad path/URL, network failure, missing `jsdom`). |
|
|
41
|
-
|
|
42
|
-
`cantTell` outcomes never affect the exit code — they're printed as a "needs human review" summary, consistent with the manual/`cantTell` mental model in [`TROUBLESHOOTING.md`](./TROUBLESHOOTING.md#should-i-treat-canttell-as-a-failure). If you need `cantTell`-aware gating, use `--json` and inspect `checksResults` yourself, or call the library directly (see [`INTEGRATION.md`](./INTEGRATION.md#ci-gating-a-build-on-the-result)).
|
|
43
|
-
|
|
44
|
-
With `--baseline`, exit code `1` means at least one *new* (not-yet-baselined) `fail` occurrence, not any `fail` occurrence — see [`BASELINE.md`](./BASELINE.md).
|
|
45
|
-
|
|
46
|
-
## Baseline / allowlist
|
|
47
|
-
|
|
48
|
-
For an existing, imperfect site, gating on every `fail` on day one is often an adoption blocker. `--write-baseline`/`--baseline` let you accept the current state once and gate CI only on genuinely new violations from then on:
|
|
49
|
-
|
|
50
|
-
```sh
|
|
51
|
-
surea11y scan ./dist/index.html --write-baseline baseline.json # once, commit the file
|
|
52
|
-
surea11y scan ./dist/index.html --baseline baseline.json # in CI, from then on
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
See [`BASELINE.md`](./BASELINE.md) for the matching semantics, file format, and known limitations.
|
|
56
|
-
|
|
57
|
-
## Custom rules
|
|
58
|
-
|
|
59
|
-
For an org-specific check that isn't (and shouldn't be) one of the built-in rules — an internal design-system convention, a company style-guide requirement — `--custom-rules <path>` registers your own rule(s) for that one scan, on top of every built-in rule:
|
|
60
|
-
|
|
61
|
-
```sh
|
|
62
|
-
surea11y scan ./dist/index.html --custom-rules ./a11y-rules.js
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
`a11y-rules.js` exports either a single rule descriptor or an array of them, using the same shape as a built-in rule module:
|
|
66
|
-
|
|
67
|
-
```js
|
|
68
|
-
// a11y-rules.js
|
|
69
|
-
module.exports = [
|
|
70
|
-
{
|
|
71
|
-
id: 'org-no-inline-onclick',
|
|
72
|
-
meta: { title: 'No inline onclick handlers', defaultSeverity: 'moderate' },
|
|
73
|
-
runInPage(ctx) {
|
|
74
|
-
const els = ctx.helpers.queryAll('[onclick]');
|
|
75
|
-
const occurrences = els.map((el) => ({
|
|
76
|
-
selector: ctx.helpers.buildSelector(el),
|
|
77
|
-
html: el.outerHTML,
|
|
78
|
-
summary: 'Inline onclick handler found.',
|
|
79
|
-
hint: 'Move event handling into an external script.'
|
|
80
|
-
}));
|
|
81
|
-
return {
|
|
82
|
-
ruleId: ctx.rule.ruleId,
|
|
83
|
-
outcome: occurrences.length ? 'fail' : 'pass',
|
|
84
|
-
occurrences
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
];
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
- `<path>` is a **local file**, `require()`d directly by the CLI — never a URL. (Unlike the scan target, which does accept a URL: fetching and executing remote code as a rule would be a very different, much riskier trust model than running a file you already have on disk.)
|
|
92
|
-
- Because the CLI runs your rule in the same Node process as the scan, `runInPage`/`applicability` can be plain functions — no `fn.toString()` string-source workaround needed (that's only required for callers, like a browser-automation binding, whose `engineOptions` crosses a serialization boundary). See [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md) for the full descriptor contract (`meta` defaulting, the `ctx` shape, etc.) — it's identical here.
|
|
93
|
-
- Repeat the flag to load rules from more than one file: `--custom-rules ./a.js --custom-rules ./b.js`.
|
|
94
|
-
- A custom rule's `id` colliding with a built-in one **overrides** that built-in for the scan, surfaced via a `console.warn` and the result's top-level `overriddenBuiltinIds` array — see [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md).
|
|
95
|
-
- The file itself is validated at load time (must export a descriptor, or array of descriptors, each with a string `id` and a function-or-source-string `runInPage`) — a malformed export exits `2` with a clear error rather than silently scanning with one fewer rule than expected.
|
|
96
|
-
- Works alongside every other flag, including `--rules`/`--exclude-rules`/`--tags` (which can target your custom rule's `id` exactly like a built-in one) and `--baseline`/`--html`/`--sarif`.
|
|
97
|
-
|
|
98
|
-
## HTML report
|
|
99
|
-
|
|
100
|
-
For a browsable view of a scan's results — hero summary, WCAG rollup grouped by conformance level, and a searchable/filterable occurrence table — rather than raw JSON or a terminal summary:
|
|
101
|
-
|
|
102
|
-
```sh
|
|
103
|
-
surea11y scan ./dist/index.html --html report.html
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Open `report.html` directly from disk; no server, no external assets. Works alongside any other output mode. See [`REPORT.md`](./REPORT.md).
|
|
107
|
-
|
|
108
|
-
## SARIF report
|
|
109
|
-
|
|
110
|
-
For GitHub Code Scanning or another SARIF-consuming dashboard:
|
|
111
|
-
|
|
112
|
-
```sh
|
|
113
|
-
surea11y scan ./dist/index.html --sarif results.sarif
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Works alongside any other output mode, and alongside `--baseline` (already-known `fail` occurrences are omitted from the SARIF output rather than re-reported). See [`SARIF.md`](./SARIF.md).
|
|
117
|
-
|
|
118
|
-
## In CI
|
|
119
|
-
|
|
120
|
-
```sh
|
|
121
|
-
npx @surea11y/core scan ./dist/index.html || exit 1
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Or, since the exit code already reflects pass/fail, just let the command's own exit code propagate — most CI systems fail the step automatically on a non-zero exit. See [`CI_INTEGRATIONS.md`](./CI_INTEGRATIONS.md) for ready-to-paste GitHub Actions and Bitbucket Pipelines templates, including a SARIF-upload example.
|
|
125
|
-
|
|
126
|
-
## A note on dependencies
|
|
127
|
-
|
|
128
|
-
The CLI is the one part of this package that depends on `jsdom` — `require('@surea11y/core')` as a library never loads it (see [`SECURITY.md`](../SECURITY.md)). If you only ever use the library API directly against your own DOM (jsdom, a real browser, whatever you already have), you're not paying for `jsdom` a second time.
|