eaa-kit 0.2.1 → 0.4.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 (56) hide show
  1. package/README.md +51 -4
  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-CPoZMXGM.js +779 -0
  6. package/dist/audit-CpXH2Mk8.js +2 -0
  7. package/dist/{baseline-CV_3lbER.js → baseline-22Y1NWxM.js} +1 -1
  8. package/dist/{baseline-CgBmzFTr.js → baseline-DB9CZGnV.js} +17 -27
  9. package/dist/cli/index.js +336 -142
  10. package/dist/collect-CFM8gEVv.js +134 -0
  11. package/dist/command-D8l_oYbV.js +77 -0
  12. package/dist/component-C3GL1Mnu.js +146 -0
  13. package/dist/component-DKd3EHOg.js +2 -0
  14. package/dist/coverage-B4IBKMO8.js +521 -0
  15. package/dist/{crawl-CtJbMNNb.js → crawl-BiI1Lau_.js} +13 -23
  16. package/dist/eleventy/index.d.ts +28 -0
  17. package/dist/eleventy/index.js +19 -0
  18. package/dist/frameworks-B4ClIJgE.js +314 -0
  19. package/dist/frameworks-etFg_O8K.js +2 -0
  20. package/dist/fs-BmPtmFke.js +40 -0
  21. package/dist/html-BGTO3ypW.js +543 -0
  22. package/dist/{impact-DvgBjupx.js → impact-DZt2oBCP.js} +15 -1
  23. package/dist/index.js +2 -2
  24. package/dist/{init-DRKIdpK1.js → init-DIWDE35F.js} +13 -10
  25. package/dist/jsdom-4IMzv0eE.js +3 -0
  26. package/dist/jsdom-DCpGSLfW.js +81 -0
  27. package/dist/{json-C9xS1PNC.js → json-DjEvy1nX.js} +19 -22
  28. package/dist/json-QQuFIw1W.js +2 -0
  29. package/dist/{load-sCkKsvGQ.js → load-5wRGLvub.js} +3 -9
  30. package/dist/nuxt/index.d.ts +35 -0
  31. package/dist/nuxt/index.js +23 -0
  32. package/dist/{playwright-DSRnXmcd.js → playwright-BWniOain.js} +70 -23
  33. package/dist/{pool-DixLeu8L.js → pool-BMevaLWD.js} +4 -12
  34. package/dist/{project-CufCqIE2.js → project-CzOnkLH6.js} +14 -21
  35. package/dist/project-MFrXcw1M.js +2 -0
  36. package/dist/remediation-Dtowi2EC.js +321 -0
  37. package/dist/{render-BO0nVrrZ.js → render-DrvXRCEn.js} +8 -16
  38. package/dist/{result-2aZPfM8w.js → result-DoamKFsp.js} +115 -1
  39. package/dist/routes-CmdRUuOs.js +265 -0
  40. package/dist/run-BMASMmwO.d.ts +45 -0
  41. package/dist/run-DB34BSOZ.js +53 -0
  42. package/dist/{sarif-eSCuI0eX.js → sarif-SR3_lLYd.js} +22 -36
  43. package/dist/{schema-CMZ8ItGk.js → schema-is6CGX2D.js} +4 -1
  44. package/dist/text-CKKpzkYM.js +43 -0
  45. package/dist/vite/index.d.ts +39 -0
  46. package/dist/vite/index.js +40 -0
  47. package/dist/webpack/index.d.ts +33 -0
  48. package/dist/webpack/index.js +20 -0
  49. package/package.json +36 -9
  50. package/dist/audit-B2dIKpJ5.js +0 -2
  51. package/dist/audit-DXpKkXsC.js +0 -950
  52. package/dist/escape-Dm1o_RAk.js +0 -21
  53. package/dist/html-BLEuzep6.js +0 -337
  54. package/dist/jsdom-BEu6Ra_2.js +0 -163
  55. package/dist/jsdom-C6dIyaxN.js +0 -3
  56. package/dist/routes-BxbSZKXC.js +0 -123
@@ -0,0 +1,134 @@
1
+ import { i as toPosix, t as exists } from "./fs-BmPtmFke.js";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { glob } from "tinyglobby";
5
+ //#region src/audit/collect.ts
6
+ /** Every HTML document a static build is expected to emit. */
7
+ const DEFAULT_INCLUDE = ["**/*.html", "**/*.htm"];
8
+ /** Vendored and tooling directories are never part of the shipped site. */
9
+ const DEFAULT_EXCLUDE = ["**/node_modules/**", "**/.git/**"];
10
+ /** Number of files read in parallel; keeps large builds under the fd limit. */
11
+ const READ_CONCURRENCY = 24;
12
+ /** A UTF-8 byte-order mark is not markup, and jsdom treats it as text. */
13
+ function stripBom(html) {
14
+ return html.charCodeAt(0) === 65279 ? html.slice(1) : html;
15
+ }
16
+ /**
17
+ * Thrown when the build directory itself is unusable. A missing or wrong
18
+ * `dist/` is a user mistake worth reporting loudly, unlike a directory that
19
+ * simply holds no HTML.
20
+ */
21
+ var BuildDirectoryError = class extends Error {
22
+ dir;
23
+ name = "BuildDirectoryError";
24
+ constructor(message, dir) {
25
+ super(message);
26
+ this.dir = dir;
27
+ }
28
+ };
29
+ /**
30
+ * Glob HTML files out of a build directory and read them.
31
+ *
32
+ * Returns pages sorted by relative path so reports and snapshots are stable
33
+ * across platforms. An empty array means "no HTML found" — the caller decides
34
+ * whether that is an error.
35
+ */
36
+ async function collectPages(dir, options = {}) {
37
+ const root = path.resolve(dir);
38
+ await assertDirectory(root, dir);
39
+ const relativePaths = (await glob(options.include ?? DEFAULT_INCLUDE, {
40
+ cwd: root,
41
+ ignore: options.exclude ?? DEFAULT_EXCLUDE,
42
+ onlyFiles: true,
43
+ dot: false,
44
+ absolute: false
45
+ })).map(toPosix).sort();
46
+ const pages = [];
47
+ for (let i = 0; i < relativePaths.length; i += READ_CONCURRENCY) {
48
+ const batch = relativePaths.slice(i, i + READ_CONCURRENCY);
49
+ const read = await Promise.all(batch.map((relativePath) => readPage(root, relativePath, options.onUnreadable)));
50
+ for (const page of read) if (page !== void 0) pages.push(page);
51
+ }
52
+ return pages;
53
+ }
54
+ async function assertDirectory(root, original) {
55
+ let stats;
56
+ try {
57
+ stats = await stat(root);
58
+ } catch (cause) {
59
+ if (cause.code === "ENOENT") throw new BuildDirectoryError(`Build directory not found: ${original}`, root);
60
+ throw new BuildDirectoryError(`Build directory is not readable: ${original} (${cause.message})`, root);
61
+ }
62
+ if (!stats.isDirectory()) throw new BuildDirectoryError(`Build path is not a directory: ${original}`, root);
63
+ }
64
+ async function readPage(root, relativePath, onUnreadable) {
65
+ const absolutePath = path.join(root, relativePath);
66
+ try {
67
+ return {
68
+ absolutePath,
69
+ relativePath,
70
+ html: stripBom(await readFile(absolutePath, "utf8"))
71
+ };
72
+ } catch (cause) {
73
+ if (onUnreadable === void 0) throw cause;
74
+ onUnreadable(relativePath, cause.message);
75
+ return;
76
+ }
77
+ }
78
+ /**
79
+ * What to suggest when a directory holds no HTML, or is not there at all.
80
+ *
81
+ * Nearly always the wrong directory rather than a site with no pages, and the
82
+ * commonest way to arrive is a framework whose build emits no browsable HTML —
83
+ * so rather than repeating "check the path", this works out what the project is
84
+ * and names the next step for it. Where static output is not possible at all,
85
+ * that step is `--url` rather than advice that cannot apply.
86
+ */
87
+ async function emptyDirectoryHint(dir, cwd = process.cwd()) {
88
+ const head = await exists(dir, cwd) ? `${dir} holds no HTML files.` : `${dir} does not exist.`;
89
+ const { detectFramework } = await import("./frameworks-etFg_O8K.js");
90
+ const { readPackageJson } = await import("./project-MFrXcw1M.js");
91
+ const detected = await detectFramework(cwd, await readPackageJson(cwd));
92
+ if (detected !== void 0) return frameworkAdvice(head, detected, cwd, dir);
93
+ return await siblingAdvice(head, cwd, dir) ?? generic(head);
94
+ }
95
+ /** Advice built from what the registry knows about this framework. */
96
+ async function frameworkAdvice(head, detected, cwd, dir) {
97
+ const { framework, outputs } = detected;
98
+ const given = dir.replace(/^\.\//, "");
99
+ for (const output of outputs) if (output !== given && await exists(output, cwd)) return `${head} ${framework.name} writes to ${output}/, not ${dir}.\n Try: eaa-kit audit ./${output}`;
100
+ const lines = [`${head} This is ${article(framework.name)} ${framework.name} project.`];
101
+ if (outputs.length === 0) {
102
+ lines.push(` It renders every page on a server and writes no HTML to disk, so there is`, ` no build directory to audit. Start it, then audit what it serves:`, ...framework.serveCommand === void 0 ? [] : [` ${framework.serveCommand}`], " eaa-kit audit --url http://localhost:8000");
103
+ return lines.join("\n");
104
+ }
105
+ if (framework.staticOutput !== void 0) lines.push(` Static output takes ${framework.staticOutput.needs} — ${framework.staticOutput.how},`, ` then: eaa-kit audit ./${outputs[0]}`);
106
+ else if (outputs.length === 1 && outputs[0] === given) lines.push(` That is where it writes, so the build has not run yet — or it failed.`);
107
+ else lines.push(` It writes to ${outputs.map((output) => `${output}/`).join(" or ")}. Run your build, then:`, ` eaa-kit audit ./${outputs[0]}`);
108
+ if (framework.serves) lines.push(" A site that renders on a server cannot be written to disk at all. Audit it", " running instead:", " eaa-kit audit --url http://localhost:3000");
109
+ return lines.join("\n");
110
+ }
111
+ /** Naming a build directory that is actually there beats listing the usual ones. */
112
+ async function siblingAdvice(head, cwd, dir) {
113
+ const { FALLBACK_OUTPUTS } = await import("./frameworks-etFg_O8K.js");
114
+ const given = dir.replace(/^\.\//, "");
115
+ const others = (await Promise.all(FALLBACK_OUTPUTS.map(async (name) => name !== given && await exists(name, cwd) ? name : void 0))).filter((name) => name !== void 0);
116
+ if (others.length === 0) return void 0;
117
+ return `${head} This project also has ${others.map((name) => `${name}/`).join(", ")} — try one of those.`;
118
+ }
119
+ function generic(head) {
120
+ return `${head} Point eaa-kit at the directory your build fills with .html files —\n commonly dist/, build/, out/ or _site/, depending on the builder.
121
+ If your site renders on a server and never writes HTML, audit it running:
122
+ eaa-kit audit --url http://localhost:3000`;
123
+ }
124
+ /**
125
+ * "a Next.js project", "an Eleventy project".
126
+ *
127
+ * By letter rather than by sound, which is wrong for words like "hour" and
128
+ * right for every framework name in the registry.
129
+ */
130
+ function article(name) {
131
+ return /^[aeiou]/i.test(name) ? "an" : "a";
132
+ }
133
+ //#endregion
134
+ export { stripBom as i, collectPages as n, emptyDirectoryHint as r, BuildDirectoryError as t };
@@ -0,0 +1,77 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import pc from "picocolors";
4
+ //#region src/cli/command.ts
5
+ /**
6
+ * What every command does around the audit itself: say what is happening, run
7
+ * the engine, and put the document somewhere.
8
+ *
9
+ * `audit` and `baseline` are siblings — one reports what a run found and the
10
+ * other writes it down — so they take the same flags, choose between the same
11
+ * two engines and fail on the same setup problems. Keeping that in one place is
12
+ * what stops the two commands drifting into disagreeing about what a run is.
13
+ */
14
+ /** Progress and diagnostics go to stderr, so the report can be piped away. */
15
+ function note(message) {
16
+ process.stderr.write(pc.dim(`${message}\n`));
17
+ }
18
+ function warn(message) {
19
+ process.stderr.write(`${pc.yellow("warning")} ${message}\n`);
20
+ }
21
+ function fail(message) {
22
+ process.stderr.write(`${pc.red("error")} ${message}\n`);
23
+ }
24
+ /**
25
+ * Advice the reader should not miss, without the `warning` prefix: nothing has
26
+ * gone wrong, but what happens next is theirs to get right.
27
+ */
28
+ function advise(message) {
29
+ process.stderr.write(pc.yellow(`${message}\n`));
30
+ }
31
+ /**
32
+ * Audit the pages with whichever engine was asked for.
33
+ *
34
+ * Returns undefined when the browser was asked for and is not usable, having
35
+ * already said so: Playwright missing is a setup problem with a specific fix,
36
+ * not a crash, and both commands turn it into exit 2.
37
+ */
38
+ async function runEngine(pages, options) {
39
+ const runnerOptions = {
40
+ cwd: options.cwd,
41
+ ...options.baseUrl === void 0 ? {} : { baseUrl: options.baseUrl },
42
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
43
+ };
44
+ if (!options.browser) {
45
+ const { runPooledAudit } = await import("./pool-BMevaLWD.js");
46
+ return runPooledAudit(pages, {
47
+ ...runnerOptions,
48
+ ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
49
+ });
50
+ }
51
+ const { BrowserUnavailableError, runBrowserAudit } = await import("./playwright-BWniOain.js");
52
+ try {
53
+ return await runBrowserAudit(options.directory, pages, runnerOptions);
54
+ } catch (cause) {
55
+ if (cause instanceof BrowserUnavailableError) {
56
+ fail(cause.message);
57
+ return;
58
+ }
59
+ throw cause;
60
+ }
61
+ }
62
+ /**
63
+ * Write a document to `output`, or to stdout when there is none. Parent
64
+ * directories are created, since a report path in CI usually names one that is
65
+ * not there yet.
66
+ */
67
+ async function emitDocument(body, output, cwd) {
68
+ if (output === void 0) {
69
+ process.stdout.write(body);
70
+ return;
71
+ }
72
+ const target = path.resolve(cwd, output);
73
+ await mkdir(path.dirname(target), { recursive: true });
74
+ await writeFile(target, body, "utf8");
75
+ }
76
+ //#endregion
77
+ export { runEngine as a, note as i, emitDocument as n, warn as o, fail as r, advise as t };
@@ -0,0 +1,146 @@
1
+ import { i as toPosix } from "./fs-BmPtmFke.js";
2
+ import { t as collapse } from "./text-CKKpzkYM.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
+ * The position comes free. The search already has to find the literal to know
101
+ * the file contains it, so recording where it found it costs one more scan of
102
+ * one string — and a report that says `Header.astro:12` opens an editor where a
103
+ * report that says `Header.astro` starts a search.
104
+ */
105
+ function componentFor(index, html) {
106
+ for (const term of searchTermsFor(html)) {
107
+ const matches = [];
108
+ for (const [file, source] of index.files) {
109
+ const offset = source.indexOf(term);
110
+ if (offset !== -1) {
111
+ matches.push({
112
+ file,
113
+ offset
114
+ });
115
+ if (matches.length > 1) break;
116
+ }
117
+ }
118
+ const only = matches.length === 1 ? matches[0] : void 0;
119
+ if (only !== void 0) {
120
+ const source = index.files.get(only.file);
121
+ return {
122
+ file: only.file,
123
+ ...positionOf(source, only.offset)
124
+ };
125
+ }
126
+ }
127
+ }
128
+ /** 1-based line and column of a byte offset in a source file. */
129
+ function positionOf(source, offset) {
130
+ let line = 1;
131
+ let lineStart = 0;
132
+ for (let i = 0; i < offset; i += 1) if (source[i] === "\n") {
133
+ line += 1;
134
+ lineStart = i + 1;
135
+ }
136
+ return {
137
+ line,
138
+ column: offset - lineStart + 1
139
+ };
140
+ }
141
+ /** `components/Header.astro:12`, as both reports name a component. */
142
+ function componentPath(location) {
143
+ return `${location.file}:${location.line}`;
144
+ }
145
+ //#endregion
146
+ export { searchTermsFor as i, componentFor as n, componentPath as r, buildComponentIndex as t };
@@ -0,0 +1,2 @@
1
+ import { n as componentFor, t as buildComponentIndex } from "./component-C3GL1Mnu.js";
2
+ export { buildComponentIndex, componentFor };