@surea11y/core 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -7
- package/LICENSE +373 -21
- package/README.md +175 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +27 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +19 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +63 -18
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +63 -31
- package/src/checks/automatic/aria-allowed-attr.js +204 -80
- package/src/checks/automatic/aria-allowed-role.js +23 -7
- package/src/checks/automatic/aria-braille-equivalent.js +34 -10
- package/src/checks/automatic/aria-conditional-attr.js +32 -14
- package/src/checks/automatic/aria-deprecated-role.js +26 -11
- package/src/checks/automatic/aria-hidden-body.js +48 -23
- package/src/checks/automatic/aria-hidden-focus.js +420 -66
- package/src/checks/automatic/aria-prohibited-attr.js +327 -60
- package/src/checks/automatic/aria-prohibited-children.js +111 -103
- package/src/checks/automatic/aria-required-attr.js +29 -15
- package/src/checks/automatic/aria-required-children.js +44 -24
- package/src/checks/automatic/aria-required-parent.js +64 -35
- package/src/checks/automatic/aria-role-name-present.js +49 -21
- package/src/checks/automatic/aria-roles-valid.js +24 -12
- package/src/checks/automatic/aria-valid-attr-value.js +46 -22
- package/src/checks/automatic/aria-valid-attr.js +19 -5
- package/src/checks/automatic/autocomplete-valid.js +76 -16
- package/src/checks/automatic/avoid-inline-spacing.js +23 -8
- package/src/checks/automatic/binary-control-name-present.js +62 -50
- package/src/checks/automatic/button-name-present.js +54 -24
- package/src/checks/automatic/bypass-blocks-present.js +51 -32
- package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
- package/src/checks/automatic/combobox-name-present.js +40 -45
- package/src/checks/automatic/contrast-computable.js +363 -341
- package/src/checks/automatic/contrast-enhanced.js +489 -466
- package/src/checks/automatic/contrast-minimum.js +488 -465
- package/src/checks/automatic/css-orientation-lock.js +51 -35
- package/src/checks/automatic/definition-list-children-valid.js +46 -25
- package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
- package/src/checks/automatic/dialog-name-present.js +47 -85
- package/src/checks/automatic/dlitem-parent-valid.js +25 -8
- package/src/checks/automatic/duplicate-id-aria.js +28 -9
- package/src/checks/automatic/embed-text-alternative-present.js +88 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
- package/src/checks/automatic/form-control-single-label.js +50 -14
- package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
- package/src/checks/automatic/iframe-focusable-content.js +265 -22
- package/src/checks/automatic/iframe-name-present.js +33 -9
- package/src/checks/automatic/iframe-title-unique.js +32 -9
- package/src/checks/automatic/img-alt-present.js +54 -52
- package/src/checks/automatic/input-image-alt-present.js +141 -112
- package/src/checks/automatic/label-in-name.js +65 -41
- package/src/checks/automatic/language-page-present.js +111 -109
- package/src/checks/automatic/link-in-text-block.js +61 -19
- package/src/checks/automatic/link-name-present.js +47 -14
- package/src/checks/automatic/list-children-valid.js +40 -33
- package/src/checks/automatic/listbox-name-present.js +41 -19
- package/src/checks/automatic/listitem-parent-valid.js +48 -13
- package/src/checks/automatic/menuitem-name-present.js +41 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
- package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
- package/src/checks/automatic/meter-name-present.js +40 -36
- package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
- package/src/checks/automatic/object-text-alternative-present.js +93 -39
- package/src/checks/automatic/option-name-present.js +40 -21
- package/src/checks/automatic/page-title-present.js +19 -6
- package/src/checks/automatic/progressbar-name-present.js +49 -44
- package/src/checks/automatic/role-img-alt-present.js +211 -159
- package/src/checks/automatic/searchbox-name-present.js +41 -19
- package/src/checks/automatic/server-side-image-map-absent.js +27 -11
- package/src/checks/automatic/slider-name-present.js +42 -47
- package/src/checks/automatic/spinbutton-name-present.js +41 -19
- package/src/checks/automatic/summary-name-present.js +39 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
- package/src/checks/automatic/svg-text-alternative-present.js +262 -230
- package/src/checks/automatic/tab-name-present.js +39 -60
- package/src/checks/automatic/table-headers-attr-valid.js +27 -10
- package/src/checks/automatic/table-th-has-data-cells.js +24 -8
- package/src/checks/automatic/target-size-minimum.js +123 -48
- package/src/checks/automatic/td-has-header.js +53 -12
- package/src/checks/automatic/textbox-name-present.js +41 -19
- package/src/checks/automatic/tooltip-name-present.js +39 -18
- package/src/checks/automatic/treeitem-name-present.js +40 -21
- package/src/checks/automatic/valid-lang.js +22 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
- package/src/checks/manual/accesskeys-manual.js +17 -6
- package/src/checks/manual/area-alt-decorative-manual.js +194 -193
- package/src/checks/manual/area-alt-quality-manual.js +184 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
- package/src/checks/manual/aria-text-manual.js +20 -11
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
- package/src/checks/manual/css-hidden-focus.js +375 -169
- package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
- package/src/checks/manual/empty-heading-manual.js +41 -24
- package/src/checks/manual/empty-table-header-manual.js +69 -31
- package/src/checks/manual/focus-order-semantics-manual.js +60 -13
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
- package/src/checks/manual/heading-order-manual.js +50 -8
- package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
- package/src/checks/manual/image-redundant-alt-manual.js +38 -8
- package/src/checks/manual/img-alt-decorative-manual.js +133 -96
- package/src/checks/manual/img-alt-quality-manual.js +178 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
- package/src/checks/manual/label-title-only-manual.js +44 -28
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
- package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
- package/src/checks/manual/landmark-one-main-manual.js +38 -43
- package/src/checks/manual/landmark-unique-manual.js +78 -67
- package/src/checks/manual/link-name-quality-manual.js +45 -12
- package/src/checks/manual/media-transcript-present-manual.js +37 -22
- package/src/checks/manual/meta-viewport-large-manual.js +19 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
- package/src/checks/manual/p-as-heading-manual.js +24 -7
- package/src/checks/manual/page-has-heading-one-manual.js +42 -32
- package/src/checks/manual/page-title-patterns-manual.js +80 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
- package/src/checks/manual/region-manual.js +244 -60
- package/src/checks/manual/scope-attr-valid-manual.js +13 -4
- package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
- package/src/checks/manual/skip-link-manual.js +42 -18
- package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
- package/src/checks/manual/tabindex-manual.js +13 -4
- package/src/checks/manual/table-duplicate-name-manual.js +22 -11
- package/src/checks/manual/table-fake-caption-manual.js +48 -10
- package/src/checks/manual/video-caption-manual.js +17 -4
- package/src/checks/manual-review.js +58 -12
- package/src/core.js +41705 -29650
- package/src/index.js +2 -0
- package/src/report.js +109 -47
- package/src/sarif.js +190 -0
- package/surea11y.browser.js +37774 -0
- package/bin/core.js +0 -348
- package/docs/CLI.md +0 -75
- package/src/catalogs/composites.wcag.js +0 -490
- 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 -970
- package/src/core/contrast-helpers.js +0 -1147
- package/src/core/dom-helpers.js +0 -4235
- package/src/core/dom-runner.js +0 -671
- package/src/core/frame-messaging.js +0 -210
- package/src/core/frame-scan.js +0 -178
- package/src/core/rollup-composites.js +0 -135
- package/src/core/rule-meta.js +0 -159
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -923
- package/src/i18n/fr.js +0 -844
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -55
- 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,348 +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
|
-
|
|
27
|
-
// Piping output to `head`/`less`/etc. closes stdout early — without this,
|
|
28
|
-
// the next write throws an unhandled EPIPE and crashes with a raw stack
|
|
29
|
-
// trace instead of just stopping quietly, like well-behaved CLI tools do.
|
|
30
|
-
process.stdout.on('error', (err) => {
|
|
31
|
-
if (err && err.code === 'EPIPE') process.exit(0);
|
|
32
|
-
throw err;
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
function printHelp() {
|
|
36
|
-
process.stdout.write(`surea11y v${pkg.version}
|
|
37
|
-
|
|
38
|
-
Usage:
|
|
39
|
-
surea11y scan <file-or-url> [options]
|
|
40
|
-
|
|
41
|
-
Options:
|
|
42
|
-
--json Print the raw result object as JSON instead of a summary
|
|
43
|
-
--locale <locale> Locale for output text (default: en)
|
|
44
|
-
--rules <ids> Comma-separated rule IDs to run (only these)
|
|
45
|
-
--exclude-rules <ids> Comma-separated rule IDs to exclude
|
|
46
|
-
--tags <tags> Comma-separated tags to run (e.g. wcag2a,wcag2aa)
|
|
47
|
-
--context <selector> CSS selector to scope the scan to a subtree
|
|
48
|
-
--write-baseline <path> Write every current "fail" occurrence to <path>; never fails the build
|
|
49
|
-
--baseline <path> Gate only on occurrences not already recorded in <path>
|
|
50
|
-
--html <path> Write a self-contained, browsable HTML report to <path>
|
|
51
|
-
-h, --help Show this help
|
|
52
|
-
-v, --version Show the installed version
|
|
53
|
-
|
|
54
|
-
Exit codes:
|
|
55
|
-
0 scan completed, no "fail" outcomes (or no *new* ones, with --baseline)
|
|
56
|
-
1 scan completed, at least one "fail" outcome (or *new* one, with --baseline)
|
|
57
|
-
2 usage error or the scan itself could not run (bad path/URL, network failure, etc.)
|
|
58
|
-
|
|
59
|
-
Examples:
|
|
60
|
-
surea11y scan ./index.html
|
|
61
|
-
surea11y scan https://example.com/ --tags wcag2a,wcag2aa
|
|
62
|
-
surea11y scan ./index.html --json > result.json
|
|
63
|
-
surea11y scan ./index.html --write-baseline baseline.json
|
|
64
|
-
surea11y scan ./index.html --baseline baseline.json
|
|
65
|
-
surea11y scan ./index.html --html report.html
|
|
66
|
-
|
|
67
|
-
See docs/BASELINE.md for the baseline/allowlist mechanism, docs/REPORT.md for the HTML report.
|
|
68
|
-
`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function parseArgs(argv) {
|
|
72
|
-
const out = { _: [], json: false };
|
|
73
|
-
for (let i = 0; i < argv.length; i++) {
|
|
74
|
-
const a = argv[i];
|
|
75
|
-
switch (a) {
|
|
76
|
-
case '--json':
|
|
77
|
-
out.json = true;
|
|
78
|
-
break;
|
|
79
|
-
case '--locale':
|
|
80
|
-
out.locale = argv[++i];
|
|
81
|
-
break;
|
|
82
|
-
case '--rules':
|
|
83
|
-
out.rules = argv[++i];
|
|
84
|
-
break;
|
|
85
|
-
case '--exclude-rules':
|
|
86
|
-
out.excludeRules = argv[++i];
|
|
87
|
-
break;
|
|
88
|
-
case '--tags':
|
|
89
|
-
out.tags = argv[++i];
|
|
90
|
-
break;
|
|
91
|
-
case '--context':
|
|
92
|
-
out.context = argv[++i];
|
|
93
|
-
break;
|
|
94
|
-
case '--baseline':
|
|
95
|
-
out.baseline = argv[++i];
|
|
96
|
-
break;
|
|
97
|
-
case '--write-baseline':
|
|
98
|
-
out.writeBaseline = argv[++i];
|
|
99
|
-
break;
|
|
100
|
-
case '--html':
|
|
101
|
-
out.html = argv[++i];
|
|
102
|
-
break;
|
|
103
|
-
case '-h':
|
|
104
|
-
case '--help':
|
|
105
|
-
out.help = true;
|
|
106
|
-
break;
|
|
107
|
-
case '-v':
|
|
108
|
-
case '--version':
|
|
109
|
-
out.version = true;
|
|
110
|
-
break;
|
|
111
|
-
default:
|
|
112
|
-
out._.push(a);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return out;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function isUrl(s) {
|
|
119
|
-
return /^https?:\/\//i.test(s);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function formatError(err) {
|
|
123
|
-
const base = err && err.message ? err.message : String(err);
|
|
124
|
-
const cause = err && err.cause && err.cause.message ? err.cause.message : (err && err.cause ? String(err.cause) : '');
|
|
125
|
-
return cause ? `${base}: ${cause}` : base;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function loadHtml(target) {
|
|
129
|
-
if (isUrl(target)) {
|
|
130
|
-
const res = await fetch(target);
|
|
131
|
-
if (!res.ok) {
|
|
132
|
-
throw new Error(`Fetching ${target} failed: HTTP ${res.status} ${res.statusText}`);
|
|
133
|
-
}
|
|
134
|
-
return { html: await res.text(), url: target };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const resolved = path.resolve(process.cwd(), target);
|
|
138
|
-
if (!fs.existsSync(resolved)) {
|
|
139
|
-
throw new Error(`No such file: ${resolved}`);
|
|
140
|
-
}
|
|
141
|
-
return { html: fs.readFileSync(resolved, 'utf8'), url: `file://${resolved}` };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function buildEngineOptions(args) {
|
|
145
|
-
const engineOptions = {};
|
|
146
|
-
if (args.locale) engineOptions.locale = args.locale;
|
|
147
|
-
if (args.rules || args.excludeRules) {
|
|
148
|
-
engineOptions.rules = {};
|
|
149
|
-
if (args.rules) engineOptions.rules.include = args.rules;
|
|
150
|
-
if (args.excludeRules) engineOptions.rules.exclude = args.excludeRules;
|
|
151
|
-
}
|
|
152
|
-
if (args.tags) engineOptions.tags = { include: args.tags };
|
|
153
|
-
return engineOptions;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function printSummary(result, baselineMatch) {
|
|
157
|
-
const byOutcome = { pass: 0, fail: 0, cantTell: 0, notApplicable: 0 };
|
|
158
|
-
for (const r of result.checksResults) {
|
|
159
|
-
if (Object.prototype.hasOwnProperty.call(byOutcome, r.outcome)) byOutcome[r.outcome] += 1;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
process.stdout.write(`\nsurea11y scan: ${result.url || '(no url)'}\n`);
|
|
163
|
-
process.stdout.write(` pass: ${byOutcome.pass} fail: ${byOutcome.fail} cantTell: ${byOutcome.cantTell} notApplicable: ${byOutcome.notApplicable}\n\n`);
|
|
164
|
-
|
|
165
|
-
const fails = result.checksResults.filter((r) => r.outcome === 'fail');
|
|
166
|
-
if (fails.length) {
|
|
167
|
-
process.stdout.write(`FAIL (${fails.length} rule(s)):\n`);
|
|
168
|
-
for (const r of fails) {
|
|
169
|
-
process.stdout.write(`\n ${r.ruleId} (${r.severity}, ${r.occurrences.length} occurrence(s))\n`);
|
|
170
|
-
for (const occ of r.occurrences.slice(0, 5)) {
|
|
171
|
-
process.stdout.write(` - ${occ.selector || '(no selector)'}\n ${occ.summary}\n`);
|
|
172
|
-
if (occ.hint) process.stdout.write(` hint: ${occ.hint}\n`);
|
|
173
|
-
}
|
|
174
|
-
if (r.occurrences.length > 5) {
|
|
175
|
-
process.stdout.write(` ... and ${r.occurrences.length - 5} more\n`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
process.stdout.write('\n');
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const cantTells = result.checksResults.filter((r) => r.outcome === 'cantTell');
|
|
182
|
-
if (cantTells.length) {
|
|
183
|
-
process.stdout.write(`cantTell — needs human review (${cantTells.length} rule(s)): ${cantTells.map((r) => r.ruleId).join(', ')}\n\n`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (baselineMatch) {
|
|
187
|
-
process.stdout.write(`baseline: ${baselineMatch.knownCount} known, ${baselineMatch.newCount} new, ${baselineMatch.staleCount} stale (no longer detected)\n`);
|
|
188
|
-
if (baselineMatch.newCount) {
|
|
189
|
-
process.stdout.write(`\nNEW (not in baseline, ${baselineMatch.newCount} occurrence(s)):\n`);
|
|
190
|
-
for (const occ of baselineMatch.newOccurrences.slice(0, 5)) {
|
|
191
|
-
process.stdout.write(` - ${occ.ruleId}: ${occ.selector || '(no selector)'}\n ${occ.summary}\n`);
|
|
192
|
-
}
|
|
193
|
-
if (baselineMatch.newOccurrences.length > 5) {
|
|
194
|
-
process.stdout.write(` ... and ${baselineMatch.newOccurrences.length - 5} more\n`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
process.stdout.write('\n');
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function loadBaselineFile(baselinePath) {
|
|
202
|
-
let raw;
|
|
203
|
-
try {
|
|
204
|
-
raw = fs.readFileSync(baselinePath, 'utf8');
|
|
205
|
-
} catch (err) {
|
|
206
|
-
throw new Error(`Could not read baseline file "${baselinePath}": ${formatError(err)}. Run with --write-baseline ${baselinePath} first to create one.`);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
let parsed;
|
|
210
|
-
try {
|
|
211
|
-
parsed = JSON.parse(raw);
|
|
212
|
-
} catch (err) {
|
|
213
|
-
throw new Error(`Baseline file "${baselinePath}" is not valid JSON: ${formatError(err)}`);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.entries)) {
|
|
217
|
-
throw new Error(`Baseline file "${baselinePath}" is not a supported baseline (expected { version: 1, entries: [...] }). Regenerate it with --write-baseline.`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return parsed;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async function runScan(args) {
|
|
224
|
-
const target = args._[0];
|
|
225
|
-
if (!target) {
|
|
226
|
-
process.stderr.write('Error: scan requires a file path or URL. See --help.\n');
|
|
227
|
-
process.exitCode = 2;
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (args.baseline && args.writeBaseline) {
|
|
232
|
-
process.stderr.write('Error: --baseline and --write-baseline cannot be used together in the same run. See --help.\n');
|
|
233
|
-
process.exitCode = 2;
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
let baselineFile = null;
|
|
238
|
-
if (args.baseline) {
|
|
239
|
-
try {
|
|
240
|
-
baselineFile = loadBaselineFile(args.baseline);
|
|
241
|
-
} catch (err) {
|
|
242
|
-
process.stderr.write(`Error: ${formatError(err)}\n`);
|
|
243
|
-
process.exitCode = 2;
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
let html, url;
|
|
249
|
-
try {
|
|
250
|
-
({ html, url } = await loadHtml(target));
|
|
251
|
-
} catch (err) {
|
|
252
|
-
process.stderr.write(`Error: ${formatError(err)}\n`);
|
|
253
|
-
process.exitCode = 2;
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
let JSDOM;
|
|
258
|
-
try {
|
|
259
|
-
({ JSDOM } = require('jsdom'));
|
|
260
|
-
} catch {
|
|
261
|
-
process.stderr.write('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');
|
|
262
|
-
process.exitCode = 2;
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const { runDomRulesInPage } = require('../src/index.js');
|
|
267
|
-
|
|
268
|
-
const dom = new JSDOM(html, { url, pretendToBeVisual: true });
|
|
269
|
-
global.window = dom.window;
|
|
270
|
-
global.document = dom.window.document;
|
|
271
|
-
|
|
272
|
-
let result;
|
|
273
|
-
try {
|
|
274
|
-
result = runDomRulesInPage(url, args.context || null, buildEngineOptions(args), null);
|
|
275
|
-
} finally {
|
|
276
|
-
dom.window.close();
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (args.html) {
|
|
280
|
-
fs.writeFileSync(args.html, renderHtmlReport(result, { title: `surea11y scan report — ${target}` }));
|
|
281
|
-
process.stderr.write(`Wrote HTML report to: ${args.html}\n`);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (args.writeBaseline) {
|
|
285
|
-
const entries = buildBaselineEntries(result);
|
|
286
|
-
const payload = { version: 1, generatedAt: new Date().toISOString(), entries };
|
|
287
|
-
fs.writeFileSync(args.writeBaseline, JSON.stringify(payload, null, 2) + '\n');
|
|
288
|
-
|
|
289
|
-
if (args.json) {
|
|
290
|
-
process.stdout.write(JSON.stringify({ ...result, baseline: { mode: 'write', path: args.writeBaseline, entries: entries.length } }, null, 2) + '\n');
|
|
291
|
-
} else {
|
|
292
|
-
printSummary(result);
|
|
293
|
-
}
|
|
294
|
-
process.stderr.write(`Wrote ${entries.length} occurrence(s) to baseline: ${args.writeBaseline}\n`);
|
|
295
|
-
process.exitCode = 0;
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (baselineFile) {
|
|
300
|
-
const match = matchBaseline(result, baselineFile.entries);
|
|
301
|
-
|
|
302
|
-
if (args.json) {
|
|
303
|
-
process.stdout.write(JSON.stringify({ ...result, baseline: { mode: 'check', ...match } }, null, 2) + '\n');
|
|
304
|
-
} else {
|
|
305
|
-
printSummary(result, match);
|
|
306
|
-
}
|
|
307
|
-
process.exitCode = match.newCount > 0 ? 1 : 0;
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (args.json) {
|
|
312
|
-
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
313
|
-
} else {
|
|
314
|
-
printSummary(result);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const hasFail = result.checksResults.some((r) => r.outcome === 'fail');
|
|
318
|
-
process.exitCode = hasFail ? 1 : 0;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
async function main() {
|
|
322
|
-
const args = parseArgs(process.argv.slice(2));
|
|
323
|
-
|
|
324
|
-
if (args.version) {
|
|
325
|
-
process.stdout.write(`${pkg.version}\n`);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
if (args.help || args._.length === 0) {
|
|
329
|
-
printHelp();
|
|
330
|
-
process.exitCode = args._.length === 0 && !args.help ? 2 : 0;
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const [command, ...rest] = args._;
|
|
335
|
-
if (command !== 'scan') {
|
|
336
|
-
process.stderr.write(`Error: unknown command "${command}". Only "scan" is supported. See --help.\n`);
|
|
337
|
-
process.exitCode = 2;
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
args._ = rest;
|
|
342
|
-
await runScan(args);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
main().catch((err) => {
|
|
346
|
-
process.stderr.write(`Error: ${formatError(err)}\n`);
|
|
347
|
-
process.exitCode = 2;
|
|
348
|
-
});
|
package/docs/CLI.md
DELETED
|
@@ -1,75 +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
|
-
| `--write-baseline <path>` | Write every current `fail` occurrence to `<path>`; never fails the build. See [`BASELINE.md`](./BASELINE.md). |
|
|
25
|
-
| `--baseline <path>` | Gate only on occurrences not already recorded in `<path>`. See [`BASELINE.md`](./BASELINE.md). |
|
|
26
|
-
| `--html <path>` | Write a self-contained, browsable HTML report to `<path>`. See [`REPORT.md`](./REPORT.md). |
|
|
27
|
-
| `-h`, `--help` | Show usage. |
|
|
28
|
-
| `-v`, `--version` | Show the installed version. |
|
|
29
|
-
|
|
30
|
-
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`.
|
|
31
|
-
|
|
32
|
-
## Exit codes
|
|
33
|
-
|
|
34
|
-
| Code | Meaning |
|
|
35
|
-
|---|---|
|
|
36
|
-
| `0` | Scan completed, no `fail` outcomes. |
|
|
37
|
-
| `1` | Scan completed, at least one `fail` outcome — the CI-gating case. |
|
|
38
|
-
| `2` | Usage error, or the scan itself couldn't run (bad path/URL, network failure, missing `jsdom`). |
|
|
39
|
-
|
|
40
|
-
`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)).
|
|
41
|
-
|
|
42
|
-
With `--baseline`, exit code `1` means at least one *new* (not-yet-baselined) `fail` occurrence, not any `fail` occurrence — see [`BASELINE.md`](./BASELINE.md).
|
|
43
|
-
|
|
44
|
-
## Baseline / allowlist
|
|
45
|
-
|
|
46
|
-
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:
|
|
47
|
-
|
|
48
|
-
```sh
|
|
49
|
-
surea11y scan ./dist/index.html --write-baseline baseline.json # once, commit the file
|
|
50
|
-
surea11y scan ./dist/index.html --baseline baseline.json # in CI, from then on
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
See [`BASELINE.md`](./BASELINE.md) for the matching semantics, file format, and known limitations.
|
|
54
|
-
|
|
55
|
-
## HTML report
|
|
56
|
-
|
|
57
|
-
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:
|
|
58
|
-
|
|
59
|
-
```sh
|
|
60
|
-
surea11y scan ./dist/index.html --html report.html
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Open `report.html` directly from disk; no server, no external assets. Works alongside any other output mode. See [`REPORT.md`](./REPORT.md).
|
|
64
|
-
|
|
65
|
-
## In CI
|
|
66
|
-
|
|
67
|
-
```sh
|
|
68
|
-
npx @surea11y/core scan ./dist/index.html || exit 1
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
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.
|
|
72
|
-
|
|
73
|
-
## A note on dependencies
|
|
74
|
-
|
|
75
|
-
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.
|