@reddoorla/maintenance 0.10.4 → 0.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -335,7 +335,7 @@ async function securityAudit(ctx) {
335
335
  }
336
336
 
337
337
  // src/audits/lighthouse.ts
338
- import { readFile as readFile4, writeFile, mkdtemp, rm } from "fs/promises";
338
+ import { readFile as readFile4, writeFile, mkdtemp, rm, readdir } from "fs/promises";
339
339
  import { tmpdir } from "os";
340
340
  import { join as join4 } from "path";
341
341
 
@@ -397,6 +397,32 @@ async function readSiteConfig(sitePath) {
397
397
  return out;
398
398
  }
399
399
 
400
+ // src/util/free-port.ts
401
+ import { createServer } from "net";
402
+ async function findFreePort() {
403
+ return new Promise((resolve, reject) => {
404
+ const server = createServer();
405
+ server.unref();
406
+ server.on("error", reject);
407
+ server.listen(0, "127.0.0.1", () => {
408
+ const addr = server.address();
409
+ if (typeof addr === "object" && addr) {
410
+ const port = addr.port;
411
+ server.close(() => resolve(port));
412
+ } else {
413
+ server.close();
414
+ reject(new Error("findFreePort: could not determine assigned port from socket"));
415
+ }
416
+ });
417
+ });
418
+ }
419
+ function withFreePort(url, port) {
420
+ const u = new URL(url);
421
+ u.hostname = "localhost";
422
+ u.port = String(port);
423
+ return u.toString();
424
+ }
425
+
400
426
  // src/audits/lighthouse.ts
401
427
  async function readJsonMaybe(path) {
402
428
  try {
@@ -406,6 +432,21 @@ async function readJsonMaybe(path) {
406
432
  return null;
407
433
  }
408
434
  }
435
+ async function readLhrEntries(resultsDir) {
436
+ const files = await readdir(resultsDir).catch(() => []);
437
+ const entries = [];
438
+ for (const f of files) {
439
+ if (!f.startsWith("lhr-") || !f.endsWith(".json")) continue;
440
+ const lhr = await readJsonMaybe(join4(resultsDir, f));
441
+ if (!lhr || !lhr.categories) continue;
442
+ const summary = {};
443
+ for (const [k, v] of Object.entries(lhr.categories)) {
444
+ if (typeof v?.score === "number") summary[k] = v.score;
445
+ }
446
+ entries.push({ url: lhr.requestedUrl, summary });
447
+ }
448
+ return entries;
449
+ }
409
450
  function averageSummaries(entries) {
410
451
  if (entries.length === 0) return {};
411
452
  const sums = {};
@@ -437,13 +478,19 @@ async function lighthouseAudit(ctx) {
437
478
  const site = ctx.site;
438
479
  const label = siteLabel(site);
439
480
  const siteCfg = await readSiteConfig(site.path);
440
- const resolvedConfig = siteCfg.lighthouseUrl ? {
481
+ const port = await findFreePort();
482
+ const baseUrl = siteCfg.lighthouseUrl ?? lighthouseConfig.ci.collect.url[0];
483
+ const resolvedConfig = {
441
484
  ...lighthouseConfig,
442
485
  ci: {
443
486
  ...lighthouseConfig.ci,
444
- collect: { ...lighthouseConfig.ci.collect, url: [siteCfg.lighthouseUrl] }
487
+ collect: {
488
+ ...lighthouseConfig.ci.collect,
489
+ url: [withFreePort(baseUrl, port)],
490
+ startServerCommand: `npm run vite:dev -- --port ${port} --strictPort`
491
+ }
445
492
  }
446
- } : lighthouseConfig;
493
+ };
447
494
  const configDir = await mkdtemp(join4(tmpdir(), "reddoor-lhci-"));
448
495
  const configPath = join4(configDir, "lighthouserc.json");
449
496
  await writeFile(configPath, JSON.stringify(resolvedConfig), "utf-8");
@@ -473,13 +520,13 @@ async function lighthouseAudit(ctx) {
473
520
  throw err;
474
521
  }
475
522
  await rm(configDir, { recursive: true, force: true });
476
- const manifest = await readJsonMaybe(join4(resultsDir, "manifest.json"));
477
- if (!manifest || manifest.length === 0) {
523
+ const manifest = await readLhrEntries(resultsDir);
524
+ if (manifest.length === 0) {
478
525
  return {
479
526
  audit: "lighthouse",
480
527
  site: label,
481
528
  status: "fail",
482
- summary: `lighthouse: no manifest written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
529
+ summary: `lighthouse: no lhr-*.json written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
483
530
  };
484
531
  }
485
532
  const assertionResults = await readJsonMaybe(join4(resultsDir, "assertion-results.json")) ?? [];
@@ -554,6 +601,37 @@ async function readJsonMaybe2(path) {
554
601
  return null;
555
602
  }
556
603
  }
604
+ function buildPlaywrightConfig(port, sitePath) {
605
+ return `import { defineConfig } from "@playwright/test";
606
+
607
+ export default defineConfig({
608
+ testDir: ".",
609
+ testMatch: /.*\\.spec\\.ts$/,
610
+ fullyParallel: true,
611
+ forbidOnly: !!process.env.CI,
612
+ retries: process.env.CI ? 2 : 0,
613
+ reporter: process.env.CI ? "github" : "list",
614
+ use: {
615
+ baseURL: "http://localhost:${port}",
616
+ trace: "on-first-retry",
617
+ },
618
+ webServer: {
619
+ // --strictPort: refuse to bump to a different port if ours is taken,
620
+ // so the audit fails loudly instead of probing a zombie.
621
+ // reuseExistingServer:false: never reuse \u2014 we control the lifecycle.
622
+ // cwd: playwright's default webServer.cwd is the config file's
623
+ // directory. Our config lives in /tmp so without this override,
624
+ // "npm run vite:dev" tries to read /tmp/.../package.json and
625
+ // ENOENTs before vite ever starts. Caltex 2026-05-28 (0.10.5).
626
+ command: "npm run vite:dev -- --port ${port} --strictPort",
627
+ url: "http://localhost:${port}/dev/a11y-fixtures",
628
+ cwd: ${JSON.stringify(sitePath)},
629
+ reuseExistingServer: false,
630
+ timeout: 120_000,
631
+ },
632
+ });
633
+ `;
634
+ }
557
635
  function buildSpec() {
558
636
  return `import { test, expect } from "@playwright/test";
559
637
  import AxeBuilder from "@axe-core/playwright";
@@ -607,19 +685,26 @@ async function a11yAudit(ctx) {
607
685
  const specDir = await mkdtemp2(join5(tmpdir2(), "reddoor-a11y-spec-"));
608
686
  const specPath = join5(specDir, "a11y.spec.ts");
609
687
  await writeFile2(specPath, buildSpec(), "utf-8");
688
+ const port = await findFreePort();
689
+ const configPath = join5(specDir, "playwright.config.ts");
690
+ await writeFile2(configPath, buildPlaywrightConfig(port, site.path), "utf-8");
610
691
  const resultsPath = join5(site.path, RESULTS_REL);
611
692
  await rm2(join5(site.path, ".reddoor-a11y"), { recursive: true, force: true });
612
693
  let raw;
613
694
  try {
614
- raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=line", specPath], {
615
- cwd: site.path,
616
- env: { ...process.env, REDDOOR_A11Y_OUTPUT: resultsPath },
617
- // playwright on a cold tree downloads Chrome, boots the site's dev
618
- // server, and runs axe over every configured route. The shared 30 s
619
- // default in runAudits is fine for deps/lint/security but starves
620
- // playwright (mirrors the lighthouse fix shipped earlier).
621
- timeoutMs: 5 * 6e4
622
- });
695
+ raw = await spawn2(
696
+ "npx",
697
+ ["--yes", "playwright", "test", `--config=${configPath}`, "--reporter=line", specPath],
698
+ {
699
+ cwd: site.path,
700
+ env: { ...process.env, REDDOOR_A11Y_OUTPUT: resultsPath },
701
+ // playwright on a cold tree downloads Chrome, boots the site's dev
702
+ // server, and runs axe over every configured route. The shared 30 s
703
+ // default in runAudits is fine for deps/lint/security but starves
704
+ // playwright (mirrors the lighthouse fix shipped earlier).
705
+ timeoutMs: 5 * 6e4
706
+ }
707
+ );
623
708
  } catch (err) {
624
709
  await rm2(specDir, { recursive: true, force: true });
625
710
  const e = err;