@domandigital/craft 0.20.5 → 0.22.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/CHANGELOG.md +23 -0
- package/CHARACTER.md +2 -1
- package/COPY.md +6 -2
- package/README.md +87 -4
- package/dist/{audit-5I2WWTL6.js → audit-LQ2TDISV.js} +3 -3
- package/dist/audit.cjs +64 -1
- package/dist/audit.js +64 -1
- package/dist/{chunk-YFS32BZ2.js → chunk-6DRBFDWT.js} +1 -1
- package/dist/{chunk-BBJOLR3R.js → chunk-6PZG4MJS.js} +5 -5
- package/dist/{chunk-PFVSKIPM.js → chunk-DE2ZAV4G.js} +4 -2
- package/dist/{chunk-GJY2VMPD.js → chunk-G47ZCBD5.js} +2 -2
- package/dist/{chunk-FBWBDBW4.js → chunk-ONA73THJ.js} +61 -0
- package/dist/{chunk-TSO5W32N.js → chunk-OVWVKMPS.js} +14 -14
- package/dist/{chunk-N32UUN3S.js → chunk-RBQCGDQQ.js} +5 -5
- package/dist/chunk-XSKLVEI6.js +368 -0
- package/dist/{chunk-DMAMGFV7.js → chunk-ZS7PQNJU.js} +4 -4
- package/dist/{cli-OH6427MC.js → cli-7DL4BKA7.js} +6 -6
- package/dist/{cli-NIH3WTNE.js → cli-7QL26YPR.js} +1 -1
- package/dist/{cli-BI6XCMCL.js → cli-AM4FLXGM.js} +5 -5
- package/dist/{cli-JQYZXFRH.js → cli-R2HBESHI.js} +5 -5
- package/dist/{cli-V4QXTBWR.js → cli-SZ4SZLUN.js} +4 -4
- package/dist/{cli-W6FNSHRN.js → cli-T7A3BU5J.js} +9 -9
- package/dist/{cli-SQC27VYC.js → cli-WZA5IXSR.js} +6 -6
- package/dist/cli-YNIJOEJK.js +244 -0
- package/dist/cli.js +3 -3
- package/dist/index.cjs +64 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +64 -1
- package/dist/phone.cjs +194 -18
- package/dist/phone.d.cts +113 -13
- package/dist/phone.d.ts +113 -13
- package/dist/phone.js +182 -18
- package/dist/{viewports-cli-C4DWICYI.js → viewports-cli-F7KJIVZL.js} +51 -86
- package/package.json +1 -1
- package/word-lists.json +1 -1
- package/dist/chunk-IJUZ7RV4.js +0 -151
- package/dist/cli-56H5IPWT.js +0 -122
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
holdHouseBlocks,
|
|
11
11
|
houseRule,
|
|
12
12
|
scanSource
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-DE2ZAV4G.js";
|
|
14
14
|
import {
|
|
15
15
|
compareCompetitor,
|
|
16
16
|
compareFacts,
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
pageCopy,
|
|
22
22
|
protectedFacts,
|
|
23
23
|
visibleText
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-ONA73THJ.js";
|
|
25
25
|
|
|
26
26
|
// src/character/config.ts
|
|
27
27
|
var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
|
|
@@ -622,7 +622,7 @@ async function pageSnapshot(target, cwd) {
|
|
|
622
622
|
return readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
|
|
623
623
|
}
|
|
624
624
|
if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL nor a snapshot file`);
|
|
625
|
-
const { snapshotUrl } = await import("./audit-
|
|
625
|
+
const { snapshotUrl } = await import("./audit-LQ2TDISV.js");
|
|
626
626
|
return snapshotUrl(target);
|
|
627
627
|
}
|
|
628
628
|
async function briefFor(flags, cwd) {
|
|
@@ -633,7 +633,7 @@ async function briefFor(flags, cwd) {
|
|
|
633
633
|
if (typeof brief === "string" && brief.trim()) return brief;
|
|
634
634
|
} else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
|
|
635
635
|
if (flags.null) {
|
|
636
|
-
const { loadNull } = await import("./cli-
|
|
636
|
+
const { loadNull } = await import("./cli-SZ4SZLUN.js");
|
|
637
637
|
return loadNull(flags.null, cwd).brief;
|
|
638
638
|
}
|
|
639
639
|
return void 0;
|
|
@@ -659,37 +659,37 @@ async function compareWithCompetitor(flags, io) {
|
|
|
659
659
|
function run(argv, io) {
|
|
660
660
|
const [command, ...rest] = argv;
|
|
661
661
|
if (command === "direction") {
|
|
662
|
-
return import("./cli-
|
|
662
|
+
return import("./cli-R2HBESHI.js").then((m) => m.runDirection(rest, io));
|
|
663
663
|
}
|
|
664
664
|
if (command === "report" || command === "retrofit") {
|
|
665
|
-
return import("./cli-
|
|
665
|
+
return import("./cli-T7A3BU5J.js").then((m) => m.runReport(command, rest, io));
|
|
666
666
|
}
|
|
667
667
|
if (command === "register") {
|
|
668
|
-
return import("./cli-
|
|
668
|
+
return import("./cli-7QL26YPR.js").then((m) => m.runRegister(rest, io));
|
|
669
669
|
}
|
|
670
670
|
if (command === "estate") {
|
|
671
|
-
return import("./cli-
|
|
671
|
+
return import("./cli-WZA5IXSR.js").then((m) => m.runEstate(rest, io));
|
|
672
672
|
}
|
|
673
673
|
if (command === "null") {
|
|
674
|
-
return import("./cli-
|
|
674
|
+
return import("./cli-SZ4SZLUN.js").then((m) => m.runNull(rest, io));
|
|
675
675
|
}
|
|
676
676
|
if (command === "calibrate") {
|
|
677
|
-
return import("./cli-
|
|
677
|
+
return import("./cli-AM4FLXGM.js").then((m) => m.runCalibrate(rest, io));
|
|
678
678
|
}
|
|
679
679
|
if (command === "tells" && rest[0] === "harvest") {
|
|
680
|
-
return import("./cli-
|
|
680
|
+
return import("./cli-SZ4SZLUN.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
|
|
681
681
|
io.err(`craft: ${error.message}`);
|
|
682
682
|
return 2;
|
|
683
683
|
});
|
|
684
684
|
}
|
|
685
685
|
if (command === "phone-first") {
|
|
686
|
-
return import("./cli-
|
|
686
|
+
return import("./cli-YNIJOEJK.js").then((m) => m.runPhoneFirst(rest, io));
|
|
687
687
|
}
|
|
688
688
|
if (command === "viewports") {
|
|
689
|
-
return import("./viewports-cli-
|
|
689
|
+
return import("./viewports-cli-F7KJIVZL.js").then((m) => m.runViewports(rest, io));
|
|
690
690
|
}
|
|
691
691
|
if (command === "snapshot" || command === "audit") {
|
|
692
|
-
return import("./cli-
|
|
692
|
+
return import("./cli-7DL4BKA7.js").then((m) => m.runAudit(command, rest, io));
|
|
693
693
|
}
|
|
694
694
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
695
695
|
io.out(HELP);
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
formatDirectionMatches,
|
|
5
5
|
summariseDirection,
|
|
6
6
|
validateDirection
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-G47ZCBD5.js";
|
|
8
8
|
import {
|
|
9
9
|
loadNull
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-6PZG4MJS.js";
|
|
11
11
|
import {
|
|
12
12
|
parseFlags
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-OVWVKMPS.js";
|
|
14
14
|
import {
|
|
15
15
|
toJson
|
|
16
16
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
rhythmOf,
|
|
31
31
|
shapeName,
|
|
32
32
|
temperatureName
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-DE2ZAV4G.js";
|
|
34
34
|
|
|
35
35
|
// src/estate/cli.ts
|
|
36
36
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -133,7 +133,7 @@ async function snapshotOf(target, cwd) {
|
|
|
133
133
|
return snap;
|
|
134
134
|
}
|
|
135
135
|
if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
|
|
136
|
-
const { snapshotUrl } = await import("./audit-
|
|
136
|
+
const { snapshotUrl } = await import("./audit-LQ2TDISV.js");
|
|
137
137
|
return snapshotUrl(target);
|
|
138
138
|
}
|
|
139
139
|
var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// src/phone/judge.ts
|
|
2
|
+
var hasReason = (entry) => typeof entry.reason === "string" && entry.reason.trim() !== "";
|
|
3
|
+
function parseBaseline(raw, where) {
|
|
4
|
+
const b = raw ?? {};
|
|
5
|
+
const entries = [...b.static ?? [], ...b.measured ?? []];
|
|
6
|
+
const bad = entries.find((e) => e.reason !== void 0 && typeof e.reason !== "string");
|
|
7
|
+
if (bad) throw new Error(`${where}: an entry has a "reason" that is not text: ${JSON.stringify(bad)}`);
|
|
8
|
+
return { ...b.note ? { note: b.note } : {}, static: b.static ?? [], measured: b.measured ?? [] };
|
|
9
|
+
}
|
|
10
|
+
var EMPTY_BASELINE = { static: [], measured: [] };
|
|
11
|
+
var MEDIA = /@(?:custom-)?media\b([^{;]*)/g;
|
|
12
|
+
var CAPS_WIDTH = /\bmax-(?:device-)?width\s*:|\bwidth\s*<|>=?\s*(?:device-)?width\b/i;
|
|
13
|
+
var VARIANT = /(?<![\w@-])max-(\[[^\]\s]+\]|[a-z0-9]+):/g;
|
|
14
|
+
var NOT_VARIANTS = /* @__PURE__ */ new Set(["width", "height", "lines", "resolution", "zoom", "inline-size", "block-size"]);
|
|
15
|
+
var OK = /mobile-first-ok\b:?(.*)/;
|
|
16
|
+
var VAR_OPACITY = /[^\s"'`]*var\([^)]*\)\]\/[0-9]+/g;
|
|
17
|
+
function reasonOf(line) {
|
|
18
|
+
const m = line?.match(OK);
|
|
19
|
+
return m ? m[1].replace(/\*\/|-->|[{}]/g, "").trim() : null;
|
|
20
|
+
}
|
|
21
|
+
function scanPhoneFirst(files, exceptions = [], options = {}) {
|
|
22
|
+
const excusedFiles = new Set(exceptions.filter((e) => e.reason?.trim()).map((e) => e.file));
|
|
23
|
+
const findings = [];
|
|
24
|
+
let excused = 0;
|
|
25
|
+
for (const { path, text } of files) {
|
|
26
|
+
if (excusedFiles.has(path)) continue;
|
|
27
|
+
const lines = text.split("\n");
|
|
28
|
+
const lineOf = (index) => text.slice(0, index).split("\n").length;
|
|
29
|
+
const found = [];
|
|
30
|
+
for (const m of text.matchAll(MEDIA)) {
|
|
31
|
+
if (CAPS_WIDTH.test(m[1])) found.push({ line: lineOf(m.index), what: `${m[0].slice(0, m[0].length - m[1].length)}${m[1].replace(/\s+/g, " ").trimEnd()}` });
|
|
32
|
+
}
|
|
33
|
+
for (const m of text.matchAll(VARIANT)) {
|
|
34
|
+
if (!NOT_VARIANTS.has(m[1])) found.push({ line: lineOf(m.index), what: m[0] });
|
|
35
|
+
}
|
|
36
|
+
const silent = /* @__PURE__ */ new Set();
|
|
37
|
+
if (options.tailwind3?.(path)) {
|
|
38
|
+
for (const m of text.matchAll(VAR_OPACITY)) {
|
|
39
|
+
silent.add(found.push({ line: lineOf(m.index), what: `${m[0]} (Tailwind 3 emits no CSS for opacity on a var() colour)` }) - 1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const order = found.map((f, i) => ({ ...f, silent: silent.has(i) })).sort((a, b) => a.line - b.line);
|
|
43
|
+
for (const { line, what, silent: cannotExcuse } of order) {
|
|
44
|
+
if (cannotExcuse) {
|
|
45
|
+
findings.push({ file: path, line, what });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const here = reasonOf(lines[line - 1]);
|
|
49
|
+
const above = reasonOf(lines[line - 2]);
|
|
50
|
+
if (here || above) {
|
|
51
|
+
excused++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
findings.push({ file: path, line, what: here === "" || above === "" ? `${what} (mobile-first-ok with no reason)` : what });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { findings, excused };
|
|
58
|
+
}
|
|
59
|
+
function checkExceptions(exceptions, exists) {
|
|
60
|
+
const problems = [];
|
|
61
|
+
exceptions.forEach((e, i) => {
|
|
62
|
+
const at = `exceptions[${i}]`;
|
|
63
|
+
const x = e;
|
|
64
|
+
if (typeof x?.file !== "string" || !x.file.trim()) problems.push(`${at}: names no file`);
|
|
65
|
+
else if (typeof x.reason !== "string" || !x.reason.trim()) problems.push(`${at} (${x.file}): no reason. Say why this file needs a max-width query, or remove the exception.`);
|
|
66
|
+
else if (!exists(x.file)) problems.push(`${at}: ${x.file} is not there. Remove the exception.`);
|
|
67
|
+
});
|
|
68
|
+
return problems;
|
|
69
|
+
}
|
|
70
|
+
var normaliseElement = (element) => element.replace(/("[^]*)$/, (text) => text.replace(/\d+/g, "#"));
|
|
71
|
+
var tally = (items, key) => {
|
|
72
|
+
const m = /* @__PURE__ */ new Map();
|
|
73
|
+
for (const item of items) {
|
|
74
|
+
const k = key(item);
|
|
75
|
+
const had = m.get(k);
|
|
76
|
+
if (had) had.count++;
|
|
77
|
+
else m.set(k, { item, count: 1 });
|
|
78
|
+
}
|
|
79
|
+
return m;
|
|
80
|
+
};
|
|
81
|
+
function judgeStatic(findings, baseline) {
|
|
82
|
+
const byFile = tally(findings, (f) => f.file);
|
|
83
|
+
const allowed = new Map(baseline.static.map((b) => [b.file, b.count]));
|
|
84
|
+
const fresh = [];
|
|
85
|
+
const stale = [];
|
|
86
|
+
let known = 0;
|
|
87
|
+
for (const [file, { count }] of byFile) {
|
|
88
|
+
const allow = allowed.get(file) ?? 0;
|
|
89
|
+
known += Math.min(count, allow);
|
|
90
|
+
if (count > allow) {
|
|
91
|
+
const here = findings.filter((f) => f.file === file);
|
|
92
|
+
const lead = allow ? `${file}: ${count} desktop-first rules, the baseline allows ${allow}. One of these is new:` : `${file}:`;
|
|
93
|
+
fresh.push(`${lead}
|
|
94
|
+
${here.map((f) => ` line ${f.line}: ${f.what}`).join("\n")}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const { file, count } of baseline.static) {
|
|
98
|
+
const now = byFile.get(file)?.count ?? 0;
|
|
99
|
+
if (now < count) stale.push(`${file}: the baseline allows ${count}, there ${now === 1 ? "is" : "are"} ${now} now. Lower it to ${now}${now ? "" : " (remove the line)"}.`);
|
|
100
|
+
}
|
|
101
|
+
return { fresh, stale, known };
|
|
102
|
+
}
|
|
103
|
+
var measuredKey = (route, check, element) => `${route}\0${check}\0${element}`;
|
|
104
|
+
function judgeMeasured(routes, baseline) {
|
|
105
|
+
const fresh = [];
|
|
106
|
+
const stale = [];
|
|
107
|
+
let known = 0;
|
|
108
|
+
const allowed = new Map(baseline.measured.map((b) => [measuredKey(b.route, b.check, b.element), b.count]));
|
|
109
|
+
const seen = /* @__PURE__ */ new Map();
|
|
110
|
+
const measured = new Set(routes.map((r) => r.route));
|
|
111
|
+
for (const { route, findings } of routes) {
|
|
112
|
+
const counted = tally(findings, (f) => measuredKey(route, f.check, normaliseElement(f.element)));
|
|
113
|
+
for (const [key, { item, count }] of counted) {
|
|
114
|
+
seen.set(key, count);
|
|
115
|
+
const allow = allowed.get(key) ?? 0;
|
|
116
|
+
known += Math.min(count, allow);
|
|
117
|
+
if (count > allow) {
|
|
118
|
+
const times = allow ? ` (${count} found, the baseline allows ${allow})` : count > 1 ? ` (${count} of them)` : "";
|
|
119
|
+
fresh.push(`${route}: ${LABEL[item.check]} ${item.element}: ${item.detail}${times}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const missing = /* @__PURE__ */ new Set();
|
|
124
|
+
for (const b of baseline.measured) {
|
|
125
|
+
const key = measuredKey(b.route, b.check, b.element);
|
|
126
|
+
if (!measured.has(b.route)) {
|
|
127
|
+
if (!missing.has(b.route)) stale.push(`${b.route}: in the baseline but not measured this run. Measure it, or remove its lines.`);
|
|
128
|
+
missing.add(b.route);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const now = seen.get(key) ?? 0;
|
|
132
|
+
if (now < b.count) stale.push(`${b.route}: ${LABEL[b.check]} ${b.element}: the baseline allows ${b.count}, ${now} found. Lower it to ${now}${now ? "" : " (remove the line)"}.`);
|
|
133
|
+
}
|
|
134
|
+
return { fresh, stale, known };
|
|
135
|
+
}
|
|
136
|
+
var LABEL = {
|
|
137
|
+
scroll: "scrolls sideways",
|
|
138
|
+
target: "small target",
|
|
139
|
+
field: "small field",
|
|
140
|
+
text: "small text",
|
|
141
|
+
title: "title cut off",
|
|
142
|
+
overlap: "text overlaps text",
|
|
143
|
+
action: "main action out of reach",
|
|
144
|
+
layers: "fixed layers cover too much",
|
|
145
|
+
landscape: "landscape screen eaten by fixed layers",
|
|
146
|
+
image: "image not sized for its place",
|
|
147
|
+
input: "wrong keyboard",
|
|
148
|
+
autocomplete: "field the phone cannot fill in",
|
|
149
|
+
tel: "phone number is not a link",
|
|
150
|
+
linelength: "line too long",
|
|
151
|
+
screens: "page too long for a phone"
|
|
152
|
+
};
|
|
153
|
+
function writeBaseline(findings, routes, note, previous) {
|
|
154
|
+
const byFile = tally(findings, (f) => f.file);
|
|
155
|
+
const measured = routes.flatMap(
|
|
156
|
+
({ route, findings: findings2 }) => [...tally(findings2, (f) => measuredKey(route, f.check, normaliseElement(f.element))).values()].map(({ item, count }) => ({ route, check: item.check, element: normaliseElement(item.element), count }))
|
|
157
|
+
);
|
|
158
|
+
const staticReasons = new Map((previous?.static ?? []).filter(hasReason).map((b) => [b.file, b.reason]));
|
|
159
|
+
const measuredReasons = new Map((previous?.measured ?? []).filter(hasReason).map((b) => [measuredKey(b.route, b.check, b.element), b.reason]));
|
|
160
|
+
return {
|
|
161
|
+
...note ? { note } : {},
|
|
162
|
+
static: [...byFile.values()].map(({ item, count }) => ({ file: item.file, count, found: findings.filter((f) => f.file === item.file).map((f) => f.what), ...staticReasons.has(item.file) ? { reason: staticReasons.get(item.file) } : {} })).sort((a, b) => a.file.localeCompare(b.file)),
|
|
163
|
+
measured: measured.map((m) => measuredReasons.has(measuredKey(m.route, m.check, m.element)) ? { ...m, reason: measuredReasons.get(measuredKey(m.route, m.check, m.element)) } : m).sort((a, b) => a.route.localeCompare(b.route) || a.check.localeCompare(b.check) || a.element.localeCompare(b.element))
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function judgeGrowth(current, base, baseName = "main") {
|
|
167
|
+
const out = [];
|
|
168
|
+
const baseStatic = new Map(base.static.map((b) => [b.file, b.count]));
|
|
169
|
+
for (const b of current.static) {
|
|
170
|
+
const was = baseStatic.get(b.file) ?? 0;
|
|
171
|
+
if (b.count > was && !hasReason(b)) out.push(`${b.file}: ${was ? `${b.count} desktop-first rules, ${baseName} allows ${was}` : `${b.count} desktop-first rule${b.count === 1 ? "" : "s"} added to the baseline`}`);
|
|
172
|
+
}
|
|
173
|
+
const baseMeasured = new Map(base.measured.map((b) => [measuredKey(b.route, b.check, b.element), b.count]));
|
|
174
|
+
for (const b of current.measured) {
|
|
175
|
+
const was = baseMeasured.get(measuredKey(b.route, b.check, b.element)) ?? 0;
|
|
176
|
+
if (b.count > was && !hasReason(b)) out.push(`${b.route}: ${LABEL[b.check]} ${b.element}: ${was ? `${b.count} in the baseline, ${baseName} allows ${was}` : `added to the baseline (${b.count})`}`);
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
var hasUnexcusedEntries = (b) => [...b.static, ...b.measured].some((e) => !hasReason(e));
|
|
181
|
+
var PAGE_FILE = /(?:^|\/)page\.(?:tsx|ts|jsx|js|mdx)$/;
|
|
182
|
+
var INTERCEPTING = /^\(\.{1,3}\)/;
|
|
183
|
+
function routesFromPages(files) {
|
|
184
|
+
const routes = /* @__PURE__ */ new Set();
|
|
185
|
+
for (const file of files) {
|
|
186
|
+
const rel = file.replace(/\\/g, "/");
|
|
187
|
+
if (!PAGE_FILE.test(rel)) continue;
|
|
188
|
+
const dirs = rel.split("/").slice(0, -1);
|
|
189
|
+
if (dirs.some((d) => INTERCEPTING.test(d) || d.startsWith("_"))) continue;
|
|
190
|
+
const url = dirs.filter((d) => !/^\(.*\)$/.test(d) && !d.startsWith("@"));
|
|
191
|
+
routes.add(`/${url.join("/")}`);
|
|
192
|
+
}
|
|
193
|
+
return [...routes].sort();
|
|
194
|
+
}
|
|
195
|
+
function routesFromManifest(manifest) {
|
|
196
|
+
const routes = /* @__PURE__ */ new Set();
|
|
197
|
+
for (const [key, route] of Object.entries(manifest ?? {})) {
|
|
198
|
+
if (!/(?:^|\/)page$/.test(key) || typeof route !== "string") continue;
|
|
199
|
+
if (route.split("/").some((d) => d.startsWith("_"))) continue;
|
|
200
|
+
routes.add(route);
|
|
201
|
+
}
|
|
202
|
+
return [...routes].sort();
|
|
203
|
+
}
|
|
204
|
+
var segmentsOf = (route) => route.split("/").filter(Boolean);
|
|
205
|
+
var rank = (seg) => seg.startsWith("[[...") ? 3 : seg.startsWith("[...") ? 2 : seg.startsWith("[") ? 1 : 0;
|
|
206
|
+
var cleanRoute = (route) => {
|
|
207
|
+
const path = route.replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
208
|
+
return path === "" ? "/" : path;
|
|
209
|
+
};
|
|
210
|
+
function routeMatches(pattern, route) {
|
|
211
|
+
if (pattern === route) return true;
|
|
212
|
+
const want = segmentsOf(pattern);
|
|
213
|
+
const have = segmentsOf(route);
|
|
214
|
+
for (let i = 0; i < want.length; i++) {
|
|
215
|
+
const seg = want[i];
|
|
216
|
+
if (seg.startsWith("[[...")) return true;
|
|
217
|
+
if (seg.startsWith("[...")) return have.length > i;
|
|
218
|
+
if (have[i] === void 0) return false;
|
|
219
|
+
if (!seg.startsWith("[") && seg !== have[i]) return false;
|
|
220
|
+
}
|
|
221
|
+
return want.length === have.length;
|
|
222
|
+
}
|
|
223
|
+
var moreSpecific = (a, b) => {
|
|
224
|
+
const x = segmentsOf(a).map(rank);
|
|
225
|
+
const y = segmentsOf(b).map(rank);
|
|
226
|
+
for (let i = 0; i < Math.min(x.length, y.length); i++) if (x[i] !== y[i]) return x[i] < y[i];
|
|
227
|
+
return x.length > y.length;
|
|
228
|
+
};
|
|
229
|
+
var excludes = (entry, page) => entry.endsWith("/*") ? page === entry.slice(0, -2) || page.startsWith(entry.slice(0, -1)) : entry === page;
|
|
230
|
+
function judgeRoutes(pages, measuredRoutes, excluded = []) {
|
|
231
|
+
const problems = [];
|
|
232
|
+
excluded.forEach((e, i) => {
|
|
233
|
+
const at = `excludeRoutes[${i}]`;
|
|
234
|
+
const x = e;
|
|
235
|
+
if (typeof x?.route !== "string" || !x.route.startsWith("/")) problems.push(`${at}: names no route (start it with /)`);
|
|
236
|
+
else if (typeof x.reason !== "string" || !x.reason.trim()) problems.push(`${at} (${x.route}): no reason. Say why the gate cannot measure this page, or remove the entry.`);
|
|
237
|
+
else if (!pages.some((p) => excludes(x.route, p))) problems.push(`${at}: ${x.route} is not a page route of the app. Remove the entry.`);
|
|
238
|
+
});
|
|
239
|
+
const named = excluded.filter((e) => typeof e?.route === "string" && typeof e.reason === "string" && e.reason.trim());
|
|
240
|
+
const covered = /* @__PURE__ */ new Set();
|
|
241
|
+
for (const route of measuredRoutes.map(cleanRoute)) {
|
|
242
|
+
const hit = pages.filter((p) => routeMatches(p, route)).sort((a, b) => moreSpecific(a, b) ? -1 : moreSpecific(b, a) ? 1 : 0)[0];
|
|
243
|
+
if (hit) covered.add(hit);
|
|
244
|
+
}
|
|
245
|
+
for (const page of pages) if (named.some((e) => excludes(e.route, page))) covered.add(page);
|
|
246
|
+
return { missing: pages.filter((p) => !covered.has(p)), problems, covered: covered.size };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/phone/viewports.ts
|
|
250
|
+
var LADDER = [
|
|
251
|
+
{ name: "320", width: 320, height: 568, phone: true },
|
|
252
|
+
{ name: "375", width: 375, height: 812, phone: true },
|
|
253
|
+
{ name: "390", width: 390, height: 844, phone: true },
|
|
254
|
+
{ name: "430", width: 430, height: 932, phone: true },
|
|
255
|
+
{ name: "844x390", width: 844, height: 390, phone: true, landscape: true },
|
|
256
|
+
{ name: "768", width: 768, height: 1024, phone: false },
|
|
257
|
+
{ name: "1024", width: 1024, height: 768, phone: false },
|
|
258
|
+
{ name: "1280", width: 1280, height: 800, phone: false },
|
|
259
|
+
{ name: "1440", width: 1440, height: 900, phone: false },
|
|
260
|
+
{ name: "1920", width: 1920, height: 1080, phone: false }
|
|
261
|
+
];
|
|
262
|
+
var SHORT_PHONE = { name: "375x667", width: 375, height: 667, phone: true };
|
|
263
|
+
var DEFAULT_SWEEP = { from: 320, to: 1920, step: 16, wideStep: 32, wideFrom: 1024, height: 800 };
|
|
264
|
+
function sweepWidths(sweep = DEFAULT_SWEEP) {
|
|
265
|
+
const out = [];
|
|
266
|
+
for (let w = sweep.from; w <= sweep.to; w += w < sweep.wideFrom ? sweep.step : sweep.wideStep) out.push(w);
|
|
267
|
+
if (out.at(-1) !== sweep.to) out.push(sweep.to);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
var DEFAULT_THRESHOLDS = {
|
|
271
|
+
maxLayerShare: 0.25,
|
|
272
|
+
landscapeMaxLayerShare: 0.5,
|
|
273
|
+
oversizeRatio: 2.5,
|
|
274
|
+
upscaleRatio: 1,
|
|
275
|
+
maxLineCh: 80,
|
|
276
|
+
overlapShare: 0.4,
|
|
277
|
+
minImagePx: 32
|
|
278
|
+
};
|
|
279
|
+
var mainActionFor = (config, route) => typeof config === "string" ? config : config ? config[route] ?? config["*"] : void 0;
|
|
280
|
+
function screenBudgetFor(budgets, route) {
|
|
281
|
+
if (!budgets) return void 0;
|
|
282
|
+
const path = cleanRoute(route);
|
|
283
|
+
let best;
|
|
284
|
+
for (const key of Object.keys(budgets)) {
|
|
285
|
+
const prefix = key.endsWith("/*") ? key.slice(0, -1) : null;
|
|
286
|
+
const hit = key === "*" ? 0 : prefix ? path === key.slice(0, -2) || path.startsWith(prefix) ? 1 + prefix.length : -1 : routeMatches(key, path) ? key === path ? 1e4 : 5e3 + key.length : -1;
|
|
287
|
+
if (hit >= 0 && (!best || hit > best.score)) best = { key, score: hit };
|
|
288
|
+
}
|
|
289
|
+
return best ? budgets[best.key] : void 0;
|
|
290
|
+
}
|
|
291
|
+
function checkScreenBudgets(budgets, where = "screens") {
|
|
292
|
+
if (budgets === void 0) return void 0;
|
|
293
|
+
if (!budgets || typeof budgets !== "object" || Array.isArray(budgets)) throw new Error(`${where} should map a route to its budget in phone screens, such as { "/": 6.5, "/services/*": 6 }`);
|
|
294
|
+
for (const [route, value] of Object.entries(budgets)) {
|
|
295
|
+
if (route !== "*" && !route.startsWith("/")) throw new Error(`${where}: "${route}" should start with / (or be *)`);
|
|
296
|
+
if (typeof value !== "number" || !(value > 0)) throw new Error(`${where}: "${route}" should be a number of phone screens above 0, not ${JSON.stringify(value)}`);
|
|
297
|
+
}
|
|
298
|
+
return budgets;
|
|
299
|
+
}
|
|
300
|
+
function formatScreens(routes, budgets) {
|
|
301
|
+
return routes.filter((r) => r.screens !== void 0).map((r) => {
|
|
302
|
+
const budget = screenBudgetFor(budgets, r.route);
|
|
303
|
+
return `${r.route} ${r.screens}${budget === void 0 ? "" : ` of ${budget}`}`;
|
|
304
|
+
}).join(", ");
|
|
305
|
+
}
|
|
306
|
+
function formatWidths(failing, tested) {
|
|
307
|
+
const plain = tested.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(a) - Number(b));
|
|
308
|
+
const rank2 = new Map(plain.map((name, i) => [name, i]));
|
|
309
|
+
const names = [...new Set(failing)];
|
|
310
|
+
const runs = [];
|
|
311
|
+
for (const name of names.filter((n) => rank2.has(n)).sort((a, b) => rank2.get(a) - rank2.get(b))) {
|
|
312
|
+
const last = runs.at(-1);
|
|
313
|
+
if (last && rank2.get(name) === rank2.get(last.at(-1)) + 1) last.push(name);
|
|
314
|
+
else runs.push([name]);
|
|
315
|
+
}
|
|
316
|
+
const out = runs.map((r) => r.length > 1 ? `${r[0]}-${r.at(-1)}` : r[0]);
|
|
317
|
+
return [...out, ...names.filter((n) => !rank2.has(n))].join(", ");
|
|
318
|
+
}
|
|
319
|
+
var keyOf = (f) => `${f.check}\0${f.element.replace(/("[^]*)$/, (t) => t.replace(/\d+/g, "#"))}`;
|
|
320
|
+
function mergeViewportFindings(perViewport, tested) {
|
|
321
|
+
const merged = /* @__PURE__ */ new Map();
|
|
322
|
+
for (const { viewport, findings } of perViewport) {
|
|
323
|
+
const counts = /* @__PURE__ */ new Map();
|
|
324
|
+
for (const f of findings) {
|
|
325
|
+
const k = keyOf(f);
|
|
326
|
+
const had = counts.get(k);
|
|
327
|
+
if (had) had.n++;
|
|
328
|
+
else counts.set(k, { first: f, n: 1 });
|
|
329
|
+
}
|
|
330
|
+
for (const [k, { first, n }] of counts) {
|
|
331
|
+
const had = merged.get(k);
|
|
332
|
+
if (had) {
|
|
333
|
+
had.at.push(viewport);
|
|
334
|
+
had.most = Math.max(had.most, n);
|
|
335
|
+
} else merged.set(k, { first, at: [viewport], most: n });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const out = [];
|
|
339
|
+
for (const { first, at, most } of merged.values()) {
|
|
340
|
+
for (let i = 0; i < most; i++) out.push({ ...first, detail: `${first.detail} (at ${formatWidths(at, tested)})` });
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export {
|
|
346
|
+
parseBaseline,
|
|
347
|
+
EMPTY_BASELINE,
|
|
348
|
+
scanPhoneFirst,
|
|
349
|
+
checkExceptions,
|
|
350
|
+
judgeStatic,
|
|
351
|
+
judgeMeasured,
|
|
352
|
+
writeBaseline,
|
|
353
|
+
judgeGrowth,
|
|
354
|
+
hasUnexcusedEntries,
|
|
355
|
+
routesFromPages,
|
|
356
|
+
routesFromManifest,
|
|
357
|
+
judgeRoutes,
|
|
358
|
+
LADDER,
|
|
359
|
+
SHORT_PHONE,
|
|
360
|
+
DEFAULT_SWEEP,
|
|
361
|
+
sweepWidths,
|
|
362
|
+
DEFAULT_THRESHOLDS,
|
|
363
|
+
mainActionFor,
|
|
364
|
+
screenBudgetFor,
|
|
365
|
+
checkScreenBudgets,
|
|
366
|
+
formatScreens,
|
|
367
|
+
mergeViewportFindings
|
|
368
|
+
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
snapshotUrl,
|
|
3
3
|
snapshotUrls
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-6DRBFDWT.js";
|
|
5
5
|
import {
|
|
6
6
|
describeTypicality,
|
|
7
7
|
loadNull
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-6PZG4MJS.js";
|
|
9
9
|
import {
|
|
10
10
|
SOURCE_FILE,
|
|
11
11
|
finish,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
parseFlags,
|
|
15
15
|
prepareReport,
|
|
16
16
|
readPaths
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-OVWVKMPS.js";
|
|
18
18
|
import {
|
|
19
19
|
toJson
|
|
20
20
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
rhythmOf,
|
|
29
29
|
scanSource,
|
|
30
30
|
typicality
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-DE2ZAV4G.js";
|
|
32
32
|
|
|
33
33
|
// src/audit/cli.ts
|
|
34
34
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -4,16 +4,16 @@ import {
|
|
|
4
4
|
merge,
|
|
5
5
|
resolvePages,
|
|
6
6
|
runAudit
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-ZS7PQNJU.js";
|
|
8
|
+
import "./chunk-6DRBFDWT.js";
|
|
9
9
|
import "./chunk-JYNXGCYZ.js";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-6PZG4MJS.js";
|
|
11
11
|
import "./chunk-OQXTQQAU.js";
|
|
12
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-OVWVKMPS.js";
|
|
13
13
|
import "./chunk-QIEDBL4J.js";
|
|
14
14
|
import "./chunk-WUACK2HX.js";
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-DE2ZAV4G.js";
|
|
16
|
+
import "./chunk-ONA73THJ.js";
|
|
17
17
|
import "./chunk-QYBAVU3P.js";
|
|
18
18
|
export {
|
|
19
19
|
describeComponents,
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
expandNullTargets,
|
|
6
6
|
loadNull
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-6PZG4MJS.js";
|
|
8
8
|
import "./chunk-OQXTQQAU.js";
|
|
9
9
|
import {
|
|
10
10
|
parseFlags
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-OVWVKMPS.js";
|
|
12
12
|
import {
|
|
13
13
|
toJson
|
|
14
14
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
fingerprint,
|
|
22
22
|
scanSource,
|
|
23
23
|
typicality
|
|
24
|
-
} from "./chunk-
|
|
25
|
-
import "./chunk-
|
|
24
|
+
} from "./chunk-DE2ZAV4G.js";
|
|
25
|
+
import "./chunk-ONA73THJ.js";
|
|
26
26
|
import {
|
|
27
27
|
SNAPSHOT_VERSION
|
|
28
28
|
} from "./chunk-QYBAVU3P.js";
|
|
@@ -188,7 +188,7 @@ async function runCalibrate(args, io) {
|
|
|
188
188
|
if (toTake.length) {
|
|
189
189
|
io.err(`craft calibrate: taking ${toTake.length} snapshot${toTake.length === 1 ? "" : "s"}`);
|
|
190
190
|
try {
|
|
191
|
-
const { snapshotUrls } = await import("./audit-
|
|
191
|
+
const { snapshotUrls } = await import("./audit-LQ2TDISV.js");
|
|
192
192
|
const results = await snapshotUrls(toTake.map((t) => t.url));
|
|
193
193
|
results.forEach((r, i) => {
|
|
194
194
|
const { entry, keep } = toTake[i];
|
|
@@ -19,10 +19,10 @@ import {
|
|
|
19
19
|
checkReason,
|
|
20
20
|
researchPrompt,
|
|
21
21
|
validateDirection
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-G47ZCBD5.js";
|
|
23
23
|
import {
|
|
24
24
|
parseFlags
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-OVWVKMPS.js";
|
|
26
26
|
import {
|
|
27
27
|
toJson
|
|
28
28
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -38,8 +38,8 @@ import {
|
|
|
38
38
|
isCream,
|
|
39
39
|
oklchToRgb,
|
|
40
40
|
parseColour
|
|
41
|
-
} from "./chunk-
|
|
42
|
-
import "./chunk-
|
|
41
|
+
} from "./chunk-DE2ZAV4G.js";
|
|
42
|
+
import "./chunk-ONA73THJ.js";
|
|
43
43
|
import "./chunk-QYBAVU3P.js";
|
|
44
44
|
|
|
45
45
|
// src/direction/cli.ts
|
|
@@ -484,7 +484,7 @@ var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
|
484
484
|
async function snapshotFrom(target, cwd) {
|
|
485
485
|
if (!target) return void 0;
|
|
486
486
|
if (/^https?:\/\//.test(target)) {
|
|
487
|
-
const { snapshotUrl } = await import("./audit-
|
|
487
|
+
const { snapshotUrl } = await import("./audit-LQ2TDISV.js");
|
|
488
488
|
return snapshotUrl(target);
|
|
489
489
|
}
|
|
490
490
|
return readSnapshot(readJson(resolve(cwd, target)), target);
|
|
@@ -9,13 +9,13 @@ import {
|
|
|
9
9
|
runHarvest,
|
|
10
10
|
runNull,
|
|
11
11
|
serveDir
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-6PZG4MJS.js";
|
|
13
13
|
import "./chunk-OQXTQQAU.js";
|
|
14
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-OVWVKMPS.js";
|
|
15
15
|
import "./chunk-QIEDBL4J.js";
|
|
16
16
|
import "./chunk-WUACK2HX.js";
|
|
17
|
-
import "./chunk-
|
|
18
|
-
import "./chunk-
|
|
17
|
+
import "./chunk-DE2ZAV4G.js";
|
|
18
|
+
import "./chunk-ONA73THJ.js";
|
|
19
19
|
import "./chunk-QYBAVU3P.js";
|
|
20
20
|
export {
|
|
21
21
|
describeTypicality,
|