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,81 @@
1
+ import { c as runOptions, i as failedPage, l as shapeResults, o as pageUrl, r as blindRulesInScope, t as DEFAULT_TAGS } from "./result-DoamKFsp.js";
2
+ import axe from "axe-core";
3
+ import { Script } from "node:vm";
4
+ import { JSDOM, VirtualConsole } from "jsdom";
5
+ //#region src/audit/runners/jsdom.ts
6
+ /** Per-page ceiling; one pathological document must not stall a CI run. */
7
+ const DEFAULT_TIMEOUT_MS = 3e4;
8
+ /**
9
+ * Audit collected pages with axe-core inside jsdom.
10
+ *
11
+ * Pages are processed sequentially: jsdom parsing and axe-core are both
12
+ * CPU-bound on the main thread, so concurrency buys nothing here. A page that
13
+ * throws or times out is recorded with an `error` and the run continues.
14
+ */
15
+ async function runJsdomAudit(pages, options = {}) {
16
+ const audits = [];
17
+ for (const page of pages) audits.push(await auditPage(page, options));
18
+ return audits;
19
+ }
20
+ async function auditPage(page, options = {}) {
21
+ const tags = options.tags ?? DEFAULT_TAGS;
22
+ const url = pageUrl(page, options.baseUrl);
23
+ const startedAt = Date.now();
24
+ const identity = {
25
+ relativePath: page.relativePath,
26
+ absolutePath: page.absolutePath,
27
+ url,
28
+ engine: "jsdom"
29
+ };
30
+ let dom;
31
+ try {
32
+ dom = createDom(page.html, url);
33
+ injectAxe(dom);
34
+ const { axe: pageAxe } = dom.window;
35
+ const results = await withTimeout(pageAxe.run(dom.window.document, runOptions(tags)), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
36
+ return shapeResults(results, {
37
+ ...identity,
38
+ durationMs: Date.now() - startedAt,
39
+ blind: blindRulesInScope(tags)
40
+ });
41
+ } catch (cause) {
42
+ return failedPage({
43
+ ...identity,
44
+ durationMs: Date.now() - startedAt
45
+ }, cause instanceof Error ? cause.message : String(cause));
46
+ } finally {
47
+ dom?.window.close();
48
+ }
49
+ }
50
+ function createDom(html, url) {
51
+ const virtualConsole = new VirtualConsole();
52
+ virtualConsole.on("jsdomError", () => {});
53
+ return new JSDOM(html, {
54
+ url,
55
+ virtualConsole,
56
+ runScripts: "outside-only",
57
+ pretendToBeVisual: true
58
+ });
59
+ }
60
+ /**
61
+ * axe-core is 1.3 MB of source. Compiling it once and re-running the compiled
62
+ * script in each window's context avoids re-parsing it for every page.
63
+ */
64
+ let axeScript;
65
+ function injectAxe(dom) {
66
+ axeScript ??= new Script(axe.source, { filename: "axe-core.js" });
67
+ axeScript.runInContext(dom.getInternalVMContext());
68
+ }
69
+ async function withTimeout(promise, ms) {
70
+ promise.catch(() => {});
71
+ let timer;
72
+ try {
73
+ return await Promise.race([promise, new Promise((_resolve, reject) => {
74
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`axe-core timed out after ${ms}ms`)), ms);
75
+ })]);
76
+ } finally {
77
+ clearTimeout(timer);
78
+ }
79
+ }
80
+ //#endregion
81
+ export { runJsdomAudit as n, auditPage as t };
@@ -1,5 +1,8 @@
1
- import { i as isImpactLevel, r as countAtOrAbove } from "./impact-DvgBjupx.js";
1
+ import { i as impactLabel, o as isImpactLevel, r as countAtOrAbove } from "./impact-DZt2oBCP.js";
2
2
  import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
3
+ import { t as elementFingerprint } from "./fingerprint-DRoneAjj.js";
4
+ import { s as ruleOutcomes } from "./result-DoamKFsp.js";
5
+ import { t as buildCoverage } from "./coverage-B4IBKMO8.js";
3
6
  import axe from "axe-core";
4
7
  /**
5
8
  * Build the machine-readable report.
@@ -29,6 +32,8 @@ function buildJsonReport(audits, options) {
29
32
  baseUrl: options.baseUrl ?? null
30
33
  },
31
34
  summary: buildSummary(audits, options.failOn),
35
+ completeness: options.completeness,
36
+ coverage: buildCoverage(audits),
32
37
  rules: buildRuleIndex(audits),
33
38
  pages: audits.map(toJsonPage)
34
39
  };
@@ -36,23 +41,14 @@ function buildJsonReport(audits, options) {
36
41
  /** Every rule mentioned by any page, keyed by id and sorted for stable diffs. */
37
42
  function buildRuleIndex(audits) {
38
43
  const index = /* @__PURE__ */ new Map();
39
- for (const audit of audits) {
40
- const outcomes = [
41
- ...audit.violations,
42
- ...audit.accepted ?? [],
43
- ...audit.incomplete,
44
- ...audit.passes,
45
- ...audit.inapplicable
46
- ];
47
- for (const outcome of outcomes) {
48
- if (index.has(outcome.ruleId)) continue;
49
- index.set(outcome.ruleId, {
50
- help: outcome.help,
51
- helpUrl: outcome.helpUrl,
52
- successCriteria: outcome.successCriteria,
53
- en301549: outcome.enClauses
54
- });
55
- }
44
+ for (const audit of audits) for (const outcome of ruleOutcomes(audit)) {
45
+ if (index.has(outcome.ruleId)) continue;
46
+ index.set(outcome.ruleId, {
47
+ help: outcome.help,
48
+ helpUrl: outcome.helpUrl,
49
+ successCriteria: outcome.successCriteria,
50
+ en301549: outcome.enClauses
51
+ });
56
52
  }
57
53
  return Object.fromEntries([...index].sort(([a], [b]) => a.localeCompare(b)));
58
54
  }
@@ -60,6 +56,7 @@ function buildRuleIndex(audits) {
60
56
  function serialiseJsonReport(report) {
61
57
  return `${JSON.stringify(report, null, 2)}\n`;
62
58
  }
59
+ /** The run's tally, exported so the HTML report reads the same numbers. */
63
60
  function buildSummary(audits, failOn) {
64
61
  const byImpact = {
65
62
  critical: 0,
@@ -80,8 +77,7 @@ function buildSummary(audits, failOn) {
80
77
  for (const finding of audit.violations) {
81
78
  violations += 1;
82
79
  violatingElements += finding.nodes.length;
83
- const impact = finding.impact;
84
- byImpact[impact && isImpactLevel(impact) ? impact : "unclassified"] += 1;
80
+ byImpact[impactLabel(finding.impact)] += 1;
85
81
  }
86
82
  for (const finding of audit.incomplete) if (finding.reason === "engine-limitation") notEvaluated += 1;
87
83
  else needsReview += 1;
@@ -123,7 +119,8 @@ function toJsonFinding(finding) {
123
119
  nodes: finding.nodes.map((node) => ({
124
120
  html: node.html,
125
121
  target: node.target,
126
- failureSummary: node.failureSummary ?? null
122
+ failureSummary: node.failureSummary ?? null,
123
+ fingerprint: elementFingerprint(finding.ruleId, node.target.join(" "), node.html)
127
124
  }))
128
125
  };
129
126
  }
@@ -138,4 +135,4 @@ function byRuleId(a, b) {
138
135
  return a.ruleId.localeCompare(b.ruleId);
139
136
  }
140
137
  //#endregion
141
- export { buildJsonReport, serialiseJsonReport };
138
+ export { buildSummary as n, serialiseJsonReport as r, buildJsonReport as t };
@@ -0,0 +1,2 @@
1
+ import { r as serialiseJsonReport, t as buildJsonReport } from "./json-DjEvy1nX.js";
2
+ export { buildJsonReport, serialiseJsonReport };
@@ -1,5 +1,6 @@
1
- import { _ as withDefault, c as object, f as safeParse, g as url, h as union, i as isoDate, l as optional, m as transform, n as email, p as string, r as enumeration, t as array, u as pipe } from "./schema-CMZ8ItGk.js";
2
- import { readFile, stat } from "node:fs/promises";
1
+ import { _ as url, a as isoDate, d as pipe, g as union, h as transform, i as enumeration, l as object, m as string, p as safeParse, r as email, t as array, u as optional, v as withDefault } from "./schema-is6CGX2D.js";
2
+ import { r as isFile } from "./fs-BmPtmFke.js";
3
+ import { readFile } from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { pathToFileURL } from "node:url";
5
6
  //#region src/config/define.ts
@@ -181,12 +182,5 @@ async function importModule(file) {
181
182
  if (module.default === void 0) throw new ConfigError(`${path.basename(file)} has no default export`, ["Expected: export default defineConfig({ … })"]);
182
183
  return module.default;
183
184
  }
184
- async function isFile(candidate) {
185
- try {
186
- return (await stat(candidate)).isFile();
187
- } catch {
188
- return false;
189
- }
190
- }
191
185
  //#endregion
192
186
  export { COMPLIANCE_STATUSES as a, ISSUE_REASONS as c, defineConfig as d, parseConfig as f, ASSESSMENT_METHODS as i, STATEMENT_LOCALES as l, findConfigFile as n, COUNTRIES as o, loadConfig as r, ConfigError as s, CONFIG_FILENAMES as t, configSchema as u };
@@ -0,0 +1,35 @@
1
+ import { n as IntegrationOptions, t as BuildAuditError } from "../run-BMASMmwO.js";
2
+ //#region src/nuxt/index.d.ts
3
+ /** The part of the Nitro instance this reads, handed over by `nitro:init`. */
4
+ interface NitroLike {
5
+ options: {
6
+ output?: {
7
+ publicDir?: string;
8
+ };
9
+ /** True for `nuxt generate`; absent for a server build. */
10
+ static?: boolean;
11
+ };
12
+ }
13
+ interface NuxtOptionsLike {
14
+ rootDir?: string;
15
+ }
16
+ interface NuxtLike {
17
+ options: NuxtOptionsLike;
18
+ hook(name: 'nitro:init', handler: (nitro: NitroLike) => void): void;
19
+ hook(name: 'close', handler: () => Promise<void>): void;
20
+ }
21
+ interface EaaKitNuxtOptions extends IntegrationOptions {
22
+ /** Directory to audit. Defaults to the one Nitro says it wrote. */
23
+ directory?: string;
24
+ /**
25
+ * Let a server build pass without auditing anything.
26
+ *
27
+ * Off by default: `nuxt build` writes no browsable HTML, and a silent pass
28
+ * over a directory with no pages in it is indistinguishable from a clean
29
+ * site.
30
+ */
31
+ allowServerBuild?: boolean;
32
+ }
33
+ declare function eaaKitModule(options?: EaaKitNuxtOptions, nuxt?: NuxtLike): void;
34
+ //#endregion
35
+ export { BuildAuditError, EaaKitNuxtOptions, NitroLike, NuxtLike, NuxtOptionsLike, eaaKitModule as default };
@@ -0,0 +1,23 @@
1
+ import { n as auditBuild, r as stderrLogger, t as BuildAuditError } from "../run-DB34BSOZ.js";
2
+ import path from "node:path";
3
+ //#region src/nuxt/index.ts
4
+ function eaaKitModule(options = {}, nuxt) {
5
+ if (nuxt === void 0) return;
6
+ let publicDir;
7
+ let prerendered = false;
8
+ nuxt.hook("nitro:init", (nitro) => {
9
+ publicDir = nitro.options.output?.publicDir;
10
+ prerendered = nitro.options.static === true;
11
+ });
12
+ nuxt.hook("close", async () => {
13
+ const directory = options.directory ?? publicDir;
14
+ if (options.directory === void 0 && !prerendered) {
15
+ if (options.allowServerBuild === true) return;
16
+ throw new BuildAuditError("eaa-kit: this was a server build, so no pages were written to disk and there was nothing to audit. Run `nuxt generate` to prerender them, audit the running site with `eaa-kit audit --url`, or set allowServerBuild to skip this.");
17
+ }
18
+ if (directory === void 0) throw new BuildAuditError("eaa-kit: Nitro reported no public directory, so there is no path to audit. Pass `directory` to the module to name one.");
19
+ await auditBuild(path.resolve(nuxt.options.rootDir ?? process.cwd(), directory), options, stderrLogger());
20
+ });
21
+ }
22
+ //#endregion
23
+ export { BuildAuditError, eaaKitModule as default };
@@ -1,4 +1,5 @@
1
- import { i as shapeResults, n as failedPage, r as runOptions, t as DEFAULT_TAGS } from "./result-2aZPfM8w.js";
1
+ import { r as isFile } from "./fs-BmPtmFke.js";
2
+ import { c as runOptions, i as failedPage, l as shapeResults, o as pageUrl, t as DEFAULT_TAGS } from "./result-DoamKFsp.js";
2
3
  import { createRequire } from "node:module";
3
4
  import { stat } from "node:fs/promises";
4
5
  import path from "node:path";
@@ -74,7 +75,7 @@ async function handle(root, requestUrl, response) {
74
75
  }
75
76
  try {
76
77
  const target = (await stat(file)).isDirectory() ? path.join(file, "index.html") : file;
77
- if (!await isReadableFile(target)) {
78
+ if (!await isFile(target)) {
78
79
  response.writeHead(404).end();
79
80
  return;
80
81
  }
@@ -88,13 +89,6 @@ async function handle(root, requestUrl, response) {
88
89
  response.writeHead(404).end();
89
90
  }
90
91
  }
91
- async function isReadableFile(candidate) {
92
- try {
93
- return (await stat(candidate)).isFile();
94
- } catch {
95
- return false;
96
- }
97
- }
98
92
  /** Resolves a request path inside the root, or nothing if it escapes it. */
99
93
  function resolveFile(root, requestUrl) {
100
94
  const { pathname } = new URL(requestUrl, "http://127.0.0.1");
@@ -151,7 +145,7 @@ async function runBrowserAudit(directory, pages, options = {}) {
151
145
  browser = await chromium.launch({ headless: true });
152
146
  } catch (cause) {
153
147
  await server?.close();
154
- throw cause;
148
+ throw launchFailure(cause);
155
149
  }
156
150
  try {
157
151
  const context = await browser.newContext({
@@ -176,7 +170,7 @@ async function auditOne(context, origin, page, options) {
176
170
  const identity = {
177
171
  relativePath: page.relativePath,
178
172
  absolutePath: page.absolutePath,
179
- url: reportedUrl(page, options.baseUrl),
173
+ url: pageUrl(page, options.baseUrl),
180
174
  engine: "browser"
181
175
  };
182
176
  const tab = await context.newPage();
@@ -218,9 +212,27 @@ async function auditOne(context, origin, page, options) {
218
212
  function servedUrl(origin, relativePath) {
219
213
  return `${origin}/${relativePath.split("/").map(encodeURIComponent).join("/")}`;
220
214
  }
221
- function reportedUrl(page, baseUrl) {
222
- if (!baseUrl) return pathToFileURL(page.absolutePath).href;
223
- return new URL(page.relativePath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).href;
215
+ /**
216
+ * A launch that failed because the browser was never downloaded, told as setup.
217
+ *
218
+ * The third setup failure, and the only one that used to arrive as a crash.
219
+ * `npm i -D playwright` does not fetch Chromium, so somebody who followed the
220
+ * install line exactly still lands here — and what they saw was Playwright's
221
+ * message boxed in ASCII, wrapped in a stack trace through this package's
222
+ * bundled internals. That reads as eaa-kit falling over, not as a step left to
223
+ * run, and the two want very different things from the reader.
224
+ *
225
+ * Anything else is passed through untouched: a launch can fail for reasons
226
+ * that really are bugs, and dressing those up as setup advice would send
227
+ * somebody off installing a browser they already have.
228
+ */
229
+ function launchFailure(cause) {
230
+ if (!(cause instanceof Error)) return cause;
231
+ const missing = /Executable doesn't exist at (.+)/.exec(cause.message);
232
+ if (missing === null) return cause;
233
+ return new BrowserUnavailableError(`Playwright is installed, but the Chromium it drives is not.
234
+ Download it with: npx playwright install chromium
235
+ It was looked for at: ${missing[1]?.trim() ?? "an unreported path"}`);
224
236
  }
225
237
  /**
226
238
  * Playwright is an optional peer dependency: the browserless path must never
@@ -229,18 +241,53 @@ function reportedUrl(page, baseUrl) {
229
241
  * problems with different fixes.
230
242
  */
231
243
  async function loadChromium(cwd = process.cwd()) {
232
- let module;
244
+ const tried = [];
245
+ const specifiers = ["playwright", "@playwright/test"];
246
+ const roots = [cwd, void 0];
247
+ for (const root of roots) for (const specifier of specifiers) {
248
+ const loaded = await tryLoad(specifier, root);
249
+ if (loaded === void 0) continue;
250
+ tried.push(`${specifier}${root === void 0 ? " (from eaa-kit)" : ""}`);
251
+ const chromium = launcherIn(loaded);
252
+ if (chromium !== void 0) return chromium;
253
+ }
254
+ if (tried.length === 0) throw new BrowserUnavailableError("Browser mode needs Playwright, which is an optional peer dependency.\n Install it with: npm i -D playwright\n Then the browser: npx playwright install chromium");
255
+ throw new BrowserUnavailableError(`Found ${tried.join(" and ")}, but no chromium launcher on it.\n This usually means an incomplete or mismatched install. Try:
256
+ npm i -D playwright@latest
257
+ npx playwright install chromium --force`);
258
+ }
259
+ /** Import a specifier, resolved from `root` when given. Undefined if absent. */
260
+ async function tryLoad(specifier, root) {
233
261
  try {
234
- const resolved = createRequire(pathToFileURL(path.join(cwd, "package.json")).href).resolve("playwright");
235
- module = await import(pathToFileURL(resolved).href);
236
- } catch {}
237
- if (module === void 0) try {
238
- module = await import("playwright");
262
+ if (root === void 0) return await import(specifier);
263
+ const require = createRequire(pathToFileURL(path.join(root, "package.json")).href);
264
+ return await import(pathToFileURL(require.resolve(specifier)).href);
239
265
  } catch {
240
- throw new BrowserUnavailableError("Browser mode needs Playwright, which is an optional peer dependency.\n Install it with: npm i -D playwright\n Then the browser: npx playwright install chromium");
266
+ return;
267
+ }
268
+ }
269
+ /**
270
+ * The chromium launcher, whichever shape the module came back in.
271
+ *
272
+ * Playwright is CommonJS. Whether `await import()` of it exposes named exports
273
+ * depends on Node's static analysis of the file succeeding, which is not
274
+ * guaranteed and differs between versions; when it does not, everything is on
275
+ * `default` instead. Reading only the named export produced "installed but
276
+ * exports no chromium launcher" against a perfectly good install.
277
+ *
278
+ * Exported for its own tests. Driving this through a real import under vitest
279
+ * proves nothing: vitest hands CommonJS back through an interop proxy that
280
+ * answers `.chromium` whether or not Node hoisted it, so both shapes look
281
+ * identical from in there and the test passes with this function broken. The
282
+ * shapes are checked here directly, and the real interop is exercised against
283
+ * real Node by scripts/test-packaged.mjs.
284
+ */
285
+ function launcherIn(module) {
286
+ const candidates = [module, module?.default];
287
+ for (const candidate of candidates) {
288
+ const chromium = candidate?.chromium;
289
+ if (chromium !== void 0 && typeof chromium.launch === "function") return chromium;
241
290
  }
242
- if (!module.chromium) throw new BrowserUnavailableError("Playwright is installed but exports no chromium launcher");
243
- return module.chromium;
244
291
  }
245
292
  //#endregion
246
293
  export { BrowserUnavailableError, runBrowserAudit };
@@ -1,6 +1,5 @@
1
- import { n as failedPage } from "./result-2aZPfM8w.js";
2
- import { stat } from "node:fs/promises";
3
- import { pathToFileURL } from "node:url";
1
+ import { r as isFile } from "./fs-BmPtmFke.js";
2
+ import { i as failedPage, o as pageUrl } from "./result-DoamKFsp.js";
4
3
  import { Worker } from "node:worker_threads";
5
4
  import { availableParallelism } from "node:os";
6
5
  //#region src/audit/runners/pool.ts
@@ -97,7 +96,7 @@ async function runPooledAudit(pages, options = {}) {
97
96
  return runWorkers(pages, runnerOptions, Math.min(workers, pages.length), entry);
98
97
  }
99
98
  async function auditHere(pages, options) {
100
- const { runJsdomAudit } = await import("./jsdom-C6dIyaxN.js");
99
+ const { runJsdomAudit } = await import("./jsdom-4IMzv0eE.js");
101
100
  return runJsdomAudit(pages, options);
102
101
  }
103
102
  async function runWorkers(pages, options, count, entry) {
@@ -159,11 +158,6 @@ function identity(page, options) {
159
158
  durationMs: 0
160
159
  };
161
160
  }
162
- /** Kept in step with the sequential runner's own URL derivation. */
163
- function pageUrl(page, baseUrl) {
164
- if (!baseUrl) return pathToFileURL(page.absolutePath).href;
165
- return new URL(page.relativePath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).href;
166
- }
167
161
  /**
168
162
  * Locate the worker entry point.
169
163
  *
@@ -180,9 +174,7 @@ async function findWorkerEntry() {
180
174
  new URL("../audit/runners/worker.js", import.meta.url),
181
175
  new URL("./worker.js", import.meta.url)
182
176
  ];
183
- for (const candidate of candidates) try {
184
- if ((await stat(candidate)).isFile()) return candidate;
185
- } catch {}
177
+ for (const candidate of candidates) if (await isFile(candidate)) return candidate;
186
178
  }
187
179
  //#endregion
188
180
  export { plannedWorkers, runPooledAudit };
@@ -1,4 +1,6 @@
1
- import { readFile, stat } from "node:fs/promises";
1
+ import { n as isDirectory, t as exists } from "./fs-BmPtmFke.js";
2
+ import { r as candidateOutputs } from "./frameworks-B4ClIJgE.js";
3
+ import { readFile } from "node:fs/promises";
2
4
  import path from "node:path";
3
5
  import { glob } from "tinyglobby";
4
6
  import { spawn } from "node:child_process";
@@ -17,15 +19,6 @@ import { spawn } from "node:child_process";
17
19
  * project's server if the build produces nothing browsable. Naming a directory
18
20
  * or passing --url skips all of it.
19
21
  */
20
- /** Build output directories, in the order worth trying them. */
21
- const BUILD_DIRECTORIES = [
22
- "dist",
23
- "out",
24
- "build",
25
- "_site",
26
- ".output/public",
27
- "public"
28
- ];
29
22
  /** Ports the common dev and preview servers use, tried if nothing is announced. */
30
23
  const KNOWN_PORTS = [
31
24
  3e3,
@@ -55,10 +48,7 @@ async function detectPackageManager(cwd) {
55
48
  ["pnpm-lock.yaml", "pnpm"],
56
49
  ["yarn.lock", "yarn"],
57
50
  ["bun.lockb", "bun"]
58
- ]) try {
59
- await stat(path.join(cwd, file));
60
- return manager;
61
- } catch {}
51
+ ]) if (await exists(path.join(cwd, file))) return manager;
62
52
  return "npm";
63
53
  }
64
54
  /**
@@ -69,13 +59,10 @@ async function detectPackageManager(cwd) {
69
59
  * assets. What makes a directory the build output is that there is HTML in it.
70
60
  */
71
61
  async function findBuildOutput(cwd) {
72
- for (const candidate of BUILD_DIRECTORIES) {
62
+ const candidates = await candidateOutputs(cwd, await readPackageJson(cwd));
63
+ for (const candidate of candidates) {
73
64
  const directory = path.join(cwd, candidate);
74
- try {
75
- if (!(await stat(directory)).isDirectory()) continue;
76
- } catch {
77
- continue;
78
- }
65
+ if (!await isDirectory(directory)) continue;
79
66
  if ((await glob(["**/*.html", "**/*.htm"], {
80
67
  cwd: directory,
81
68
  ignore: ["**/node_modules/**"],
@@ -239,6 +226,12 @@ async function autoDetectSource(cwd, options = {}) {
239
226
  };
240
227
  }
241
228
  const pkg = await readPackageJson(cwd);
229
+ const { detectFramework } = await import("./frameworks-etFg_O8K.js");
230
+ const detected = await detectFramework(cwd, pkg);
231
+ if (detected !== void 0 && detected.framework.outputs.length === 0) {
232
+ step(`${detected.framework.name} renders on a server and writes no HTML to disk`);
233
+ return { steps };
234
+ }
242
235
  if (pkg === void 0) return void 0;
243
236
  const scripts = pkg.scripts ?? {};
244
237
  if (options.noBuild) return void 0;
@@ -278,4 +271,4 @@ async function autoDetectSource(cwd, options = {}) {
278
271
  };
279
272
  }
280
273
  //#endregion
281
- export { autoDetectSource };
274
+ export { runScript as a, readPackageJson as i, detectPackageManager as n, startServer as o, findBuildOutput as r, autoDetectSource as t };
@@ -0,0 +1,2 @@
1
+ import { i as readPackageJson, t as autoDetectSource } from "./project-CzOnkLH6.js";
2
+ export { autoDetectSource, readPackageJson };