@reddoorla/maintenance 0.49.0 → 0.50.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.
- package/dist/cli/bin.js +624 -48
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +3 -5
- package/dist/cli/commands/audit.js +434 -14
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.d.ts +84 -2
- package/dist/index.js +532 -32
- package/dist/index.js.map +1 -1
- package/dist/recipes/sync-configs.d.ts +1 -1
- package/dist/{types-DeKpgkG-.d.ts → types-QG-QhCYh.d.ts} +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as Site, a as AuditResult, A as AuditName } from '../../types-
|
|
1
|
+
import { S as Site, a as AuditResult, A as AuditName } from '../../types-QG-QhCYh.js';
|
|
2
2
|
|
|
3
3
|
type AuditCommandOptions = {
|
|
4
4
|
only?: string;
|
|
@@ -32,10 +32,8 @@ declare function parseConcurrency(value: string | undefined): boolean | number;
|
|
|
32
32
|
* null when there's nothing to warn about. Keeps the mixed-provenance result
|
|
33
33
|
* table from silently confusing the operator. */
|
|
34
34
|
declare function deployedUrlNotice(which: AuditName[], url: string | undefined, cwd: string): string | null;
|
|
35
|
-
/** A fleet site needs a local checkout unless every requested audit
|
|
36
|
-
*
|
|
37
|
-
* site is checkout-free exactly when it has a `deployedUrl` and lighthouse is
|
|
38
|
-
* the only requested audit. */
|
|
35
|
+
/** A fleet site needs a local checkout unless every requested audit is checkout-free AND the site
|
|
36
|
+
* has a `deployedUrl` for them to run against. */
|
|
39
37
|
declare function auditNeedsCheckout(site: Site, which: AuditName[]): boolean;
|
|
40
38
|
/** Apply a single-site `--url` to the resolved sites. Returns the input
|
|
41
39
|
* untouched when no url is given; otherwise requires exactly one site and
|
|
@@ -159,6 +159,14 @@ function mapRow(rec) {
|
|
|
159
159
|
securityVulnsHigh: f["Security Vulns High"] ?? null,
|
|
160
160
|
securityVulnsModerate: f["Security Vulns Moderate"] ?? null,
|
|
161
161
|
securityVulnsLow: f["Security Vulns Low"] ?? null,
|
|
162
|
+
lastSecurityAuditAt: f["Last security audit at"] ?? null,
|
|
163
|
+
certDaysRemaining: f["Cert days remaining"] ?? null,
|
|
164
|
+
domainCheckedAt: f["Domain checked at"] ?? null,
|
|
165
|
+
crossbrowserOk: typeof f["Crossbrowser OK"] === "boolean" ? f["Crossbrowser OK"] : null,
|
|
166
|
+
mobileOk: typeof f["Mobile OK"] === "boolean" ? f["Mobile OK"] : null,
|
|
167
|
+
linksOk: typeof f["Links OK"] === "boolean" ? f["Links OK"] : null,
|
|
168
|
+
brokenLinks: typeof f["Broken links"] === "number" ? f["Broken links"] : null,
|
|
169
|
+
browserCheckedAt: f["Browser checked at"] ?? null,
|
|
162
170
|
copyIntro: trimToNull(f["Copy \u2014 Intro"]),
|
|
163
171
|
copyContact: trimToNull(f["Copy \u2014 Contact"]),
|
|
164
172
|
copyFooter: trimToNull(f["Copy \u2014 Footer"]),
|
|
@@ -220,7 +228,24 @@ function securityFields(counts) {
|
|
|
220
228
|
"Security Vulns Critical": counts.critical,
|
|
221
229
|
"Security Vulns High": counts.high,
|
|
222
230
|
"Security Vulns Moderate": counts.moderate,
|
|
223
|
-
"Security Vulns Low": counts.low
|
|
231
|
+
"Security Vulns Low": counts.low,
|
|
232
|
+
// Stamp freshness alongside the counts so the Security Updates auto-tick can require a recent
|
|
233
|
+
// audit (a clean count from months ago must not silently keep ticking the box).
|
|
234
|
+
"Last security audit at": (/* @__PURE__ */ new Date()).toISOString()
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function domainFields(result) {
|
|
238
|
+
const fields = { "Domain checked at": result.checkedAt };
|
|
239
|
+
if (result.certDaysRemaining !== null) fields["Cert days remaining"] = result.certDaysRemaining;
|
|
240
|
+
return fields;
|
|
241
|
+
}
|
|
242
|
+
function browserFields(r) {
|
|
243
|
+
return {
|
|
244
|
+
"Crossbrowser OK": r.desktopOk,
|
|
245
|
+
"Mobile OK": r.mobileOk,
|
|
246
|
+
"Links OK": r.linksOk,
|
|
247
|
+
"Broken links": r.brokenLinks,
|
|
248
|
+
"Browser checked at": r.checkedAt
|
|
224
249
|
};
|
|
225
250
|
}
|
|
226
251
|
async function updateScores(base, recordId, scores) {
|
|
@@ -241,6 +266,8 @@ async function updateAuditFields(base, recordId, audits) {
|
|
|
241
266
|
if (audits.a11y) Object.assign(fields, a11yFields(audits.a11y));
|
|
242
267
|
if (audits.deps) Object.assign(fields, depsFields(audits.deps));
|
|
243
268
|
if (audits.security) Object.assign(fields, securityFields(audits.security));
|
|
269
|
+
if (audits.domain) Object.assign(fields, domainFields(audits.domain));
|
|
270
|
+
if (audits.browser) Object.assign(fields, browserFields(audits.browser));
|
|
244
271
|
await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
|
|
245
272
|
return fields;
|
|
246
273
|
}
|
|
@@ -435,6 +462,53 @@ var init_security_airtable = __esm({
|
|
|
435
462
|
}
|
|
436
463
|
});
|
|
437
464
|
|
|
465
|
+
// src/audits/domain-airtable.ts
|
|
466
|
+
function hasDomainResult(result) {
|
|
467
|
+
if (result.audit !== "domain") return false;
|
|
468
|
+
const d = result.details;
|
|
469
|
+
return !!d && typeof d.checkedAt === "string";
|
|
470
|
+
}
|
|
471
|
+
function domainResultFromAudit(result) {
|
|
472
|
+
if (result.audit !== "domain") {
|
|
473
|
+
throw new Error(`Expected a 'domain' AuditResult, got '${result.audit}'`);
|
|
474
|
+
}
|
|
475
|
+
const d = result.details;
|
|
476
|
+
return {
|
|
477
|
+
certDaysRemaining: typeof d?.certDaysRemaining === "number" ? d.certDaysRemaining : null,
|
|
478
|
+
checkedAt: typeof d?.checkedAt === "string" ? d.checkedAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
var init_domain_airtable = __esm({
|
|
482
|
+
"src/audits/domain-airtable.ts"() {
|
|
483
|
+
"use strict";
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// src/audits/browser-airtable.ts
|
|
488
|
+
function hasBrowserResult(result) {
|
|
489
|
+
if (result.audit !== "browser") return false;
|
|
490
|
+
const d = result.details;
|
|
491
|
+
return !!d && typeof d.checkedAt === "string";
|
|
492
|
+
}
|
|
493
|
+
function browserFieldsFromAudit(result) {
|
|
494
|
+
if (result.audit !== "browser") {
|
|
495
|
+
throw new Error(`Expected a 'browser' AuditResult, got '${result.audit}'`);
|
|
496
|
+
}
|
|
497
|
+
const d = result.details;
|
|
498
|
+
return {
|
|
499
|
+
desktopOk: d?.desktopOk === true,
|
|
500
|
+
mobileOk: d?.mobileOk === true,
|
|
501
|
+
linksOk: d?.linksOk === true,
|
|
502
|
+
brokenLinks: typeof d?.brokenLinks === "number" ? d.brokenLinks : 0,
|
|
503
|
+
checkedAt: typeof d?.checkedAt === "string" ? d.checkedAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
var init_browser_airtable = __esm({
|
|
507
|
+
"src/audits/browser-airtable.ts"() {
|
|
508
|
+
"use strict";
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
438
512
|
// src/audits/write-audits-to-airtable.ts
|
|
439
513
|
var write_audits_to_airtable_exports = {};
|
|
440
514
|
__export(write_audits_to_airtable_exports, {
|
|
@@ -445,22 +519,14 @@ __export(write_audits_to_airtable_exports, {
|
|
|
445
519
|
async function writeAuditsToAirtable(args) {
|
|
446
520
|
const { base, websites, slug, results } = args;
|
|
447
521
|
const lhResult = results.find((r) => r.audit === "lighthouse");
|
|
448
|
-
if (!lhResult) {
|
|
449
|
-
throw Object.assign(
|
|
450
|
-
new Error(
|
|
451
|
-
"--write-airtable requires a lighthouse result; did you pass --only without lighthouse?"
|
|
452
|
-
),
|
|
453
|
-
{ exitCode: 2 }
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
522
|
const target = websites.find((w) => siteSlug(w.name) === slug);
|
|
457
523
|
if (!target) {
|
|
458
524
|
throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
|
|
459
525
|
}
|
|
460
526
|
const writes = [];
|
|
461
527
|
const audits = {};
|
|
462
|
-
const lhHasScores = hasRealScores(lhResult);
|
|
463
|
-
if (lhHasScores) {
|
|
528
|
+
const lhHasScores = lhResult ? hasRealScores(lhResult) : false;
|
|
529
|
+
if (lhResult && lhHasScores) {
|
|
464
530
|
const scores = lighthouseScoresFromResult(lhResult);
|
|
465
531
|
audits.scores = scores;
|
|
466
532
|
writes.push({ audit: "lighthouse", counts: scores });
|
|
@@ -483,10 +549,22 @@ async function writeAuditsToAirtable(args) {
|
|
|
483
549
|
audits.security = counts;
|
|
484
550
|
writes.push({ audit: "security", counts });
|
|
485
551
|
}
|
|
552
|
+
const dom = results.find((r) => r.audit === "domain");
|
|
553
|
+
if (dom && hasDomainResult(dom)) {
|
|
554
|
+
const result = domainResultFromAudit(dom);
|
|
555
|
+
audits.domain = result;
|
|
556
|
+
writes.push({ audit: "domain", counts: result });
|
|
557
|
+
}
|
|
558
|
+
const browser = results.find((r) => r.audit === "browser");
|
|
559
|
+
if (browser && hasBrowserResult(browser)) {
|
|
560
|
+
const fields = browserFieldsFromAudit(browser);
|
|
561
|
+
audits.browser = fields;
|
|
562
|
+
writes.push({ audit: "browser", counts: fields });
|
|
563
|
+
}
|
|
486
564
|
if (Object.keys(audits).length > 0) {
|
|
487
565
|
await updateAuditFields(base, target.id, audits);
|
|
488
566
|
}
|
|
489
|
-
if (!lhHasScores) {
|
|
567
|
+
if (lhResult && !lhHasScores) {
|
|
490
568
|
const persisted = writes.map((w) => w.audit);
|
|
491
569
|
throw Object.assign(
|
|
492
570
|
new Error(
|
|
@@ -537,6 +615,8 @@ var init_write_audits_to_airtable = __esm({
|
|
|
537
615
|
init_a11y_airtable();
|
|
538
616
|
init_deps_airtable();
|
|
539
617
|
init_security_airtable();
|
|
618
|
+
init_domain_airtable();
|
|
619
|
+
init_browser_airtable();
|
|
540
620
|
}
|
|
541
621
|
});
|
|
542
622
|
|
|
@@ -1485,13 +1565,348 @@ async function a11yAudit(ctx) {
|
|
|
1485
1565
|
}
|
|
1486
1566
|
}
|
|
1487
1567
|
|
|
1568
|
+
// src/audits/domain.ts
|
|
1569
|
+
import { promises as dnsPromises } from "dns";
|
|
1570
|
+
import tls from "tls";
|
|
1571
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
1572
|
+
async function checkDomain(url, deps) {
|
|
1573
|
+
let host;
|
|
1574
|
+
try {
|
|
1575
|
+
host = new URL(url).hostname;
|
|
1576
|
+
} catch {
|
|
1577
|
+
return { resolved: false, certDaysRemaining: null };
|
|
1578
|
+
}
|
|
1579
|
+
try {
|
|
1580
|
+
await deps.lookup(host);
|
|
1581
|
+
} catch {
|
|
1582
|
+
return { resolved: false, certDaysRemaining: null };
|
|
1583
|
+
}
|
|
1584
|
+
let validTo;
|
|
1585
|
+
try {
|
|
1586
|
+
validTo = await deps.certValidTo(host);
|
|
1587
|
+
} catch {
|
|
1588
|
+
validTo = null;
|
|
1589
|
+
}
|
|
1590
|
+
if (!validTo || Number.isNaN(validTo.getTime()))
|
|
1591
|
+
return { resolved: true, certDaysRemaining: null };
|
|
1592
|
+
return {
|
|
1593
|
+
resolved: true,
|
|
1594
|
+
certDaysRemaining: Math.floor((validTo.getTime() - deps.now.getTime()) / MS_PER_DAY)
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
function defaultDomainDeps(now) {
|
|
1598
|
+
return {
|
|
1599
|
+
lookup: async (host) => {
|
|
1600
|
+
await dnsPromises.lookup(host);
|
|
1601
|
+
},
|
|
1602
|
+
certValidTo: (host) => new Promise((resolvePromise) => {
|
|
1603
|
+
const socket = tls.connect(
|
|
1604
|
+
{ host, port: 443, servername: host, timeout: 1e4, rejectUnauthorized: true },
|
|
1605
|
+
() => {
|
|
1606
|
+
const cert = socket.authorized ? socket.getPeerCertificate() : null;
|
|
1607
|
+
socket.end();
|
|
1608
|
+
const validTo = cert && cert.valid_to ? new Date(cert.valid_to) : null;
|
|
1609
|
+
resolvePromise(validTo);
|
|
1610
|
+
}
|
|
1611
|
+
);
|
|
1612
|
+
socket.on("error", () => resolvePromise(null));
|
|
1613
|
+
socket.on("timeout", () => {
|
|
1614
|
+
socket.destroy();
|
|
1615
|
+
resolvePromise(null);
|
|
1616
|
+
});
|
|
1617
|
+
}),
|
|
1618
|
+
now
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
async function domainAudit(ctx) {
|
|
1622
|
+
const { site } = ctx;
|
|
1623
|
+
const label = siteLabel(site);
|
|
1624
|
+
if (!site.deployedUrl) {
|
|
1625
|
+
return { audit: "domain", site: label, status: "skip", summary: "no deployed URL" };
|
|
1626
|
+
}
|
|
1627
|
+
const now = ctx.now ?? /* @__PURE__ */ new Date();
|
|
1628
|
+
const deps = ctx.domainDeps ?? defaultDomainDeps(now);
|
|
1629
|
+
const check = await checkDomain(site.deployedUrl, deps);
|
|
1630
|
+
const checkedAt = now.toISOString();
|
|
1631
|
+
const status = check.resolved && check.certDaysRemaining !== null && check.certDaysRemaining > 14 ? "pass" : "warn";
|
|
1632
|
+
const summary = !check.resolved ? "did not resolve" : check.certDaysRemaining === null ? "resolved, no usable TLS cert" : `resolved, cert ${check.certDaysRemaining}d remaining`;
|
|
1633
|
+
return {
|
|
1634
|
+
audit: "domain",
|
|
1635
|
+
site: label,
|
|
1636
|
+
status,
|
|
1637
|
+
summary,
|
|
1638
|
+
details: { resolved: check.resolved, certDaysRemaining: check.certDaysRemaining, checkedAt }
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// src/audits/route-discovery.ts
|
|
1643
|
+
var DEFAULT_CAP = 15;
|
|
1644
|
+
function parseSitemapUrls(xml) {
|
|
1645
|
+
const out = [];
|
|
1646
|
+
const re = /<loc>\s*([^<\s]+)\s*<\/loc>/gi;
|
|
1647
|
+
let m;
|
|
1648
|
+
while ((m = re.exec(xml)) !== null) {
|
|
1649
|
+
const url = m[1];
|
|
1650
|
+
if (url) out.push(url.trim());
|
|
1651
|
+
}
|
|
1652
|
+
return out;
|
|
1653
|
+
}
|
|
1654
|
+
function parseHtmlLinks(html, baseUrl) {
|
|
1655
|
+
const out = /* @__PURE__ */ new Set();
|
|
1656
|
+
const re = /<a\b[^>]*\bhref\s*=\s*["']([^"']+)["']/gi;
|
|
1657
|
+
let m;
|
|
1658
|
+
while ((m = re.exec(html)) !== null) {
|
|
1659
|
+
const href = m[1];
|
|
1660
|
+
if (!href || href.startsWith("#") || /^(mailto:|tel:|javascript:)/i.test(href)) continue;
|
|
1661
|
+
try {
|
|
1662
|
+
const u = new URL(href, baseUrl);
|
|
1663
|
+
if (u.origin !== new URL(baseUrl).origin) continue;
|
|
1664
|
+
out.add(u.pathname);
|
|
1665
|
+
} catch {
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
return [...out];
|
|
1669
|
+
}
|
|
1670
|
+
function family(pathname) {
|
|
1671
|
+
return pathname.split("/").filter(Boolean)[0] ?? "";
|
|
1672
|
+
}
|
|
1673
|
+
function sampleRoutePaths(urlsOrPaths, cap = DEFAULT_CAP) {
|
|
1674
|
+
const seen = /* @__PURE__ */ new Set(["/"]);
|
|
1675
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1676
|
+
for (const raw of urlsOrPaths) {
|
|
1677
|
+
let pathname;
|
|
1678
|
+
try {
|
|
1679
|
+
pathname = raw.startsWith("/") ? new URL(raw, "https://x.invalid").pathname : new URL(raw).pathname;
|
|
1680
|
+
} catch {
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
if (pathname === "/") continue;
|
|
1684
|
+
if (seen.has(pathname)) continue;
|
|
1685
|
+
seen.add(pathname);
|
|
1686
|
+
const fam = family(pathname);
|
|
1687
|
+
const arr = buckets.get(fam) ?? [];
|
|
1688
|
+
arr.push(pathname);
|
|
1689
|
+
buckets.set(fam, arr);
|
|
1690
|
+
}
|
|
1691
|
+
const result = ["/"];
|
|
1692
|
+
const families = [...buckets.values()];
|
|
1693
|
+
let guard = 0;
|
|
1694
|
+
while (result.length < cap && families.some((f) => f.length > 0) && guard++ < 1e4) {
|
|
1695
|
+
for (const fam of families) {
|
|
1696
|
+
if (result.length >= cap) break;
|
|
1697
|
+
const next = fam.shift();
|
|
1698
|
+
if (next) result.push(next);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
return result;
|
|
1702
|
+
}
|
|
1703
|
+
function familyCountsOf(paths) {
|
|
1704
|
+
const counts = {};
|
|
1705
|
+
for (const p of paths) {
|
|
1706
|
+
const key = p === "/" ? "/" : `/${family(p)}`;
|
|
1707
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
1708
|
+
}
|
|
1709
|
+
return counts;
|
|
1710
|
+
}
|
|
1711
|
+
async function discoverRoutes(deployedUrl, deps, cap = DEFAULT_CAP) {
|
|
1712
|
+
const origin = new URL(deployedUrl).origin;
|
|
1713
|
+
const abs = (paths) => paths.map((p) => new URL(p, origin).href);
|
|
1714
|
+
const sitemapXml = await deps.fetchText(new URL("/sitemap.xml", origin).href);
|
|
1715
|
+
if (sitemapXml) {
|
|
1716
|
+
const urls = parseSitemapUrls(sitemapXml);
|
|
1717
|
+
if (urls.length > 0) {
|
|
1718
|
+
const paths = sampleRoutePaths(urls, cap);
|
|
1719
|
+
return { routes: abs(paths), source: "sitemap", familyCounts: familyCountsOf(paths) };
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
const homeHtml = await deps.fetchText(origin);
|
|
1723
|
+
if (homeHtml) {
|
|
1724
|
+
const links = parseHtmlLinks(homeHtml, origin);
|
|
1725
|
+
if (links.length > 0) {
|
|
1726
|
+
const paths = sampleRoutePaths(links, cap);
|
|
1727
|
+
return { routes: abs(paths), source: "homepage-links", familyCounts: familyCountsOf(paths) };
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
return { routes: [new URL("/", origin).href], source: "root-only", familyCounts: { "/": 1 } };
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// src/audits/browser.ts
|
|
1734
|
+
function isBroken(status) {
|
|
1735
|
+
return status === null || status >= 400;
|
|
1736
|
+
}
|
|
1737
|
+
function summarizeBrowser(routes, links, familyCounts) {
|
|
1738
|
+
const desktopChecks = routes.flatMap((r) => r.desktop);
|
|
1739
|
+
const mobileChecks = routes.flatMap((r) => r.mobile);
|
|
1740
|
+
const desktopOk = routes.length > 0 && routes.every((r) => r.desktop.length > 0 && r.desktop.every((d) => d.ok));
|
|
1741
|
+
const mobileOk = routes.length > 0 && routes.every((r) => r.mobile.length > 0 && r.mobile.every((m) => m.ok));
|
|
1742
|
+
const brokenLinks = links.filter((l) => isBroken(l.status)).length;
|
|
1743
|
+
const linksOk = links.length > 0 && brokenLinks === 0;
|
|
1744
|
+
const engines = [...new Set(desktopChecks.map((d) => d.engine))];
|
|
1745
|
+
const devices2 = [...new Set(mobileChecks.map((m) => m.device))];
|
|
1746
|
+
const families = Object.entries(familyCounts).map(([f, n]) => f === "/" ? "/" : `${f} \xD7${n}`).join(", ");
|
|
1747
|
+
const note = `${routes.length} routes (${families}); desktop ${engines.join("/") || "\u2014"}; mobile ${devices2.join("/") || "\u2014"}; ${links.length} links, ${brokenLinks} broken`;
|
|
1748
|
+
return { desktopOk, mobileOk, linksOk, brokenLinks, routesChecked: routes.length, note };
|
|
1749
|
+
}
|
|
1750
|
+
async function browserAudit(ctx) {
|
|
1751
|
+
const { site } = ctx;
|
|
1752
|
+
const label = siteLabel(site);
|
|
1753
|
+
if (!site.deployedUrl) {
|
|
1754
|
+
return { audit: "browser", site: label, status: "skip", summary: "no deployed URL" };
|
|
1755
|
+
}
|
|
1756
|
+
const now = ctx.now ?? /* @__PURE__ */ new Date();
|
|
1757
|
+
const discoverDeps = ctx.discoverDeps ?? defaultDiscoverDeps();
|
|
1758
|
+
const runner = ctx.browserRunner ?? await defaultBrowserRunner();
|
|
1759
|
+
try {
|
|
1760
|
+
const discovered = await discoverRoutes(site.deployedUrl, discoverDeps);
|
|
1761
|
+
const routeResults = await runner.probe(discovered.routes);
|
|
1762
|
+
const internalLinks = [...new Set(routeResults.flatMap((r) => r.links))];
|
|
1763
|
+
const linkResults = await runner.checkLinks(internalLinks);
|
|
1764
|
+
const summary = summarizeBrowser(
|
|
1765
|
+
routeResults,
|
|
1766
|
+
linkResults,
|
|
1767
|
+
discovered.familyCounts ?? familyCountsOf(discovered.routes)
|
|
1768
|
+
);
|
|
1769
|
+
const status = summary.desktopOk && summary.mobileOk && summary.linksOk ? "pass" : "warn";
|
|
1770
|
+
return {
|
|
1771
|
+
audit: "browser",
|
|
1772
|
+
site: label,
|
|
1773
|
+
status,
|
|
1774
|
+
summary: summary.note,
|
|
1775
|
+
details: { ...summary, checkedAt: now.toISOString() }
|
|
1776
|
+
};
|
|
1777
|
+
} finally {
|
|
1778
|
+
await runner.close?.();
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
function defaultDiscoverDeps() {
|
|
1782
|
+
return {
|
|
1783
|
+
fetchText: async (url) => {
|
|
1784
|
+
try {
|
|
1785
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
1786
|
+
if (!res.ok) return null;
|
|
1787
|
+
return await res.text();
|
|
1788
|
+
} catch {
|
|
1789
|
+
return null;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
var DESKTOP_VIEWPORT = { width: 1366, height: 900 };
|
|
1795
|
+
var PAGE_TIMEOUT_MS = 3e4;
|
|
1796
|
+
async function defaultBrowserRunner() {
|
|
1797
|
+
const { chromium, firefox, webkit, devices: devices2 } = await import("@playwright/test");
|
|
1798
|
+
const desktopEngines = [
|
|
1799
|
+
{ engine: "chromium", type: chromium },
|
|
1800
|
+
{ engine: "firefox", type: firefox },
|
|
1801
|
+
{ engine: "webkit", type: webkit }
|
|
1802
|
+
];
|
|
1803
|
+
const mobileTargets = [
|
|
1804
|
+
{ device: "Pixel 7", descriptor: devices2["Pixel 7"] },
|
|
1805
|
+
{ device: "iPhone 14", descriptor: devices2["iPhone 14"] }
|
|
1806
|
+
];
|
|
1807
|
+
return {
|
|
1808
|
+
async probe(urls) {
|
|
1809
|
+
const results = [];
|
|
1810
|
+
const browsers = await Promise.all(desktopEngines.map((e) => e.type.launch()));
|
|
1811
|
+
const mobileBrowsers = await Promise.all(mobileTargets.map(() => chromium.launch()));
|
|
1812
|
+
try {
|
|
1813
|
+
for (const url of urls) {
|
|
1814
|
+
const desktop = [];
|
|
1815
|
+
const linkSet = /* @__PURE__ */ new Set();
|
|
1816
|
+
for (let i = 0; i < desktopEngines.length; i++) {
|
|
1817
|
+
const engine = desktopEngines[i].engine;
|
|
1818
|
+
const browser = browsers[i];
|
|
1819
|
+
const ctx = await browser.newContext({ viewport: DESKTOP_VIEWPORT });
|
|
1820
|
+
const page = await ctx.newPage();
|
|
1821
|
+
const errors = [];
|
|
1822
|
+
page.on("pageerror", (e) => errors.push(String(e)));
|
|
1823
|
+
let ok = false;
|
|
1824
|
+
try {
|
|
1825
|
+
const resp = await page.goto(url, {
|
|
1826
|
+
waitUntil: "domcontentloaded",
|
|
1827
|
+
timeout: PAGE_TIMEOUT_MS
|
|
1828
|
+
});
|
|
1829
|
+
const hasMain = await page.locator("main, [role=main]").first().isVisible().catch(() => false);
|
|
1830
|
+
ok = !!resp && resp.ok() && errors.length === 0 && hasMain;
|
|
1831
|
+
if (engine === "chromium") {
|
|
1832
|
+
const hrefs = await page.evaluate("Array.from(document.querySelectorAll('a[href]')).map((a) => a.href)").catch(() => []);
|
|
1833
|
+
const origin = new URL(url).origin;
|
|
1834
|
+
for (const h of hrefs) {
|
|
1835
|
+
try {
|
|
1836
|
+
if (new URL(h).origin === origin) linkSet.add(new URL(h).href);
|
|
1837
|
+
} catch {
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
} catch {
|
|
1842
|
+
ok = false;
|
|
1843
|
+
} finally {
|
|
1844
|
+
await ctx.close().catch(() => {
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
desktop.push({ engine, ok });
|
|
1848
|
+
}
|
|
1849
|
+
const mobile = [];
|
|
1850
|
+
for (let i = 0; i < mobileTargets.length; i++) {
|
|
1851
|
+
const { device, descriptor } = mobileTargets[i];
|
|
1852
|
+
const browser = mobileBrowsers[i];
|
|
1853
|
+
const ctx = await browser.newContext({ ...descriptor });
|
|
1854
|
+
const page = await ctx.newPage();
|
|
1855
|
+
const errors = [];
|
|
1856
|
+
page.on("pageerror", (e) => errors.push(String(e)));
|
|
1857
|
+
let ok = false;
|
|
1858
|
+
try {
|
|
1859
|
+
const resp = await page.goto(url, {
|
|
1860
|
+
waitUntil: "domcontentloaded",
|
|
1861
|
+
timeout: PAGE_TIMEOUT_MS
|
|
1862
|
+
});
|
|
1863
|
+
const overflow = await page.evaluate("document.documentElement.scrollWidth > window.innerWidth + 2").catch(() => true);
|
|
1864
|
+
ok = !!resp && resp.ok() && errors.length === 0 && !overflow;
|
|
1865
|
+
} catch {
|
|
1866
|
+
ok = false;
|
|
1867
|
+
} finally {
|
|
1868
|
+
await ctx.close().catch(() => {
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
mobile.push({ device, ok });
|
|
1872
|
+
}
|
|
1873
|
+
results.push({ url, desktop, mobile, links: [...linkSet] });
|
|
1874
|
+
}
|
|
1875
|
+
} finally {
|
|
1876
|
+
await Promise.all([...browsers, ...mobileBrowsers].map((b) => b.close().catch(() => {
|
|
1877
|
+
})));
|
|
1878
|
+
}
|
|
1879
|
+
return results;
|
|
1880
|
+
},
|
|
1881
|
+
async checkLinks(urls) {
|
|
1882
|
+
const out = [];
|
|
1883
|
+
for (const url of urls) {
|
|
1884
|
+
let status;
|
|
1885
|
+
try {
|
|
1886
|
+
let res = await fetch(url, { method: "HEAD", redirect: "follow" });
|
|
1887
|
+
if (res.status === 405 || res.status === 501) {
|
|
1888
|
+
res = await fetch(url, { method: "GET", redirect: "follow" });
|
|
1889
|
+
}
|
|
1890
|
+
status = res.status;
|
|
1891
|
+
} catch {
|
|
1892
|
+
status = null;
|
|
1893
|
+
}
|
|
1894
|
+
out.push({ url, status });
|
|
1895
|
+
}
|
|
1896
|
+
return out;
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1488
1901
|
// src/audits/index.ts
|
|
1489
1902
|
var REGISTRY = {
|
|
1490
1903
|
deps: depsAudit,
|
|
1491
1904
|
lint: lintAudit,
|
|
1492
1905
|
security: securityAudit,
|
|
1493
1906
|
lighthouse: lighthouseAudit,
|
|
1494
|
-
a11y: a11yAudit
|
|
1907
|
+
a11y: a11yAudit,
|
|
1908
|
+
domain: domainAudit,
|
|
1909
|
+
browser: browserAudit
|
|
1495
1910
|
};
|
|
1496
1911
|
var ALL_AUDIT_NAMES = Object.keys(REGISTRY);
|
|
1497
1912
|
var DEFAULT_AUDIT_TIMEOUT_MS = 3e4;
|
|
@@ -1902,8 +2317,13 @@ function deployedUrlNotice(which, url, cwd) {
|
|
|
1902
2317
|
if (others.length === 0) return null;
|
|
1903
2318
|
return `note: --url only affects lighthouse; ${others.join(", ")} ran against the local checkout at ${cwd}`;
|
|
1904
2319
|
}
|
|
2320
|
+
var CHECKOUT_FREE_AUDITS = /* @__PURE__ */ new Set([
|
|
2321
|
+
"lighthouse",
|
|
2322
|
+
"domain",
|
|
2323
|
+
"browser"
|
|
2324
|
+
]);
|
|
1905
2325
|
function auditNeedsCheckout(site, which) {
|
|
1906
|
-
const deployedCapable = site.deployedUrl !== void 0 && which.every((n) => n
|
|
2326
|
+
const deployedCapable = site.deployedUrl !== void 0 && which.every((n) => CHECKOUT_FREE_AUDITS.has(n));
|
|
1907
2327
|
return !deployedCapable;
|
|
1908
2328
|
}
|
|
1909
2329
|
function applyDeployedUrl(sites, url) {
|