@domandigital/craft 0.10.0 → 0.11.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.
@@ -0,0 +1,206 @@
1
+ import {
2
+ snapshotUrl,
3
+ snapshotUrls
4
+ } from "./chunk-JP75P2DU.js";
5
+ import {
6
+ describeTypicality,
7
+ loadNull,
8
+ typicality
9
+ } from "./chunk-3OZQ4C5R.js";
10
+ import {
11
+ SOURCE_FILE,
12
+ finish,
13
+ loadConfig,
14
+ loadExceptions,
15
+ parseFlags,
16
+ prepareReport,
17
+ readPaths,
18
+ toJson
19
+ } from "./chunk-2AJB27BJ.js";
20
+ import {
21
+ fingerprint
22
+ } from "./chunk-QMYERCBB.js";
23
+ import {
24
+ SNAPSHOT_VERSION,
25
+ auditSnapshot,
26
+ scanSource
27
+ } from "./chunk-3NGWXZZE.js";
28
+
29
+ // src/audit/cli.ts
30
+ import { existsSync, readFileSync, writeFileSync } from "fs";
31
+ import { resolve } from "path";
32
+
33
+ // src/audit/pages.ts
34
+ var decode = (s) => s.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'");
35
+ function parseSitemap(xml) {
36
+ const locs = [...xml.matchAll(/<loc>\s*([^<]+?)\s*<\/loc>/gi)].map((m) => decode(m[1]));
37
+ return /<sitemapindex[\s>]/i.test(xml) ? { urls: [], sitemaps: locs } : { urls: locs, sitemaps: [] };
38
+ }
39
+ function parseUrlList(text) {
40
+ return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
41
+ }
42
+ function parseViewports(value) {
43
+ const widths = value.split(",").map((w) => w.trim());
44
+ return widths.map((w) => {
45
+ const width = Number(w);
46
+ if (!Number.isInteger(width) || width < 240 || width > 3840) throw new Error(`--viewport: "${w}" is not a width in px between 240 and 3840`);
47
+ return { width, height: width < 600 ? 844 : width < 1024 ? 1024 : 900 };
48
+ });
49
+ }
50
+
51
+ // src/audit/cli.ts
52
+ var fmt = (o) => o ? `oklch(${o.l.toFixed(3)} ${o.c.toFixed(3)} ${o.h.toFixed(1)})` : "none";
53
+ function describeFingerprint(fp) {
54
+ return [
55
+ "Fingerprint",
56
+ ` accent ${fmt(fp.accent)}`,
57
+ ` ground ${fmt(fp.ground)}`,
58
+ ` display ${fp.display.family} (${fp.display.class})`,
59
+ ` body ${fp.body.family} (${fp.body.class})`,
60
+ ` roundness ${fp.roundness === null ? "no buttons" : fp.roundness.toFixed(2)}`,
61
+ ` motion ${Math.round(fp.motion * 100)}% of sections below the fold reveal on scroll`,
62
+ ` effects ${fp.effects.join(", ") || "none"}`,
63
+ ` layout ${fp.layout.join(" > ") || "none"}`
64
+ ].join("\n");
65
+ }
66
+ function merge(a, b) {
67
+ const byTell = { ...a.summary.byTell };
68
+ for (const [k, v] of Object.entries(b.summary.byTell)) byTell[k] = (byTell[k] ?? 0) + v;
69
+ return {
70
+ catalogueVersion: a.catalogueVersion,
71
+ findings: [...a.findings, ...b.findings],
72
+ rejectedExceptions: a.rejectedExceptions,
73
+ excepted: [...a.excepted, ...b.excepted],
74
+ suppressed: [...a.suppressed, ...b.suppressed],
75
+ summary: {
76
+ files: a.summary.files + b.summary.files,
77
+ findings: a.summary.findings + b.summary.findings,
78
+ byGeneration: { 1: a.summary.byGeneration[1] + b.summary.byGeneration[1], 2: a.summary.byGeneration[2] + b.summary.byGeneration[2], 3: a.summary.byGeneration[3] + b.summary.byGeneration[3] },
79
+ byTell,
80
+ blocking: a.summary.blocking + b.summary.blocking
81
+ }
82
+ };
83
+ }
84
+ function readSnapshot(path) {
85
+ const data = JSON.parse(readFileSync(path, "utf8"));
86
+ if (data.version !== SNAPSHOT_VERSION) throw new Error(`${path} is snapshot version ${String(data.version)}; this craft reads ${SNAPSHOT_VERSION}`);
87
+ return data;
88
+ }
89
+ async function readText(source, cwd) {
90
+ if (/^https?:\/\//i.test(source)) {
91
+ const res = await fetch(source);
92
+ if (!res.ok) throw new Error(`${source}: HTTP ${res.status}`);
93
+ return res.text();
94
+ }
95
+ const path = resolve(cwd, source);
96
+ if (!existsSync(path)) throw new Error(`no such file: ${source}`);
97
+ return readFileSync(path, "utf8");
98
+ }
99
+ async function resolvePages(source, cwd) {
100
+ const text = await readText(source, cwd);
101
+ if (!/^\s*<(?:\?xml|urlset|sitemapindex)/i.test(text)) return parseUrlList(text);
102
+ const { urls, sitemaps } = parseSitemap(text);
103
+ const nested = await Promise.all(sitemaps.map(async (s) => parseSitemap(await readText(s, cwd)).urls));
104
+ const all = [...urls, ...nested.flat()];
105
+ if (all.length === 0) throw new Error(`${source}: no <loc> entries`);
106
+ return [...new Set(all)];
107
+ }
108
+ function viewportsOf(flags) {
109
+ if (flags.viewport) return parseViewports(flags.viewport);
110
+ if (flags.width || flags.height) return [{ width: Number(flags.width ?? 1440), height: Number(flags.height ?? 900) }];
111
+ return [void 0];
112
+ }
113
+ async function runAudit(command, args, io) {
114
+ try {
115
+ const flags = parseFlags(args);
116
+ if (typeof flags === "string") throw new Error(flags);
117
+ const target = flags.positional[0];
118
+ if (command === "snapshot" && (flags.pages || flags.viewport)) throw new Error("craft snapshot takes one page; use --width and --height");
119
+ if (flags.pages && target) throw new Error("give a page or --pages, not both");
120
+ if (!target && !flags.pages) throw new Error(`usage: craft ${command} <url${command === "audit" ? " | snapshot.json> | --pages <sitemap.xml | urls.txt>" : ">"}`);
121
+ const viewports = viewportsOf(flags);
122
+ const audited = [];
123
+ const failed = [];
124
+ const local = target ? resolve(io.cwd, target) : "";
125
+ if (target && command === "audit" && /\.json$/i.test(target) && existsSync(local)) {
126
+ audited.push({ url: target, viewport: null, snapshot: readSnapshot(local) });
127
+ } else if (target && viewports.length === 1) {
128
+ const snapshot = await snapshotUrl(target, { viewport: viewports[0] });
129
+ audited.push({ url: target, viewport: viewports[0] ?? null, snapshot });
130
+ } else {
131
+ const urls = flags.pages ? await resolvePages(flags.pages, io.cwd) : [target];
132
+ for (const viewport of viewports) {
133
+ for (const r of await snapshotUrls(urls, { viewport })) {
134
+ if (r.snapshot) audited.push({ url: r.url, viewport: viewport ?? null, snapshot: r.snapshot });
135
+ else failed.push({ url: r.url, viewport: viewport ?? null, error: r.error ?? "no snapshot" });
136
+ }
137
+ }
138
+ }
139
+ if (audited.length === 0) throw new Error(`no page could be measured:
140
+ ${failed.map((f) => ` ${f.url}: ${f.error}`).join("\n")}`);
141
+ const single = audited.length === 1 && failed.length === 0;
142
+ if (flags.out) {
143
+ if (!single) throw new Error("--out saves one snapshot; drop --pages and --viewport, or snapshot each page");
144
+ writeFileSync(resolve(io.cwd, flags.out), `${JSON.stringify(audited[0].snapshot, null, 2)}
145
+ `);
146
+ }
147
+ if (command === "snapshot") {
148
+ const { snapshot } = audited[0];
149
+ if (!flags.out) io.out(JSON.stringify(snapshot, null, 2));
150
+ else io.out(`craft snapshot: ${snapshot.url} saved to ${flags.out}`);
151
+ return 0;
152
+ }
153
+ const root = flags.repo ? resolve(io.cwd, flags.repo) : io.cwd;
154
+ const exceptions = loadExceptions(flags, root);
155
+ const config = loadConfig(flags, root);
156
+ const label = (a) => viewports.length > 1 && a.viewport ? `${a.snapshot.url} @ ${a.viewport.width}px` : a.snapshot.url;
157
+ let report = audited.map((a) => {
158
+ const r = auditSnapshot(a.snapshot, { exceptions });
159
+ return { ...r, findings: r.findings.map((f) => ({ ...f, path: label(a) })) };
160
+ }).reduce((acc, r) => merge(acc, r));
161
+ if (flags.repo) {
162
+ report = merge(scanSource(readPaths(["."], root, SOURCE_FILE, void 0, config.ignore), { exceptions }), report);
163
+ }
164
+ const prepared = prepareReport(report, flags, io, config);
165
+ if (!prepared) return 0;
166
+ const nulls = flags.null ? flags.null.split(",").map((p) => loadNull(p.trim(), io.cwd)) : [];
167
+ const pooled = nulls.flatMap((m, i) => m.runs.map((r) => nulls.length > 1 ? { ...r, id: `${i + 1}/${r.id}` } : r));
168
+ const pages = audited.map((a) => {
169
+ const fp = fingerprint(a.snapshot);
170
+ return { url: a.snapshot.url, viewport: a.viewport, fingerprint: fp, typicality: nulls.length ? typicality(fp, pooled) : null };
171
+ });
172
+ const unmeasured = failed.length > 0 && flags.strict ? 1 : 0;
173
+ if (flags.json) {
174
+ const extra = single ? { fingerprint: pages[0].fingerprint, typicality: pages[0].typicality, snapshot: flags.out ?? null } : {};
175
+ io.out(toJson({ report: prepared, ...extra, pages, failed }));
176
+ return Math.max(prepared.summary.blocking > 0 || flags.strict && prepared.summary.findings > 0 ? 1 : 0, unmeasured);
177
+ }
178
+ const title = single ? `craft audit ${audited[0].snapshot.url}` : `craft audit: ${audited.length} page${audited.length === 1 ? "" : "s"}`;
179
+ const code = finish(prepared, { ...flags, json: false }, title, io);
180
+ for (const page of pages) {
181
+ io.out("");
182
+ if (!single) io.out(`${page.url}${page.viewport ? ` @ ${page.viewport.width}px` : ""}`);
183
+ io.out(describeFingerprint(page.fingerprint));
184
+ if (page.typicality) {
185
+ io.out("");
186
+ io.out(describeTypicality(page.typicality, pooled.length, nulls.length));
187
+ }
188
+ }
189
+ if (failed.length) {
190
+ io.out("");
191
+ io.out("Not measured (never counts as a pass):");
192
+ for (const f of failed) io.out(` ${f.url}${f.viewport ? ` @ ${f.viewport.width}px` : ""}: ${f.error}`);
193
+ }
194
+ return Math.max(code, unmeasured);
195
+ } catch (error) {
196
+ io.err(`craft: ${error.message}`);
197
+ return 2;
198
+ }
199
+ }
200
+
201
+ export {
202
+ describeFingerprint,
203
+ merge,
204
+ resolvePages,
205
+ runAudit
206
+ };
@@ -6,8 +6,8 @@ import {
6
6
  nullPath,
7
7
  runHarvest,
8
8
  runNull
9
- } from "./chunk-APO4BT2F.js";
10
- import "./chunk-JKIDWHG3.js";
9
+ } from "./chunk-3OZQ4C5R.js";
10
+ import "./chunk-2AJB27BJ.js";
11
11
  import "./chunk-QMYERCBB.js";
12
12
  import "./chunk-3NGWXZZE.js";
13
13
  export {
@@ -3,25 +3,28 @@ import {
3
3
  } from "./chunk-JWHC67ZS.js";
4
4
  import {
5
5
  merge
6
- } from "./chunk-U2JTFQKO.js";
7
- import "./chunk-TSKRBIOC.js";
6
+ } from "./chunk-MSQ57PJU.js";
7
+ import "./chunk-JP75P2DU.js";
8
8
  import {
9
9
  SIBLING_AT,
10
10
  compareToEstate,
11
11
  loadEstate
12
- } from "./chunk-2NQFIDLE.js";
12
+ } from "./chunk-HOHHQB4N.js";
13
13
  import {
14
14
  loadNull,
15
15
  typicality
16
- } from "./chunk-APO4BT2F.js";
16
+ } from "./chunk-3OZQ4C5R.js";
17
17
  import {
18
18
  COPY_FILE,
19
19
  NOT_COPY_DIR,
20
20
  SOURCE_FILE,
21
+ applySeverity,
22
+ loadConfig,
21
23
  loadExceptions,
22
24
  parseFlags,
23
- readPaths
24
- } from "./chunk-JKIDWHG3.js";
25
+ readPaths,
26
+ toJson
27
+ } from "./chunk-2AJB27BJ.js";
25
28
  import {
26
29
  fingerprint
27
30
  } from "./chunk-QMYERCBB.js";
@@ -259,17 +262,20 @@ async function runReport(command, args, io) {
259
262
  snap = JSON.parse(readFileSync(local, "utf8"));
260
263
  if (snap.version !== SNAPSHOT_VERSION) throw new Error(`${target2} is not a snapshot`);
261
264
  } else {
262
- const { snapshotUrl } = await import("./audit-EGDNWSQB.js");
265
+ const { snapshotUrl } = await import("./audit-QIPXL7E4.js");
263
266
  snap = await snapshotUrl(target2);
264
267
  }
265
268
  const fp = fingerprint(snap);
266
269
  const repo = flags.repo ? resolve(io.cwd, flags.repo) : null;
267
270
  const exceptions = loadExceptions(flags, repo ?? io.cwd);
271
+ const config = loadConfig(flags, repo ?? io.cwd);
268
272
  let findings = auditSnapshot(snap, { exceptions });
269
273
  if (repo) {
270
- findings = merge(scanSource(readPaths(["."], repo, SOURCE_FILE), { exceptions }), findings);
271
- findings = merge(findings, checkCopy(readPaths(["."], repo, COPY_FILE, (d) => NOT_COPY_DIR(d) || d === "docs"), { exceptions }));
274
+ findings = merge(scanSource(readPaths(["."], repo, SOURCE_FILE, void 0, config.ignore), { exceptions }), findings);
275
+ const copyPaths = config.copyPaths ?? ["."];
276
+ findings = merge(findings, checkCopy(readPaths(copyPaths, repo, COPY_FILE, (d) => NOT_COPY_DIR(d) || d === "docs", config.ignore), { exceptions }));
272
277
  }
278
+ findings = applySeverity(findings, config);
273
279
  const nulls = flags.null ? flags.null.split(",").map((p) => loadNull(p.trim(), io.cwd)) : [];
274
280
  const typical = nulls.length ? typicality(fp, nulls.flatMap((m, i) => m.runs.map((r) => nulls.length > 1 ? { ...r, id: `${i + 1}/${r.id}` } : r))) : null;
275
281
  let estate = null;
@@ -297,8 +303,8 @@ async function runReport(command, args, io) {
297
303
  } else io.out(plan);
298
304
  return 0;
299
305
  }
300
- io.out(flags.json ? JSON.stringify(report, null, 2) : formatCharacter(report));
301
- if (flags.out) writeFileSync(resolve(io.cwd, flags.out), `${JSON.stringify(report, null, 2)}
306
+ io.out(flags.json ? toJson(report) : formatCharacter(report));
307
+ if (flags.out) writeFileSync(resolve(io.cwd, flags.out), `${toJson(report)}
302
308
  `);
303
309
  return flags.strict && report.verdict !== "decided" ? 1 : 0;
304
310
  } catch (error) {
@@ -1,15 +1,17 @@
1
1
  import {
2
2
  describeFingerprint,
3
3
  merge,
4
+ resolvePages,
4
5
  runAudit
5
- } from "./chunk-U2JTFQKO.js";
6
- import "./chunk-TSKRBIOC.js";
7
- import "./chunk-APO4BT2F.js";
8
- import "./chunk-JKIDWHG3.js";
6
+ } from "./chunk-MSQ57PJU.js";
7
+ import "./chunk-JP75P2DU.js";
8
+ import "./chunk-3OZQ4C5R.js";
9
+ import "./chunk-2AJB27BJ.js";
9
10
  import "./chunk-QMYERCBB.js";
10
11
  import "./chunk-3NGWXZZE.js";
11
12
  export {
12
13
  describeFingerprint,
13
14
  merge,
15
+ resolvePages,
14
16
  runAudit
15
17
  };
@@ -3,8 +3,9 @@ import {
3
3
  validateDirection
4
4
  } from "./chunk-JWHC67ZS.js";
5
5
  import {
6
- parseFlags
7
- } from "./chunk-JKIDWHG3.js";
6
+ parseFlags,
7
+ toJson
8
+ } from "./chunk-2AJB27BJ.js";
8
9
  import {
9
10
  fingerprint
10
11
  } from "./chunk-QMYERCBB.js";
@@ -287,7 +288,7 @@ function runDirection(args, io) {
287
288
  fingerprint: current,
288
289
  pathExists: (p) => existsSync(resolve(dirname(file), p))
289
290
  });
290
- if (flags.json) io.out(JSON.stringify(report, null, 2));
291
+ if (flags.json) io.out(toJson(report));
291
292
  else {
292
293
  for (const p of report.problems) io.out(` ${p.severity === "error" ? "error" : "warn "} ${p.at || "(file)"} ${p.message}`);
293
294
  io.out(`craft direction: ${report.valid ? "valid" : "not valid"}, ${report.decided} of 7 choices decided with a reason.`);
@@ -3,9 +3,9 @@ import {
3
3
  formatPairs,
4
4
  loadEstate,
5
5
  runEstate
6
- } from "./chunk-2NQFIDLE.js";
7
- import "./chunk-APO4BT2F.js";
8
- import "./chunk-JKIDWHG3.js";
6
+ } from "./chunk-HOHHQB4N.js";
7
+ import "./chunk-3OZQ4C5R.js";
8
+ import "./chunk-2AJB27BJ.js";
9
9
  import "./chunk-QMYERCBB.js";
10
10
  import "./chunk-3NGWXZZE.js";
11
11
  export {
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  run
4
- } from "./chunk-JKIDWHG3.js";
4
+ } from "./chunk-2AJB27BJ.js";
5
5
  import "./chunk-3NGWXZZE.js";
6
6
 
7
7
  // src/cli.ts