@diegovelasquezweb/a11y-engine 0.1.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/LICENSE +21 -0
- package/README.md +20 -0
- package/assets/discovery/crawler-config.json +11 -0
- package/assets/discovery/stack-detection.json +33 -0
- package/assets/remediation/axe-check-maps.json +31 -0
- package/assets/remediation/code-patterns.json +109 -0
- package/assets/remediation/guardrails.json +24 -0
- package/assets/remediation/intelligence.json +4166 -0
- package/assets/remediation/source-boundaries.json +46 -0
- package/assets/reporting/compliance-config.json +173 -0
- package/assets/reporting/manual-checks.json +944 -0
- package/assets/reporting/wcag-reference.json +588 -0
- package/package.json +37 -0
- package/scripts/audit.mjs +326 -0
- package/scripts/core/asset-loader.mjs +54 -0
- package/scripts/core/toolchain.mjs +102 -0
- package/scripts/core/utils.mjs +105 -0
- package/scripts/engine/analyzer.mjs +1022 -0
- package/scripts/engine/dom-scanner.mjs +685 -0
- package/scripts/engine/source-scanner.mjs +300 -0
- package/scripts/reports/builders/checklist.mjs +307 -0
- package/scripts/reports/builders/html.mjs +766 -0
- package/scripts/reports/builders/md.mjs +96 -0
- package/scripts/reports/builders/pdf.mjs +259 -0
- package/scripts/reports/renderers/findings.mjs +188 -0
- package/scripts/reports/renderers/html.mjs +452 -0
- package/scripts/reports/renderers/md.mjs +595 -0
- package/scripts/reports/renderers/pdf.mjs +551 -0
- package/scripts/reports/renderers/utils.mjs +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 diegovelasquezweb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @diegovelasquezweb/a11y-engine
|
|
2
|
+
|
|
3
|
+
WCAG 2.2 AA accessibility audit engine. Scanner, analyzer, and report builders.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i @diegovelasquezweb/a11y-engine
|
|
9
|
+
npx playwright install chromium
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx a11y-audit --base-url https://example.com --max-routes 5
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Options
|
|
19
|
+
|
|
20
|
+
See `a11y-audit --help` for full CLI reference.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"blockedExtensions": [
|
|
3
|
+
"pdf", "jpg", "jpeg", "png", "gif", "svg", "webp", "ico",
|
|
4
|
+
"css", "js", "mjs", "json", "xml", "zip", "tar", "gz",
|
|
5
|
+
"mp4", "mp3", "webm", "wav", "woff", "woff2", "ttf", "otf", "eot",
|
|
6
|
+
"avif", "csv", "txt", "map", "wasm"
|
|
7
|
+
],
|
|
8
|
+
"paginationParams": [
|
|
9
|
+
"page", "paged", "p", "pg", "offset", "cursor", "start", "from", "skip", "limit", "per_page"
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"platformStructureDetectors": [
|
|
3
|
+
["wordpress", ["wp-content/themes"]],
|
|
4
|
+
["drupal", ["web/themes", "themes"]],
|
|
5
|
+
["shopify", ["sections", "snippets", "layout", "templates"]]
|
|
6
|
+
],
|
|
7
|
+
"uiLibraryPackageDetectors": [
|
|
8
|
+
["@radix-ui", "radix"],
|
|
9
|
+
["@headlessui", "headless-ui"],
|
|
10
|
+
["@chakra-ui", "chakra"],
|
|
11
|
+
["@mantine", "mantine"],
|
|
12
|
+
["@mui", "material-ui"],
|
|
13
|
+
["antd", "ant-design"],
|
|
14
|
+
["@shopify/polaris", "polaris"],
|
|
15
|
+
["@react-aria", "react-aria"],
|
|
16
|
+
["ariakit", "ariakit"],
|
|
17
|
+
["primevue", "primevue"],
|
|
18
|
+
["vuetify", "vuetify"],
|
|
19
|
+
["swiper", "swiper"]
|
|
20
|
+
],
|
|
21
|
+
"frameworkPackageDetectors": [
|
|
22
|
+
["next", "nextjs"],
|
|
23
|
+
["gatsby", "gatsby"],
|
|
24
|
+
["nuxt", "nuxt"],
|
|
25
|
+
["@nuxt/core", "nuxt"],
|
|
26
|
+
["@angular/core", "angular"],
|
|
27
|
+
["astro", "astro"],
|
|
28
|
+
["@sveltejs/kit", "svelte"],
|
|
29
|
+
["svelte", "svelte"],
|
|
30
|
+
["vue", "vue"],
|
|
31
|
+
["react", "react"]
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"failureModes": {
|
|
3
|
+
"button-has-visible-text": "missing_visible_text",
|
|
4
|
+
"has-visible-text": "missing_visible_text",
|
|
5
|
+
"aria-label": "missing_aria_label",
|
|
6
|
+
"aria-labelledby": "missing_or_invalid_aria_labelledby",
|
|
7
|
+
"explicit-label": "missing_accessible_label",
|
|
8
|
+
"implicit-label": "missing_accessible_label",
|
|
9
|
+
"color-contrast-enhanced": "insufficient_color_contrast",
|
|
10
|
+
"color-contrast": "insufficient_color_contrast",
|
|
11
|
+
"form-field-multiple-labels": "conflicting_label_association",
|
|
12
|
+
"label": "missing_accessible_label",
|
|
13
|
+
"select-name": "missing_select_name",
|
|
14
|
+
"presentational-role": "missing_accessible_name",
|
|
15
|
+
"only-listitems": "invalid_list_structure",
|
|
16
|
+
"listitem": "orphaned_list_item",
|
|
17
|
+
"heading-order": "invalid_heading_order",
|
|
18
|
+
"meta-viewport": "zoom_disabled",
|
|
19
|
+
"region": "content_outside_landmarks"
|
|
20
|
+
},
|
|
21
|
+
"relationshipHints": {
|
|
22
|
+
"aria-labelledby": "aria_labelledby_references_missing_or_invalid_target",
|
|
23
|
+
"explicit-label": "label_not_associated_with_control",
|
|
24
|
+
"implicit-label": "control_has_no_wrapped_label",
|
|
25
|
+
"form-field-multiple-labels": "multiple_labels_reference_same_control",
|
|
26
|
+
"label": "control_has_no_accessible_label_source",
|
|
27
|
+
"select-name": "select_has_no_accessible_label_source",
|
|
28
|
+
"aria-label": "control_has_no_accessible_name_source",
|
|
29
|
+
"presentational-role": "presentational_role_removed_name_source"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"patterns": [
|
|
3
|
+
{
|
|
4
|
+
"id": "placeholder-only-label",
|
|
5
|
+
"title": "Input uses placeholder as its only label",
|
|
6
|
+
"severity": "Critical",
|
|
7
|
+
"wcag": "WCAG 1.3.1 / 4.1.2 A",
|
|
8
|
+
"wcag_criterion": "1.3.1",
|
|
9
|
+
"wcag_level": "A",
|
|
10
|
+
"type": "structural",
|
|
11
|
+
"fix_description": "Placeholder text is not a label — it disappears on input and is not reliably announced by screen readers. Fix using one of these approaches:\n\n**Option A — Visually hidden label (preferred when no visible label exists):**\n```jsx\n<label htmlFor=\"field-id\" className=\"sr-only\">Field name</label>\n<input id=\"field-id\" placeholder=\"...\" />\n```\n\n**Option B — aria-label (acceptable for icon-only inputs like search):**\n```jsx\n<input aria-label=\"Search\" placeholder=\"Search\" />\n```\n\n**Option C — Visible label already exists nearby (wire it up):**\n```jsx\n<label htmlFor=\"field-id\">Field name</label>\n<input id=\"field-id\" placeholder=\"...\" />\n```\n\nNever rely on `placeholder` alone as the accessible name. Always verify the label is announced correctly with a screen reader.",
|
|
12
|
+
"requires_manual_verification": true,
|
|
13
|
+
"regex": "\\bplaceholder=[\"']",
|
|
14
|
+
"globs": ["**/*.tsx", "**/*.jsx", "**/*.html", "**/*.vue", "**/*.svelte", "**/*.astro"],
|
|
15
|
+
"context_reject_regex": "aria-label|aria-labelledby|for=|htmlFor=|<label",
|
|
16
|
+
"context_window": 6
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "mouseover-without-focus",
|
|
20
|
+
"title": "Hover handler has no keyboard focus equivalent",
|
|
21
|
+
"severity": "Serious",
|
|
22
|
+
"wcag": "WCAG 2.1.1 A",
|
|
23
|
+
"wcag_criterion": "2.1.1",
|
|
24
|
+
"wcag_level": "A",
|
|
25
|
+
"type": "structural",
|
|
26
|
+
"fix_description": "Add `onFocus`/`onBlur` (or `onFocusIn`/`onFocusOut`) handlers alongside every `onMouseOver`/`onMouseEnter` handler so keyboard users receive the same interaction.",
|
|
27
|
+
"requires_manual_verification": true,
|
|
28
|
+
"regex": "onMouseOver=|onMouseEnter=",
|
|
29
|
+
"globs": ["**/*.tsx", "**/*.jsx", "**/*.vue", "**/*.svelte", "**/*.html"],
|
|
30
|
+
"context_reject_regex": "onFocus[^I]|onFocusIn",
|
|
31
|
+
"context_window": 10
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "new-window-no-warning",
|
|
35
|
+
"title": "Link opens in new tab without warning",
|
|
36
|
+
"severity": "Serious",
|
|
37
|
+
"wcag": "WCAG 3.2.2 A",
|
|
38
|
+
"wcag_criterion": "3.2.2",
|
|
39
|
+
"wcag_level": "A",
|
|
40
|
+
"type": "structural",
|
|
41
|
+
"fix_description": "Add visible text such as `(opens in new tab)` or an `aria-label` containing that warning. Always pair `target=\"_blank\"` with `rel=\"noopener noreferrer\"`.",
|
|
42
|
+
"requires_manual_verification": true,
|
|
43
|
+
"regex": "target=[\"']_blank[\"']",
|
|
44
|
+
"globs": ["**/*.tsx", "**/*.jsx", "**/*.vue", "**/*.svelte", "**/*.html", "**/*.astro"],
|
|
45
|
+
"context_reject_regex": "new.tab|new.window|opens.in|sr-only",
|
|
46
|
+
"context_window": 5
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "spa-route-title",
|
|
50
|
+
"title": "SPA navigation does not update document.title",
|
|
51
|
+
"severity": "Serious",
|
|
52
|
+
"wcag": "WCAG 2.4.2 A",
|
|
53
|
+
"wcag_criterion": "2.4.2",
|
|
54
|
+
"wcag_level": "A",
|
|
55
|
+
"type": "structural",
|
|
56
|
+
"fix_description": "Set `document.title` after every router navigation call to reflect the new route's page title. Screen reader users rely on the title to know when the page has changed.",
|
|
57
|
+
"requires_manual_verification": true,
|
|
58
|
+
"regex": "router\\.push\\(|router\\.replace\\(|navigate\\(|useNavigate\\(",
|
|
59
|
+
"globs": ["**/*.tsx", "**/*.jsx", "**/*.ts", "**/*.js", "**/*.vue"],
|
|
60
|
+
"context_reject_regex": "document\\.title",
|
|
61
|
+
"context_window": 20
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "focus-outline-suppressed",
|
|
65
|
+
"title": "Focus outline suppressed without replacement",
|
|
66
|
+
"severity": "Serious",
|
|
67
|
+
"wcag": "WCAG 2.4.7 AA",
|
|
68
|
+
"wcag_criterion": "2.4.7",
|
|
69
|
+
"wcag_level": "AA",
|
|
70
|
+
"type": "style",
|
|
71
|
+
"fix_description": "Replace `outline: none` / `outline: 0` with a `:focus-visible` rule that renders a clearly visible custom focus indicator (e.g., a 2px solid high-contrast outline). Never suppress focus outlines globally.",
|
|
72
|
+
"requires_manual_verification": true,
|
|
73
|
+
"regex": "outline:\\s*none|outline:\\s*0|focus:outline-none",
|
|
74
|
+
"globs": ["**/*.css", "**/*.scss", "**/*.sass", "**/*.tsx", "**/*.jsx"],
|
|
75
|
+
"context_reject_regex": ":focus-visible",
|
|
76
|
+
"context_window": 5
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "orientation-lock",
|
|
80
|
+
"title": "Screen orientation locked programmatically",
|
|
81
|
+
"severity": "Moderate",
|
|
82
|
+
"wcag": "WCAG 1.3.4 AA",
|
|
83
|
+
"wcag_criterion": "1.3.4",
|
|
84
|
+
"wcag_level": "AA",
|
|
85
|
+
"type": "structural",
|
|
86
|
+
"fix_description": "Remove the programmatic orientation lock. If a specific orientation is essential for the content, provide an accessible alternative layout for the other orientation.",
|
|
87
|
+
"requires_manual_verification": false,
|
|
88
|
+
"regex": "screen\\.orientation\\.lock\\(|lockOrientation\\(",
|
|
89
|
+
"globs": ["**/*.ts", "**/*.js", "**/*.tsx", "**/*.jsx", "**/*.vue", "**/*.svelte"],
|
|
90
|
+
"context_reject_regex": null,
|
|
91
|
+
"context_window": 0
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "character-key-shortcut",
|
|
95
|
+
"title": "Single-character accesskey shortcut with no override mechanism",
|
|
96
|
+
"severity": "Moderate",
|
|
97
|
+
"wcag": "WCAG 2.1.4 A",
|
|
98
|
+
"wcag_criterion": "2.1.4",
|
|
99
|
+
"wcag_level": "A",
|
|
100
|
+
"type": "structural",
|
|
101
|
+
"fix_description": "Remove the `accesskey` attribute or provide a user-facing mechanism to remap or disable it. Single-character shortcuts conflict with screen reader and speech-input keystroke commands.",
|
|
102
|
+
"requires_manual_verification": false,
|
|
103
|
+
"regex": "\\baccesskey=",
|
|
104
|
+
"globs": ["**/*.html", "**/*.tsx", "**/*.jsx", "**/*.vue", "**/*.svelte", "**/*.astro"],
|
|
105
|
+
"context_reject_regex": null,
|
|
106
|
+
"context_window": 0
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"shared": [
|
|
3
|
+
"**Element Verification**: Use the **Evidence from DOM** HTML to verify you are editing the correct element before applying any fix. Accessibility fixes are context-sensitive.",
|
|
4
|
+
"**Global Component Check**: If an issue repeats across multiple pages, it is likely inside a Global Component (e.g., `Header.tsx`, `Button.vue`). Fix it once at the source.",
|
|
5
|
+
"**Verification**: After applying a fix, explain *why* it resolves the WCAG criterion mentioned.",
|
|
6
|
+
"**No Placeholders**: Do not add \"todo\" comments. Provide the complete code fix.",
|
|
7
|
+
"**Backend Root Cause**: Some axe findings originate from server-side output (PHP warnings, WP_DEBUG stack traces rendering into the DOM) rather than frontend components. Identify by evidence containing server-generated debug output: PHP warnings, file paths like `wp-config.php` or `.env`, line numbers, or raw error messages in the DOM. For these findings: (1) explain the server-side root cause to the user in plain language, (2) do not attempt any frontend fix, (3) mark as 'Out of scope — backend root cause', (4) exclude from the fix queue and count as unresolved/unactionable in the Step 6 summary.",
|
|
8
|
+
"**Design Tokens (Tailwind)**: When fixing color contrast or visual issues, check `package.json` for the Tailwind version. v3: tokens in `tailwind.config.ts` or `tailwind.config.js`. v4: tokens in `@theme { … }` blocks inside CSS files (e.g. `app/globals.css`); a `tailwind.config.js` with `@config` may also exist — check both. Never report a missing config as an error in v4 projects."
|
|
9
|
+
],
|
|
10
|
+
"stack": {
|
|
11
|
+
"react": "**React Project**: Edit `.tsx`/`.jsx` files in `src/`. **NEVER** edit `dist/`, `build/`, or `.cache/`. Source of Truth: fix at the component, not the compiled output.",
|
|
12
|
+
"nextjs": "**Next.js Project**: Edit `.tsx`/`.jsx` files in `app/`, `pages/`, or `components/`. **NEVER** edit `.next/` or `out/`. Source of Truth: fix at the component or page.",
|
|
13
|
+
"gatsby": "**Gatsby Project**: Edit `.tsx`/`.jsx` in `src/pages/` or `src/components/`. **NEVER** edit `public/` or `.cache/`. Source of Truth: fix at the component.",
|
|
14
|
+
"vue": "**Vue Project**: Edit `.vue` files in `src/`. **NEVER** edit `dist/` or `node_modules/`. Source of Truth: fix at the component.",
|
|
15
|
+
"nuxt": "**Nuxt Project**: Edit `.vue` files in `pages/`, `components/`, or `layouts/`. **NEVER** edit `.nuxt/` or `dist/`. Source of Truth: fix at the component.",
|
|
16
|
+
"angular": "**Angular Project**: Edit `.component.html` and `.component.ts` in `src/`. **NEVER** edit `dist/`. Source of Truth: fix at the component.",
|
|
17
|
+
"astro": "**Astro Project**: Edit `.astro` files in `src/`. **NEVER** edit `dist/` or `.astro/`. Source of Truth: fix at the component or page.",
|
|
18
|
+
"shopify": "**Shopify Project**: Edit `.liquid` files in `sections/`, `snippets/`, or `layout/`. **NEVER** edit compiled assets in `assets/*.min.js` or modify `config/settings_schema.json`. Source of Truth: fix at the template, not the rendered DOM.",
|
|
19
|
+
"wordpress": "**WordPress Project**: Work only in `wp-content/themes/`. **NEVER** edit `wp-content/plugins/`, `wp-admin/`, `wp-includes/`, `wp-config.php`, or any cached file. Source of Truth: fix at the theme template.",
|
|
20
|
+
"svelte": "**Svelte/SvelteKit Project**: Edit `.svelte` files in `src/`. **NEVER** edit `.svelte-kit/`, `build/`, or `dist/`. Source of Truth: fix at the component, not the compiled output.",
|
|
21
|
+
"drupal": "**Drupal Project**: Search for `.html.twig` in `web/themes/` or `themes/`. Clear cache with `drush cr` after changes. **NEVER** edit compiled or cached files.",
|
|
22
|
+
"generic": "**Framework & CMS Awareness**: This project may use React, Vue, Next.js, Astro, **Shopify** (.liquid), **WordPress** (.php), or **Drupal** (.twig).\n - **NEVER** edit compiled, minified, or cached files (e.g., `dist/`, `.next/`, `build/`, `wp-content/cache/`, `assets/*.min.js`).\n - **Source of Truth**: Traced DOM violations must be fixed at the **Source Component** or **Server-side Template**. Edit the origin, not the output."
|
|
23
|
+
}
|
|
24
|
+
}
|