@tideworthy/tidysites 0.1.9 → 0.1.11

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/README.md CHANGED
@@ -22,8 +22,8 @@ nothing else; it lives in `.tidysites/key`, which is kept out of git.
22
22
  | `tidysites status` | what changed here since the pull |
23
23
  | `tidysites validate` | every file against the content model, with the file and the field in each message |
24
24
  | `tidysites push [--prune] [--force] [--dry] [--only pages]` | validate, then write the created and updated entries back (with `--prune`, delete the entries whose files are gone), then pull |
25
- | `tidysites shot <path or url> [--mobile] [--tablet] [--desktop] [--wide] [--all] [--first-screen] [--live]` | a screenshot of a page of the preview site, the live site, or any address, saved under `.tidysites/shots/`; none of the width flags means desktop and mobile; a port compares all four. A page taller than 2400px comes in 1600px bands (`<name>-band-01.jpg`, …), top to bottom, that an image reader can take in one at a time |
26
- | `tidysites measure <path or url> [--mobile] [--tablet] [--desktop] [--wide] [--all] [--full]` | the page as the browser lays it out: every heading, paragraph, button, picture and section box with its computed type (size, line height, weight, family, colour) and box (position, size, padding, margin, background), saved as JSON under `.tidysites/measures/` and printed. Measure the old page and the new one at the same width and style to the numbers |
25
+ | `tidysites shot <path or url> [--mobile] [--tablet] [--desktop] [--wide] [--all] [--first-screen] [--live]` | a screenshot of a page of the preview site, the live site, or any address, saved under `.tidysites/shots/`; none of the width flags means desktop and mobile; a port compares all four. A page taller than 2400px comes in 1600px bands (`<name>-band-01.jpg`, …), top to bottom, that an image reader can take in one at a time. `--menu` opens every navigation menu (burgers, `<details>`, hover dropdowns) before the capture; `--open a,b` clicks those selectors and `--hover a,b` shows the submenus under them, for a popup, a tab or one dropdown |
26
+ | `tidysites measure <path or url> [--mobile] [--tablet] [--desktop] [--wide] [--all] [--full]` | the page as the browser lays it out: every heading, paragraph, button, picture and section box with its computed type (size, line height, weight, family, colour) and box (position, size, padding, margin, background), saved as JSON under `.tidysites/measures/` and printed. Measure the old page and the new one at the same width and style to the numbers. The same `--menu`, `--open` and `--hover` as `shot`, so a dropdown's open type is measured |
27
27
  | `tidysites redirect [add <old path> <new path> \| rm <old path>]` | the website's redirects: an old address that must keep working after a port goes here; they ship with the next publish |
28
28
  | `tidysites pages` | every address the website renders |
29
29
  | `tidysites page <path> [--html]` | the rendered page, section by section, with the key each section carries |
@@ -59,9 +59,12 @@ out of a file is cleared on the website at the next push. `id` and
59
59
  a file you changed and have not pushed; it keeps the file and the difference
60
60
  shows in `status`.
61
61
 
62
+ Styling: a row's `.css` file is scoped to that row (every rule is prefixed with the row's own selector), so it cannot style the rest of the page; rules for the whole page, the sections and the theme's classes go in `business/business.css`.
63
+
62
64
  Images: an image field, a hero's image or a Columns image slot takes
63
65
  `{ "$media": { "url": "https://...", "alt": "what it shows" } }` and the
64
- push fetches the file into the website's library. An image already in the
66
+ push fetches the file into the website's library; a file the library already
67
+ holds (the same bytes) is reused, not added again. An image already in the
65
68
  library keeps the value the pull gave it.
66
69
 
67
70
  A picture in custom HTML is written as a plain `<img src="{{ s.image.url }}" alt="{{ s.image.alt }}">`: a stored picture gets its resized variants (`srcset`, and a `sizes` hint when none is written) on the way out, so a phone downloads a phone-sized file; write your own `sizes` when the picture is narrower than half the screen on a laptop, and your own `srcset` to take over entirely. Pictures in the first section load at once and the first of them asks to go first; pictures below load when they are needed, unless the markup says `loading="eager"`.
package/bin/tidysites.mjs CHANGED
@@ -7,8 +7,8 @@
7
7
  * status what changed here since the pull
8
8
  * validate check the files against the content model
9
9
  * push [--prune] [--force] [--dry] write the changes back, then pull
10
- * shot <path|url> [--mobile] [--tablet] [--desktop] [--wide] [--all] [--first-screen] screenshot to .tidysites/shots (none = desktop and mobile); a tall page comes in 1600px bands
11
- * measure <path|url> [--mobile] [--tablet] [--desktop] [--wide] [--all] [--full] the computed type and boxes of a page: the numbers to style to and diff
10
+ * shot <path|url> [--mobile] [--tablet] [--desktop] [--wide] [--all] [--first-screen] [--menu] [--open sel,..] [--hover sel,..] screenshot to .tidysites/shots (none = desktop and mobile); a tall page comes in 1600px bands; --menu opens every navigation menu first
11
+ * measure <path|url> [--mobile] [--tablet] [--desktop] [--wide] [--all] [--full] [--menu] [--open sel,..] [--hover sel,..] the computed type and boxes of a page: the numbers to style to and diff
12
12
  * redirect [add <old> <new> | rm <old>] the website's redirects (an old address that must keep working goes here)
13
13
  * pages every address the website renders
14
14
  * page <path> [--html] the rendered page, section by section
@@ -189,9 +189,13 @@ async function pull({ dir, cfg }, collections = null, { overwrite = new Set(), m
189
189
  // website's version underneath it, so status shows the difference and a push carries the new stamp.
190
190
  const files = {};
191
191
  const kept = [];
192
+ const removedHere = readRemoved(dir);
193
+ const heldBack = [];
192
194
  for (const [collection, entries] of Object.entries(doc.content)) {
193
195
  for (const e of entries) {
194
196
  const key = `${collection}/${e.slug ?? e.id}`;
197
+ // Removed here with `tidysites rm` and not pruned yet: the website still has it, the folder does not get it back.
198
+ if (removedHere.has(key) && !overwrite.has(key)) { heldBack.push(key); continue; }
195
199
  const mine = local[key];
196
200
  const was = prev?.entries?.[key];
197
201
  // An entry this push just wrote is the website's now: its file takes the website's version ($media
@@ -244,6 +248,7 @@ async function pull({ dir, cfg }, collections = null, { overwrite = new Set(), m
244
248
  for (const c of doc.collections.filter((c) => c.count)) say(` ${c.slug.padEnd(20)} ${c.count}`);
245
249
  if (snap.menus) say(` ${'menus'.padEnd(20)} ${Object.keys(snap.menus).length}`);
246
250
  for (const k of kept) say(` kept your unpushed changes in ${k}`);
251
+ if (heldBack.length) say(` ${heldBack.length} entr${heldBack.length === 1 ? 'y' : 'ies'} you removed here stay removed (the website still has them until \`push --prune\`)`);
247
252
  }
248
253
 
249
254
  /** The menus alone: files and the snapshot's menus, the entries untouched. */
@@ -338,7 +343,9 @@ async function push(ctx) {
338
343
  }
339
344
  process.exit(1);
340
345
  }
341
- say(`Pushed: ${r.content.created} created, ${r.content.updated} updated${r.images ? `, ${r.images} image${r.images === 1 ? '' : 's'} added` : ''}${r.removed ? `, ${r.removed} removed` : ''}.`);
346
+ say(`Pushed: ${r.content.created} created, ${r.content.updated} updated${r.images ? `, ${r.images} image${r.images === 1 ? '' : 's'} added` : ''}${r.images_reused ? `, ${r.images_reused} already in the library` : ''}${r.removed ? `, ${r.removed} removed` : ''}.`);
347
+ // Removed from the website now: the folder stops holding them back.
348
+ if (removeKeys.length) { const rem = readRemoved(ctx.dir); for (const k of removeKeys) rem.delete(k); writeRemoved(ctx.dir, rem); }
342
349
  if (r.missing?.length) say(`Not found to remove: ${r.missing.map((m) => `${m.collection}/${m.slug}`).join(', ')}`);
343
350
  }
344
351
  for (const name of menus) {
@@ -363,7 +370,7 @@ async function shot(ctx, target = positional[0] ?? '/', opts = {}) {
363
370
  // --desktop, --mobile, --tablet, --wide, any of them together; --all for every width; none = desktop and mobile.
364
371
  const picked = ['desktop', 'mobile', 'tablet', 'wide'].filter((v) => flags[v]);
365
372
  const view = flags.all ? 'all' : picked.length ? picked.join(',') : (opts.view ?? 'both');
366
- const args = { site_id: ctx.cfg.site, view, full_page: !(flags['first-screen'] || opts.firstScreen), ...(isUrl ? { url: target } : { path: target, site: flags.live ? 'live' : 'preview' }) };
373
+ const args = { site_id: ctx.cfg.site, view, full_page: !(flags['first-screen'] || opts.firstScreen), ...openStates(), ...(isUrl ? { url: target } : { path: target, site: flags.live ? 'live' : 'preview' }) };
367
374
  const { json, images } = await tool(ctx.cfg, 'screenshot_website', args);
368
375
  const outDir = join(ctx.dir, META, 'shots');
369
376
  mkdirSync(outDir, { recursive: true });
@@ -382,13 +389,23 @@ async function shot(ctx, target = positional[0] ?? '/', opts = {}) {
382
389
  if (!images.length) say(json ? JSON.stringify(json).slice(0, 500) : 'No image came back.');
383
390
  }
384
391
 
392
+ /** What a shot or a measure opens first: --menu (every navigation menu), --open <selector,...> (clicked), --hover <selector,...> (their submenus shown). */
393
+ function openStates() {
394
+ const list = (v) => (v ? String(v).split(',').map((x) => x.trim()).filter(Boolean) : []);
395
+ const out = {};
396
+ if (flags.menu) out.menu = true;
397
+ if (flags.open) out.open = list(flags.open);
398
+ if (flags.hover) out.hover = list(flags.hover);
399
+ return out;
400
+ }
401
+
385
402
  /** The computed type and boxes of a page, old or new, at one or several widths: the numbers a port styles to. */
386
403
  async function measure(ctx) {
387
404
  const target = positional[0] ?? '/';
388
405
  const isUrl = /^https?:\/\//.test(target);
389
406
  const picked = ['desktop', 'mobile', 'tablet', 'wide'].filter((v) => flags[v]);
390
407
  const view = flags.all ? 'all' : picked.length ? picked.join(',') : 'desktop';
391
- const args = { site_id: ctx.cfg.site, view, ...(isUrl ? { url: target } : { path: target, site: flags.live ? 'live' : 'preview' }) };
408
+ const args = { site_id: ctx.cfg.site, view, ...openStates(), ...(isUrl ? { url: target } : { path: target, site: flags.live ? 'live' : 'preview' }) };
392
409
  const { json } = await tool(ctx.cfg, 'measure_website', args);
393
410
  if (!json?.views) fail('No measurements came back.');
394
411
  const outDir = join(ctx.dir, META, 'measures');
@@ -556,12 +573,21 @@ function rmCmd({ dir }) {
556
573
  const f = join(colDir, `${sl}.${ext}`);
557
574
  if (existsSync(f)) { rmSync(f); hit = true; }
558
575
  }
559
- if (hit) { removed++; say(`- ${collection}/${sl}`); } else say(`${collection}/${sl}: no such entry file`);
576
+ if (hit) { removed++; say(`- ${collection}/${sl}`); rememberRemoved(dir, `${collection}/${sl}`); } else say(`${collection}/${sl}: no such entry file`);
560
577
  }
561
578
  }
562
579
  say(`${removed} entr${removed === 1 ? 'y' : 'ies'} removed from the folder; \`tidysites push --prune\` removes them from the website.`);
563
580
  }
564
581
 
582
+ /**
583
+ * Entries removed here and not yet pruned from the website. A pull (the one after a failed push included) does not
584
+ * write them back; `push --prune` removes them from the website and forgets them. The list lives beside the snapshot.
585
+ */
586
+ const REMOVED_FILE = 'removed.json';
587
+ function readRemoved(dir) { return new Set(readJson(join(dir, META, REMOVED_FILE)) ?? []); }
588
+ function writeRemoved(dir, set) { writeFileSync(join(dir, META, REMOVED_FILE), JSON.stringify([...set].sort(), null, '\t')); }
589
+ function rememberRemoved(dir, key) { const s = readRemoved(dir); s.add(key); writeRemoved(dir, s); }
590
+
565
591
  async function publish(ctx) {
566
592
  if (!flags.yes) fail('Publishing makes the live site a copy of the preview site. Look at it first (`tidysites shot /`), then run `tidysites publish --yes`.');
567
593
  // A website live on the client's own domain is never published from here: the website refuses (409 live_domain) and says who may.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tideworthy/tidysites",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Pull a Tidysites website to files, edit, validate against its content model, push it back, screenshot the result.",
5
5
  "type": "module",
6
6
  "bin": {