automatica11y 0.4.1 → 0.6.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 (42) hide show
  1. package/README.md +37 -130
  2. package/package.json +28 -2
  3. package/skills/automatica11y-runner/SKILL.md +16 -3
  4. package/skills/automatica11y-runner/references/fixtures.md +22 -0
  5. package/src/cli.js +1 -0
  6. package/src/commands/common.js +14 -11
  7. package/src/frameworks/angular-errors.js +27 -0
  8. package/src/frameworks/angular-selectors.js +36 -0
  9. package/src/frameworks/angular.js +156 -0
  10. package/src/frameworks/html.js +163 -0
  11. package/src/frameworks/index.js +12 -7
  12. package/src/frameworks/react.js +2 -1
  13. package/src/frameworks/vue.js +2 -1
  14. package/src/globals.d.ts +123 -6
  15. package/src/harness/bundle.js +2 -1
  16. package/src/harness/generate/angular-recipes.js +360 -0
  17. package/src/harness/generate/probe.js +16 -5
  18. package/src/harness/npm-install.js +43 -11
  19. package/src/harness/shadow.js +1 -1
  20. package/src/harness/storybook.js +18 -5
  21. package/src/plan/build-plan.js +1 -0
  22. package/src/plan/classify.js +39 -11
  23. package/src/plan/mapping.js +5 -5
  24. package/src/plan/resolve-npm.js +47 -17
  25. package/src/plan/subpath.js +17 -0
  26. package/src/report/comparison.js +4 -1
  27. package/src/report/index.js +1 -1
  28. package/src/report/parts.js +13 -1
  29. package/src/report/single.js +1 -1
  30. package/src/run/audit-npm.js +93 -26
  31. package/src/run/fail-check.js +10 -2
  32. package/src/run/generate-fixture.js +5 -4
  33. package/src/run/run-plan.js +8 -7
  34. package/src/run/summary.js +1 -1
  35. package/src/schema.js +9 -2
  36. package/src/tiers/computed/checks.js +3 -1
  37. package/src/tiers/conditions/kit.js +3 -3
  38. package/src/tiers/interactions/archetypes.js +6 -2
  39. package/src/tiers/interactions/focus-indicator.js +4 -2
  40. package/src/tiers/interactions/helpers.js +2 -1
  41. package/src/tiers/rules/ibm.js +2 -2
  42. package/src/tiers/rules/index.js +1 -1
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The Angular adapter, for Angular 22 and newer. Angular libraries ship partially compiled, and Angular compiles them in the
3
+ * browser (JIT) once `@angular/compiler` is loaded, so nothing here runs the Angular linker or a TypeScript transform.
4
+ * A fixture is plain JavaScript that defines a component by calling the `Component` decorator as a function (see the fixtures
5
+ * guide). Every framework adapter has this shape (see index.js).
6
+ */
7
+ import { existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { generateAngular } from "../harness/generate/angular-recipes.js";
10
+ import { attributeText, markupFor } from "./angular-selectors.js";
11
+
12
+ /** The oldest Angular this version supports. */
13
+ export const ANGULAR_FLOOR = 22;
14
+ const FLOOR = ANGULAR_FLOOR;
15
+
16
+ /**
17
+ * Does a peer range allow Angular 22 or newer? A part counts when it names version 22 or later, or has no upper limit (`>=15`, `*`).
18
+ * `^20 || ^21` doesn't, and `^21 || ^22` does.
19
+ */
20
+ function allowsSupported(range) {
21
+ return String(range)
22
+ .split("||")
23
+ .some((part) => {
24
+ const text = part.trim();
25
+ const major = /(\d+)/.exec(text);
26
+ if (!major || /^[*x]?$/.test(text)) return true;
27
+ if (/^>=?/.test(text)) return true;
28
+ return Number(major[1]) >= FLOOR;
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Loads the compiler, then the whole target, and records what each Angular export is: its kind, selectors, inputs, outputs,
34
+ * `exportAs`, and whether it's standalone. Services get their method names, and modules the classes they declare.
35
+ * Exports with no Angular definition (functions, tokens, types) are left out.
36
+ */
37
+ export const discoverEntry = (pkg) => `import "@angular/compiler";
38
+ import * as lib from ${JSON.stringify(pkg)};
39
+ // A class can be exported under more than one name (a button that is also the anchor), and each name is a record.
40
+ const namesOf = new Map();
41
+ for (const key of Object.keys(lib)) if (typeof lib[key] === "function") namesOf.set(lib[key], [...(namesOf.get(lib[key]) ?? []), key]);
42
+ const listed = (items) => (typeof items === "function" ? items() : items) ?? [];
43
+ const out = [];
44
+ for (const key of Object.keys(lib)) {
45
+ const value = lib[key];
46
+ if (typeof value !== "function") continue;
47
+ try {
48
+ const cmp = value["ɵcmp"];
49
+ const dir = value["ɵdir"];
50
+ const mod = value["ɵmod"];
51
+ const def = cmp ?? dir;
52
+ if (def) {
53
+ out.push({ name: key, type: "function", parts: [], angular: { kind: cmp ? "component" : "directive", selectors: def.selectors ?? [], inputs: Object.keys(def.inputs ?? {}), outputs: Object.keys(def.outputs ?? {}), exportAs: def.exportAs ?? [], standalone: Boolean(def.standalone) } });
54
+ } else if (mod) {
55
+ const named = (items) => listed(items).flatMap((c) => namesOf.get(c) ?? []);
56
+ out.push({ name: key, type: "function", parts: [], angular: { kind: "module", declares: named(mod.declarations), exports: named(mod.exports) } });
57
+ } else if (value["ɵprov"]) {
58
+ const methods = Object.getOwnPropertyNames(value.prototype ?? {}).filter((m) => m !== "constructor" && !m.startsWith("ɵ") && typeof value.prototype[m] === "function");
59
+ out.push({ name: key, type: "function", parts: [], angular: { kind: "service", methods } });
60
+ }
61
+ } catch (error) {
62
+ out.push({ name: key, type: "function", parts: [], angular: { kind: "error", message: String(error).split("\\n")[0] } });
63
+ }
64
+ }
65
+ // A component that isn't standalone is used through the module that declares it.
66
+ for (const item of out) {
67
+ if (item.angular.kind === "module" || item.angular.standalone !== false) continue;
68
+ item.angular.moduleName = out.find((m) => m.angular.kind === "module" && m.angular.declares.includes(item.name))?.name ?? null;
69
+ }
70
+ window.__a11yExports = out;
71
+ `;
72
+
73
+ /**
74
+ * Mounts a fixture. The runtime compiler goes first. `zone.js` is loaded only when a library brought it in, because Angular 22
75
+ * runs without it. The fixture's default export is its component class, and an optional `providers` export adds application providers.
76
+ * A failure to start is logged and kept on `window.__error`, so the report can say why.
77
+ */
78
+ export const entry = (fixturePath, _pkg, context = {}) => `import "@angular/compiler";
79
+ ${context.zone ? 'import "zone.js";\n' : ""}import { bootstrapApplication } from "@angular/platform-browser";
80
+ import * as fixture from ${JSON.stringify(fixturePath)};
81
+ const Fixture = fixture.default;
82
+ const selector = Fixture?.["ɵcmp"]?.selectors?.[0]?.[0] || "app-fixture";
83
+ document.getElementById("root").append(document.createElement(selector));
84
+ try {
85
+ await bootstrapApplication(Fixture, { providers: fixture.providers ?? [] });
86
+ } catch (error) {
87
+ window.__error = String(error);
88
+ console.error(error);
89
+ }
90
+ `;
91
+
92
+ /**
93
+ * A fixture for a button or a link, from the class's own selector. A class that isn't standalone is imported through its module.
94
+ * A class whose selector can't be written from its name alone (a class name, a :not() form) is left for a person to write.
95
+ */
96
+ export function template(archetype, pkg, exportName, info) {
97
+ const want = { button: "button", link: "a" }[archetype];
98
+ const angular = info?.angular;
99
+ if (!want || !angular || (angular.kind !== "component" && angular.kind !== "directive")) return null;
100
+ // A button class may name its own element (`p-button`), which a link class may not: a link has to be an anchor.
101
+ const match = markupFor(angular.selectors, { prefer: want }) ?? (archetype === "button" ? markupFor(angular.selectors, {}) : null);
102
+ if (!match || (archetype === "link" && match.tag !== want)) return null;
103
+ const imports = angular.standalone ? [exportName] : angular.moduleName ? [angular.moduleName] : null;
104
+ if (!imports) return null;
105
+ const attrs = [attributeText(match.attrs), "data-a11y-trigger", "data-a11y-root", want === "a" ? 'href="#top"' : 'type="button"'].filter(Boolean).join(" ");
106
+ const html = `<${match.tag} ${attrs}>${want === "a" ? "Read more" : "Save"}</${match.tag}>`;
107
+ return `import { Component } from "@angular/core";
108
+ import { ${imports.join(", ")} } from ${JSON.stringify(pkg)};
109
+
110
+ class Fixture {}
111
+ Component({ selector: "app-fixture", imports: [${imports.join(", ")}], template: \`${html}\` })(Fixture);
112
+ export default Fixture;
113
+ `;
114
+ }
115
+
116
+ /** Keeps one copy of every Angular package and of rxjs, so a library and a fixture never load two. Secondary entry points need the resolver, not an alias. */
117
+ function singleCopy(workDir) {
118
+ return {
119
+ name: "single-angular-copy",
120
+ setup(build) {
121
+ build.onResolve({ filter: /^(@angular\/|rxjs(\/|$)|zone\.js(\/|$))/ }, async (args) => {
122
+ if (args.pluginData === "single-angular-copy") return undefined;
123
+ const result = await build.resolve(args.path, { resolveDir: workDir, kind: args.kind, pluginData: "single-angular-copy" });
124
+ return result.errors.length ? undefined : result;
125
+ });
126
+ },
127
+ };
128
+ }
129
+
130
+ export default {
131
+ id: "angular",
132
+ label: "Angular",
133
+ kind: "npm-angular",
134
+ noun: "export",
135
+ extension: "js",
136
+ runtime: ["@angular/core", "@angular/common", "@angular/compiler", "@angular/platform-browser", "rxjs"],
137
+ /** @returns {import("./index.js").AdapterDetection | null} */
138
+ detect(meta) {
139
+ const peers = meta.peerDependencies ?? {};
140
+ const deps = meta.dependencies ?? {};
141
+ const range = peers["@angular/core"] ?? deps["@angular/core"];
142
+ if (range === undefined) return null;
143
+ if (!allowsSupported(range)) return { kind: "npm-unsupported", framework: "Angular", reason: `The package needs @angular/core ${range}. Only Angular ${FLOOR} and newer is supported.` };
144
+ return { kind: "npm-angular", framework: "Angular", reason: `The package lists @angular/core as a ${"@angular/core" in peers ? "peer dependency" : "dependency"}.` };
145
+ },
146
+ /** What the install left behind that the entry needs to know about. */
147
+ inspect: (workDir) => ({ zone: existsSync(join(workDir, "node_modules", "zone.js")) }),
148
+ // Fixtures can be TypeScript. esbuild strips the types, and legacy decorators are what Angular's JIT reads. It can't emit
149
+ // constructor parameter metadata, so a TypeScript fixture gets its services from inject().
150
+ bundle: (workDir) => ({ alias: {}, plugins: [singleCopy(workDir)], esbuild: { tsconfigRaw: { compilerOptions: { experimentalDecorators: true, useDefineForClassFields: false } } } }),
151
+ entry,
152
+ discoverEntry,
153
+ template,
154
+ generate: (input) => generateAngular(input),
155
+ describe: (npm) => `Angular${npm.angular ? ` (core ${npm.angular})` : ""}`,
156
+ };
@@ -0,0 +1,163 @@
1
+ /**
2
+ * The plain HTML adapter, for packages with no framework: stylesheets, and scripts that wire up behavior on markup the page
3
+ * already has. A fixture is a snippet of markup. The tool loads the target's stylesheets, puts the markup on the page, then
4
+ * loads the scripts, so a script that looks for its elements when it starts finds them.
5
+ *
6
+ * What loads is named by the target itself. In a list such as `npm:a/components.css,b/interactions.iife.js`, an entry whose
7
+ * sub-path ends in `.css` is a stylesheet, and one that ends in `.js` or `.mjs` is a script. Every other entry is just a package.
8
+ */
9
+
10
+ const STYLE = /\.css$/i;
11
+ const SCRIPT = /\.(m?js)$/i;
12
+
13
+ /** @param {string | null | undefined} subpath */
14
+ export const isStyle = (subpath) => Boolean(subpath && STYLE.test(subpath));
15
+ /** @param {string | null | undefined} subpath */
16
+ export const isScript = (subpath) => Boolean(subpath && SCRIPT.test(subpath));
17
+ /** Is this sub-path a file the page loads, rather than something a framework imports? */
18
+ export const isAsset = (subpath) => isStyle(subpath) || isScript(subpath);
19
+
20
+ /**
21
+ * The stylesheets and scripts a list of packages names, in the order they were written.
22
+ * @param {Array<{ name: string, subpath: string | null }>} entries
23
+ * @returns {{ styles: string[], scripts: string[] }}
24
+ */
25
+ export function assetsOf(entries) {
26
+ const spec = (entry) => `${entry.name}/${entry.subpath}`;
27
+ return {
28
+ styles: entries.filter((entry) => isStyle(entry.subpath)).map(spec),
29
+ scripts: entries.filter((entry) => isScript(entry.subpath)).map(spec),
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Does the registry's metadata say a package is plain HTML and CSS? It has to ship a stylesheet or a browser script, and it can't
35
+ * need a framework (those are decided before this runs).
36
+ * @param {any} meta
37
+ */
38
+ export function shipsBrowserAssets(meta) {
39
+ if (typeof meta?.style === "string" || typeof meta?.unpkg === "string" || typeof meta?.jsdelivr === "string") return true;
40
+ const keys = meta?.exports && typeof meta.exports === "object" ? Object.keys(meta.exports) : [];
41
+ return keys.some((key) => STYLE.test(key) || /\.(iife|umd)\.m?js$/.test(key));
42
+ }
43
+
44
+ /** Does the package have a JavaScript entry to import? Then loading it is what tells plain HTML from web components. */
45
+ function hasScriptEntry(meta) {
46
+ if (typeof meta?.main === "string" || typeof meta?.module === "string") return true;
47
+ const exported = meta?.exports;
48
+ if (typeof exported === "string") return true;
49
+ return Boolean(exported && typeof exported === "object" && exported["."]);
50
+ }
51
+
52
+ /**
53
+ * Mounts a fixture. The stylesheets come first, as imports, so they load with the page. The markup goes on the page next. A
54
+ * `<script>` inside the markup is made again so the browser runs it, because markup set by the page never runs its scripts.
55
+ * Then the target's scripts load, one after the other.
56
+ * @param {string} fixturePath
57
+ * @param {string} _pkg
58
+ * @param {{ assets?: { styles: string[], scripts: string[] } }} [context]
59
+ */
60
+ export const entry = (fixturePath, _pkg, context = {}) => {
61
+ const { styles = [], scripts = [] } = context.assets ?? {};
62
+ return `${styles.map((spec) => `import ${JSON.stringify(spec)};`).join("\n")}
63
+ import markup from ${JSON.stringify(fixturePath)};
64
+ const root = document.getElementById("root");
65
+ root.innerHTML = markup;
66
+ for (const old of [...root.querySelectorAll("script")]) {
67
+ const script = document.createElement("script");
68
+ for (const { name, value } of old.attributes) script.setAttribute(name, value);
69
+ script.textContent = old.textContent;
70
+ old.replaceWith(script);
71
+ }
72
+ try {
73
+ ${scripts.map((spec) => ` await import(${JSON.stringify(spec)});`).join("\n")}
74
+ } catch (error) {
75
+ window.__error = String(error);
76
+ console.error(error);
77
+ }
78
+ `;
79
+ };
80
+
81
+ /**
82
+ * The bare native markup for an archetype, with no class names and nothing guessed from a stylesheet. It tests what the target's
83
+ * styles and scripts do to ordinary elements. A component built on classes needs a fixture a person writes. The browser does the
84
+ * opening and closing (`<dialog>`, `<details>`, popovers), so a template needs no script of its own except where there's no
85
+ * native way (a dialog's trigger, a message that appears).
86
+ */
87
+ const TEMPLATES = {
88
+ button: '<button type="button" data-a11y-trigger data-a11y-root>Save</button>\n',
89
+ link: '<a href="#top" data-a11y-trigger data-a11y-root>Read more</a>\n',
90
+ "form-field": `<fieldset data-a11y-root>
91
+ <legend>Contact details</legend>
92
+ <p><label for="name">Name</label> <input id="name" name="name" type="text" autocomplete="name" data-a11y-trigger></p>
93
+ <p><label for="about">About you</label> <textarea id="about" name="about"></textarea></p>
94
+ <p><label for="size">Size</label> <select id="size" name="size"><option>Small</option><option>Medium</option><option>Large</option></select></p>
95
+ <p><input id="news" name="news" type="checkbox"> <label for="news">Send me news</label></p>
96
+ <fieldset>
97
+ <legend>Contact me by</legend>
98
+ <p><input id="by-email" name="by" type="radio" value="email"> <label for="by-email">Email</label></p>
99
+ <p><input id="by-phone" name="by" type="radio" value="phone"> <label for="by-phone">Phone</label></p>
100
+ </fieldset>
101
+ <p><label for="email">Email address</label> <input id="email" name="email" type="email" aria-invalid="true" aria-describedby="email-error"> <span id="email-error">Enter an email address like name@example.com.</span></p>
102
+ </fieldset>
103
+ `,
104
+ dialog: `<button type="button" id="open-dialog" data-a11y-trigger>Open dialog</button>
105
+ <dialog id="dialog" data-a11y-root aria-labelledby="dialog-title">
106
+ <h2 id="dialog-title">Edit profile</h2>
107
+ <p>Update your details.</p>
108
+ <form method="dialog"><button>Close</button></form>
109
+ </dialog>
110
+ <script>document.getElementById("open-dialog").addEventListener("click", () => document.getElementById("dialog").showModal());</script>
111
+ `,
112
+ accordion: `<details name="faq">
113
+ <summary data-a11y-trigger>Shipping</summary>
114
+ <div data-a11y-root><p>Orders ship within two business days.</p></div>
115
+ </details>
116
+ <details name="faq">
117
+ <summary>Returns</summary>
118
+ <div><p>Returns are free for 30 days.</p></div>
119
+ </details>
120
+ `,
121
+ "live-region": `<button type="button" id="show-message" data-a11y-trigger>Show message</button>
122
+ <div id="message-region" role="status" data-a11y-root></div>
123
+ <script>document.getElementById("show-message").addEventListener("click", () => {
124
+ document.getElementById("message-region").textContent = "Saved.";
125
+ });</script>
126
+ `,
127
+ menu: `<button type="button" data-a11y-trigger popovertarget="actions" aria-haspopup="menu">Actions</button>
128
+ <div id="actions" popover data-a11y-root role="menu" aria-label="Actions">
129
+ <button type="button" role="menuitem">Copy</button>
130
+ <button type="button" role="menuitem">Paste</button>
131
+ </div>
132
+ `,
133
+ tooltip: `<button type="button" data-a11y-trigger interestfor="save-tip">Save</button>
134
+ <div id="save-tip" popover="hint" role="tooltip" data-a11y-root>Saves your work.</div>
135
+ `,
136
+ };
137
+
138
+ export default {
139
+ id: "html",
140
+ label: "HTML",
141
+ kind: "npm-html",
142
+ noun: "element",
143
+ extension: "html",
144
+ runtime: [],
145
+ /** @returns {import("./index.js").AdapterDetection | null} */
146
+ detect(meta) {
147
+ // A package that also has a JavaScript entry could be web components, so the run decides after it loads the package.
148
+ if (!shipsBrowserAssets(meta) || hasScriptEntry(meta)) return null;
149
+ return { kind: "npm-html", framework: "HTML", reason: "The package ships a stylesheet or a browser script and needs no framework." };
150
+ },
151
+ /** What the page loads, from the packages the target lists. */
152
+ inspect: (/** @type {string} */ _workDir, /** @type {Array<{ name: string, subpath: string | null }>} */ entries = []) => ({ assets: assetsOf(entries) }),
153
+ bundle: () => ({ alias: {}, esbuild: { loader: { ".html": /** @type {"text"} */ ("text") } } }),
154
+ entry,
155
+ discoverEntry: () => "window.__a11yExports = [];\n",
156
+ template: (/** @type {string} */ archetype) => TEMPLATES[/** @type {keyof typeof TEMPLATES} */ (archetype)] ?? null,
157
+ generate: () => ({ candidates: [], reason: "Plain HTML has no exports or selectors to build a fixture from." }),
158
+ describe: (npm) => {
159
+ const loaded = /** @type {any} */ (npm).assets;
160
+ return `plain HTML${loaded ? ` (${loaded.styles.length} ${loaded.styles.length === 1 ? "stylesheet" : "stylesheets"}, ${loaded.scripts.length} ${loaded.scripts.length === 1 ? "script" : "scripts"})` : ""}`;
161
+ },
162
+ };
163
+
@@ -6,6 +6,8 @@
6
6
  * To add a framework, write a module with the same shape as react.js, add it here, and add its id to FLAVORS and its kind to
7
7
  * TARGET_KINDS in schema.js. A test checks that the two lists agree.
8
8
  *
9
+ * @typedef {"npm-react" | "npm-vue" | "npm-angular" | "npm-html" | "npm-wc" | "npm-unsupported"} AdapterKind
10
+ * @typedef {{ kind: AdapterKind, framework: string, reason: string }} AdapterDetection
9
11
  * @typedef {{
10
12
  * id: string,
11
13
  * label: string,
@@ -13,21 +15,24 @@
13
15
  * noun: string,
14
16
  * extension: string,
15
17
  * runtime: string[],
16
- * detect: (meta: any) => { kind: string, framework: string, reason: string } | null,
17
- * bundle: (workDir: string) => { alias: Record<string, string>, define?: Record<string, string>, esbuild: Record<string, any> },
18
- * entry: (fixturePath: string, pkg: string) => string,
18
+ * detect: (meta: unknown) => AdapterDetection | null,
19
+ * inspect?: (workDir: string, entries?: Array<{ name: string, subpath: string | null }>) => Record<string, unknown>,
20
+ * bundle: (workDir: string) => { alias: Record<string, string>, define?: Record<string, string>, plugins?: import("esbuild").Plugin[], esbuild: import("esbuild").BuildOptions },
21
+ * entry: (fixturePath: string, pkg: string, context?: Record<string, unknown>) => string,
19
22
  * discoverEntry: (pkg: string) => string,
20
- * template: (archetype: string, pkg: string, name?: string) => string | null,
21
- * generate: (input: any) => { candidates: Array<{ id: string, summary: string, source: string, used: string[] }>, reason: string | null },
22
- * describe: (npm: any) => string,
23
+ * template: (archetype: string, pkg: string, name?: string, info?: unknown) => string | null,
24
+ * generate: (input: unknown) => { candidates: Array<{ id: string, summary: string, source: string, used: string[] }>, reason: string | null },
25
+ * describe: (npm: unknown) => string,
23
26
  * }} Adapter
24
27
  */
28
+ import angular from "./angular.js";
29
+ import html from "./html.js";
25
30
  import react from "./react.js";
26
31
  import vue from "./vue.js";
27
32
  import wc from "./wc.js";
28
33
 
29
34
  /** @type {Record<string, Adapter>} */
30
- export const ADAPTERS = { react, vue, wc };
35
+ export const ADAPTERS = { react, vue, angular, html, wc };
31
36
 
32
37
  /** The adapter for a flavor (`react`, `vue`, or `wc`). */
33
38
  export function adapterFor(id) {
@@ -55,6 +55,7 @@ export default {
55
55
  /** Packages the fixture and the library must share one copy of, and that the install makes sure are there. */
56
56
  runtime: ["react", "react-dom"],
57
57
  /** Is this package React, from its registry metadata alone? */
58
+ /** @returns {import("./index.js").AdapterDetection | null} */
58
59
  detect(meta) {
59
60
  const peers = meta.peerDependencies ?? {};
60
61
  const deps = meta.dependencies ?? {};
@@ -66,7 +67,7 @@ export default {
66
67
  bundle: (workDir) => ({
67
68
  // One copy of React for the library and the fixture, or hooks break.
68
69
  alias: { react: join(workDir, "node_modules", "react"), "react-dom": join(workDir, "node_modules", "react-dom") },
69
- esbuild: { jsx: "automatic" },
70
+ esbuild: { jsx: /** @type {"automatic"} */ ("automatic") },
70
71
  }),
71
72
  entry,
72
73
  discoverEntry,
@@ -62,6 +62,7 @@ export default {
62
62
  noun: "export",
63
63
  extension: "jsx",
64
64
  runtime: ["vue"],
65
+ /** @returns {import("./index.js").AdapterDetection | null} */
65
66
  detect(meta) {
66
67
  const peers = meta.peerDependencies ?? {};
67
68
  const deps = meta.dependencies ?? {};
@@ -79,7 +80,7 @@ export default {
79
80
  alias: { vue: join(workDir, "node_modules", "vue") },
80
81
  define: { __VUE_OPTIONS_API__: "true", __VUE_PROD_DEVTOOLS__: "false", __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false" },
81
82
  // Vue has no JSX runtime of its own, so JSX becomes h() calls. The shim is injected wherever h or Fragment is used without being declared.
82
- esbuild: { jsx: "transform", jsxFactory: "h", jsxFragment: "Fragment", inject: [shim] },
83
+ esbuild: { jsx: /** @type {"transform"} */ ("transform"), jsxFactory: "h", jsxFragment: "Fragment", inject: [shim] },
83
84
  };
84
85
  },
85
86
  entry,
package/src/globals.d.ts CHANGED
@@ -1,12 +1,129 @@
1
1
  /** Properties our init scripts and helpers put on the page's window. They only exist inside the browser. */
2
+ type A11yColor = number[];
3
+ type A11yBox = { x: number; y: number; width: number; height: number };
4
+ type A11yTextPart = { key: number; text: string; color: A11yColor | null; size: number; weight: string; backdrop: A11yColor | null; undetermined: string | null };
5
+ type A11yFocusPart = {
6
+ outlineStyle: string;
7
+ outlineWidth: string;
8
+ outlineColor: string;
9
+ boxShadow: string;
10
+ borderTopStyle: string;
11
+ borderTopColor: string;
12
+ borderTopWidth: string;
13
+ backgroundColor: string;
14
+ color: string;
15
+ textDecorationLine: string;
16
+ rendered: boolean;
17
+ };
18
+ type A11yFocusSnapshot = { parts: Record<string, A11yFocusPart>; box: A11yBox };
19
+
20
+ interface A11yHelpers {
21
+ uid(node: Node | null): number | null;
22
+ queryDeep(selector: string): Element | null;
23
+ queryAllDeep(selector: string): Element[];
24
+ deepActive(): Element | null;
25
+ within(root: Node | null, node: Node | null): boolean;
26
+ visible(element: Element | null): boolean;
27
+ snapshot(): {
28
+ clicks: number;
29
+ triggerTag: string | null;
30
+ triggerRole: string | null;
31
+ expanded: string | null;
32
+ rootExists: boolean;
33
+ rootVisible: boolean;
34
+ rootModal: boolean;
35
+ activeIsTrigger: boolean;
36
+ activeInRoot: boolean;
37
+ activeUid: number | null;
38
+ activeTag: string | null;
39
+ activeText: string;
40
+ activeRole: string | null;
41
+ bodyActive: boolean;
42
+ };
43
+ items(selector: string): Array<{ uid: number | null; text: string; active: boolean; selected: boolean; hasSelected: boolean; visible: boolean }>;
44
+ live(): {
45
+ ancestorUids: Array<number | null>;
46
+ present: boolean;
47
+ visible: boolean;
48
+ text: string;
49
+ named: boolean;
50
+ region: { uid: number | null; role: string | null; ariaLive: string | null; politeness: string; isMessage: boolean } | null;
51
+ regionUids: Array<number | null>;
52
+ };
53
+ focusDismiss(): { found: false } | { found: true; tag: string; label: string };
54
+ nameSources(): string[] | null;
55
+ errorInfo(): { invalid: boolean; ariaInvalid: boolean; linked: string[]; live: string[] } | null;
56
+ focusSnapshot(): A11yFocusSnapshot | null;
57
+ remember(): void;
58
+ lastSnapshot(): A11yFocusSnapshot | null;
59
+ inputValue(): string | null;
60
+ }
61
+
62
+ interface A11yMeasure {
63
+ rgba(css: string): A11yColor | null;
64
+ text(scope?: string): A11yTextPart[] | null;
65
+ boundary(): {
66
+ outside: A11yColor | null;
67
+ undetermined: string | null;
68
+ parts: Array<{ kind: string; color: A11yColor; width?: number }>;
69
+ graphics: Array<{ kind: string; color: A11yColor }>;
70
+ hasText: boolean;
71
+ inputLike: boolean;
72
+ box: A11yBox;
73
+ } | null;
74
+ focusStyles(atRest: boolean): {
75
+ outside: A11yColor | null;
76
+ inside: A11yColor | null;
77
+ undetermined: string | null;
78
+ outline: { width: number; offset: number; color: A11yColor | null } | null;
79
+ shadows: Array<{ inset: boolean; x: number; y: number; blur: number; spread: number; color: A11yColor | null }>;
80
+ border: { width: number; color: A11yColor } | null;
81
+ box: A11yBox;
82
+ } | null;
83
+ compareShots(before: string, after: string): Promise<{ changed: number; strong: number; max: number; width: number; height: number }>;
84
+ }
85
+
86
+ interface A11yConditions {
87
+ describe(element: Element | null): string;
88
+ animations(): Array<{ kind: string; name: string; target: string; duration: number | null; iterations: number | "infinite"; props: string[]; moving: boolean }>;
89
+ overflow(rootSelector?: string): {
90
+ viewportWidth: number;
91
+ scrollWidth: number;
92
+ offenders: Array<{ element: string; right: number; width: number; left: number }>;
93
+ offenderCount: number;
94
+ root: { element: string; left: number; right: number } | null;
95
+ };
96
+ clipping(): Array<{ visuallyHidden: boolean; element: string; hidesX: boolean; hidesY: boolean; overX: number; overY: number; text: string }>;
97
+ applySpacing(): void;
98
+ translucentSurfaces(): Array<{ element: string; background: string; backdropFilter: string }>;
99
+ forcedColorOptOuts(): string[];
100
+ appearance(): { colorScheme: string; background: string; color: string };
101
+ }
102
+
103
+ interface PackageExport {
104
+ name: string;
105
+ type: string;
106
+ parts: string[];
107
+ }
108
+
109
+ type AngularExportInfo =
110
+ | { kind: "component" | "directive"; selectors: Array<Array<string | number>>; inputs: string[]; outputs: string[]; exportAs: string[]; standalone: boolean; moduleName?: string | null }
111
+ | { kind: "module"; declares: string[]; exports: string[] }
112
+ | { kind: "service"; methods: string[] }
113
+ | { kind: "error"; message: string };
114
+
115
+ interface AngularPackageExport extends PackageExport {
116
+ angular: AngularExportInfo;
117
+ }
118
+
2
119
  interface Window {
3
- __a11y: any;
4
- __a11yMeasure: any;
5
- __a11yConditions: any;
6
- __vsr: any;
120
+ __a11y: A11yHelpers;
121
+ __a11yMeasure: A11yMeasure;
122
+ __a11yConditions: A11yConditions;
123
+ __vsr: Pick<typeof import("@guidepup/virtual-screen-reader"), "Virtual" | "virtual">;
7
124
  __a11yClicks: number;
8
- __a11yLast: any;
9
- __a11yExports?: any;
125
+ __a11yLast: Element | null;
126
+ __a11yExports?: Array<PackageExport | AngularPackageExport>;
10
127
  __a11yDefined?: string[];
11
128
  __a11yClosedShadowHosts?: string[];
12
129
  }
@@ -42,7 +42,8 @@ export async function bundleEntries({ entries, outdir, workDir, framework = null
42
42
  absWorkingDir: workDir,
43
43
  nodePaths: [join(workDir, "node_modules")],
44
44
  alias: settings.alias,
45
- loader: ASSET_LOADERS,
45
+ plugins: settings.plugins ?? [],
46
+ loader: { ...ASSET_LOADERS, ...(settings.esbuild.loader ?? {}) },
46
47
  define: { "process.env.NODE_ENV": '"development"', ...(settings.define ?? {}) },
47
48
  logLevel: "silent",
48
49
  });