@ngbracket/a11y-devtools 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/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @ngbracket/a11y-devtools
2
+
3
+ Dev-only, in-app accessibility auditing for Angular that maps each axe violation
4
+ back to **the component that rendered it** — the attribution React overlay tools
5
+ (`@axe-core/react`, `axe-mode`, TanStack a11y) structurally can't do.
6
+
7
+ Report `♿ UserCardComponent — 2 issue(s)` instead of a wall of CSS selectors.
8
+
9
+ Part of the `@ngbracket` Angular tooling family. See
10
+ [`docs/findings.md`](docs/findings.md) for the research and scope.
11
+
12
+ ## Status
13
+
14
+ Ships attribution + axe scan + grouped console reporter + the dev-only provider
15
+ + the visual in-app overlay (severity-coloured highlights, click-to-scroll).
16
+
17
+ ## How the attribution works
18
+
19
+ Angular publishes debug helpers on the `window.ng` global in dev mode.
20
+ `getOwningComponent(node)` returns the component whose view contains a DOM node,
21
+ so any axe-flagged element resolves to its owning component. These helpers exist
22
+ **only in dev builds** — which is exactly right: the tool is dev-only, and the
23
+ global's absence in prod is the signal to no-op.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm i -D @ngbracket/a11y-devtools
29
+ ```
30
+
31
+ Peer deps: `@angular/core >=18`, `rxjs >=7`.
32
+
33
+ ## Usage
34
+
35
+ ```ts
36
+ // app.config.ts (dev configuration)
37
+ import { provideA11yDevtools } from '@ngbracket/a11y-devtools';
38
+
39
+ export const appConfig = {
40
+ providers: [
41
+ // ...your providers
42
+ provideA11yDevtools(),
43
+ ],
44
+ };
45
+ ```
46
+
47
+ It rescans whenever the app settles (zoneless-aware, via `ApplicationRef.isStable`)
48
+ and logs violations grouped by owning component. Options:
49
+
50
+ ```ts
51
+ provideA11yDevtools({
52
+ root: () => document.querySelector('main')!, // scan scope; default document
53
+ log: true, // grouped console output; default true
54
+ overlay: true, // in-app visual highlights over flagged nodes; default false
55
+ debounceMs: 500, // quiet window after stabilization before scanning
56
+ });
57
+ ```
58
+
59
+ You can also scan on demand:
60
+
61
+ ```ts
62
+ import { runA11yScan, scan } from '@ngbracket/a11y-devtools';
63
+
64
+ const findings = await runA11yScan(); // scan + grouped log
65
+ const raw = await scan(document.body); // findings only
66
+ ```
67
+
68
+ ## Production weight
69
+
70
+ In production `provideA11yDevtools()` is a **no-op** and axe-core is never loaded.
71
+ axe-core is a **dynamic import**, so it lands in a lazy chunk that prod never
72
+ fetches, and the package is `sideEffects: false` so an unused import tree-shakes
73
+ away entirely. For a hard guarantee, include the provider only in your dev
74
+ bootstrap config (e.g. behind `isDevMode()`).
75
+
76
+ This is **enforced in CI**: `src/testing/prod-weight.spec.ts` bundles the entry
77
+ with esbuild and walks the module graph — axe-core must be reachable *only*
78
+ through a dynamic import, never a static one. Turning `import('axe-core')` into a
79
+ static import (or adding a top-level side effect) fails the build.
80
+
81
+ ## Develop
82
+
83
+ ```bash
84
+ npm install --legacy-peer-deps
85
+ npm test # vitest + jsdom + Angular TestBed (real axe)
86
+ npm run build # tsc -> dist/ (ESM + .d.ts)
87
+ ```
88
+
89
+ ## Roadmap
90
+
91
+ - **`host` / `hostDirectives` a11y** via `getDirectives(el)` — the runtime cases
92
+ the item-2 lint plugin can't see statically.
93
+ - Per-component filtering and a violation count badge.
94
+
95
+ Done: attribution · axe scan · grouped console reporter · dev-only provider ·
96
+ in-app overlay · CI prod-weight guard.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Attribution core: given a DOM node that axe flagged, return the name of the
3
+ * component responsible for it — the differentiator React overlay tools can't do.
4
+ *
5
+ * Angular's debug helpers are NOT public named exports of `@angular/core`; they
6
+ * are methods on the `window.ng` global published in dev mode
7
+ * (`publishDefaultGlobalUtils`). `getComponent` resolves a component's host
8
+ * element to its instance; `getOwningComponent` resolves any inner node to the
9
+ * component whose view contains it. Try host first, then owner. The global's
10
+ * absence is the non-dev signal — callers should treat `null` as "not available".
11
+ */
12
+ export interface NgDebugGlobal {
13
+ getComponent(element: Element): unknown;
14
+ getOwningComponent(element: Element | object): unknown;
15
+ }
16
+ export declare function ngDebug(): NgDebugGlobal | undefined;
17
+ export declare function resolveOwningComponentName(node: Element): string | null;
@@ -0,0 +1,15 @@
1
+ export function ngDebug() {
2
+ return globalThis.ng;
3
+ }
4
+ function nameOf(instance) {
5
+ if (!instance || typeof instance !== 'object')
6
+ return null;
7
+ return instance.constructor.name ?? null;
8
+ }
9
+ export function resolveOwningComponentName(node) {
10
+ const ng = ngDebug();
11
+ if (!ng)
12
+ return null;
13
+ const component = ng.getComponent(node) ?? ng.getOwningComponent(node);
14
+ return nameOf(component);
15
+ }
@@ -0,0 +1,6 @@
1
+ export { provideA11yDevtools, type A11yDevtoolsOptions } from './provider';
2
+ export { runA11yScan, type RunOptions } from './runner';
3
+ export { scan, type A11yFinding, type Impact } from './scan';
4
+ export { logFindings, type Logger } from './report';
5
+ export { resolveOwningComponentName, ngDebug, type NgDebugGlobal } from './attribution';
6
+ export { createOverlay, OVERLAY_ATTR, OVERLAY_EXCLUDE_SELECTOR, type A11yOverlay, type OverlayOptions, } from './overlay';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { provideA11yDevtools } from './provider';
2
+ export { runA11yScan } from './runner';
3
+ export { scan } from './scan';
4
+ export { logFindings } from './report';
5
+ export { resolveOwningComponentName, ngDebug } from './attribution';
6
+ export { createOverlay, OVERLAY_ATTR, OVERLAY_EXCLUDE_SELECTOR, } from './overlay';
@@ -0,0 +1,29 @@
1
+ import type { A11yFinding } from './scan';
2
+ /**
3
+ * Attribute marking the overlay's own DOM. `scan()` excludes anything under it
4
+ * so the tool never reports violations against its own highlights.
5
+ */
6
+ export declare const OVERLAY_ATTR = "data-ngb-a11y-overlay";
7
+ /** Selector form of {@link OVERLAY_ATTR}, passed to axe as an exclude. */
8
+ export declare const OVERLAY_EXCLUDE_SELECTOR = "[data-ngb-a11y-overlay]";
9
+ export interface A11yOverlay {
10
+ /** Draw a highlight over each finding's node, replacing the previous set. */
11
+ render(findings: A11yFinding[]): void;
12
+ /** Remove all highlights but keep the overlay live. */
13
+ clear(): void;
14
+ /** Tear down: remove the container and detach scroll/resize listeners. */
15
+ destroy(): void;
16
+ }
17
+ export interface OverlayOptions {
18
+ /** Document to render into. Defaults to the global `document`. */
19
+ document?: Document;
20
+ }
21
+ /**
22
+ * A dev-only visual overlay: absolutely-positioned boxes drawn over the nodes
23
+ * axe flagged, coloured by impact, click-to-scroll to the offending element.
24
+ * Plain DOM — no Angular component — so it stays framework-agnostic, carries no
25
+ * change-detection cost, and its own DOM is trivially excluded from scans via
26
+ * {@link OVERLAY_ATTR}. Boxes reposition on scroll/resize so they track their
27
+ * targets as the page moves.
28
+ */
29
+ export declare function createOverlay(options?: OverlayOptions): A11yOverlay;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Attribute marking the overlay's own DOM. `scan()` excludes anything under it
3
+ * so the tool never reports violations against its own highlights.
4
+ */
5
+ export const OVERLAY_ATTR = 'data-ngb-a11y-overlay';
6
+ /** Selector form of {@link OVERLAY_ATTR}, passed to axe as an exclude. */
7
+ export const OVERLAY_EXCLUDE_SELECTOR = `[${OVERLAY_ATTR}]`;
8
+ /** Border/label colour per axe impact. `null` impact falls back to `none`. */
9
+ const IMPACT_COLOR = {
10
+ critical: '#d32029',
11
+ serious: '#e8710a',
12
+ moderate: '#c9a227',
13
+ minor: '#3b7dd8',
14
+ none: '#8a8a8a',
15
+ };
16
+ function colorFor(impact) {
17
+ return IMPACT_COLOR[impact ?? 'none'];
18
+ }
19
+ /**
20
+ * A dev-only visual overlay: absolutely-positioned boxes drawn over the nodes
21
+ * axe flagged, coloured by impact, click-to-scroll to the offending element.
22
+ * Plain DOM — no Angular component — so it stays framework-agnostic, carries no
23
+ * change-detection cost, and its own DOM is trivially excluded from scans via
24
+ * {@link OVERLAY_ATTR}. Boxes reposition on scroll/resize so they track their
25
+ * targets as the page moves.
26
+ */
27
+ export function createOverlay(options = {}) {
28
+ const doc = options.document ?? document;
29
+ const container = doc.createElement('div');
30
+ container.setAttribute(OVERLAY_ATTR, '');
31
+ Object.assign(container.style, {
32
+ position: 'fixed',
33
+ inset: '0',
34
+ pointerEvents: 'none', // pass clicks through; individual boxes opt back in
35
+ zIndex: '2147483646',
36
+ });
37
+ doc.body.appendChild(container);
38
+ let highlights = [];
39
+ const reposition = () => {
40
+ for (const { target, box } of highlights) {
41
+ positionBox(box, target);
42
+ }
43
+ };
44
+ // rAF-throttle so a stream of scroll events collapses to one layout read.
45
+ const raf = doc.defaultView?.requestAnimationFrame?.bind(doc.defaultView);
46
+ let scheduled = false;
47
+ const onViewportChange = () => {
48
+ if (!raf) {
49
+ reposition();
50
+ return;
51
+ }
52
+ if (scheduled)
53
+ return;
54
+ scheduled = true;
55
+ raf(() => {
56
+ scheduled = false;
57
+ reposition();
58
+ });
59
+ };
60
+ const win = doc.defaultView;
61
+ win?.addEventListener('scroll', onViewportChange, { passive: true, capture: true });
62
+ win?.addEventListener('resize', onViewportChange, { passive: true });
63
+ function clear() {
64
+ for (const { box } of highlights)
65
+ box.remove();
66
+ highlights = [];
67
+ }
68
+ function render(findings) {
69
+ clear();
70
+ for (const finding of findings) {
71
+ const target = resolveTarget(doc, finding.target);
72
+ if (!target)
73
+ continue; // node gone since the scan (e.g. re-rendered)
74
+ const box = buildBox(doc, finding);
75
+ box.addEventListener('click', () => flashAndScroll(target, box));
76
+ container.appendChild(box);
77
+ positionBox(box, target);
78
+ highlights.push({ finding, target, box });
79
+ }
80
+ }
81
+ function destroy() {
82
+ clear();
83
+ win?.removeEventListener('scroll', onViewportChange, { capture: true });
84
+ win?.removeEventListener('resize', onViewportChange);
85
+ container.remove();
86
+ }
87
+ return { render, clear, destroy };
88
+ }
89
+ /** Resolve a finding's target selector to a node, tolerating a bad selector. */
90
+ function resolveTarget(doc, target) {
91
+ try {
92
+ return doc.querySelector(target);
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ }
98
+ function buildBox(doc, finding) {
99
+ const color = colorFor(finding.impact);
100
+ const box = doc.createElement('div');
101
+ box.setAttribute('data-impact', finding.impact ?? 'none');
102
+ box.title = `${finding.id}: ${finding.help}`;
103
+ Object.assign(box.style, {
104
+ position: 'fixed',
105
+ boxSizing: 'border-box',
106
+ border: `2px solid ${color}`,
107
+ borderRadius: '2px',
108
+ background: `${color}1a`, // ~10% alpha
109
+ pointerEvents: 'auto',
110
+ cursor: 'pointer',
111
+ });
112
+ const label = doc.createElement('span');
113
+ label.textContent = finding.component
114
+ ? `${finding.component} · ${finding.id}`
115
+ : finding.id;
116
+ Object.assign(label.style, {
117
+ position: 'absolute',
118
+ top: '0',
119
+ left: '0',
120
+ transform: 'translateY(-100%)',
121
+ padding: '1px 4px',
122
+ font: '11px/1.4 ui-monospace, monospace',
123
+ color: '#fff',
124
+ background: color,
125
+ whiteSpace: 'nowrap',
126
+ });
127
+ box.appendChild(label);
128
+ return box;
129
+ }
130
+ /** Anchor `box` (position:fixed) over `target` using viewport coordinates. */
131
+ function positionBox(box, target) {
132
+ const rect = target.getBoundingClientRect();
133
+ Object.assign(box.style, {
134
+ top: `${rect.top}px`,
135
+ left: `${rect.left}px`,
136
+ width: `${rect.width}px`,
137
+ height: `${rect.height}px`,
138
+ });
139
+ }
140
+ /** Scroll the offending node into view and briefly emphasise its highlight. */
141
+ function flashAndScroll(target, box) {
142
+ target.scrollIntoView({ block: 'center', behavior: 'smooth' });
143
+ const original = box.style.boxShadow;
144
+ box.style.boxShadow = '0 0 0 4px rgba(255,255,255,0.6)';
145
+ box.ownerDocument.defaultView?.setTimeout(() => {
146
+ box.style.boxShadow = original;
147
+ }, 600);
148
+ }
@@ -0,0 +1,24 @@
1
+ import { type EnvironmentProviders } from '@angular/core';
2
+ import type { Logger } from './report';
3
+ export interface A11yDevtoolsOptions {
4
+ /** Element/Document to scan. Defaults to `document`. */
5
+ root?: () => Element | Document;
6
+ /** Log grouped findings to the console. Default true. */
7
+ log?: boolean;
8
+ /** Sink for reporting; defaults to `console`. */
9
+ logger?: Logger;
10
+ /** Draw an in-app visual overlay over each flagged node. Default false. */
11
+ overlay?: boolean;
12
+ /** Quiet window after stabilization before scanning. Default 500ms. */
13
+ debounceMs?: number;
14
+ }
15
+ /**
16
+ * Dev-only in-app accessibility auditing. Rescans whenever the application
17
+ * settles (zoneless-aware, via `ApplicationRef.isStable`) and reports each
18
+ * violation against the component that rendered it.
19
+ *
20
+ * In production this is a no-op and axe-core is never loaded, so it carries no
21
+ * runtime weight. For a hard guarantee, include the provider only in your dev
22
+ * bootstrap config.
23
+ */
24
+ export declare function provideA11yDevtools(options?: A11yDevtoolsOptions): EnvironmentProviders;
@@ -0,0 +1,44 @@
1
+ import { ApplicationRef, DestroyRef, inject, isDevMode, makeEnvironmentProviders, provideEnvironmentInitializer, } from '@angular/core';
2
+ import { debounceTime, filter } from 'rxjs';
3
+ import { runA11yScan } from './runner';
4
+ import { createOverlay } from './overlay';
5
+ /**
6
+ * Dev-only in-app accessibility auditing. Rescans whenever the application
7
+ * settles (zoneless-aware, via `ApplicationRef.isStable`) and reports each
8
+ * violation against the component that rendered it.
9
+ *
10
+ * In production this is a no-op and axe-core is never loaded, so it carries no
11
+ * runtime weight. For a hard guarantee, include the provider only in your dev
12
+ * bootstrap config.
13
+ */
14
+ export function provideA11yDevtools(options = {}) {
15
+ if (!isDevMode()) {
16
+ return makeEnvironmentProviders([]);
17
+ }
18
+ const { root, log = true, logger, overlay = false, debounceMs = 500 } = options;
19
+ return makeEnvironmentProviders([
20
+ provideEnvironmentInitializer(() => {
21
+ const appRef = inject(ApplicationRef);
22
+ const destroyRef = inject(DestroyRef);
23
+ const overlayView = overlay ? createOverlay() : undefined;
24
+ let scanning = false;
25
+ const subscription = appRef.isStable
26
+ .pipe(filter((stable) => stable), debounceTime(debounceMs))
27
+ .subscribe(() => {
28
+ if (scanning)
29
+ return; // don't stack rescans while one is in flight
30
+ scanning = true;
31
+ runA11yScan(root?.(), { log, logger })
32
+ .then((findings) => overlayView?.render(findings))
33
+ .catch(() => undefined)
34
+ .finally(() => {
35
+ scanning = false;
36
+ });
37
+ });
38
+ destroyRef.onDestroy(() => {
39
+ subscription.unsubscribe();
40
+ overlayView?.destroy();
41
+ });
42
+ }),
43
+ ]);
44
+ }
@@ -0,0 +1,14 @@
1
+ import type { A11yFinding } from './scan';
2
+ /** Minimal console-shaped sink, so reporting is testable without the real console. */
3
+ export interface Logger {
4
+ groupCollapsed(label: string): void;
5
+ groupEnd(): void;
6
+ warn(...args: unknown[]): void;
7
+ info(...args: unknown[]): void;
8
+ }
9
+ /**
10
+ * Log findings grouped by the component that owns them — the readable form the
11
+ * attribution makes possible ("♿ UserCardComponent — 2 issues" beats a list of
12
+ * CSS selectors).
13
+ */
14
+ export declare function logFindings(findings: A11yFinding[], logger?: Logger): void;
package/dist/report.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Log findings grouped by the component that owns them — the readable form the
3
+ * attribution makes possible ("♿ UserCardComponent — 2 issues" beats a list of
4
+ * CSS selectors).
5
+ */
6
+ export function logFindings(findings, logger = console) {
7
+ if (findings.length === 0) {
8
+ logger.info('♿ a11y-devtools: no violations found');
9
+ return;
10
+ }
11
+ const byComponent = new Map();
12
+ for (const finding of findings) {
13
+ const key = finding.component ?? '(unknown component)';
14
+ const bucket = byComponent.get(key);
15
+ if (bucket)
16
+ bucket.push(finding);
17
+ else
18
+ byComponent.set(key, [finding]);
19
+ }
20
+ for (const [component, items] of byComponent) {
21
+ logger.groupCollapsed(`♿ ${component} — ${items.length} issue(s)`);
22
+ for (const finding of items) {
23
+ logger.warn(`${finding.impact ?? 'n/a'} · ${finding.id}: ${finding.help}`, `\n ${finding.target}\n ${finding.helpUrl}`);
24
+ }
25
+ logger.groupEnd();
26
+ }
27
+ }
@@ -0,0 +1,10 @@
1
+ import { type A11yFinding } from './scan';
2
+ import { type Logger } from './report';
3
+ export interface RunOptions {
4
+ /** Log grouped findings to the console. Default true. */
5
+ log?: boolean;
6
+ /** Sink for reporting; defaults to `console`. */
7
+ logger?: Logger;
8
+ }
9
+ /** Scan `root`, optionally report, and return the findings. */
10
+ export declare function runA11yScan(root?: Element | Document, options?: RunOptions): Promise<A11yFinding[]>;
package/dist/runner.js ADDED
@@ -0,0 +1,10 @@
1
+ import { scan } from './scan';
2
+ import { logFindings } from './report';
3
+ /** Scan `root`, optionally report, and return the findings. */
4
+ export async function runA11yScan(root, options = {}) {
5
+ const findings = await scan(root ?? document);
6
+ if (options.log !== false) {
7
+ logFindings(findings, options.logger);
8
+ }
9
+ return findings;
10
+ }
package/dist/scan.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { RunOptions } from 'axe-core';
2
+ export type Impact = 'minor' | 'moderate' | 'serious' | 'critical' | null;
3
+ /** One axe violation node, enriched with the component that rendered it. */
4
+ export interface A11yFinding {
5
+ /** axe rule id, e.g. `image-alt`. */
6
+ id: string;
7
+ impact: Impact;
8
+ help: string;
9
+ helpUrl: string;
10
+ /** Owning component name, or null when attribution isn't available (prod). */
11
+ component: string | null;
12
+ /** CSS selector axe reported for the node. */
13
+ target: string;
14
+ html: string;
15
+ }
16
+ /**
17
+ * Run axe over `root` and return findings enriched with owning component names.
18
+ * axe-core is loaded via dynamic import so a prod build never pulls it into the
19
+ * main bundle (the provider also no-ops outside dev mode). Concurrent calls are
20
+ * serialized because axe cannot run more than once at a time.
21
+ */
22
+ export declare function scan(root?: Element | Document, options?: RunOptions): Promise<A11yFinding[]>;
package/dist/scan.js ADDED
@@ -0,0 +1,46 @@
1
+ import { resolveOwningComponentName } from './attribution';
2
+ import { OVERLAY_EXCLUDE_SELECTOR } from './overlay';
3
+ // axe-core is a singleton and throws if a run starts while another is in flight.
4
+ // Chain runs so overlapping callers (e.g. rapid rescans) serialize safely.
5
+ let runChain = Promise.resolve();
6
+ /**
7
+ * Run axe over `root` and return findings enriched with owning component names.
8
+ * axe-core is loaded via dynamic import so a prod build never pulls it into the
9
+ * main bundle (the provider also no-ops outside dev mode). Concurrent calls are
10
+ * serialized because axe cannot run more than once at a time.
11
+ */
12
+ export function scan(root = document, options) {
13
+ const result = runChain.then(() => runAxeOnce(root, options));
14
+ runChain = result.catch(() => undefined);
15
+ return result;
16
+ }
17
+ async function runAxeOnce(root, options) {
18
+ const axe = (await import('axe-core')).default;
19
+ // Scan within `root` but never flag the overlay's own highlights.
20
+ const context = { include: root, exclude: [OVERLAY_EXCLUDE_SELECTOR] };
21
+ const results = await axe.run(context, options ?? {});
22
+ const doc = root instanceof Document ? root : (root.ownerDocument ?? document);
23
+ const findings = [];
24
+ for (const violation of results.violations) {
25
+ for (const node of violation.nodes) {
26
+ const target = Array.isArray(node.target) ? String(node.target[0]) : String(node.target);
27
+ let element = null;
28
+ try {
29
+ element = doc.querySelector(target);
30
+ }
31
+ catch {
32
+ element = null;
33
+ }
34
+ findings.push({
35
+ id: violation.id,
36
+ impact: (violation.impact ?? null),
37
+ help: violation.help,
38
+ helpUrl: violation.helpUrl,
39
+ component: element ? resolveOwningComponentName(element) : null,
40
+ target,
41
+ html: node.html,
42
+ });
43
+ }
44
+ }
45
+ return findings;
46
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@ngbracket/a11y-devtools",
3
+ "version": "0.1.0",
4
+ "description": "Dev-only in-app accessibility auditing for Angular that maps each axe violation back to the component that rendered it — the attribution React overlay tools can't do.",
5
+ "license": "MIT",
6
+ "author": "Duncan Faulkner",
7
+ "contributors": [
8
+ "Lara Sanz"
9
+ ],
10
+ "homepage": "https://ngbracket.com",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/ngbracket/a11y-devtools"
14
+ },
15
+ "keywords": [
16
+ "angular",
17
+ "accessibility",
18
+ "a11y",
19
+ "devtools",
20
+ "axe-core",
21
+ "axe",
22
+ "ngbracket"
23
+ ],
24
+ "type": "module",
25
+ "sideEffects": false,
26
+ "main": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "engines": {
38
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json",
42
+ "test": "vitest run",
43
+ "test:watch": "vitest",
44
+ "prepublishOnly": "npm run build"
45
+ },
46
+ "dependencies": {
47
+ "axe-core": "^4.13.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@angular/core": ">=18.0.0",
51
+ "rxjs": ">=7.0.0"
52
+ },
53
+ "devDependencies": {
54
+ "@analogjs/vite-plugin-angular": "^2.7.2",
55
+ "@analogjs/vitest-angular": "^2.7.2",
56
+ "@angular/build": "^22.1.7",
57
+ "@angular/common": "^22.1.5",
58
+ "@angular/compiler": "^22.1.5",
59
+ "@angular/compiler-cli": "^22.1.5",
60
+ "@angular/core": "^22.1.5",
61
+ "@angular/platform-browser": "^22.1.5",
62
+ "@angular/platform-browser-dynamic": "^22.1.5",
63
+ "@oxc-project/runtime": "^0.149.0",
64
+ "@types/node": "^26.5.0",
65
+ "jsdom": "^30.0.1",
66
+ "rxjs": "^7.8.2",
67
+ "tslib": "^2.8.1",
68
+ "typescript": "^6.0.3",
69
+ "vitest": "^4.1.11"
70
+ }
71
+ }