@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.
- package/CHANGELOG.md +19 -0
- package/README.md +35 -0
- package/dist/{audit-EGDNWSQB.js → audit-QIPXL7E4.js} +1 -1
- package/dist/audit.cjs +3 -1
- package/dist/audit.js +3 -1
- package/dist/{chunk-JKIDWHG3.js → chunk-2AJB27BJ.js} +282 -29
- package/dist/{chunk-APO4BT2F.js → chunk-3OZQ4C5R.js} +5 -4
- package/dist/{chunk-2NQFIDLE.js → chunk-HOHHQB4N.js} +7 -6
- package/dist/{chunk-TSKRBIOC.js → chunk-JP75P2DU.js} +3 -1
- package/dist/chunk-MSQ57PJU.js +206 -0
- package/dist/{cli-PRWTNFVP.js → cli-4ESXOLKV.js} +2 -2
- package/dist/{cli-ADQQURNR.js → cli-DBZMVYYV.js} +17 -11
- package/dist/{cli-EK3GSAOW.js → cli-T5RD2BTK.js} +6 -4
- package/dist/{cli-2WBGTBKD.js → cli-U3SZVHSK.js} +4 -3
- package/dist/{cli-VQUNNIZM.js → cli-VREYWCT4.js} +3 -3
- package/dist/cli.js +1 -1
- package/dist/index.cjs +216 -0
- package/dist/index.d.cts +185 -1
- package/dist/index.d.ts +185 -1
- package/dist/index.js +202 -0
- package/package.json +13 -6
- package/dist/chunk-U2JTFQKO.js +0 -112
|
@@ -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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/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
|
+
};
|
|
@@ -3,25 +3,28 @@ import {
|
|
|
3
3
|
} from "./chunk-JWHC67ZS.js";
|
|
4
4
|
import {
|
|
5
5
|
merge
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
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-
|
|
12
|
+
} from "./chunk-HOHHQB4N.js";
|
|
13
13
|
import {
|
|
14
14
|
loadNull,
|
|
15
15
|
typicality
|
|
16
|
-
} from "./chunk-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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 ?
|
|
301
|
-
if (flags.out) writeFileSync(resolve(io.cwd, flags.out), `${
|
|
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-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
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
|
-
|
|
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(
|
|
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-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
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 {
|