@domandigital/craft 0.15.0 → 0.17.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.
@@ -2,31 +2,42 @@ import {
2
2
  generate
3
3
  } from "./chunk-OQXTQQAU.js";
4
4
  import {
5
- parseFlags,
5
+ parseFlags
6
+ } from "./chunk-O5B5ELJH.js";
7
+ import {
6
8
  toJson
7
- } from "./chunk-MUVFECFC.js";
9
+ } from "./chunk-QIEDBL4J.js";
8
10
  import {
9
11
  readSnapshot
10
12
  } from "./chunk-WUACK2HX.js";
11
13
  import {
12
14
  CATALOGUE_VERSION,
15
+ MIN_RUNS,
13
16
  NULL_VERSION,
14
17
  auditSnapshot,
15
18
  checkCopy,
16
19
  extractHtml,
17
- extractStrings,
18
20
  fingerprint,
19
21
  harvest,
20
22
  hueSwatch,
21
23
  nullPrompt,
22
24
  scanSource
23
- } from "./chunk-LFNSB6W7.js";
25
+ } from "./chunk-WFB6F3BN.js";
26
+ import {
27
+ extractStrings
28
+ } from "./chunk-4QWEAL4G.js";
24
29
 
25
30
  // src/null/cli.ts
26
31
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
27
- import { join, relative, resolve } from "path";
32
+ import { createServer } from "http";
33
+ import { extname, join, relative, resolve, sep } from "path";
28
34
  import { pathToFileURL } from "url";
29
35
  var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
36
+ var NULL_USAGE = [
37
+ 'usage: craft null build --brief "<text>" --out <dir> [--runs 20] [--parallel 4] [--model <name>]',
38
+ ' craft null import <dir> --builder <name> --brief "<text>" [--out <dir>] [--model <name>]',
39
+ ' craft null prompt --brief "<text>"'
40
+ ].join("\n");
30
41
  var pad = (n) => String(n).padStart(2, "0");
31
42
  async function pool(items, size, work) {
32
43
  const queue = [...items];
@@ -38,6 +49,15 @@ function nullPath(target, cwd) {
38
49
  const abs = resolve(cwd, target);
39
50
  return existsSync(abs) && statSync(abs).isDirectory() ? join(abs, "null.json") : abs;
40
51
  }
52
+ function expandNullTargets(targets, cwd) {
53
+ return targets.flatMap((t) => {
54
+ const abs = resolve(cwd, t);
55
+ if (existsSync(abs) && statSync(abs).isDirectory() && !existsSync(join(abs, "null.json"))) {
56
+ return readdirSync(abs).sort().map((d) => join(abs, d, "null.json")).filter((p) => existsSync(p));
57
+ }
58
+ return [t];
59
+ });
60
+ }
41
61
  function loadNull(target, cwd) {
42
62
  const path = nullPath(target, cwd);
43
63
  if (!existsSync(path)) throw new Error(`no null model at ${target}. Build one with craft null build.`);
@@ -45,10 +65,12 @@ function loadNull(target, cwd) {
45
65
  if (model.version !== NULL_VERSION) throw new Error(`${target} is null model version ${String(model.version)}; this craft reads ${NULL_VERSION}`);
46
66
  return model;
47
67
  }
48
- function measureRun(id, html, snapshot) {
68
+ function measureRun(id, html, snapshot, styles = []) {
49
69
  const file = { path: `${id}.html`, text: html };
50
- const tells = new Set([...auditSnapshot(snapshot).findings, ...scanSource([file]).findings].map((f) => f.tell));
51
- const copy = [...new Set(extractStrings(file).filter((b) => !b.literal).map((b) => b.text.replace(/\s+/g, " ").trim()).filter(Boolean))];
70
+ snapshot = { ...snapshot, home: snapshot.home ?? true };
71
+ const tells = new Set([...auditSnapshot(snapshot).findings, ...scanSource([file, ...styles]).findings].map((f) => f.tell));
72
+ let copy = [...new Set(extractStrings(file).filter((b) => !b.literal).map((b) => b.text.replace(/\s+/g, " ").trim()).filter(Boolean))];
73
+ if (copy.length === 0) copy = [...new Set(snapshot.sections.flatMap((s) => s.text ?? []).map((t) => t.replace(/\s+/g, " ").trim()).filter(Boolean))];
52
74
  return { id, fingerprint: fingerprint(snapshot), tells: [...tells].sort(), copy };
53
75
  }
54
76
  async function build(flags, io) {
@@ -98,7 +120,7 @@ async function build(flags, io) {
98
120
  const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
99
121
  if (toShoot.length) {
100
122
  io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
101
- const { snapshotUrls } = await import("./audit-MTUC24XU.js");
123
+ const { snapshotUrls } = await import("./audit-4SFSLCKY.js");
102
124
  const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
103
125
  results.forEach((r, i) => {
104
126
  const id = toShoot[i];
@@ -127,6 +149,142 @@ async function build(flags, io) {
127
149
  }
128
150
  return 0;
129
151
  }
152
+ function findImportPages(dir) {
153
+ const pages = [];
154
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
155
+ if (e.isFile() && /\.html?$/i.test(e.name)) pages.push({ id: e.name.replace(/\.html?$/i, ""), root: dir, entry: e.name });
156
+ else if (e.isDirectory() && existsSync(join(dir, e.name, "index.html"))) pages.push({ id: e.name, root: join(dir, e.name), entry: "index.html" });
157
+ }
158
+ const ids = pages.map((p) => p.id);
159
+ const twice = ids.find((id, i) => ids.indexOf(id) !== i);
160
+ if (twice) throw new Error(`"${twice}" is both a file and a folder; rename one`);
161
+ return pages.sort((a, b) => a.id.localeCompare(b.id));
162
+ }
163
+ var TYPES = {
164
+ ".html": "text/html",
165
+ ".htm": "text/html",
166
+ ".css": "text/css",
167
+ ".js": "text/javascript",
168
+ ".mjs": "text/javascript",
169
+ ".json": "application/json",
170
+ ".svg": "image/svg+xml",
171
+ ".png": "image/png",
172
+ ".jpg": "image/jpeg",
173
+ ".jpeg": "image/jpeg",
174
+ ".webp": "image/webp",
175
+ ".avif": "image/avif",
176
+ ".gif": "image/gif",
177
+ ".ico": "image/x-icon",
178
+ ".woff": "font/woff",
179
+ ".woff2": "font/woff2",
180
+ ".ttf": "font/ttf",
181
+ ".otf": "font/otf"
182
+ };
183
+ function serveDir(root) {
184
+ const server = createServer((req, res) => {
185
+ let file;
186
+ try {
187
+ file = resolve(root, `.${decodeURIComponent(new URL(req.url ?? "/", "http://local").pathname)}`);
188
+ } catch {
189
+ res.statusCode = 400;
190
+ return void res.end();
191
+ }
192
+ if (file !== root && !file.startsWith(root + sep)) {
193
+ res.statusCode = 403;
194
+ return void res.end();
195
+ }
196
+ if (existsSync(file) && statSync(file).isDirectory()) file = join(file, "index.html");
197
+ if (!existsSync(file)) {
198
+ res.statusCode = 404;
199
+ return void res.end();
200
+ }
201
+ res.setHeader("content-type", TYPES[extname(file).toLowerCase()] ?? "application/octet-stream");
202
+ res.end(readFileSync(file));
203
+ });
204
+ return new Promise((done, fail) => {
205
+ server.once("error", fail);
206
+ server.listen(
207
+ 0,
208
+ "127.0.0.1",
209
+ () => done({
210
+ base: `http://127.0.0.1:${server.address().port}`,
211
+ close: () => new Promise((closed) => server.close(() => closed()))
212
+ })
213
+ );
214
+ });
215
+ }
216
+ function stylesheets(root, id) {
217
+ const found = [];
218
+ const walk = (dir) => {
219
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
220
+ const path = join(dir, e.name);
221
+ if (e.isDirectory() && e.name !== "node_modules" && !e.name.startsWith(".")) walk(path);
222
+ else if (e.isFile() && e.name.endsWith(".css") && statSync(path).size <= 1e6) found.push({ path: `${id}/${relative(root, path)}`, text: readFileSync(path, "utf8") });
223
+ }
224
+ };
225
+ walk(root);
226
+ return found;
227
+ }
228
+ async function importPages(flags, io) {
229
+ const usage = 'usage: craft null import <dir> --builder <name> --brief "<who, where, what they do>" [--out <dir>] [--model <name>]';
230
+ const from = flags.positional[1];
231
+ const brief = flags.brief?.trim();
232
+ if (!from || flags.positional.length > 2 || !flags.builder || !brief || brief.split(/\s+/).length < 5) throw new Error(usage);
233
+ const dir = resolve(io.cwd, from);
234
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) throw new Error(`no directory at ${from}`);
235
+ const pages = findImportPages(dir);
236
+ if (pages.length < MIN_RUNS) throw new Error(`${from} holds ${pages.length} page${pages.length === 1 ? "" : "s"}; a null model needs at least ${MIN_RUNS}`);
237
+ const out = resolve(io.cwd, flags.out ?? from);
238
+ const shots = join(out, "snapshots");
239
+ mkdirSync(shots, { recursive: true });
240
+ const failed = [];
241
+ const toShoot = pages.filter((p) => !existsSync(join(shots, `${p.id}.json`)));
242
+ if (toShoot.length) {
243
+ io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"} from ${flags.builder}`);
244
+ const { snapshotUrls } = await import("./audit-4SFSLCKY.js");
245
+ const servers = /* @__PURE__ */ new Map();
246
+ try {
247
+ for (const p of toShoot) if (!servers.has(p.root)) servers.set(p.root, await serveDir(p.root));
248
+ const urls = toShoot.map((p) => `${servers.get(p.root)?.base}/${p.entry === "index.html" ? "" : encodeURIComponent(p.entry)}`);
249
+ const results = await snapshotUrls(urls);
250
+ results.forEach((r, i) => {
251
+ const p = toShoot[i];
252
+ if (r.snapshot) writeFileSync(join(shots, `${p.id}.json`), `${JSON.stringify({ ...r.snapshot, url: relative(out, join(p.root, p.entry)) }, null, 2)}
253
+ `);
254
+ else failed.push(`${p.id}: the page would not render: ${r.error}`);
255
+ });
256
+ } finally {
257
+ await Promise.all([...servers.values()].map((s) => s.close()));
258
+ }
259
+ }
260
+ const measured = pages.filter((p) => existsSync(join(shots, `${p.id}.json`))).map(
261
+ (p) => measureRun(
262
+ p.id,
263
+ readFileSync(join(p.root, p.entry), "utf8"),
264
+ readSnapshot(readJson(join(shots, `${p.id}.json`)), `${p.id}.json`),
265
+ p.root === dir ? [] : stylesheets(p.root, p.id)
266
+ )
267
+ );
268
+ if (measured.length < MIN_RUNS) {
269
+ for (const f of failed) io.err(` ${f}`);
270
+ throw new Error(`only ${measured.length} of ${pages.length} pages rendered; a null model needs at least ${MIN_RUNS}`);
271
+ }
272
+ const model = {
273
+ version: NULL_VERSION,
274
+ brief,
275
+ prompt: nullPrompt(brief),
276
+ model: flags.model ?? "unknown",
277
+ builder: flags.builder,
278
+ catalogueVersion: CATALOGUE_VERSION,
279
+ builtAt: (/* @__PURE__ */ new Date()).toISOString(),
280
+ runs: measured
281
+ };
282
+ writeFileSync(join(out, "null.json"), `${JSON.stringify(model, null, 2)}
283
+ `);
284
+ io.out(`craft null: ${measured.length} of ${pages.length} pages from ${flags.builder} in ${relative(io.cwd, join(out, "null.json")) || "null.json"}`);
285
+ for (const f of failed) io.err(` ${f}`);
286
+ return measured.length < pages.length ? 1 : 0;
287
+ }
130
288
  function copyTell(phrase) {
131
289
  const text = `We found that ${phrase} was there.`;
132
290
  const hit = checkCopy([{ path: "phrase.md", text }]).findings[0];
@@ -162,7 +320,13 @@ async function runNull(args, io) {
162
320
  try {
163
321
  const flags = parseFlags(args);
164
322
  if (typeof flags === "string") throw new Error(flags);
165
- if (flags.positional[0] !== "build") throw new Error('usage: craft null build --brief "<text>" --out <dir> [--runs 20] [--parallel 4] [--model <name>]');
323
+ if (flags.positional[0] === "import") return await importPages(flags, io);
324
+ if (flags.positional[0] === "prompt") {
325
+ if (!flags.brief || flags.brief.trim().split(/\s+/).length < 5) throw new Error('usage: craft null prompt --brief "<who, where, what they do>"');
326
+ io.out(nullPrompt(flags.brief));
327
+ return 0;
328
+ }
329
+ if (flags.positional[0] !== "build") throw new Error(NULL_USAGE);
166
330
  return await build(flags, io);
167
331
  } catch (error) {
168
332
  io.err(`craft: ${error.message}`);
@@ -174,14 +338,7 @@ function runHarvest(args, io) {
174
338
  if (typeof flags === "string") throw new Error(flags);
175
339
  const targets = flags.positional.slice(1);
176
340
  if (targets.length === 0) throw new Error("usage: craft tells harvest <null.json | dir>... [--share 0.25] [--json]");
177
- const paths = targets.flatMap((t) => {
178
- const abs = resolve(io.cwd, t);
179
- if (existsSync(abs) && statSync(abs).isDirectory() && !existsSync(join(abs, "null.json"))) {
180
- return readdirSync(abs).map((d) => join(abs, d, "null.json")).filter((p) => existsSync(p));
181
- }
182
- return [t];
183
- });
184
- const models = paths.map((p) => loadNull(p, io.cwd));
341
+ const models = expandNullTargets(targets, io.cwd).map((p) => loadNull(p, io.cwd));
185
342
  const minShare = flags.share === void 0 ? void 0 : Number(flags.share);
186
343
  if (minShare !== void 0 && !(minShare > 0 && minShare <= 1)) throw new Error("--share needs a number above 0 and at most 1");
187
344
  const candidates = harvest(models, { minShare, copyTell });
@@ -191,8 +348,11 @@ function runHarvest(args, io) {
191
348
 
192
349
  export {
193
350
  nullPath,
351
+ expandNullTargets,
194
352
  loadNull,
195
353
  measureRun,
354
+ findImportPages,
355
+ serveDir,
196
356
  formatHarvest,
197
357
  describeTypicality,
198
358
  runNull,
@@ -1,23 +1,31 @@
1
1
  import {
2
2
  describeTypicality,
3
+ expandNullTargets,
4
+ findImportPages,
3
5
  formatHarvest,
4
6
  loadNull,
5
7
  measureRun,
6
8
  nullPath,
7
9
  runHarvest,
8
- runNull
9
- } from "./chunk-RXXRXV3V.js";
10
+ runNull,
11
+ serveDir
12
+ } from "./chunk-ZUYERAFM.js";
10
13
  import "./chunk-OQXTQQAU.js";
11
- import "./chunk-MUVFECFC.js";
14
+ import "./chunk-O5B5ELJH.js";
15
+ import "./chunk-QIEDBL4J.js";
12
16
  import "./chunk-WUACK2HX.js";
13
- import "./chunk-LFNSB6W7.js";
17
+ import "./chunk-WFB6F3BN.js";
18
+ import "./chunk-4QWEAL4G.js";
14
19
  import "./chunk-QYBAVU3P.js";
15
20
  export {
16
21
  describeTypicality,
22
+ expandNullTargets,
23
+ findImportPages,
17
24
  formatHarvest,
18
25
  loadNull,
19
26
  measureRun,
20
27
  nullPath,
21
28
  runHarvest,
22
- runNull
29
+ runNull,
30
+ serveDir
23
31
  };
@@ -4,13 +4,15 @@ import {
4
4
  formatPairs,
5
5
  loadEstate,
6
6
  runEstate
7
- } from "./chunk-QU6Q2QZZ.js";
8
- import "./chunk-YDURAOYR.js";
9
- import "./chunk-RXXRXV3V.js";
7
+ } from "./chunk-RJVG5KA6.js";
8
+ import "./chunk-6TSDYETQ.js";
9
+ import "./chunk-ZUYERAFM.js";
10
10
  import "./chunk-OQXTQQAU.js";
11
- import "./chunk-MUVFECFC.js";
11
+ import "./chunk-O5B5ELJH.js";
12
+ import "./chunk-QIEDBL4J.js";
12
13
  import "./chunk-WUACK2HX.js";
13
- import "./chunk-LFNSB6W7.js";
14
+ import "./chunk-WFB6F3BN.js";
15
+ import "./chunk-4QWEAL4G.js";
14
16
  import "./chunk-QYBAVU3P.js";
15
17
  export {
16
18
  formatComponentPairs,