@surea11y/core 1.3.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.
Files changed (163) hide show
  1. package/CHANGELOG.md +46 -2
  2. package/README.md +109 -35
  3. package/bin/surea11y-core.js +20 -0
  4. package/docs/API_STABILITY.md +26 -0
  5. package/docs/CI_INTEGRATIONS.md +7 -7
  6. package/docs/ENGINE_OPTIONS.md +1 -1
  7. package/docs/I18N.md +12 -9
  8. package/docs/INTEGRATION.md +1 -1
  9. package/docs/LIMITATIONS.md +1 -1
  10. package/docs/REPORT.md +1 -1
  11. package/package.json +50 -16
  12. package/src/baseline.js +0 -0
  13. package/src/checks/automatic/area-alt-present.js +4 -6
  14. package/src/checks/automatic/aria-allowed-attr.js +15 -51
  15. package/src/checks/automatic/aria-allowed-role.js +2 -0
  16. package/src/checks/automatic/aria-braille-equivalent.js +2 -0
  17. package/src/checks/automatic/aria-conditional-attr.js +8 -7
  18. package/src/checks/automatic/aria-deprecated-role.js +4 -3
  19. package/src/checks/automatic/aria-hidden-body.js +6 -4
  20. package/src/checks/automatic/aria-hidden-focus.js +12 -13
  21. package/src/checks/automatic/aria-prohibited-attr.js +98 -105
  22. package/src/checks/automatic/aria-prohibited-children.js +56 -87
  23. package/src/checks/automatic/aria-required-attr.js +6 -7
  24. package/src/checks/automatic/aria-required-children.js +7 -10
  25. package/src/checks/automatic/aria-required-parent.js +20 -25
  26. package/src/checks/automatic/aria-role-name-present.js +2 -0
  27. package/src/checks/automatic/aria-roles-valid.js +2 -0
  28. package/src/checks/automatic/aria-valid-attr-value.js +18 -15
  29. package/src/checks/automatic/aria-valid-attr.js +2 -0
  30. package/src/checks/automatic/autocomplete-valid.js +2 -0
  31. package/src/checks/automatic/avoid-inline-spacing.js +3 -2
  32. package/src/checks/automatic/binary-control-name-present.js +2 -0
  33. package/src/checks/automatic/button-name-present.js +7 -7
  34. package/src/checks/automatic/bypass-blocks-present.js +9 -7
  35. package/src/checks/automatic/canvas-text-alternative-present.js +2 -0
  36. package/src/checks/automatic/combobox-name-present.js +2 -0
  37. package/src/checks/automatic/contrast-computable.js +2 -0
  38. package/src/checks/automatic/contrast-enhanced.js +2 -0
  39. package/src/checks/automatic/contrast-minimum.js +2 -0
  40. package/src/checks/automatic/css-orientation-lock.js +21 -27
  41. package/src/checks/automatic/definition-list-children-valid.js +6 -6
  42. package/src/checks/automatic/deprecated-elements-not-used.js +4 -2
  43. package/src/checks/automatic/dialog-name-present.js +10 -10
  44. package/src/checks/automatic/dlitem-parent-valid.js +2 -0
  45. package/src/checks/automatic/duplicate-id-aria.js +4 -3
  46. package/src/checks/automatic/embed-text-alternative-present.js +2 -0
  47. package/src/checks/automatic/form-control-programmatic-label-present.js +2 -0
  48. package/src/checks/automatic/form-control-single-label.js +6 -7
  49. package/src/checks/automatic/html-xml-lang-mismatch.js +2 -0
  50. package/src/checks/automatic/iframe-focusable-content.js +246 -18
  51. package/src/checks/automatic/iframe-name-present.js +2 -0
  52. package/src/checks/automatic/iframe-title-unique.js +3 -1
  53. package/src/checks/automatic/img-alt-present.js +7 -9
  54. package/src/checks/automatic/input-image-alt-present.js +4 -6
  55. package/src/checks/automatic/label-in-name.js +15 -19
  56. package/src/checks/automatic/language-page-present.js +2 -0
  57. package/src/checks/automatic/link-in-text-block.js +2 -0
  58. package/src/checks/automatic/link-name-present.js +2 -0
  59. package/src/checks/automatic/list-children-valid.js +14 -24
  60. package/src/checks/automatic/listbox-name-present.js +2 -0
  61. package/src/checks/automatic/listitem-parent-valid.js +30 -7
  62. package/src/checks/automatic/menuitem-name-present.js +2 -0
  63. package/src/checks/automatic/meta-refresh-no-exceptions.js +4 -4
  64. package/src/checks/automatic/meta-refresh-timing-absent.js +2 -0
  65. package/src/checks/automatic/meta-viewport-zoom-enabled.js +2 -0
  66. package/src/checks/automatic/meter-name-present.js +4 -3
  67. package/src/checks/automatic/nested-interactive-controls-absent.js +27 -5
  68. package/src/checks/automatic/object-text-alternative-present.js +2 -0
  69. package/src/checks/automatic/option-name-present.js +2 -0
  70. package/src/checks/automatic/page-title-present.js +2 -0
  71. package/src/checks/automatic/progressbar-name-present.js +8 -10
  72. package/src/checks/automatic/role-img-alt-present.js +4 -4
  73. package/src/checks/automatic/searchbox-name-present.js +2 -0
  74. package/src/checks/automatic/server-side-image-map-absent.js +4 -3
  75. package/src/checks/automatic/slider-name-present.js +2 -0
  76. package/src/checks/automatic/spinbutton-name-present.js +2 -0
  77. package/src/checks/automatic/summary-name-present.js +2 -0
  78. package/src/checks/automatic/svg-image-text-alternative-present.js +2 -0
  79. package/src/checks/automatic/svg-text-alternative-present.js +17 -5
  80. package/src/checks/automatic/tab-name-present.js +2 -0
  81. package/src/checks/automatic/table-headers-attr-valid.js +3 -2
  82. package/src/checks/automatic/table-th-has-data-cells.js +2 -0
  83. package/src/checks/automatic/target-size-minimum.js +5 -0
  84. package/src/checks/automatic/td-has-header.js +24 -1
  85. package/src/checks/automatic/textbox-name-present.js +2 -0
  86. package/src/checks/automatic/tooltip-name-present.js +2 -0
  87. package/src/checks/automatic/treeitem-name-present.js +2 -0
  88. package/src/checks/automatic/valid-lang.js +2 -0
  89. package/src/checks/automatic/video-poster-text-alternative-present.js +2 -0
  90. package/src/checks/manual/accesskeys-manual.js +3 -1
  91. package/src/checks/manual/area-alt-decorative-manual.js +2 -0
  92. package/src/checks/manual/area-alt-quality-manual.js +2 -0
  93. package/src/checks/manual/aria-checked-state-mismatch-manual.js +14 -23
  94. package/src/checks/manual/aria-text-manual.js +6 -5
  95. package/src/checks/manual/canvas-text-alternative-quality-manual.js +2 -0
  96. package/src/checks/manual/css-hidden-focus.js +184 -9
  97. package/src/checks/manual/embed-text-alternative-quality-manual.js +18 -13
  98. package/src/checks/manual/empty-heading-manual.js +17 -17
  99. package/src/checks/manual/empty-table-header-manual.js +52 -25
  100. package/src/checks/manual/focus-order-semantics-manual.js +16 -4
  101. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -0
  102. package/src/checks/manual/heading-order-manual.js +28 -1
  103. package/src/checks/manual/identical-links-same-purpose-manual.js +2 -0
  104. package/src/checks/manual/image-redundant-alt-manual.js +21 -1
  105. package/src/checks/manual/img-alt-decorative-manual.js +2 -0
  106. package/src/checks/manual/img-alt-quality-manual.js +2 -0
  107. package/src/checks/manual/input-image-alt-decorative-manual.js +2 -0
  108. package/src/checks/manual/input-image-alt-quality-manual.js +2 -0
  109. package/src/checks/manual/label-title-only-manual.js +29 -22
  110. package/src/checks/manual/landmark-banner-is-top-level-manual.js +54 -47
  111. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +42 -26
  112. package/src/checks/manual/landmark-main-is-top-level-manual.js +41 -24
  113. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +16 -23
  114. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +14 -21
  115. package/src/checks/manual/landmark-no-duplicate-main-manual.js +10 -13
  116. package/src/checks/manual/landmark-one-main-manual.js +12 -23
  117. package/src/checks/manual/landmark-unique-manual.js +37 -52
  118. package/src/checks/manual/link-name-quality-manual.js +2 -0
  119. package/src/checks/manual/media-transcript-present-manual.js +2 -0
  120. package/src/checks/manual/meta-viewport-large-manual.js +3 -1
  121. package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -0
  122. package/src/checks/manual/no-autoplay-audio-manual.js +2 -0
  123. package/src/checks/manual/object-text-alternative-quality-manual.js +2 -0
  124. package/src/checks/manual/p-as-heading-manual.js +2 -0
  125. package/src/checks/manual/page-has-heading-one-manual.js +12 -11
  126. package/src/checks/manual/page-title-patterns-manual.js +2 -0
  127. package/src/checks/manual/presentation-role-conflict-manual.js +51 -37
  128. package/src/checks/manual/region-manual.js +27 -36
  129. package/src/checks/manual/scope-attr-valid-manual.js +3 -1
  130. package/src/checks/manual/scrollable-region-focusable-manual.js +2 -0
  131. package/src/checks/manual/skip-link-manual.js +7 -6
  132. package/src/checks/manual/svg-text-alternative-quality-manual.js +2 -0
  133. package/src/checks/manual/tabindex-manual.js +3 -1
  134. package/src/checks/manual/table-duplicate-name-manual.js +5 -4
  135. package/src/checks/manual/table-fake-caption-manual.js +24 -3
  136. package/src/checks/manual/video-caption-manual.js +2 -0
  137. package/src/checks/manual-review.js +2 -0
  138. package/src/core.js +6772 -2158
  139. package/src/index.js +2 -0
  140. package/src/report.js +51 -9
  141. package/src/sarif.js +18 -3
  142. package/surea11y.browser.js +2731 -999
  143. package/bin/core.js +0 -473
  144. package/docs/CLI.md +0 -128
  145. package/src/catalogs/composites.wcag.js +0 -454
  146. package/src/checks/rules-and-tags.full.csv +0 -19
  147. package/src/checks/rules-and-tags.full.json +0 -259
  148. package/src/core/aria-helpers.js +0 -1211
  149. package/src/core/contrast-helpers.js +0 -1302
  150. package/src/core/dom-helpers.js +0 -4493
  151. package/src/core/dom-runner.js +0 -787
  152. package/src/core/frame-messaging.js +0 -261
  153. package/src/core/frame-scan.js +0 -190
  154. package/src/core/rollup-composites.js +0 -127
  155. package/src/core/rule-meta.js +0 -176
  156. package/src/coverage/wcag-facets.js +0 -1079
  157. package/src/coverage/wcag-version-map.js +0 -84
  158. package/src/i18n/en.js +0 -1228
  159. package/src/i18n/fr.js +0 -1185
  160. package/src/policy/contracts.js +0 -18
  161. package/src/policy/resolvePolicy.js +0 -59
  162. package/src/policy/schemas/engine-options.schema.json +0 -103
  163. 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.