@domandigital/craft 0.19.1 → 0.19.3
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 +13 -0
- package/README.md +44 -0
- package/dist/{chunk-P37LLKL4.js → chunk-6M2LK7QE.js} +2 -2
- package/dist/{chunk-DUUDGR56.js → chunk-BK4AS2RU.js} +20 -8
- package/dist/{chunk-ORFILP5L.js → chunk-D2FTTM4F.js} +1 -1
- package/dist/{chunk-37ARRJAD.js → chunk-XVMKZ4SM.js} +2 -2
- package/dist/{cli-AACBHRUG.js → cli-4LEAEOOV.js} +4 -4
- package/dist/cli-6LCOSHMW.js +249 -0
- package/dist/{cli-TH76ATN3.js → cli-APYEOHFV.js} +1 -1
- package/dist/{cli-7OUA6ZSE.js → cli-Q5X3W6I4.js} +2 -2
- package/dist/{cli-3P44X6E3.js → cli-QAYH2OX5.js} +3 -3
- package/dist/{cli-L3VGKWZH.js → cli-TMLHMNWU.js} +2 -2
- package/dist/{cli-4D7Q5BCE.js → cli-WN2GHYPF.js} +3 -3
- package/dist/cli.js +1 -1
- package/dist/phone.cjs +275 -0
- package/dist/phone.d.cts +185 -0
- package/dist/phone.d.ts +185 -0
- package/dist/phone.js +237 -0
- package/package.json +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @domandigital/craft
|
|
2
2
|
|
|
3
|
+
## 0.19.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 95af0f3: `craft phone-first` folds digits only in an element's text, so `<h1>` stays `<h1>` in a baseline, and says how many of a breach it found ("3 of them", "2 found, the baseline allows 1") (DOM-775).
|
|
8
|
+
|
|
9
|
+
## 0.19.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 4ebd2d5: The phone-first gate for portals (DOM-775). `@domandigital/craft/phone` measures a route at 375px from inside a portal's own signed-in Playwright suite (sideways scroll against the layout viewport, a missing device-width viewport meta, targets under 44px, fields under 16px, text under 12px, a cut-off title), and `craft phone-first` judges those routes and the source's max-width queries and Tailwind `max-*` variants against a baseline that only shrinks.
|
|
14
|
+
- 51cded2: `measurePhone` leaves out what is parked off the page until it is needed, such as a skip link before focus or a closed drawer, as create-site's measure.mjs does (DOM-775).
|
|
15
|
+
|
|
3
16
|
## 0.19.1
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -222,6 +222,50 @@ restraint rule) and art direction (`craft direction`, `craft estate`).
|
|
|
222
222
|
under a passing average, and a truncated report fails rather than reading as
|
|
223
223
|
"no report".
|
|
224
224
|
|
|
225
|
+
## The phone-first gate for portals
|
|
226
|
+
|
|
227
|
+
STANDARD.md section 10 as a gate for an app behind a sign-in, such as a client
|
|
228
|
+
portal (portal standard rule 10, DOM-775). create-site's `measure.mjs` and
|
|
229
|
+
`mobile-first.mjs` do the same job for a site; a portal's pages need a
|
|
230
|
+
signed-in user and fixture data, which only the portal's own Playwright suite
|
|
231
|
+
can set up, so the gate comes in two halves.
|
|
232
|
+
|
|
233
|
+
The portal's suite opens each key route at `PHONE` (375 x 812, touch) and
|
|
234
|
+
measures it:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { test } from "@playwright/test";
|
|
238
|
+
import { PHONE, measurePhone } from "@domandigital/craft/phone";
|
|
239
|
+
|
|
240
|
+
test.use(PHONE);
|
|
241
|
+
for (const route of ["/portal", "/portal/invoices"]) {
|
|
242
|
+
test(`phone first: ${route}`, async ({ page }) => {
|
|
243
|
+
await page.goto(route);
|
|
244
|
+
await measurePhone(page, { route }); // writes phone-first/measured/portal-invoices.json
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Then `craft phone-first --measured phone-first/measured` judges every route,
|
|
250
|
+
and the source folders `phone-first.json` names, against the repo's baseline:
|
|
251
|
+
|
|
252
|
+
- **Static:** a `max-width` media query (any spelling, range syntax too) or a
|
|
253
|
+
Tailwind `max-*:` variant fails. A genuine exception takes
|
|
254
|
+
`mobile-first-ok: <reason>` on its line or the line above, or the whole file
|
|
255
|
+
in `phone-first.json` `exceptions` as `{ "file", "reason" }`.
|
|
256
|
+
- **Measured at 375px:** the page scrolls sideways (against the layout
|
|
257
|
+
viewport: under mobile emulation `innerWidth` grows with the page, so it
|
|
258
|
+
never shows the scroll), a page with no `width=device-width` viewport meta,
|
|
259
|
+
a target under 44 x 44 (a link inside a sentence is exempt; a field's label
|
|
260
|
+
counts), a text field under 16px, text under 12px, a cut-off `h1`.
|
|
261
|
+
|
|
262
|
+
`phone-first.baseline.json` lists what a repo already breaks, counted per file
|
|
263
|
+
and per route (digits folded, so fixture dates do not split one button into
|
|
264
|
+
many). The gate fails on anything the baseline does not list, and on a
|
|
265
|
+
baseline that claims more than is there, so it only shrinks: fix a breach and
|
|
266
|
+
lower its count in the same PR. `--update-baseline` writes the baseline that
|
|
267
|
+
makes the current run pass; run it once when the gate goes in.
|
|
268
|
+
|
|
225
269
|
## API
|
|
226
270
|
|
|
227
271
|
| Area | Exports |
|
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
} from "./chunk-4LJFZH2I.js";
|
|
8
8
|
import {
|
|
9
9
|
loadNull
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-D2FTTM4F.js";
|
|
11
11
|
import {
|
|
12
12
|
parseFlags
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-BK4AS2RU.js";
|
|
14
14
|
import {
|
|
15
15
|
toJson
|
|
16
16
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -391,6 +391,7 @@ Usage
|
|
|
391
391
|
craft register list [--json]
|
|
392
392
|
craft register brief <plain|persuasive|warm|literary> [--json]
|
|
393
393
|
craft register check <file> --as <register> [--json]
|
|
394
|
+
craft phone-first [--config phone-first.json] [--measured <dir>] [--update-baseline] [--json]
|
|
394
395
|
|
|
395
396
|
scan Markup, component code and stylesheets (source and compiled CSS).
|
|
396
397
|
--staged reads the git index, for a pre-commit hook.
|
|
@@ -445,6 +446,14 @@ register Writing in a chosen register (REGISTERS.md). brief prints what to
|
|
|
445
446
|
do before drafting; check places a draft of five or more
|
|
446
447
|
paragraphs against the human baselines for that register. Never
|
|
447
448
|
fails: exit 0, or 2 on bad usage.
|
|
449
|
+
phone-first The phone-first gate for a portal or app (STANDARD.md section
|
|
450
|
+
10). Reads phone-first.json; fails on a max-width media query or a
|
|
451
|
+
Tailwind max-* variant in its source folders and, with --measured,
|
|
452
|
+
on what @domandigital/craft/phone measured at 375px (sideways
|
|
453
|
+
scroll, targets under 44px, fields under 16px, text under 12px, a
|
|
454
|
+
cut-off title), unless phone-first.baseline.json already lists it.
|
|
455
|
+
A baseline that claims more than is there fails too: it only
|
|
456
|
+
shrinks. Exit 1 on a breach, 2 on bad usage.
|
|
448
457
|
|
|
449
458
|
ci options (scan, copy, audit)
|
|
450
459
|
--baseline <file> Report only findings the baseline does not already
|
|
@@ -612,7 +621,7 @@ async function briefFor(flags, cwd) {
|
|
|
612
621
|
if (typeof brief === "string" && brief.trim()) return brief;
|
|
613
622
|
} else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
|
|
614
623
|
if (flags.null) {
|
|
615
|
-
const { loadNull } = await import("./cli-
|
|
624
|
+
const { loadNull } = await import("./cli-TMLHMNWU.js");
|
|
616
625
|
return loadNull(flags.null, cwd).brief;
|
|
617
626
|
}
|
|
618
627
|
return void 0;
|
|
@@ -638,31 +647,34 @@ async function compareWithCompetitor(flags, io) {
|
|
|
638
647
|
function run(argv, io) {
|
|
639
648
|
const [command, ...rest] = argv;
|
|
640
649
|
if (command === "direction") {
|
|
641
|
-
return import("./cli-
|
|
650
|
+
return import("./cli-APYEOHFV.js").then((m) => m.runDirection(rest, io));
|
|
642
651
|
}
|
|
643
652
|
if (command === "report" || command === "retrofit") {
|
|
644
|
-
return import("./cli-
|
|
653
|
+
return import("./cli-4LEAEOOV.js").then((m) => m.runReport(command, rest, io));
|
|
645
654
|
}
|
|
646
655
|
if (command === "register") {
|
|
647
656
|
return import("./cli-VPARW6FY.js").then((m) => m.runRegister(rest, io));
|
|
648
657
|
}
|
|
649
658
|
if (command === "estate") {
|
|
650
|
-
return import("./cli-
|
|
659
|
+
return import("./cli-QAYH2OX5.js").then((m) => m.runEstate(rest, io));
|
|
651
660
|
}
|
|
652
661
|
if (command === "null") {
|
|
653
|
-
return import("./cli-
|
|
662
|
+
return import("./cli-TMLHMNWU.js").then((m) => m.runNull(rest, io));
|
|
654
663
|
}
|
|
655
664
|
if (command === "calibrate") {
|
|
656
|
-
return import("./cli-
|
|
665
|
+
return import("./cli-Q5X3W6I4.js").then((m) => m.runCalibrate(rest, io));
|
|
657
666
|
}
|
|
658
667
|
if (command === "tells" && rest[0] === "harvest") {
|
|
659
|
-
return import("./cli-
|
|
668
|
+
return import("./cli-TMLHMNWU.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
|
|
660
669
|
io.err(`craft: ${error.message}`);
|
|
661
670
|
return 2;
|
|
662
671
|
});
|
|
663
672
|
}
|
|
673
|
+
if (command === "phone-first") {
|
|
674
|
+
return import("./cli-6LCOSHMW.js").then((m) => m.runPhoneFirst(rest, io));
|
|
675
|
+
}
|
|
664
676
|
if (command === "snapshot" || command === "audit") {
|
|
665
|
-
return import("./cli-
|
|
677
|
+
return import("./cli-WN2GHYPF.js").then((m) => m.runAudit(command, rest, io));
|
|
666
678
|
}
|
|
667
679
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
668
680
|
io.out(HELP);
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
describeTypicality,
|
|
7
7
|
loadNull
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-D2FTTM4F.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-BK4AS2RU.js";
|
|
18
18
|
import {
|
|
19
19
|
toJson
|
|
20
20
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -3,21 +3,21 @@ import {
|
|
|
3
3
|
} from "./chunk-OKVHWXYV.js";
|
|
4
4
|
import {
|
|
5
5
|
merge
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-XVMKZ4SM.js";
|
|
7
7
|
import "./chunk-VDUIIWG7.js";
|
|
8
8
|
import "./chunk-JYNXGCYZ.js";
|
|
9
9
|
import {
|
|
10
10
|
SIBLING_AT,
|
|
11
11
|
compareToEstate,
|
|
12
12
|
loadEstate
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-6M2LK7QE.js";
|
|
14
14
|
import {
|
|
15
15
|
PAGE_TYPES,
|
|
16
16
|
validateDirection
|
|
17
17
|
} from "./chunk-4LJFZH2I.js";
|
|
18
18
|
import {
|
|
19
19
|
loadNull
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-D2FTTM4F.js";
|
|
21
21
|
import "./chunk-OQXTQQAU.js";
|
|
22
22
|
import {
|
|
23
23
|
COPY_FILE,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
loadExceptions,
|
|
29
29
|
parseFlags,
|
|
30
30
|
readPaths
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-BK4AS2RU.js";
|
|
32
32
|
import {
|
|
33
33
|
toJson
|
|
34
34
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SOURCE_FILE,
|
|
3
|
+
readPaths
|
|
4
|
+
} from "./chunk-BK4AS2RU.js";
|
|
5
|
+
import {
|
|
6
|
+
toJson
|
|
7
|
+
} from "./chunk-QIEDBL4J.js";
|
|
8
|
+
import "./chunk-WK2WMWTH.js";
|
|
9
|
+
import "./chunk-CZ7KT452.js";
|
|
10
|
+
import "./chunk-QYBAVU3P.js";
|
|
11
|
+
|
|
12
|
+
// src/phone/cli.ts
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
14
|
+
import { join, resolve } from "path";
|
|
15
|
+
|
|
16
|
+
// src/phone/judge.ts
|
|
17
|
+
var EMPTY_BASELINE = { static: [], measured: [] };
|
|
18
|
+
var MEDIA = /@(?:custom-)?media\b([^{;]*)/g;
|
|
19
|
+
var CAPS_WIDTH = /\bmax-(?:device-)?width\s*:|\bwidth\s*<|>=?\s*(?:device-)?width\b/i;
|
|
20
|
+
var VARIANT = /(?<![\w@-])max-(\[[^\]\s]+\]|[a-z0-9]+):/g;
|
|
21
|
+
var NOT_VARIANTS = /* @__PURE__ */ new Set(["width", "height", "lines", "resolution", "zoom", "inline-size", "block-size"]);
|
|
22
|
+
var OK = /mobile-first-ok\b:?(.*)/;
|
|
23
|
+
function reasonOf(line) {
|
|
24
|
+
const m = line?.match(OK);
|
|
25
|
+
return m ? m[1].replace(/\*\/|-->|[{}]/g, "").trim() : null;
|
|
26
|
+
}
|
|
27
|
+
function scanPhoneFirst(files, exceptions = []) {
|
|
28
|
+
const excusedFiles = new Set(exceptions.filter((e) => e.reason?.trim()).map((e) => e.file));
|
|
29
|
+
const findings = [];
|
|
30
|
+
let excused = 0;
|
|
31
|
+
for (const { path, text } of files) {
|
|
32
|
+
if (excusedFiles.has(path)) continue;
|
|
33
|
+
const lines = text.split("\n");
|
|
34
|
+
const lineOf = (index) => text.slice(0, index).split("\n").length;
|
|
35
|
+
const found = [];
|
|
36
|
+
for (const m of text.matchAll(MEDIA)) {
|
|
37
|
+
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()}` });
|
|
38
|
+
}
|
|
39
|
+
for (const m of text.matchAll(VARIANT)) {
|
|
40
|
+
if (!NOT_VARIANTS.has(m[1])) found.push({ line: lineOf(m.index), what: m[0] });
|
|
41
|
+
}
|
|
42
|
+
for (const { line, what } of found.sort((a, b) => a.line - b.line)) {
|
|
43
|
+
const here = reasonOf(lines[line - 1]);
|
|
44
|
+
const above = reasonOf(lines[line - 2]);
|
|
45
|
+
if (here || above) {
|
|
46
|
+
excused++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
findings.push({ file: path, line, what: here === "" || above === "" ? `${what} (mobile-first-ok with no reason)` : what });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { findings, excused };
|
|
53
|
+
}
|
|
54
|
+
function checkExceptions(exceptions, exists) {
|
|
55
|
+
const problems = [];
|
|
56
|
+
exceptions.forEach((e, i) => {
|
|
57
|
+
const at = `exceptions[${i}]`;
|
|
58
|
+
const x = e;
|
|
59
|
+
if (typeof x?.file !== "string" || !x.file.trim()) problems.push(`${at}: names no file`);
|
|
60
|
+
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.`);
|
|
61
|
+
else if (!exists(x.file)) problems.push(`${at}: ${x.file} is not there. Remove the exception.`);
|
|
62
|
+
});
|
|
63
|
+
return problems;
|
|
64
|
+
}
|
|
65
|
+
var normaliseElement = (element) => element.replace(/("[^]*)$/, (text) => text.replace(/\d+/g, "#"));
|
|
66
|
+
var tally = (items, key) => {
|
|
67
|
+
const m = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const item of items) {
|
|
69
|
+
const k = key(item);
|
|
70
|
+
const had = m.get(k);
|
|
71
|
+
if (had) had.count++;
|
|
72
|
+
else m.set(k, { item, count: 1 });
|
|
73
|
+
}
|
|
74
|
+
return m;
|
|
75
|
+
};
|
|
76
|
+
function judgeStatic(findings, baseline) {
|
|
77
|
+
const byFile = tally(findings, (f) => f.file);
|
|
78
|
+
const allowed = new Map(baseline.static.map((b) => [b.file, b.count]));
|
|
79
|
+
const fresh = [];
|
|
80
|
+
const stale = [];
|
|
81
|
+
let known = 0;
|
|
82
|
+
for (const [file, { count }] of byFile) {
|
|
83
|
+
const allow = allowed.get(file) ?? 0;
|
|
84
|
+
known += Math.min(count, allow);
|
|
85
|
+
if (count > allow) {
|
|
86
|
+
const here = findings.filter((f) => f.file === file);
|
|
87
|
+
const lead = allow ? `${file}: ${count} desktop-first rules, the baseline allows ${allow}. One of these is new:` : `${file}:`;
|
|
88
|
+
fresh.push(`${lead}
|
|
89
|
+
${here.map((f) => ` line ${f.line}: ${f.what}`).join("\n")}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const { file, count } of baseline.static) {
|
|
93
|
+
const now = byFile.get(file)?.count ?? 0;
|
|
94
|
+
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)"}.`);
|
|
95
|
+
}
|
|
96
|
+
return { fresh, stale, known };
|
|
97
|
+
}
|
|
98
|
+
var measuredKey = (route, check, element) => `${route}\0${check}\0${element}`;
|
|
99
|
+
function judgeMeasured(routes, baseline) {
|
|
100
|
+
const fresh = [];
|
|
101
|
+
const stale = [];
|
|
102
|
+
let known = 0;
|
|
103
|
+
const allowed = new Map(baseline.measured.map((b) => [measuredKey(b.route, b.check, b.element), b.count]));
|
|
104
|
+
const seen = /* @__PURE__ */ new Map();
|
|
105
|
+
const measured = new Set(routes.map((r) => r.route));
|
|
106
|
+
for (const { route, findings } of routes) {
|
|
107
|
+
const counted = tally(findings, (f) => measuredKey(route, f.check, normaliseElement(f.element)));
|
|
108
|
+
for (const [key, { item, count }] of counted) {
|
|
109
|
+
seen.set(key, count);
|
|
110
|
+
const allow = allowed.get(key) ?? 0;
|
|
111
|
+
known += Math.min(count, allow);
|
|
112
|
+
if (count > allow) {
|
|
113
|
+
const times = allow ? ` (${count} found, the baseline allows ${allow})` : count > 1 ? ` (${count} of them)` : "";
|
|
114
|
+
fresh.push(`${route}: ${LABEL[item.check]} ${item.element}: ${item.detail}${times}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const missing = /* @__PURE__ */ new Set();
|
|
119
|
+
for (const b of baseline.measured) {
|
|
120
|
+
const key = measuredKey(b.route, b.check, b.element);
|
|
121
|
+
if (!measured.has(b.route)) {
|
|
122
|
+
if (!missing.has(b.route)) stale.push(`${b.route}: in the baseline but not measured this run. Measure it, or remove its lines.`);
|
|
123
|
+
missing.add(b.route);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const now = seen.get(key) ?? 0;
|
|
127
|
+
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)"}.`);
|
|
128
|
+
}
|
|
129
|
+
return { fresh, stale, known };
|
|
130
|
+
}
|
|
131
|
+
var LABEL = {
|
|
132
|
+
scroll: "scrolls sideways",
|
|
133
|
+
target: "small target",
|
|
134
|
+
field: "small field",
|
|
135
|
+
text: "small text",
|
|
136
|
+
title: "title cut off"
|
|
137
|
+
};
|
|
138
|
+
function writeBaseline(findings, routes, note) {
|
|
139
|
+
const byFile = tally(findings, (f) => f.file);
|
|
140
|
+
const measured = routes.flatMap(
|
|
141
|
+
({ 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 }))
|
|
142
|
+
);
|
|
143
|
+
return {
|
|
144
|
+
...note ? { note } : {},
|
|
145
|
+
static: [...byFile.values()].map(({ item, count }) => ({ file: item.file, count, found: findings.filter((f) => f.file === item.file).map((f) => f.what) })).sort((a, b) => a.file.localeCompare(b.file)),
|
|
146
|
+
measured: measured.sort((a, b) => a.route.localeCompare(b.route) || a.check.localeCompare(b.check) || a.element.localeCompare(b.element))
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/phone/cli.ts
|
|
151
|
+
var USAGE = "usage: craft phone-first [--config phone-first.json] [--measured <dir>] [--update-baseline] [--json]";
|
|
152
|
+
function parse(args) {
|
|
153
|
+
const flags = { config: "phone-first.json", update: false, json: false };
|
|
154
|
+
for (let i = 0; i < args.length; i++) {
|
|
155
|
+
const a = args[i];
|
|
156
|
+
if (a === "--update-baseline") flags.update = true;
|
|
157
|
+
else if (a === "--json") flags.json = true;
|
|
158
|
+
else if (a === "--config" || a === "--measured") {
|
|
159
|
+
const v = args[++i];
|
|
160
|
+
if (!v || v.startsWith("--")) return `${a} needs a value`;
|
|
161
|
+
if (a === "--config") flags.config = v;
|
|
162
|
+
else flags.measured = v;
|
|
163
|
+
} else return `unknown argument ${a}`;
|
|
164
|
+
}
|
|
165
|
+
return flags;
|
|
166
|
+
}
|
|
167
|
+
function loadConfig(path) {
|
|
168
|
+
if (!existsSync(path)) throw new Error(`no ${path}. Write one: { "source": ["<dirs with styles and components>"], "baseline": "phone-first.baseline.json" }`);
|
|
169
|
+
const c = JSON.parse(readFileSync(path, "utf8"));
|
|
170
|
+
if (!Array.isArray(c.source) || c.source.length === 0 || c.source.some((s) => typeof s !== "string")) throw new Error(`${path}: "source" should list the folders to scan`);
|
|
171
|
+
if (c.ignore !== void 0 && (!Array.isArray(c.ignore) || c.ignore.some((s) => typeof s !== "string"))) throw new Error(`${path}: "ignore" should be a list of globs`);
|
|
172
|
+
if (c.exceptions !== void 0 && !Array.isArray(c.exceptions)) throw new Error(`${path}: "exceptions" should be a list of { "file", "reason" }`);
|
|
173
|
+
return { source: c.source, ignore: c.ignore ?? [], exceptions: c.exceptions ?? [], baseline: c.baseline ?? "phone-first.baseline.json" };
|
|
174
|
+
}
|
|
175
|
+
function loadBaseline(path) {
|
|
176
|
+
if (!existsSync(path)) return EMPTY_BASELINE;
|
|
177
|
+
const b = JSON.parse(readFileSync(path, "utf8"));
|
|
178
|
+
return { ...b.note ? { note: b.note } : {}, static: b.static ?? [], measured: b.measured ?? [] };
|
|
179
|
+
}
|
|
180
|
+
function loadMeasured(dir) {
|
|
181
|
+
if (!existsSync(dir)) throw new Error(`--measured ${dir}: no such folder. Run the portal's phone-first specs first; they write it.`);
|
|
182
|
+
const routes = readdirSync(dir).filter((f) => f.endsWith(".json")).sort().map((f) => JSON.parse(readFileSync(join(dir, f), "utf8")));
|
|
183
|
+
if (routes.length === 0) throw new Error(`--measured ${dir}: no measured routes in it. Did the phone-first specs run?`);
|
|
184
|
+
return routes;
|
|
185
|
+
}
|
|
186
|
+
function runPhoneFirst(args, io) {
|
|
187
|
+
const flags = parse(args);
|
|
188
|
+
if (typeof flags === "string") {
|
|
189
|
+
io.err(`craft phone-first: ${flags}
|
|
190
|
+
${USAGE}`);
|
|
191
|
+
return 2;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const config = loadConfig(resolve(io.cwd, flags.config));
|
|
195
|
+
const baselinePath = resolve(io.cwd, config.baseline);
|
|
196
|
+
const baseline = loadBaseline(baselinePath);
|
|
197
|
+
const exceptionProblems = checkExceptions(config.exceptions, (f) => existsSync(resolve(io.cwd, f)));
|
|
198
|
+
const sources = config.source.filter((s2) => existsSync(resolve(io.cwd, s2)));
|
|
199
|
+
const missing = config.source.filter((s2) => !sources.includes(s2));
|
|
200
|
+
if (missing.length) throw new Error(`${flags.config}: source ${missing.join(", ")} not found`);
|
|
201
|
+
const files = readPaths(sources, io.cwd, SOURCE_FILE, void 0, config.ignore);
|
|
202
|
+
const { findings, excused } = scanPhoneFirst(files, config.exceptions);
|
|
203
|
+
const routes = flags.measured ? loadMeasured(resolve(io.cwd, flags.measured)) : null;
|
|
204
|
+
if (flags.update) {
|
|
205
|
+
const next = writeBaseline(findings, routes ?? [], baseline.note);
|
|
206
|
+
if (!routes) next.measured = baseline.measured;
|
|
207
|
+
writeFileSync(baselinePath, `${JSON.stringify(next, null, 2)}
|
|
208
|
+
`);
|
|
209
|
+
io.out(`craft phone-first: wrote ${config.baseline} (${next.static.reduce((n, s2) => n + s2.count, 0)} static, ${next.measured.reduce((n, m2) => n + m2.count, 0)} measured)`);
|
|
210
|
+
return exceptionProblems.length ? 1 : 0;
|
|
211
|
+
}
|
|
212
|
+
const s = judgeStatic(findings, baseline);
|
|
213
|
+
const m = routes ? judgeMeasured(routes, baseline) : { fresh: [], stale: [], known: 0 };
|
|
214
|
+
const failed = exceptionProblems.length + s.fresh.length + s.stale.length + m.fresh.length + m.stale.length > 0;
|
|
215
|
+
if (flags.json) {
|
|
216
|
+
io.out(toJson({ pass: !failed, files: files.length, excused, routes: routes?.map((r) => r.route) ?? null, exceptionProblems, static: s, measured: m }));
|
|
217
|
+
return failed ? 1 : 0;
|
|
218
|
+
}
|
|
219
|
+
const section = (title, lines) => lines.length ? `
|
|
220
|
+
${title}
|
|
221
|
+
${lines.map((l) => ` ${l}`).join("\n")}` : "";
|
|
222
|
+
const known = s.known + m.known;
|
|
223
|
+
const summary = [
|
|
224
|
+
`${files.length} files${routes ? ` and ${routes.length} route${routes.length === 1 ? "" : "s"} at 375px` : ""}`,
|
|
225
|
+
excused ? `${excused} recorded exception${excused === 1 ? "" : "s"}` : null,
|
|
226
|
+
known ? `${known} known breach${known === 1 ? "" : "es"} in ${config.baseline}` : null
|
|
227
|
+
].filter(Boolean).join(", ");
|
|
228
|
+
if (!failed) {
|
|
229
|
+
io.out(`craft phone-first: pass. ${summary}.`);
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
io.err(
|
|
233
|
+
[
|
|
234
|
+
`craft phone-first: fail. ${summary}.`,
|
|
235
|
+
section("The exceptions:", exceptionProblems),
|
|
236
|
+
section("Built desktop first (write the phone as the base styles and widen with min-width; a genuine exception takes `mobile-first-ok: <reason>` on its line):", s.fresh),
|
|
237
|
+
section("Measured at 375px:", m.fresh),
|
|
238
|
+
section("The baseline claims more than is there (good: lower it, so the fix stays fixed):", [...s.stale, ...m.stale])
|
|
239
|
+
].join("")
|
|
240
|
+
);
|
|
241
|
+
return 1;
|
|
242
|
+
} catch (error) {
|
|
243
|
+
io.err(`craft phone-first: ${error.message}`);
|
|
244
|
+
return 2;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export {
|
|
248
|
+
runPhoneFirst
|
|
249
|
+
};
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
expandNullTargets,
|
|
6
6
|
loadNull
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-D2FTTM4F.js";
|
|
8
8
|
import "./chunk-OQXTQQAU.js";
|
|
9
9
|
import {
|
|
10
10
|
parseFlags
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-BK4AS2RU.js";
|
|
12
12
|
import {
|
|
13
13
|
toJson
|
|
14
14
|
} from "./chunk-QIEDBL4J.js";
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
formatPairs,
|
|
5
5
|
loadEstate,
|
|
6
6
|
runEstate
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-6M2LK7QE.js";
|
|
8
8
|
import "./chunk-4LJFZH2I.js";
|
|
9
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-D2FTTM4F.js";
|
|
10
10
|
import "./chunk-OQXTQQAU.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-BK4AS2RU.js";
|
|
12
12
|
import "./chunk-QIEDBL4J.js";
|
|
13
13
|
import "./chunk-WUACK2HX.js";
|
|
14
14
|
import "./chunk-WK2WMWTH.js";
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
runHarvest,
|
|
10
10
|
runNull,
|
|
11
11
|
serveDir
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-D2FTTM4F.js";
|
|
13
13
|
import "./chunk-OQXTQQAU.js";
|
|
14
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-BK4AS2RU.js";
|
|
15
15
|
import "./chunk-QIEDBL4J.js";
|
|
16
16
|
import "./chunk-WUACK2HX.js";
|
|
17
17
|
import "./chunk-WK2WMWTH.js";
|
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
merge,
|
|
5
5
|
resolvePages,
|
|
6
6
|
runAudit
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-XVMKZ4SM.js";
|
|
8
8
|
import "./chunk-VDUIIWG7.js";
|
|
9
9
|
import "./chunk-JYNXGCYZ.js";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-D2FTTM4F.js";
|
|
11
11
|
import "./chunk-OQXTQQAU.js";
|
|
12
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-BK4AS2RU.js";
|
|
13
13
|
import "./chunk-QIEDBL4J.js";
|
|
14
14
|
import "./chunk-WUACK2HX.js";
|
|
15
15
|
import "./chunk-WK2WMWTH.js";
|