@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
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_SWEEP,
|
|
3
|
+
DEFAULT_THRESHOLDS,
|
|
2
4
|
EMPTY_BASELINE,
|
|
5
|
+
LADDER,
|
|
6
|
+
SHORT_PHONE,
|
|
7
|
+
checkScreenBudgets,
|
|
8
|
+
formatScreens,
|
|
3
9
|
judgeMeasured,
|
|
10
|
+
mainActionFor,
|
|
11
|
+
mergeViewportFindings,
|
|
12
|
+
parseBaseline,
|
|
13
|
+
screenBudgetFor,
|
|
14
|
+
sweepWidths,
|
|
4
15
|
writeBaseline
|
|
5
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-XSKLVEI6.js";
|
|
6
17
|
import {
|
|
7
18
|
loadPlaywright
|
|
8
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-6DRBFDWT.js";
|
|
9
20
|
import "./chunk-JYNXGCYZ.js";
|
|
10
21
|
import {
|
|
11
22
|
toJson
|
|
12
23
|
} from "./chunk-QIEDBL4J.js";
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
24
|
+
import "./chunk-DE2ZAV4G.js";
|
|
25
|
+
import "./chunk-ONA73THJ.js";
|
|
15
26
|
import "./chunk-QYBAVU3P.js";
|
|
16
27
|
|
|
17
28
|
// src/phone/viewports-cli.ts
|
|
@@ -25,7 +36,7 @@ var PHONE = {
|
|
|
25
36
|
isMobile: true,
|
|
26
37
|
hasTouch: true
|
|
27
38
|
};
|
|
28
|
-
var PHONE_FLOORS = { targetPx: 44, fieldFontPx: 16, textPx: 12 };
|
|
39
|
+
var PHONE_FLOORS = { targetPx: 44, fieldFontPx: 16, textPx: 12, fieldHeightPx: 48 };
|
|
29
40
|
var DEFAULT_MEASURED_DIR = "phone-first/measured";
|
|
30
41
|
var routeFile = (route) => `${route === "/" ? "home" : route.replace(/^\/+|\/+$/g, "").replace(/[^\w.-]+/g, "-")}.json`;
|
|
31
42
|
|
|
@@ -36,6 +47,7 @@ import { join } from "path";
|
|
|
36
47
|
// src/phone/measure-in-page.ts
|
|
37
48
|
function measurePhoneInPage(floors) {
|
|
38
49
|
const out = [];
|
|
50
|
+
const fieldHeightPx = floors.fieldHeightPx ?? 48;
|
|
39
51
|
const only = floors.only;
|
|
40
52
|
const on = (c) => !only || only.includes(c);
|
|
41
53
|
const vw = document.documentElement.clientWidth;
|
|
@@ -79,6 +91,9 @@ function measurePhoneInPage(floors) {
|
|
|
79
91
|
if (!shown(e) || quiet(e) || e.tagName === "INPUT" && NOT_TEXT.has(e.type)) continue;
|
|
80
92
|
const px = parseFloat(getComputedStyle(e).fontSize);
|
|
81
93
|
if (px < floors.fieldFontPx) out.push({ check: "field", element: describe(e), detail: `set at ${px}px, under ${floors.fieldFontPx}px, so Safari on iOS zooms on focus` });
|
|
94
|
+
const box = e.getBoundingClientRect();
|
|
95
|
+
const height = Math.round(box.height);
|
|
96
|
+
if (box.width > 1 && height > 1 && height < fieldHeightPx) out.push({ check: "field", element: describe(e), detail: `${height}px tall, under ${fieldHeightPx}px (use min-h-12)` });
|
|
82
97
|
}
|
|
83
98
|
for (const e of on("text") ? Array.from(document.body.querySelectorAll("*")) : []) {
|
|
84
99
|
if (!shown(e) || quiet(e) || ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"].includes(e.tagName)) continue;
|
|
@@ -385,76 +400,6 @@ function measureViewportInPage(arg) {
|
|
|
385
400
|
return out;
|
|
386
401
|
}
|
|
387
402
|
|
|
388
|
-
// src/phone/viewports.ts
|
|
389
|
-
var LADDER = [
|
|
390
|
-
{ name: "320", width: 320, height: 568, phone: true },
|
|
391
|
-
{ name: "375", width: 375, height: 812, phone: true },
|
|
392
|
-
{ name: "390", width: 390, height: 844, phone: true },
|
|
393
|
-
{ name: "430", width: 430, height: 932, phone: true },
|
|
394
|
-
{ name: "844x390", width: 844, height: 390, phone: true, landscape: true },
|
|
395
|
-
{ name: "768", width: 768, height: 1024, phone: false },
|
|
396
|
-
{ name: "1024", width: 1024, height: 768, phone: false },
|
|
397
|
-
{ name: "1280", width: 1280, height: 800, phone: false },
|
|
398
|
-
{ name: "1440", width: 1440, height: 900, phone: false },
|
|
399
|
-
{ name: "1920", width: 1920, height: 1080, phone: false }
|
|
400
|
-
];
|
|
401
|
-
var SHORT_PHONE = { name: "375x667", width: 375, height: 667, phone: true };
|
|
402
|
-
var DEFAULT_SWEEP = { from: 320, to: 1920, step: 16, wideStep: 32, wideFrom: 1024, height: 800 };
|
|
403
|
-
function sweepWidths(sweep = DEFAULT_SWEEP) {
|
|
404
|
-
const out = [];
|
|
405
|
-
for (let w = sweep.from; w <= sweep.to; w += w < sweep.wideFrom ? sweep.step : sweep.wideStep) out.push(w);
|
|
406
|
-
if (out.at(-1) !== sweep.to) out.push(sweep.to);
|
|
407
|
-
return out;
|
|
408
|
-
}
|
|
409
|
-
var DEFAULT_THRESHOLDS = {
|
|
410
|
-
maxLayerShare: 0.25,
|
|
411
|
-
landscapeMaxLayerShare: 0.5,
|
|
412
|
-
oversizeRatio: 2.5,
|
|
413
|
-
upscaleRatio: 1,
|
|
414
|
-
maxLineCh: 80,
|
|
415
|
-
overlapShare: 0.4,
|
|
416
|
-
minImagePx: 32
|
|
417
|
-
};
|
|
418
|
-
var mainActionFor = (config, route) => typeof config === "string" ? config : config ? config[route] ?? config["*"] : void 0;
|
|
419
|
-
function formatWidths(failing, tested) {
|
|
420
|
-
const plain = tested.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(a) - Number(b));
|
|
421
|
-
const rank = new Map(plain.map((name, i) => [name, i]));
|
|
422
|
-
const names = [...new Set(failing)];
|
|
423
|
-
const runs = [];
|
|
424
|
-
for (const name of names.filter((n) => rank.has(n)).sort((a, b) => rank.get(a) - rank.get(b))) {
|
|
425
|
-
const last = runs.at(-1);
|
|
426
|
-
if (last && rank.get(name) === rank.get(last.at(-1)) + 1) last.push(name);
|
|
427
|
-
else runs.push([name]);
|
|
428
|
-
}
|
|
429
|
-
const out = runs.map((r) => r.length > 1 ? `${r[0]}-${r.at(-1)}` : r[0]);
|
|
430
|
-
return [...out, ...names.filter((n) => !rank.has(n))].join(", ");
|
|
431
|
-
}
|
|
432
|
-
var keyOf = (f) => `${f.check}\0${f.element.replace(/("[^]*)$/, (t) => t.replace(/\d+/g, "#"))}`;
|
|
433
|
-
function mergeViewportFindings(perViewport, tested) {
|
|
434
|
-
const merged = /* @__PURE__ */ new Map();
|
|
435
|
-
for (const { viewport, findings } of perViewport) {
|
|
436
|
-
const counts = /* @__PURE__ */ new Map();
|
|
437
|
-
for (const f of findings) {
|
|
438
|
-
const k = keyOf(f);
|
|
439
|
-
const had = counts.get(k);
|
|
440
|
-
if (had) had.n++;
|
|
441
|
-
else counts.set(k, { first: f, n: 1 });
|
|
442
|
-
}
|
|
443
|
-
for (const [k, { first, n }] of counts) {
|
|
444
|
-
const had = merged.get(k);
|
|
445
|
-
if (had) {
|
|
446
|
-
had.at.push(viewport);
|
|
447
|
-
had.most = Math.max(had.most, n);
|
|
448
|
-
} else merged.set(k, { first, at: [viewport], most: n });
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
const out = [];
|
|
452
|
-
for (const { first, at, most } of merged.values()) {
|
|
453
|
-
for (let i = 0; i < most; i++) out.push({ ...first, detail: `${first.detail} (at ${formatWidths(at, tested)})` });
|
|
454
|
-
}
|
|
455
|
-
return out;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
403
|
// src/phone/run.ts
|
|
459
404
|
function plan(options) {
|
|
460
405
|
const ladder = options.ladder ?? LADDER;
|
|
@@ -510,6 +455,7 @@ async function collect(page, options) {
|
|
|
510
455
|
const mainAction = mainActionFor(options.mainAction, options.route);
|
|
511
456
|
const perViewport = [];
|
|
512
457
|
const tested = [];
|
|
458
|
+
let screens;
|
|
513
459
|
for (const stop of plan(options)) {
|
|
514
460
|
await page.setViewportSize({ width: stop.width, height: stop.height });
|
|
515
461
|
await settle(page, stop.ladder ? 150 : 60);
|
|
@@ -518,20 +464,36 @@ async function collect(page, options) {
|
|
|
518
464
|
const findings = [];
|
|
519
465
|
if (stop.ladder) findings.push(...await page.evaluate(measurePhoneInPage, { ...floors, only: stop.touch ? ["target", "field", "text"] : ["text"] }));
|
|
520
466
|
findings.push(...await page.evaluate(measureViewportInPage, { groups: stop.groups, thresholds, mainAction, metaViewport: stop.width < 768 }));
|
|
467
|
+
if (stop.name === SHORT_PHONE.name) {
|
|
468
|
+
const height = await page.evaluate(() => document.documentElement.scrollHeight, void 0);
|
|
469
|
+
screens = Math.round(height / stop.height * 10) / 10;
|
|
470
|
+
const budget = screenBudgetFor(options.screens, options.route);
|
|
471
|
+
if (budget !== void 0 && screens > budget) findings.push({ check: "screens", element: "<page>", detail: `${screens} phone screens to the footer at ${stop.width} x ${stop.height}, over the budget of ${budget} for ${options.route}` });
|
|
472
|
+
}
|
|
521
473
|
perViewport.push({ viewport: stop.name, findings });
|
|
522
474
|
tested.push(stop.name);
|
|
523
475
|
}
|
|
524
|
-
return { perViewport, tested };
|
|
476
|
+
return { perViewport, tested, ...screens !== void 0 ? { screens } : {} };
|
|
477
|
+
}
|
|
478
|
+
function mergeCollected(collected) {
|
|
479
|
+
const screens = collected.find((c) => c.screens !== void 0)?.screens;
|
|
480
|
+
return { perViewport: collected.flatMap((c) => c.perViewport), tested: collected.flatMap((c) => c.tested), ...screens !== void 0 ? { screens } : {} };
|
|
525
481
|
}
|
|
526
482
|
function finish(route, collected, out) {
|
|
527
|
-
const result = {
|
|
483
|
+
const result = {
|
|
484
|
+
route,
|
|
485
|
+
viewports: collected.tested,
|
|
486
|
+
...collected.screens !== void 0 ? { screens: collected.screens } : {},
|
|
487
|
+
findings: mergeViewportFindings(collected.perViewport, collected.tested),
|
|
488
|
+
perViewport: collected.perViewport
|
|
489
|
+
};
|
|
528
490
|
if (out !== false) writeMeasured(result, out ?? DEFAULT_MEASURED_DIR);
|
|
529
491
|
return result;
|
|
530
492
|
}
|
|
531
493
|
function writeMeasured(result, dir) {
|
|
532
494
|
mkdirSync(dir, { recursive: true });
|
|
533
|
-
const { route, viewport, viewports, findings } = result;
|
|
534
|
-
writeFileSync(join(dir, routeFile(route)), `${JSON.stringify({ route, ...viewport ? { viewport } : {}, ...viewports ? { viewports } : {}, findings }, null, 1)}
|
|
495
|
+
const { route, viewport, viewports, screens, findings } = result;
|
|
496
|
+
writeFileSync(join(dir, routeFile(route)), `${JSON.stringify({ route, ...viewport ? { viewport } : {}, ...viewports ? { viewports } : {}, ...screens !== void 0 ? { screens } : {}, findings }, null, 1)}
|
|
535
497
|
`);
|
|
536
498
|
}
|
|
537
499
|
var DESKTOP = { viewport: { width: 1024, height: 768 }, deviceScaleFactor: 1 };
|
|
@@ -562,7 +524,7 @@ async function measureSite(browser, options) {
|
|
|
562
524
|
result.errors.push({ route, error: failed });
|
|
563
525
|
continue;
|
|
564
526
|
}
|
|
565
|
-
result.routes.push(finish(route,
|
|
527
|
+
result.routes.push(finish(route, mergeCollected(collected), options.out));
|
|
566
528
|
}
|
|
567
529
|
return result;
|
|
568
530
|
}
|
|
@@ -596,8 +558,7 @@ function loadConfig(path, cwd) {
|
|
|
596
558
|
}
|
|
597
559
|
function loadBaseline(path) {
|
|
598
560
|
if (!existsSync(path)) return EMPTY_BASELINE;
|
|
599
|
-
|
|
600
|
-
return { ...b.note ? { note: b.note } : {}, static: b.static ?? [], measured: b.measured ?? [] };
|
|
561
|
+
return parseBaseline(JSON.parse(readFileSync(path, "utf8")), path);
|
|
601
562
|
}
|
|
602
563
|
async function runViewports(args, io) {
|
|
603
564
|
const flags = parse(args);
|
|
@@ -629,6 +590,7 @@ ${USAGE}`);
|
|
|
629
590
|
...config.mainAction ? { mainAction: config.mainAction } : {},
|
|
630
591
|
...config.thresholds ? { thresholds: config.thresholds } : {},
|
|
631
592
|
...config.floors ? { floors: config.floors } : {},
|
|
593
|
+
...config.screens ? { screens: checkScreenBudgets(config.screens, "viewports.json screens") } : {},
|
|
632
594
|
sweep: flags.sweep ? config.sweep : false,
|
|
633
595
|
...captures ? {
|
|
634
596
|
onViewport: async (page, viewport, route) => {
|
|
@@ -642,7 +604,7 @@ ${USAGE}`);
|
|
|
642
604
|
const baseline = loadBaseline(baselinePath);
|
|
643
605
|
const measuredRoutes = new Set(site.routes.map((r) => r.route));
|
|
644
606
|
if (flags.update) {
|
|
645
|
-
const next = writeBaseline([], site.routes, baseline.note);
|
|
607
|
+
const next = writeBaseline([], site.routes, baseline.note, baseline);
|
|
646
608
|
next.static = baseline.static;
|
|
647
609
|
next.measured = [...baseline.measured.filter((m) => !measuredRoutes.has(m.route)), ...next.measured].sort((a, b) => a.route.localeCompare(b.route) || a.check.localeCompare(b.check) || a.element.localeCompare(b.element));
|
|
648
610
|
writeFileSync2(baselinePath, `${JSON.stringify(next, null, 2)}
|
|
@@ -653,7 +615,7 @@ ${USAGE}`);
|
|
|
653
615
|
const judged = judgeMeasured(site.routes, { ...baseline, measured: baseline.measured.filter((m) => measuredRoutes.has(m.route)) });
|
|
654
616
|
const failed = site.errors.length + judged.fresh.length + judged.stale.length > 0;
|
|
655
617
|
if (flags.json) {
|
|
656
|
-
io.out(toJson({ pass: !failed, routes: site.routes.map((r) => ({ route: r.route, viewports: r.viewports, findings: r.findings.length })), errors: site.errors, measured: judged }));
|
|
618
|
+
io.out(toJson({ pass: !failed, routes: site.routes.map((r) => ({ route: r.route, viewports: r.viewports, screens: r.screens ?? null, findings: r.findings.length })), errors: site.errors, measured: judged }));
|
|
657
619
|
return failed ? 1 : 0;
|
|
658
620
|
}
|
|
659
621
|
const section = (title, lines) => lines.length ? `
|
|
@@ -661,13 +623,16 @@ ${title}
|
|
|
661
623
|
${lines.map((l) => ` ${l}`).join("\n")}` : "";
|
|
662
624
|
const viewports = site.routes[0]?.viewports ?? [];
|
|
663
625
|
const summary = `${site.routes.length} route${site.routes.length === 1 ? "" : "s"} at ${viewports.length} viewports${judged.known ? `, ${judged.known} known breach${judged.known === 1 ? "" : "es"} in the baseline` : ""}`;
|
|
626
|
+
const screens = formatScreens(site.routes, config.screens);
|
|
627
|
+
const screensLine = screens ? `
|
|
628
|
+
phone screens to the footer: ${screens}` : "";
|
|
664
629
|
if (!failed) {
|
|
665
|
-
io.out(`craft viewports: pass. ${summary}
|
|
630
|
+
io.out(`craft viewports: pass. ${summary}.${screensLine}`);
|
|
666
631
|
return 0;
|
|
667
632
|
}
|
|
668
633
|
io.err(
|
|
669
634
|
[
|
|
670
|
-
`craft viewports: fail. ${summary}
|
|
635
|
+
`craft viewports: fail. ${summary}.${screensLine}`,
|
|
671
636
|
section("Did not answer (not measured, so not passed):", site.errors.map((e) => `${e.route}: ${e.error}`)),
|
|
672
637
|
section("Measured across the viewports, phone first:", judged.fresh),
|
|
673
638
|
section("The baseline claims more than is there (good: lower it, so the fix stays fixed):", judged.stale)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@domandigital/craft",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The Doman Digital house craft standard as numbers: OKLCh colour ramps that snap to existing anchors, derived semantic tokens with measured WCAG and APCA contrast, one shared motion vocabulary for CSS and JS, and a base stylesheet. Zero dependencies.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/word-lists.json
CHANGED
package/dist/chunk-IJUZ7RV4.js
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
// src/phone/judge.ts
|
|
2
|
-
var EMPTY_BASELINE = { static: [], measured: [] };
|
|
3
|
-
var MEDIA = /@(?:custom-)?media\b([^{;]*)/g;
|
|
4
|
-
var CAPS_WIDTH = /\bmax-(?:device-)?width\s*:|\bwidth\s*<|>=?\s*(?:device-)?width\b/i;
|
|
5
|
-
var VARIANT = /(?<![\w@-])max-(\[[^\]\s]+\]|[a-z0-9]+):/g;
|
|
6
|
-
var NOT_VARIANTS = /* @__PURE__ */ new Set(["width", "height", "lines", "resolution", "zoom", "inline-size", "block-size"]);
|
|
7
|
-
var OK = /mobile-first-ok\b:?(.*)/;
|
|
8
|
-
function reasonOf(line) {
|
|
9
|
-
const m = line?.match(OK);
|
|
10
|
-
return m ? m[1].replace(/\*\/|-->|[{}]/g, "").trim() : null;
|
|
11
|
-
}
|
|
12
|
-
function scanPhoneFirst(files, exceptions = []) {
|
|
13
|
-
const excusedFiles = new Set(exceptions.filter((e) => e.reason?.trim()).map((e) => e.file));
|
|
14
|
-
const findings = [];
|
|
15
|
-
let excused = 0;
|
|
16
|
-
for (const { path, text } of files) {
|
|
17
|
-
if (excusedFiles.has(path)) continue;
|
|
18
|
-
const lines = text.split("\n");
|
|
19
|
-
const lineOf = (index) => text.slice(0, index).split("\n").length;
|
|
20
|
-
const found = [];
|
|
21
|
-
for (const m of text.matchAll(MEDIA)) {
|
|
22
|
-
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()}` });
|
|
23
|
-
}
|
|
24
|
-
for (const m of text.matchAll(VARIANT)) {
|
|
25
|
-
if (!NOT_VARIANTS.has(m[1])) found.push({ line: lineOf(m.index), what: m[0] });
|
|
26
|
-
}
|
|
27
|
-
for (const { line, what } of found.sort((a, b) => a.line - b.line)) {
|
|
28
|
-
const here = reasonOf(lines[line - 1]);
|
|
29
|
-
const above = reasonOf(lines[line - 2]);
|
|
30
|
-
if (here || above) {
|
|
31
|
-
excused++;
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
findings.push({ file: path, line, what: here === "" || above === "" ? `${what} (mobile-first-ok with no reason)` : what });
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return { findings, excused };
|
|
38
|
-
}
|
|
39
|
-
function checkExceptions(exceptions, exists) {
|
|
40
|
-
const problems = [];
|
|
41
|
-
exceptions.forEach((e, i) => {
|
|
42
|
-
const at = `exceptions[${i}]`;
|
|
43
|
-
const x = e;
|
|
44
|
-
if (typeof x?.file !== "string" || !x.file.trim()) problems.push(`${at}: names no file`);
|
|
45
|
-
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.`);
|
|
46
|
-
else if (!exists(x.file)) problems.push(`${at}: ${x.file} is not there. Remove the exception.`);
|
|
47
|
-
});
|
|
48
|
-
return problems;
|
|
49
|
-
}
|
|
50
|
-
var normaliseElement = (element) => element.replace(/("[^]*)$/, (text) => text.replace(/\d+/g, "#"));
|
|
51
|
-
var tally = (items, key) => {
|
|
52
|
-
const m = /* @__PURE__ */ new Map();
|
|
53
|
-
for (const item of items) {
|
|
54
|
-
const k = key(item);
|
|
55
|
-
const had = m.get(k);
|
|
56
|
-
if (had) had.count++;
|
|
57
|
-
else m.set(k, { item, count: 1 });
|
|
58
|
-
}
|
|
59
|
-
return m;
|
|
60
|
-
};
|
|
61
|
-
function judgeStatic(findings, baseline) {
|
|
62
|
-
const byFile = tally(findings, (f) => f.file);
|
|
63
|
-
const allowed = new Map(baseline.static.map((b) => [b.file, b.count]));
|
|
64
|
-
const fresh = [];
|
|
65
|
-
const stale = [];
|
|
66
|
-
let known = 0;
|
|
67
|
-
for (const [file, { count }] of byFile) {
|
|
68
|
-
const allow = allowed.get(file) ?? 0;
|
|
69
|
-
known += Math.min(count, allow);
|
|
70
|
-
if (count > allow) {
|
|
71
|
-
const here = findings.filter((f) => f.file === file);
|
|
72
|
-
const lead = allow ? `${file}: ${count} desktop-first rules, the baseline allows ${allow}. One of these is new:` : `${file}:`;
|
|
73
|
-
fresh.push(`${lead}
|
|
74
|
-
${here.map((f) => ` line ${f.line}: ${f.what}`).join("\n")}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
for (const { file, count } of baseline.static) {
|
|
78
|
-
const now = byFile.get(file)?.count ?? 0;
|
|
79
|
-
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)"}.`);
|
|
80
|
-
}
|
|
81
|
-
return { fresh, stale, known };
|
|
82
|
-
}
|
|
83
|
-
var measuredKey = (route, check, element) => `${route}\0${check}\0${element}`;
|
|
84
|
-
function judgeMeasured(routes, baseline) {
|
|
85
|
-
const fresh = [];
|
|
86
|
-
const stale = [];
|
|
87
|
-
let known = 0;
|
|
88
|
-
const allowed = new Map(baseline.measured.map((b) => [measuredKey(b.route, b.check, b.element), b.count]));
|
|
89
|
-
const seen = /* @__PURE__ */ new Map();
|
|
90
|
-
const measured = new Set(routes.map((r) => r.route));
|
|
91
|
-
for (const { route, findings } of routes) {
|
|
92
|
-
const counted = tally(findings, (f) => measuredKey(route, f.check, normaliseElement(f.element)));
|
|
93
|
-
for (const [key, { item, count }] of counted) {
|
|
94
|
-
seen.set(key, count);
|
|
95
|
-
const allow = allowed.get(key) ?? 0;
|
|
96
|
-
known += Math.min(count, allow);
|
|
97
|
-
if (count > allow) {
|
|
98
|
-
const times = allow ? ` (${count} found, the baseline allows ${allow})` : count > 1 ? ` (${count} of them)` : "";
|
|
99
|
-
fresh.push(`${route}: ${LABEL[item.check]} ${item.element}: ${item.detail}${times}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
const missing = /* @__PURE__ */ new Set();
|
|
104
|
-
for (const b of baseline.measured) {
|
|
105
|
-
const key = measuredKey(b.route, b.check, b.element);
|
|
106
|
-
if (!measured.has(b.route)) {
|
|
107
|
-
if (!missing.has(b.route)) stale.push(`${b.route}: in the baseline but not measured this run. Measure it, or remove its lines.`);
|
|
108
|
-
missing.add(b.route);
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
const now = seen.get(key) ?? 0;
|
|
112
|
-
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)"}.`);
|
|
113
|
-
}
|
|
114
|
-
return { fresh, stale, known };
|
|
115
|
-
}
|
|
116
|
-
var LABEL = {
|
|
117
|
-
scroll: "scrolls sideways",
|
|
118
|
-
target: "small target",
|
|
119
|
-
field: "small field",
|
|
120
|
-
text: "small text",
|
|
121
|
-
title: "title cut off",
|
|
122
|
-
overlap: "text overlaps text",
|
|
123
|
-
action: "main action out of reach",
|
|
124
|
-
layers: "fixed layers cover too much",
|
|
125
|
-
landscape: "landscape screen eaten by fixed layers",
|
|
126
|
-
image: "image not sized for its place",
|
|
127
|
-
input: "wrong keyboard",
|
|
128
|
-
autocomplete: "field the phone cannot fill in",
|
|
129
|
-
tel: "phone number is not a link",
|
|
130
|
-
linelength: "line too long"
|
|
131
|
-
};
|
|
132
|
-
function writeBaseline(findings, routes, note) {
|
|
133
|
-
const byFile = tally(findings, (f) => f.file);
|
|
134
|
-
const measured = routes.flatMap(
|
|
135
|
-
({ 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 }))
|
|
136
|
-
);
|
|
137
|
-
return {
|
|
138
|
-
...note ? { note } : {},
|
|
139
|
-
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)),
|
|
140
|
-
measured: measured.sort((a, b) => a.route.localeCompare(b.route) || a.check.localeCompare(b.check) || a.element.localeCompare(b.element))
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export {
|
|
145
|
-
EMPTY_BASELINE,
|
|
146
|
-
scanPhoneFirst,
|
|
147
|
-
checkExceptions,
|
|
148
|
-
judgeStatic,
|
|
149
|
-
judgeMeasured,
|
|
150
|
-
writeBaseline
|
|
151
|
-
};
|
package/dist/cli-56H5IPWT.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
EMPTY_BASELINE,
|
|
3
|
-
checkExceptions,
|
|
4
|
-
judgeMeasured,
|
|
5
|
-
judgeStatic,
|
|
6
|
-
scanPhoneFirst,
|
|
7
|
-
writeBaseline
|
|
8
|
-
} from "./chunk-IJUZ7RV4.js";
|
|
9
|
-
import {
|
|
10
|
-
SOURCE_FILE,
|
|
11
|
-
readPaths
|
|
12
|
-
} from "./chunk-TSO5W32N.js";
|
|
13
|
-
import {
|
|
14
|
-
toJson
|
|
15
|
-
} from "./chunk-QIEDBL4J.js";
|
|
16
|
-
import "./chunk-PFVSKIPM.js";
|
|
17
|
-
import "./chunk-FBWBDBW4.js";
|
|
18
|
-
import "./chunk-QYBAVU3P.js";
|
|
19
|
-
|
|
20
|
-
// src/phone/cli.ts
|
|
21
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
22
|
-
import { join, resolve } from "path";
|
|
23
|
-
var USAGE = "usage: craft phone-first [--config phone-first.json] [--measured <dir>] [--update-baseline] [--json]";
|
|
24
|
-
function parse(args) {
|
|
25
|
-
const flags = { config: "phone-first.json", update: false, json: false };
|
|
26
|
-
for (let i = 0; i < args.length; i++) {
|
|
27
|
-
const a = args[i];
|
|
28
|
-
if (a === "--update-baseline") flags.update = true;
|
|
29
|
-
else if (a === "--json") flags.json = true;
|
|
30
|
-
else if (a === "--config" || a === "--measured") {
|
|
31
|
-
const v = args[++i];
|
|
32
|
-
if (!v || v.startsWith("--")) return `${a} needs a value`;
|
|
33
|
-
if (a === "--config") flags.config = v;
|
|
34
|
-
else flags.measured = v;
|
|
35
|
-
} else return `unknown argument ${a}`;
|
|
36
|
-
}
|
|
37
|
-
return flags;
|
|
38
|
-
}
|
|
39
|
-
function loadConfig(path) {
|
|
40
|
-
if (!existsSync(path)) throw new Error(`no ${path}. Write one: { "source": ["<dirs with styles and components>"], "baseline": "phone-first.baseline.json" }`);
|
|
41
|
-
const c = JSON.parse(readFileSync(path, "utf8"));
|
|
42
|
-
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`);
|
|
43
|
-
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`);
|
|
44
|
-
if (c.exceptions !== void 0 && !Array.isArray(c.exceptions)) throw new Error(`${path}: "exceptions" should be a list of { "file", "reason" }`);
|
|
45
|
-
return { source: c.source, ignore: c.ignore ?? [], exceptions: c.exceptions ?? [], baseline: c.baseline ?? "phone-first.baseline.json" };
|
|
46
|
-
}
|
|
47
|
-
function loadBaseline(path) {
|
|
48
|
-
if (!existsSync(path)) return EMPTY_BASELINE;
|
|
49
|
-
const b = JSON.parse(readFileSync(path, "utf8"));
|
|
50
|
-
return { ...b.note ? { note: b.note } : {}, static: b.static ?? [], measured: b.measured ?? [] };
|
|
51
|
-
}
|
|
52
|
-
function loadMeasured(dir) {
|
|
53
|
-
if (!existsSync(dir)) throw new Error(`--measured ${dir}: no such folder. Run the portal's phone-first specs first; they write it.`);
|
|
54
|
-
const routes = readdirSync(dir).filter((f) => f.endsWith(".json")).sort().map((f) => JSON.parse(readFileSync(join(dir, f), "utf8")));
|
|
55
|
-
if (routes.length === 0) throw new Error(`--measured ${dir}: no measured routes in it. Did the phone-first specs run?`);
|
|
56
|
-
return routes;
|
|
57
|
-
}
|
|
58
|
-
function runPhoneFirst(args, io) {
|
|
59
|
-
const flags = parse(args);
|
|
60
|
-
if (typeof flags === "string") {
|
|
61
|
-
io.err(`craft phone-first: ${flags}
|
|
62
|
-
${USAGE}`);
|
|
63
|
-
return 2;
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
const config = loadConfig(resolve(io.cwd, flags.config));
|
|
67
|
-
const baselinePath = resolve(io.cwd, config.baseline);
|
|
68
|
-
const baseline = loadBaseline(baselinePath);
|
|
69
|
-
const exceptionProblems = checkExceptions(config.exceptions, (f) => existsSync(resolve(io.cwd, f)));
|
|
70
|
-
const sources = config.source.filter((s2) => existsSync(resolve(io.cwd, s2)));
|
|
71
|
-
const missing = config.source.filter((s2) => !sources.includes(s2));
|
|
72
|
-
if (missing.length) throw new Error(`${flags.config}: source ${missing.join(", ")} not found`);
|
|
73
|
-
const files = readPaths(sources, io.cwd, SOURCE_FILE, void 0, config.ignore);
|
|
74
|
-
const { findings, excused } = scanPhoneFirst(files, config.exceptions);
|
|
75
|
-
const routes = flags.measured ? loadMeasured(resolve(io.cwd, flags.measured)) : null;
|
|
76
|
-
if (flags.update) {
|
|
77
|
-
const next = writeBaseline(findings, routes ?? [], baseline.note);
|
|
78
|
-
if (!routes) next.measured = baseline.measured;
|
|
79
|
-
writeFileSync(baselinePath, `${JSON.stringify(next, null, 2)}
|
|
80
|
-
`);
|
|
81
|
-
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)`);
|
|
82
|
-
return exceptionProblems.length ? 1 : 0;
|
|
83
|
-
}
|
|
84
|
-
const s = judgeStatic(findings, baseline);
|
|
85
|
-
const m = routes ? judgeMeasured(routes, baseline) : { fresh: [], stale: [], known: 0 };
|
|
86
|
-
const failed = exceptionProblems.length + s.fresh.length + s.stale.length + m.fresh.length + m.stale.length > 0;
|
|
87
|
-
if (flags.json) {
|
|
88
|
-
io.out(toJson({ pass: !failed, files: files.length, excused, routes: routes?.map((r) => r.route) ?? null, exceptionProblems, static: s, measured: m }));
|
|
89
|
-
return failed ? 1 : 0;
|
|
90
|
-
}
|
|
91
|
-
const wide = routes?.some((r) => r.viewports) ?? false;
|
|
92
|
-
const section = (title, lines) => lines.length ? `
|
|
93
|
-
${title}
|
|
94
|
-
${lines.map((l) => ` ${l}`).join("\n")}` : "";
|
|
95
|
-
const known = s.known + m.known;
|
|
96
|
-
const summary = [
|
|
97
|
-
`${files.length} files${routes ? ` and ${routes.length} route${routes.length === 1 ? "" : "s"} at ${wide ? "every viewport" : "375px"}` : ""}`,
|
|
98
|
-
excused ? `${excused} recorded exception${excused === 1 ? "" : "s"}` : null,
|
|
99
|
-
known ? `${known} known breach${known === 1 ? "" : "es"} in ${config.baseline}` : null
|
|
100
|
-
].filter(Boolean).join(", ");
|
|
101
|
-
if (!failed) {
|
|
102
|
-
io.out(`craft phone-first: pass. ${summary}.`);
|
|
103
|
-
return 0;
|
|
104
|
-
}
|
|
105
|
-
io.err(
|
|
106
|
-
[
|
|
107
|
-
`craft phone-first: fail. ${summary}.`,
|
|
108
|
-
section("The exceptions:", exceptionProblems),
|
|
109
|
-
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),
|
|
110
|
-
section(wide ? "Measured across the viewports, phone first:" : "Measured at 375px:", m.fresh),
|
|
111
|
-
section("The baseline claims more than is there (good: lower it, so the fix stays fixed):", [...s.stale, ...m.stale])
|
|
112
|
-
].join("")
|
|
113
|
-
);
|
|
114
|
-
return 1;
|
|
115
|
-
} catch (error) {
|
|
116
|
-
io.err(`craft phone-first: ${error.message}`);
|
|
117
|
-
return 2;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
export {
|
|
121
|
-
runPhoneFirst
|
|
122
|
-
};
|