eaa-kit 0.2.1 → 0.3.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 (43) hide show
  1. package/README.md +2 -1
  2. package/dist/astro/index.d.ts +7 -34
  3. package/dist/astro/index.js +6 -26
  4. package/dist/audit/runners/worker.js +1 -1
  5. package/dist/audit-CuG2hYyo.js +2 -0
  6. package/dist/audit-DcL73mOC.js +635 -0
  7. package/dist/{baseline-CV_3lbER.js → baseline-CuKFq4IF.js} +1 -1
  8. package/dist/{baseline-CgBmzFTr.js → baseline-s9F3fXTN.js} +16 -26
  9. package/dist/cli/index.js +80 -143
  10. package/dist/collect-BkAQ0viT.js +123 -0
  11. package/dist/command-Dxpa00Ha.js +77 -0
  12. package/dist/component-7kEBjv_y.js +111 -0
  13. package/dist/{crawl-CtJbMNNb.js → crawl-Oxt2Gaqo.js} +13 -23
  14. package/dist/frameworks-BYa3tULg.js +243 -0
  15. package/dist/frameworks-DaDqrJOw.js +2 -0
  16. package/dist/fs-BmPtmFke.js +40 -0
  17. package/dist/{html-BLEuzep6.js → html-DKiI_3gs.js} +167 -86
  18. package/dist/{impact-DvgBjupx.js → impact-YdoOtFqm.js} +15 -1
  19. package/dist/index.js +2 -2
  20. package/dist/{init-DRKIdpK1.js → init-DiLiYvN1.js} +13 -10
  21. package/dist/{jsdom-BEu6Ra_2.js → jsdom-BjpF-2V-.js} +2 -7
  22. package/dist/jsdom-X4KYfTp8.js +3 -0
  23. package/dist/json-B0Y7rNjt.js +2 -0
  24. package/dist/{json-C9xS1PNC.js → json-Cnv9nd6U.js} +13 -21
  25. package/dist/{load-sCkKsvGQ.js → load-UYXLqGV9.js} +2 -8
  26. package/dist/manual-Vz-oX1I_.js +239 -0
  27. package/dist/{playwright-DSRnXmcd.js → playwright-DYFsGUNd.js} +70 -23
  28. package/dist/{pool-DixLeu8L.js → pool-BWkWZiJW.js} +4 -12
  29. package/dist/{project-CufCqIE2.js → project-DW08TseF.js} +8 -21
  30. package/dist/{render-BO0nVrrZ.js → render-DI_aCnAZ.js} +7 -15
  31. package/dist/{result-2aZPfM8w.js → result-DLxd2Eip.js} +38 -1
  32. package/dist/{routes-BxbSZKXC.js → routes-C2Cgf6Ko.js} +4 -8
  33. package/dist/run-BMASMmwO.d.ts +45 -0
  34. package/dist/run-BW6CVuND.js +36 -0
  35. package/dist/{sarif-eSCuI0eX.js → sarif-DB3WG7T9.js} +11 -33
  36. package/dist/text-BFmNtMsV.js +43 -0
  37. package/dist/vite/index.d.ts +39 -0
  38. package/dist/vite/index.js +40 -0
  39. package/package.json +8 -3
  40. package/dist/audit-B2dIKpJ5.js +0 -2
  41. package/dist/audit-DXpKkXsC.js +0 -950
  42. package/dist/escape-Dm1o_RAk.js +0 -21
  43. package/dist/jsdom-C6dIyaxN.js +0 -3
@@ -0,0 +1,111 @@
1
+ import { i as toPosix } from "./fs-BmPtmFke.js";
2
+ import { t as collapse } from "./text-BFmNtMsV.js";
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { glob } from "tinyglobby";
6
+ //#region src/audit/component.ts
7
+ /**
8
+ * Which source file a failing element was written in.
9
+ *
10
+ * Route mapping names the page, and on a site built from components the page is
11
+ * not where the fix goes: a header with a missing `alt` appears on every page
12
+ * that renders it and is written in none of them. The Issues view can already
13
+ * tell that one element is shared across pages; this says which file to open.
14
+ *
15
+ * The method is deliberately dumb. Take a literal out of the failing markup —
16
+ * an image path, a link target, an id — and look for it in the project's own
17
+ * source. Frameworks do emit source positions, but only in development builds,
18
+ * and an auditor runs against production output. A literal survives every
19
+ * compiler.
20
+ *
21
+ * It never guesses. A literal found in two files names neither, because a wrong
22
+ * file is worse than none: it sends somebody to edit code that was not the
23
+ * cause, and costs more than the minute it saved.
24
+ */
25
+ /** Where component source is expected to live. */
26
+ const SOURCE_GLOBS = ["**/*.{tsx,jsx,ts,js,mjs,vue,svelte,astro,mdx,md,html,php,erb,twig,liquid,hbs}"];
27
+ /** Never source: dependencies, build output, version control. */
28
+ const NEVER = [
29
+ "**/node_modules/**",
30
+ "**/.git/**",
31
+ "**/dist/**",
32
+ "**/build/**",
33
+ "**/out/**",
34
+ "**/_site/**",
35
+ "**/.next/**",
36
+ "**/.nuxt/**",
37
+ "**/.svelte-kit/**",
38
+ "**/.output/**",
39
+ "**/coverage/**",
40
+ "**/*.min.js"
41
+ ];
42
+ /** Files read into the index. A ceiling, not a target. */
43
+ const MAX_FILES = 2e3;
44
+ /** Bytes per file. A source file past this is generated or vendored. */
45
+ const MAX_BYTES = 524288;
46
+ /**
47
+ * Read the project's source once, so every element can be looked up against it.
48
+ *
49
+ * Once rather than per element: a site with forty violations would otherwise
50
+ * walk the tree forty times for an answer that does not change.
51
+ */
52
+ async function buildComponentIndex(cwd) {
53
+ const found = await glob([...SOURCE_GLOBS], {
54
+ cwd,
55
+ ignore: [...NEVER],
56
+ onlyFiles: true,
57
+ dot: false
58
+ });
59
+ const files = /* @__PURE__ */ new Map();
60
+ for (const relative of found.slice(0, MAX_FILES).sort()) try {
61
+ const source = await readFile(path.resolve(cwd, relative), "utf8");
62
+ if (source.length <= MAX_BYTES) files.set(toPosix(relative), source);
63
+ } catch {}
64
+ return { files };
65
+ }
66
+ /**
67
+ * Literals worth searching for, most distinctive first.
68
+ *
69
+ * An image path or a link target is written by hand and survives compilation.
70
+ * A class name may be generated, and text content may be interpolated, so both
71
+ * come after. Anything short is dropped: `/` appears in every file.
72
+ */
73
+ function searchTermsFor(html) {
74
+ const terms = [];
75
+ const add = (value) => {
76
+ if (value === void 0) return;
77
+ const trimmed = value.trim();
78
+ if (trimmed.length < 4 || trimmed.includes("${")) return;
79
+ if (!terms.includes(trimmed)) terms.push(trimmed);
80
+ };
81
+ for (const attribute of [
82
+ "src",
83
+ "href",
84
+ "id",
85
+ "data-testid",
86
+ "name",
87
+ "action"
88
+ ]) add(new RegExp(`\\s${attribute}\\s*=\\s*["']([^"']+)["']`, "i").exec(html)?.[1]);
89
+ const text = collapse(html.replace(/<[^>]*>/g, " "));
90
+ if (text.length >= 8) add(text.slice(0, 60));
91
+ return terms;
92
+ }
93
+ /**
94
+ * The one source file this element was written in, if exactly one claims it.
95
+ *
96
+ * Returns undefined when nothing matches and when more than one does. The
97
+ * second case is the important one: naming a file that merely happens to
98
+ * contain the same path sends somebody to edit the wrong component.
99
+ */
100
+ function componentFor(index, html) {
101
+ for (const term of searchTermsFor(html)) {
102
+ const matches = [];
103
+ for (const [file, source] of index.files) if (source.includes(term)) {
104
+ matches.push(file);
105
+ if (matches.length > 1) break;
106
+ }
107
+ if (matches.length === 1) return matches[0];
108
+ }
109
+ }
110
+ //#endregion
111
+ export { buildComponentIndex, componentFor };
@@ -1,3 +1,4 @@
1
+ import { i as stripBom } from "./collect-BkAQ0viT.js";
1
2
  /** Requests in flight at once. Politeness, not throughput. */
2
3
  const REQUEST_CONCURRENCY = 4;
3
4
  var CrawlError = class extends Error {
@@ -142,31 +143,18 @@ function disallowedPaths(robots) {
142
143
  return paths;
143
144
  }
144
145
  /**
145
- * Paths this crawler must not visit, from the site's own robots.txt.
146
+ * One of the two files a site publishes about itself, or undefined.
146
147
  *
147
148
  * Read directly rather than through fetchPage, which correctly refuses anything
148
- * that is not a page. A site with no robots.txt disallows nothing.
149
+ * that is not a page. A site that does not publish one is the ordinary case,
150
+ * not a failure, so it comes back empty either way.
149
151
  */
150
- async function blockedPaths(entry, impl) {
152
+ async function fetchSiteFile(entry, impl, name) {
151
153
  try {
152
- const response = await impl(new URL("/robots.txt", entry).href, { redirect: "follow" });
153
- return response.ok ? disallowedPaths(await response.text()) : [];
154
+ const response = await impl(new URL(name, entry).href, { redirect: "follow" });
155
+ return response.ok ? await response.text() : void 0;
154
156
  } catch {
155
- return [];
156
- }
157
- }
158
- /**
159
- * Page URLs the site lists for itself.
160
- *
161
- * Worth one request: a sitemap finds pages nothing links to, which link
162
- * following alone never reaches.
163
- */
164
- async function sitemapUrls(entry, impl) {
165
- try {
166
- const response = await impl(new URL("/sitemap.xml", entry).href, { redirect: "follow" });
167
- return response.ok ? urlsFromSitemap(await response.text(), entry) : [];
168
- } catch {
169
- return [];
157
+ return;
170
158
  }
171
159
  }
172
160
  /**
@@ -182,7 +170,8 @@ async function crawlSite(entry, options = {}) {
182
170
  const maxDepth = options.maxDepth ?? 3;
183
171
  const timeoutMs = options.timeoutMs ?? 15e3;
184
172
  const failures = [];
185
- const blocked = options.ignoreRobots ? [] : await blockedPaths(entry, impl);
173
+ const robots = options.ignoreRobots ? void 0 : await fetchSiteFile(entry, impl, "/robots.txt");
174
+ const blocked = robots === void 0 ? [] : disallowedPaths(robots);
186
175
  const allowed = (url) => !blocked.some((path) => url.pathname.startsWith(path));
187
176
  let discovery = "links";
188
177
  const queue = [];
@@ -195,7 +184,8 @@ async function crawlSite(entry, options = {}) {
195
184
  depth
196
185
  });
197
186
  };
198
- const listed = await sitemapUrls(entry, impl);
187
+ const sitemap = await fetchSiteFile(entry, impl, "/sitemap.xml");
188
+ const listed = sitemap === void 0 ? [] : urlsFromSitemap(sitemap, entry);
199
189
  if (listed.length > 0) {
200
190
  discovery = "sitemap";
201
191
  for (const url of listed) enqueue(url, 0);
@@ -220,7 +210,7 @@ async function crawlSite(entry, options = {}) {
220
210
  pages.push({
221
211
  absolutePath: url.href,
222
212
  relativePath: pageIdentity(url),
223
- html: html.charCodeAt(0) === 65279 ? html.slice(1) : html
213
+ html: stripBom(html)
224
214
  });
225
215
  options.onProgress?.(pages.length, queue.length);
226
216
  if (item.depth < maxDepth) for (const link of linksFrom(html, url)) enqueue(link, item.depth + 1);
@@ -0,0 +1,243 @@
1
+ import { t as exists } from "./fs-BmPtmFke.js";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ //#region src/audit/frameworks.ts
5
+ /**
6
+ * Most specific first. Several of these depend on Vite, so Vite is last: a
7
+ * SvelteKit project is a SvelteKit project, not a Vite one.
8
+ *
9
+ * Several carry `files` as well as `packages` even though they are npm
10
+ * packages. A config file named after the framework is as good an identifier as
11
+ * the dependency, and it still works where package.json is missing, unreadable,
12
+ * or belongs to a workspace root rather than the project in front of us.
13
+ */
14
+ const FRAMEWORKS = [
15
+ {
16
+ id: "next",
17
+ name: "Next.js",
18
+ packages: ["next"],
19
+ files: [
20
+ "next.config.js",
21
+ "next.config.mjs",
22
+ "next.config.ts"
23
+ ],
24
+ outputs: ["out"],
25
+ configs: [
26
+ "next.config.js",
27
+ "next.config.mjs",
28
+ "next.config.ts"
29
+ ],
30
+ outputPattern: /distDir\s*:\s*['"`]([^'"`]+)['"`]/,
31
+ staticOutput: {
32
+ needs: "output: 'export'",
33
+ how: "add output: 'export' to your Next config, then build"
34
+ },
35
+ serves: true
36
+ },
37
+ {
38
+ id: "nuxt",
39
+ name: "Nuxt",
40
+ packages: ["nuxt"],
41
+ files: ["nuxt.config.ts", "nuxt.config.js"],
42
+ outputs: [".output/public", "dist"],
43
+ staticOutput: {
44
+ needs: "nuxt generate",
45
+ how: "run nuxt generate rather than nuxt build"
46
+ },
47
+ serves: true
48
+ },
49
+ {
50
+ id: "sveltekit",
51
+ name: "SvelteKit",
52
+ packages: ["@sveltejs/kit"],
53
+ files: ["svelte.config.js"],
54
+ outputs: ["build", ".svelte-kit/output/prerendered/pages"],
55
+ staticOutput: {
56
+ needs: "@sveltejs/adapter-static",
57
+ how: "use @sveltejs/adapter-static, then build"
58
+ },
59
+ serves: true
60
+ },
61
+ {
62
+ id: "remix",
63
+ name: "React Router / Remix",
64
+ packages: [
65
+ "@remix-run/react",
66
+ "react-router",
67
+ "@react-router/dev"
68
+ ],
69
+ outputs: ["build/client"],
70
+ serves: true
71
+ },
72
+ {
73
+ id: "astro",
74
+ name: "Astro",
75
+ packages: ["astro"],
76
+ files: [
77
+ "astro.config.mjs",
78
+ "astro.config.js",
79
+ "astro.config.ts"
80
+ ],
81
+ outputs: ["dist"],
82
+ configs: [
83
+ "astro.config.mjs",
84
+ "astro.config.js",
85
+ "astro.config.ts"
86
+ ],
87
+ outputPattern: /outDir\s*:\s*['"`]([^'"`]+)['"`]/,
88
+ serves: true
89
+ },
90
+ {
91
+ id: "gatsby",
92
+ name: "Gatsby",
93
+ packages: ["gatsby"],
94
+ outputs: ["public"],
95
+ serves: false
96
+ },
97
+ {
98
+ id: "docusaurus",
99
+ name: "Docusaurus",
100
+ packages: ["@docusaurus/core"],
101
+ outputs: ["build"],
102
+ serves: false
103
+ },
104
+ {
105
+ id: "vitepress",
106
+ name: "VitePress",
107
+ packages: ["vitepress"],
108
+ files: [".vitepress/config.ts", "docs/.vitepress/config.ts"],
109
+ outputs: [".vitepress/dist", "docs/.vitepress/dist"],
110
+ serves: false
111
+ },
112
+ {
113
+ id: "eleventy",
114
+ name: "Eleventy",
115
+ packages: ["@11ty/eleventy"],
116
+ outputs: ["_site"],
117
+ configs: [
118
+ ".eleventy.js",
119
+ "eleventy.config.js",
120
+ "eleventy.config.mjs"
121
+ ],
122
+ outputPattern: /output\s*:\s*['"`]([^'"`]+)['"`]/,
123
+ serves: false
124
+ },
125
+ {
126
+ id: "angular",
127
+ name: "Angular",
128
+ packages: ["@angular/core"],
129
+ outputs: ["dist"],
130
+ serves: false
131
+ },
132
+ {
133
+ id: "cra",
134
+ name: "Create React App",
135
+ packages: ["react-scripts"],
136
+ outputs: ["build"],
137
+ serves: false
138
+ },
139
+ {
140
+ id: "hugo",
141
+ name: "Hugo",
142
+ packages: [],
143
+ files: [
144
+ "hugo.toml",
145
+ "hugo.yaml",
146
+ "config.toml"
147
+ ],
148
+ outputs: ["public"],
149
+ serves: false
150
+ },
151
+ {
152
+ id: "jekyll",
153
+ name: "Jekyll",
154
+ packages: [],
155
+ files: ["_config.yml"],
156
+ outputs: ["_site"],
157
+ serves: false
158
+ },
159
+ {
160
+ id: "vite",
161
+ name: "Vite",
162
+ packages: ["vite"],
163
+ outputs: ["dist"],
164
+ configs: [
165
+ "vite.config.ts",
166
+ "vite.config.js",
167
+ "vite.config.mjs"
168
+ ],
169
+ outputPattern: /outDir\s*:\s*['"`]([^'"`]+)['"`]/,
170
+ serves: true
171
+ }
172
+ ];
173
+ /** Output directories to try when nothing was recognised. */
174
+ const FALLBACK_OUTPUTS = [
175
+ "dist",
176
+ "out",
177
+ "build",
178
+ "_site",
179
+ "public",
180
+ ".output/public"
181
+ ];
182
+ /**
183
+ * The framework this project uses, if it is one this knows.
184
+ *
185
+ * Dependencies decide it where there are any, because a package.json states
186
+ * what a project is far more reliably than a file lying in the root. The
187
+ * file-based entries exist for Hugo and Jekyll, which have no package.json to
188
+ * read.
189
+ */
190
+ async function detectFramework(cwd, pkg) {
191
+ const deps = {
192
+ ...pkg?.dependencies,
193
+ ...pkg?.devDependencies
194
+ };
195
+ for (const framework of FRAMEWORKS) {
196
+ const byPackage = framework.packages.some((name) => deps[name] !== void 0);
197
+ let byFile = false;
198
+ if (!byPackage && framework.files !== void 0) {
199
+ for (const file of framework.files) if (await exists(file, cwd)) {
200
+ byFile = true;
201
+ break;
202
+ }
203
+ }
204
+ if (!byPackage && !byFile) continue;
205
+ const configured = await outputFromConfig(cwd, framework);
206
+ const outputs = configured === void 0 ? [...framework.outputs] : [configured, ...framework.outputs];
207
+ return {
208
+ framework,
209
+ outputs: [...new Set(outputs)]
210
+ };
211
+ }
212
+ }
213
+ /**
214
+ * A custom output directory, read out of the framework's config file.
215
+ *
216
+ * Read with a pattern rather than executed. A config file is code, and this runs
217
+ * before anything has decided the project is worth trusting; a regex that
218
+ * misses a computed value is a directory not found, which the caller already
219
+ * handles, while running the file to find out is a different class of risk
220
+ * entirely.
221
+ */
222
+ async function outputFromConfig(cwd, framework) {
223
+ if (framework.configs === void 0 || framework.outputPattern === void 0) return void 0;
224
+ for (const name of framework.configs) {
225
+ let source;
226
+ try {
227
+ source = await readFile(path.resolve(cwd, name), "utf8");
228
+ } catch {
229
+ continue;
230
+ }
231
+ const value = framework.outputPattern.exec(source)?.[1];
232
+ if (value === void 0 || value === "") continue;
233
+ if (path.isAbsolute(value)) continue;
234
+ return value.replace(/^\.\//, "");
235
+ }
236
+ }
237
+ /** Every output directory worth trying, framework-aware, most likely first. */
238
+ async function candidateOutputs(cwd, pkg) {
239
+ const detected = await detectFramework(cwd, pkg);
240
+ return [.../* @__PURE__ */ new Set([...detected?.outputs ?? [], ...FALLBACK_OUTPUTS])];
241
+ }
242
+ //#endregion
243
+ export { outputFromConfig as a, detectFramework as i, FRAMEWORKS as n, candidateOutputs as r, FALLBACK_OUTPUTS as t };
@@ -0,0 +1,2 @@
1
+ import { i as detectFramework, t as FALLBACK_OUTPUTS } from "./frameworks-BYa3tULg.js";
2
+ export { FALLBACK_OUTPUTS, detectFramework };
@@ -0,0 +1,40 @@
1
+ import { stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ //#region src/fs.ts
4
+ /**
5
+ * Asking the filesystem a yes-or-no question.
6
+ *
7
+ * Six modules had their own copy of the same try/stat/catch, each answering a
8
+ * slightly different question and each getting it right by hand. A path that
9
+ * cannot be stat'd is not a path of the kind asked about, which is the only
10
+ * behaviour any caller here wants from an unreadable directory.
11
+ */
12
+ /** Whether anything is there, resolved against `root` when one is given. */
13
+ async function exists(target, root) {
14
+ try {
15
+ await stat(root === void 0 ? target : path.resolve(root, target));
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ async function isFile(target) {
22
+ try {
23
+ return (await stat(target)).isFile();
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+ async function isDirectory(target) {
29
+ try {
30
+ return (await stat(target)).isDirectory();
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+ /** Platform-native separators to POSIX, so paths in reports match everywhere. */
36
+ function toPosix(filePath) {
37
+ return filePath.split(path.sep).join("/");
38
+ }
39
+ //#endregion
40
+ export { toPosix as i, isDirectory as n, isFile as r, exists as t };