@diegovelasquezweb/a11y-engine 0.3.0 → 0.3.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.
@@ -29,5 +29,207 @@
29
29
  ["svelte", "svelte"],
30
30
  ["vue", "vue"],
31
31
  ["react", "react"]
32
+ ],
33
+ "domFrameworkDetectors": [
34
+ {
35
+ "id": "nextjs",
36
+ "type": "framework",
37
+ "signals": [
38
+ { "kind": "global", "key": "__NEXT_DATA__" },
39
+ { "kind": "global", "key": "__next" },
40
+ { "kind": "selector", "value": "script#__NEXT_DATA__" },
41
+ { "kind": "selector", "value": "div#__next" },
42
+ { "kind": "scriptSrc", "pattern": "/_next/" },
43
+ { "kind": "meta", "name": "next-head-count" }
44
+ ]
45
+ },
46
+ {
47
+ "id": "nuxt",
48
+ "type": "framework",
49
+ "signals": [
50
+ { "kind": "global", "key": "__NUXT__" },
51
+ { "kind": "global", "key": "$nuxt" },
52
+ { "kind": "selector", "value": "div#__nuxt" },
53
+ { "kind": "scriptSrc", "pattern": "/_nuxt/" },
54
+ { "kind": "meta", "name": "generator", "pattern": "Nuxt" }
55
+ ]
56
+ },
57
+ {
58
+ "id": "gatsby",
59
+ "type": "framework",
60
+ "signals": [
61
+ { "kind": "global", "key": "___gatsby" },
62
+ { "kind": "selector", "value": "div#___gatsby" },
63
+ { "kind": "meta", "name": "generator", "pattern": "Gatsby" }
64
+ ]
65
+ },
66
+ {
67
+ "id": "angular",
68
+ "type": "framework",
69
+ "signals": [
70
+ { "kind": "global", "key": "ng" },
71
+ { "kind": "selector", "value": "[ng-version]" },
72
+ { "kind": "selector", "value": "app-root" }
73
+ ]
74
+ },
75
+ {
76
+ "id": "svelte",
77
+ "type": "framework",
78
+ "signals": [
79
+ { "kind": "global", "key": "__svelte" },
80
+ { "kind": "selector", "value": "[data-svelte-h]" },
81
+ { "kind": "meta", "name": "generator", "pattern": "Svelte" }
82
+ ]
83
+ },
84
+ {
85
+ "id": "astro",
86
+ "type": "framework",
87
+ "signals": [
88
+ { "kind": "selector", "value": "[data-astro-cid]" },
89
+ { "kind": "meta", "name": "generator", "pattern": "Astro" },
90
+ { "kind": "selector", "value": "astro-island" }
91
+ ]
92
+ },
93
+ {
94
+ "id": "remix",
95
+ "type": "framework",
96
+ "signals": [
97
+ { "kind": "global", "key": "__remixContext" },
98
+ { "kind": "global", "key": "__remixManifest" }
99
+ ]
100
+ },
101
+ {
102
+ "id": "vue",
103
+ "type": "framework",
104
+ "signals": [
105
+ { "kind": "global", "key": "__VUE__" },
106
+ { "kind": "selector", "value": "[data-v-]" },
107
+ { "kind": "selector", "value": "div#app[data-v-app]" }
108
+ ]
109
+ },
110
+ {
111
+ "id": "react",
112
+ "type": "framework",
113
+ "signals": [
114
+ { "kind": "selector", "value": "[data-reactroot]" },
115
+ { "kind": "selector", "value": "[data-reactid]" },
116
+ { "kind": "global", "key": "__REACT_DEVTOOLS_GLOBAL_HOOK__" }
117
+ ]
118
+ }
119
+ ],
120
+ "domCmsDetectors": [
121
+ {
122
+ "id": "wordpress",
123
+ "type": "cms",
124
+ "signals": [
125
+ { "kind": "meta", "name": "generator", "pattern": "WordPress" },
126
+ { "kind": "scriptSrc", "pattern": "/wp-content/" },
127
+ { "kind": "scriptSrc", "pattern": "/wp-includes/" },
128
+ { "kind": "selector", "value": "link[href*='wp-content']" },
129
+ { "kind": "selector", "value": "body.wp-site-blocks" }
130
+ ]
131
+ },
132
+ {
133
+ "id": "shopify",
134
+ "type": "cms",
135
+ "signals": [
136
+ { "kind": "global", "key": "Shopify" },
137
+ { "kind": "scriptSrc", "pattern": "cdn.shopify.com" },
138
+ { "kind": "meta", "name": "shopify-digital-wallet" },
139
+ { "kind": "selector", "value": "link[href*='cdn.shopify']" }
140
+ ]
141
+ },
142
+ {
143
+ "id": "drupal",
144
+ "type": "cms",
145
+ "signals": [
146
+ { "kind": "meta", "name": "generator", "pattern": "Drupal" },
147
+ { "kind": "global", "key": "Drupal" },
148
+ { "kind": "scriptSrc", "pattern": "/sites/default/files/" }
149
+ ]
150
+ },
151
+ {
152
+ "id": "wix",
153
+ "type": "cms",
154
+ "signals": [
155
+ { "kind": "meta", "name": "generator", "pattern": "Wix" },
156
+ { "kind": "scriptSrc", "pattern": "static.parastorage.com" },
157
+ { "kind": "scriptSrc", "pattern": "static.wixstatic.com" }
158
+ ]
159
+ },
160
+ {
161
+ "id": "squarespace",
162
+ "type": "cms",
163
+ "signals": [
164
+ { "kind": "meta", "name": "generator", "pattern": "Squarespace" },
165
+ { "kind": "scriptSrc", "pattern": "static1.squarespace.com" }
166
+ ]
167
+ },
168
+ {
169
+ "id": "webflow",
170
+ "type": "cms",
171
+ "signals": [
172
+ { "kind": "meta", "name": "generator", "pattern": "Webflow" },
173
+ { "kind": "selector", "value": "html.w-mod-js" },
174
+ { "kind": "scriptSrc", "pattern": "assets.website-files.com" }
175
+ ]
176
+ },
177
+ {
178
+ "id": "joomla",
179
+ "type": "cms",
180
+ "signals": [
181
+ { "kind": "meta", "name": "generator", "pattern": "Joomla" },
182
+ { "kind": "scriptSrc", "pattern": "/media/system/js/" }
183
+ ]
184
+ },
185
+ {
186
+ "id": "magento",
187
+ "type": "cms",
188
+ "signals": [
189
+ { "kind": "scriptSrc", "pattern": "/static/version" },
190
+ { "kind": "selector", "value": "script[data-requiremodule]" },
191
+ { "kind": "global", "key": "require" }
192
+ ]
193
+ }
194
+ ],
195
+ "domUiLibraryDetectors": [
196
+ {
197
+ "id": "bootstrap",
198
+ "signals": [
199
+ { "kind": "selector", "value": "link[href*='bootstrap']" },
200
+ { "kind": "scriptSrc", "pattern": "bootstrap" },
201
+ { "kind": "selector", "value": ".container .row .col" }
202
+ ]
203
+ },
204
+ {
205
+ "id": "tailwindcss",
206
+ "signals": [
207
+ { "kind": "selector", "value": "style[data-precedence]" },
208
+ { "kind": "selector", "value": "link[href*='tailwind']" },
209
+ { "kind": "meta", "name": "generator", "pattern": "Tailwind" }
210
+ ]
211
+ },
212
+ {
213
+ "id": "material-ui",
214
+ "signals": [
215
+ { "kind": "selector", "value": "[class*='MuiButton']" },
216
+ { "kind": "selector", "value": "[class*='MuiTypography']" },
217
+ { "kind": "selector", "value": "[class*='MuiPaper']" }
218
+ ]
219
+ },
220
+ {
221
+ "id": "jquery",
222
+ "signals": [
223
+ { "kind": "global", "key": "jQuery" },
224
+ { "kind": "global", "key": "$" }
225
+ ]
226
+ },
227
+ {
228
+ "id": "foundation",
229
+ "signals": [
230
+ { "kind": "global", "key": "Foundation" },
231
+ { "kind": "selector", "value": "link[href*='foundation']" }
232
+ ]
233
+ }
32
234
  ]
33
235
  }
@@ -1 +1 @@
1
- export default {"platformStructureDetectors":[["wordpress",["wp-content/themes"]],["drupal",["web/themes","themes"]],["shopify",["sections","snippets","layout","templates"]]],"uiLibraryPackageDetectors":[["@radix-ui","radix"],["@headlessui","headless-ui"],["@chakra-ui","chakra"],["@mantine","mantine"],["@mui","material-ui"],["antd","ant-design"],["@shopify/polaris","polaris"],["@react-aria","react-aria"],["ariakit","ariakit"],["primevue","primevue"],["vuetify","vuetify"],["swiper","swiper"]],"frameworkPackageDetectors":[["next","nextjs"],["gatsby","gatsby"],["nuxt","nuxt"],["@nuxt/core","nuxt"],["@angular/core","angular"],["astro","astro"],["@sveltejs/kit","svelte"],["svelte","svelte"],["vue","vue"],["react","react"]]};
1
+ export default {"platformStructureDetectors":[["wordpress",["wp-content/themes"]],["drupal",["web/themes","themes"]],["shopify",["sections","snippets","layout","templates"]]],"uiLibraryPackageDetectors":[["@radix-ui","radix"],["@headlessui","headless-ui"],["@chakra-ui","chakra"],["@mantine","mantine"],["@mui","material-ui"],["antd","ant-design"],["@shopify/polaris","polaris"],["@react-aria","react-aria"],["ariakit","ariakit"],["primevue","primevue"],["vuetify","vuetify"],["swiper","swiper"]],"frameworkPackageDetectors":[["next","nextjs"],["gatsby","gatsby"],["nuxt","nuxt"],["@nuxt/core","nuxt"],["@angular/core","angular"],["astro","astro"],["@sveltejs/kit","svelte"],["svelte","svelte"],["vue","vue"],["react","react"]],"domFrameworkDetectors":[{"id":"nextjs","type":"framework","signals":[{"kind":"global","key":"__NEXT_DATA__"},{"kind":"global","key":"__next"},{"kind":"selector","value":"script#__NEXT_DATA__"},{"kind":"selector","value":"div#__next"},{"kind":"scriptSrc","pattern":"/_next/"},{"kind":"meta","name":"next-head-count"}]},{"id":"nuxt","type":"framework","signals":[{"kind":"global","key":"__NUXT__"},{"kind":"global","key":"$nuxt"},{"kind":"selector","value":"div#__nuxt"},{"kind":"scriptSrc","pattern":"/_nuxt/"},{"kind":"meta","name":"generator","pattern":"Nuxt"}]},{"id":"gatsby","type":"framework","signals":[{"kind":"global","key":"___gatsby"},{"kind":"selector","value":"div#___gatsby"},{"kind":"meta","name":"generator","pattern":"Gatsby"}]},{"id":"angular","type":"framework","signals":[{"kind":"global","key":"ng"},{"kind":"selector","value":"[ng-version]"},{"kind":"selector","value":"app-root"}]},{"id":"svelte","type":"framework","signals":[{"kind":"global","key":"__svelte"},{"kind":"selector","value":"[data-svelte-h]"},{"kind":"meta","name":"generator","pattern":"Svelte"}]},{"id":"astro","type":"framework","signals":[{"kind":"selector","value":"[data-astro-cid]"},{"kind":"meta","name":"generator","pattern":"Astro"},{"kind":"selector","value":"astro-island"}]},{"id":"remix","type":"framework","signals":[{"kind":"global","key":"__remixContext"},{"kind":"global","key":"__remixManifest"}]},{"id":"vue","type":"framework","signals":[{"kind":"global","key":"__VUE__"},{"kind":"selector","value":"[data-v-]"},{"kind":"selector","value":"div#app[data-v-app]"}]},{"id":"react","type":"framework","signals":[{"kind":"selector","value":"[data-reactroot]"},{"kind":"selector","value":"[data-reactid]"},{"kind":"global","key":"__REACT_DEVTOOLS_GLOBAL_HOOK__"}]}],"domCmsDetectors":[{"id":"wordpress","type":"cms","signals":[{"kind":"meta","name":"generator","pattern":"WordPress"},{"kind":"scriptSrc","pattern":"/wp-content/"},{"kind":"scriptSrc","pattern":"/wp-includes/"},{"kind":"selector","value":"link[href*='wp-content']"},{"kind":"selector","value":"body.wp-site-blocks"}]},{"id":"shopify","type":"cms","signals":[{"kind":"global","key":"Shopify"},{"kind":"scriptSrc","pattern":"cdn.shopify.com"},{"kind":"meta","name":"shopify-digital-wallet"},{"kind":"selector","value":"link[href*='cdn.shopify']"}]},{"id":"drupal","type":"cms","signals":[{"kind":"meta","name":"generator","pattern":"Drupal"},{"kind":"global","key":"Drupal"},{"kind":"scriptSrc","pattern":"/sites/default/files/"}]},{"id":"wix","type":"cms","signals":[{"kind":"meta","name":"generator","pattern":"Wix"},{"kind":"scriptSrc","pattern":"static.parastorage.com"},{"kind":"scriptSrc","pattern":"static.wixstatic.com"}]},{"id":"squarespace","type":"cms","signals":[{"kind":"meta","name":"generator","pattern":"Squarespace"},{"kind":"scriptSrc","pattern":"static1.squarespace.com"}]},{"id":"webflow","type":"cms","signals":[{"kind":"meta","name":"generator","pattern":"Webflow"},{"kind":"selector","value":"html.w-mod-js"},{"kind":"scriptSrc","pattern":"assets.website-files.com"}]},{"id":"joomla","type":"cms","signals":[{"kind":"meta","name":"generator","pattern":"Joomla"},{"kind":"scriptSrc","pattern":"/media/system/js/"}]},{"id":"magento","type":"cms","signals":[{"kind":"scriptSrc","pattern":"/static/version"},{"kind":"selector","value":"script[data-requiremodule]"},{"kind":"global","key":"require"}]}],"domUiLibraryDetectors":[{"id":"bootstrap","signals":[{"kind":"selector","value":"link[href*='bootstrap']"},{"kind":"scriptSrc","pattern":"bootstrap"},{"kind":"selector","value":".container .row .col"}]},{"id":"tailwindcss","signals":[{"kind":"selector","value":"style[data-precedence]"},{"kind":"selector","value":"link[href*='tailwind']"},{"kind":"meta","name":"generator","pattern":"Tailwind"}]},{"id":"material-ui","signals":[{"kind":"selector","value":"[class*='MuiButton']"},{"kind":"selector","value":"[class*='MuiTypography']"},{"kind":"selector","value":"[class*='MuiPaper']"}]},{"id":"jquery","signals":[{"kind":"global","key":"jQuery"},{"kind":"global","key":"$"}]},{"id":"foundation","signals":[{"kind":"global","key":"Foundation"},{"kind":"selector","value":"link[href*='foundation']"}]}]};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegovelasquezweb/a11y-engine",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "WCAG 2.2 AA accessibility audit engine — scanner, analyzer, and report builders",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -755,6 +755,7 @@ function computeTestingMethodology(payload) {
755
755
  pages_scanned: scanned,
756
756
  pages_errored: errored,
757
757
  framework_detected: payload.projectContext?.framework || "Not detected",
758
+ cms_detected: payload.projectContext?.cms || "Not detected",
758
759
  manual_testing: "Not performed (automated scan only)",
759
760
  assistive_tech_tested: "None (automated scan only)",
760
761
  };
@@ -772,6 +773,7 @@ function buildFindings(inputPayload, cliArgs) {
772
773
  const routes = inputPayload.routes || [];
773
774
  const ctx = inputPayload.projectContext || {
774
775
  framework: null,
776
+ cms: null,
775
777
  uiLibraries: [],
776
778
  };
777
779
  if (cliArgs?.framework) ctx.framework = cliArgs.framework;
@@ -337,14 +337,18 @@ export async function discoverRoutes(page, baseUrl, maxRoutes, crawlDepth = 2) {
337
337
 
338
338
  /**
339
339
  * Detects the web framework and UI libraries used by analyzing package.json and file structure.
340
+ * @param {string|null} [explicitProjectDir=null] - Explicit project directory. Falls back to env/cwd.
340
341
  * @returns {Object} An object containing detected framework and UI libraries.
341
342
  */
342
- function detectProjectContext() {
343
+ function detectProjectContext(explicitProjectDir = null) {
343
344
  const uiLibraries = [];
344
345
  let pkgFramework = null;
345
346
  let fileFramework = null;
346
347
 
347
- const projectDir = process.env.A11Y_PROJECT_DIR || process.cwd();
348
+ const projectDir = explicitProjectDir || process.env.A11Y_PROJECT_DIR || null;
349
+ if (!projectDir) {
350
+ return { framework: null, uiLibraries: [] };
351
+ }
348
352
 
349
353
  try {
350
354
  const pkgPath = path.join(projectDir, "package.json");
@@ -388,6 +392,126 @@ function detectProjectContext() {
388
392
  return { framework: resolvedFramework, uiLibraries };
389
393
  }
390
394
 
395
+ /**
396
+ * Detects the web framework, CMS, and UI libraries by inspecting the live page DOM,
397
+ * window globals, script sources, and meta tags. This works for any remote URL
398
+ * without needing access to the project source code.
399
+ * @param {import("playwright").Page} page - The Playwright page object (already navigated).
400
+ * @returns {Promise<{ framework: string|null, cms: string|null, uiLibraries: string[] }>}
401
+ */
402
+ async function detectProjectContextFromDom(page) {
403
+ const frameworkDetectors = STACK_DETECTION.domFrameworkDetectors || [];
404
+ const cmsDetectors = STACK_DETECTION.domCmsDetectors || [];
405
+ const uiDetectors = STACK_DETECTION.domUiLibraryDetectors || [];
406
+
407
+ const result = await page.evaluate(({ frameworkDetectors, cmsDetectors, uiDetectors }) => {
408
+ function checkSignals(signals) {
409
+ let matched = 0;
410
+ for (const signal of signals) {
411
+ try {
412
+ if (signal.kind === "global") {
413
+ if (typeof window[signal.key] !== "undefined" && window[signal.key] !== null) {
414
+ matched++;
415
+ }
416
+ } else if (signal.kind === "selector") {
417
+ if (document.querySelector(signal.value)) {
418
+ matched++;
419
+ }
420
+ } else if (signal.kind === "scriptSrc") {
421
+ const scripts = document.querySelectorAll("script[src]");
422
+ for (const s of scripts) {
423
+ if (s.getAttribute("src")?.includes(signal.pattern)) {
424
+ matched++;
425
+ break;
426
+ }
427
+ }
428
+ } else if (signal.kind === "meta") {
429
+ const metas = document.querySelectorAll(`meta[name="${signal.name}"],meta[property="${signal.name}"]`);
430
+ if (metas.length > 0) {
431
+ if (signal.pattern) {
432
+ for (const m of metas) {
433
+ if (m.getAttribute("content")?.toLowerCase().includes(signal.pattern.toLowerCase())) {
434
+ matched++;
435
+ break;
436
+ }
437
+ }
438
+ } else {
439
+ matched++;
440
+ }
441
+ }
442
+ }
443
+ } catch {
444
+ // ignore individual signal errors
445
+ }
446
+ }
447
+ return matched;
448
+ }
449
+
450
+ function detectBest(detectors) {
451
+ let best = null;
452
+ let bestScore = 0;
453
+ for (const detector of detectors) {
454
+ const score = checkSignals(detector.signals);
455
+ if (score > 0 && score > bestScore) {
456
+ bestScore = score;
457
+ best = detector.id;
458
+ }
459
+ }
460
+ return best;
461
+ }
462
+
463
+ function detectAllUiLibs(detectors) {
464
+ const found = [];
465
+ for (const detector of detectors) {
466
+ let total = 0;
467
+ let strongSignals = 0;
468
+ for (const signal of detector.signals) {
469
+ try {
470
+ let hit = false;
471
+ if (signal.kind === "global") {
472
+ hit = typeof window[signal.key] !== "undefined" && window[signal.key] !== null;
473
+ } else if (signal.kind === "selector") {
474
+ hit = !!document.querySelector(signal.value);
475
+ } else if (signal.kind === "scriptSrc") {
476
+ const scripts = document.querySelectorAll("script[src]");
477
+ for (const s of scripts) {
478
+ if (s.getAttribute("src")?.includes(signal.pattern)) { hit = true; break; }
479
+ }
480
+ } else if (signal.kind === "meta") {
481
+ const metas = document.querySelectorAll(`meta[name="${signal.name}"],meta[property="${signal.name}"]`);
482
+ for (const m of metas) {
483
+ if (!signal.pattern || m.getAttribute("content")?.toLowerCase().includes(signal.pattern.toLowerCase())) {
484
+ hit = true; break;
485
+ }
486
+ }
487
+ }
488
+ if (hit) {
489
+ total++;
490
+ if (signal.kind !== "selector") strongSignals++;
491
+ }
492
+ } catch {}
493
+ }
494
+ if (total >= 2 || strongSignals >= 1) {
495
+ found.push(detector.id);
496
+ }
497
+ }
498
+ return found;
499
+ }
500
+
501
+ const framework = detectBest(frameworkDetectors);
502
+ const cms = detectBest(cmsDetectors);
503
+ const uiLibraries = detectAllUiLibs(uiDetectors);
504
+
505
+ return { framework, cms, uiLibraries };
506
+ }, { frameworkDetectors, cmsDetectors, uiDetectors });
507
+
508
+ if (result.framework) log.info(`DOM detection: framework=${result.framework}`);
509
+ if (result.cms) log.info(`DOM detection: cms=${result.cms}`);
510
+ if (result.uiLibraries.length) log.info(`DOM detection: uiLibraries=${result.uiLibraries.join(", ")}`);
511
+
512
+ return result;
513
+ }
514
+
391
515
  /**
392
516
  * Navigates to a route and performs an axe-core accessibility analysis.
393
517
  * @param {import("playwright").Page} page - The Playwright page object.
@@ -820,6 +944,7 @@ export async function runDomScanner(options = {}, callbacks = {}) {
820
944
  crawlDepth: Math.min(Math.max(options.crawlDepth ?? DEFAULTS.crawlDepth, 1), 3),
821
945
  viewport: options.viewport || null,
822
946
  axeTags: options.axeTags || null,
947
+ projectDir: options.projectDir || null,
823
948
  };
824
949
 
825
950
  if (!args.baseUrl) throw new Error("Missing required option: baseUrl");
@@ -859,14 +984,33 @@ async function _runDomScannerInternal(args) {
859
984
  const page = await context.newPage();
860
985
 
861
986
  let routes = [];
862
- let projectContext = { framework: null, uiLibraries: [] };
987
+ let projectContext = { framework: null, cms: null, uiLibraries: [] };
863
988
  try {
864
989
  await page.goto(baseUrl, {
865
990
  waitUntil: args.waitUntil,
866
991
  timeout: args.timeoutMs,
867
992
  });
868
993
 
869
- projectContext = detectProjectContext();
994
+ // 1. File-system / package.json detection (works when projectDir is available)
995
+ const repoCtx = detectProjectContext(args.projectDir || null);
996
+
997
+ // 2. DOM/runtime detection (always works for any remote URL)
998
+ let domCtx = { framework: null, cms: null, uiLibraries: [] };
999
+ try {
1000
+ domCtx = await detectProjectContextFromDom(page);
1001
+ } catch (err) {
1002
+ log.warn(`DOM stack detection failed (non-fatal): ${err.message}`);
1003
+ }
1004
+
1005
+ // 3. Merge: repo detection takes priority, DOM fills gaps
1006
+ projectContext = {
1007
+ framework: repoCtx.framework || domCtx.framework || null,
1008
+ cms: domCtx.cms || null,
1009
+ uiLibraries: [...new Set([
1010
+ ...(repoCtx.uiLibraries || []),
1011
+ ...(domCtx.uiLibraries || []),
1012
+ ])],
1013
+ };
870
1014
 
871
1015
  const cliRoutes = parseRoutesArg(args.routes, origin);
872
1016
 
package/scripts/index.mjs CHANGED
@@ -481,6 +481,7 @@ export async function runAudit(options) {
481
481
  onlyRule: options.onlyRule,
482
482
  excludeSelectors: options.excludeSelectors,
483
483
  screenshotsDir: options.screenshotsDir,
484
+ projectDir: options.projectDir,
484
485
  },
485
486
  { onProgress },
486
487
  );