@tideworthy/tidysites 0.1.10 → 0.1.12
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 +5 -2
- package/bin/tidysites.mjs +67 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,8 +22,9 @@ 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
|
+
| `tidysites speed <path or url> [--mobile\|--desktop] [--live] [--against <url>]` | PageSpeed Insights (Google's Lighthouse) for a page of the preview or live site, or any address: scores, lab and field metrics, the largest-paint element and the opportunities with the files they name, saved under `.tidysites/speed/`. `--against <url>` tests the old page in the same call for a side-by-side. Run it on the live site before calling a build done; what the content cannot fix goes in the report |
|
|
27
28
|
| `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
29
|
| `tidysites pages` | every address the website renders |
|
|
29
30
|
| `tidysites page <path> [--html]` | the rendered page, section by section, with the key each section carries |
|
|
@@ -59,6 +60,8 @@ out of a file is cleared on the website at the next push. `id` and
|
|
|
59
60
|
a file you changed and have not pushed; it keeps the file and the difference
|
|
60
61
|
shows in `status`.
|
|
61
62
|
|
|
63
|
+
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`.
|
|
64
|
+
|
|
62
65
|
Images: an image field, a hero's image or a Columns image slot takes
|
|
63
66
|
`{ "$media": { "url": "https://...", "alt": "what it shows" } }` and the
|
|
64
67
|
push fetches the file into the website's library; a file the library already
|
package/bin/tidysites.mjs
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
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
|
+
* speed <path|url> [--mobile|--desktop] [--live] [--against <url>] PageSpeed Insights for a page, and the old page beside it
|
|
12
13
|
* redirect [add <old> <new> | rm <old>] the website's redirects (an old address that must keep working goes here)
|
|
13
14
|
* pages every address the website renders
|
|
14
15
|
* page <path> [--html] the rendered page, section by section
|
|
@@ -189,9 +190,13 @@ async function pull({ dir, cfg }, collections = null, { overwrite = new Set(), m
|
|
|
189
190
|
// website's version underneath it, so status shows the difference and a push carries the new stamp.
|
|
190
191
|
const files = {};
|
|
191
192
|
const kept = [];
|
|
193
|
+
const removedHere = readRemoved(dir);
|
|
194
|
+
const heldBack = [];
|
|
192
195
|
for (const [collection, entries] of Object.entries(doc.content)) {
|
|
193
196
|
for (const e of entries) {
|
|
194
197
|
const key = `${collection}/${e.slug ?? e.id}`;
|
|
198
|
+
// Removed here with `tidysites rm` and not pruned yet: the website still has it, the folder does not get it back.
|
|
199
|
+
if (removedHere.has(key) && !overwrite.has(key)) { heldBack.push(key); continue; }
|
|
195
200
|
const mine = local[key];
|
|
196
201
|
const was = prev?.entries?.[key];
|
|
197
202
|
// An entry this push just wrote is the website's now: its file takes the website's version ($media
|
|
@@ -244,6 +249,7 @@ async function pull({ dir, cfg }, collections = null, { overwrite = new Set(), m
|
|
|
244
249
|
for (const c of doc.collections.filter((c) => c.count)) say(` ${c.slug.padEnd(20)} ${c.count}`);
|
|
245
250
|
if (snap.menus) say(` ${'menus'.padEnd(20)} ${Object.keys(snap.menus).length}`);
|
|
246
251
|
for (const k of kept) say(` kept your unpushed changes in ${k}`);
|
|
252
|
+
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
253
|
}
|
|
248
254
|
|
|
249
255
|
/** The menus alone: files and the snapshot's menus, the entries untouched. */
|
|
@@ -339,6 +345,8 @@ async function push(ctx) {
|
|
|
339
345
|
process.exit(1);
|
|
340
346
|
}
|
|
341
347
|
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` : ''}.`);
|
|
348
|
+
// Removed from the website now: the folder stops holding them back.
|
|
349
|
+
if (removeKeys.length) { const rem = readRemoved(ctx.dir); for (const k of removeKeys) rem.delete(k); writeRemoved(ctx.dir, rem); }
|
|
342
350
|
if (r.missing?.length) say(`Not found to remove: ${r.missing.map((m) => `${m.collection}/${m.slug}`).join(', ')}`);
|
|
343
351
|
}
|
|
344
352
|
for (const name of menus) {
|
|
@@ -363,7 +371,7 @@ async function shot(ctx, target = positional[0] ?? '/', opts = {}) {
|
|
|
363
371
|
// --desktop, --mobile, --tablet, --wide, any of them together; --all for every width; none = desktop and mobile.
|
|
364
372
|
const picked = ['desktop', 'mobile', 'tablet', 'wide'].filter((v) => flags[v]);
|
|
365
373
|
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' }) };
|
|
374
|
+
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
375
|
const { json, images } = await tool(ctx.cfg, 'screenshot_website', args);
|
|
368
376
|
const outDir = join(ctx.dir, META, 'shots');
|
|
369
377
|
mkdirSync(outDir, { recursive: true });
|
|
@@ -382,13 +390,23 @@ async function shot(ctx, target = positional[0] ?? '/', opts = {}) {
|
|
|
382
390
|
if (!images.length) say(json ? JSON.stringify(json).slice(0, 500) : 'No image came back.');
|
|
383
391
|
}
|
|
384
392
|
|
|
393
|
+
/** What a shot or a measure opens first: --menu (every navigation menu), --open <selector,...> (clicked), --hover <selector,...> (their submenus shown). */
|
|
394
|
+
function openStates() {
|
|
395
|
+
const list = (v) => (v ? String(v).split(',').map((x) => x.trim()).filter(Boolean) : []);
|
|
396
|
+
const out = {};
|
|
397
|
+
if (flags.menu) out.menu = true;
|
|
398
|
+
if (flags.open) out.open = list(flags.open);
|
|
399
|
+
if (flags.hover) out.hover = list(flags.hover);
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
402
|
+
|
|
385
403
|
/** The computed type and boxes of a page, old or new, at one or several widths: the numbers a port styles to. */
|
|
386
404
|
async function measure(ctx) {
|
|
387
405
|
const target = positional[0] ?? '/';
|
|
388
406
|
const isUrl = /^https?:\/\//.test(target);
|
|
389
407
|
const picked = ['desktop', 'mobile', 'tablet', 'wide'].filter((v) => flags[v]);
|
|
390
408
|
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' }) };
|
|
409
|
+
const args = { site_id: ctx.cfg.site, view, ...openStates(), ...(isUrl ? { url: target } : { path: target, site: flags.live ? 'live' : 'preview' }) };
|
|
392
410
|
const { json } = await tool(ctx.cfg, 'measure_website', args);
|
|
393
411
|
if (!json?.views) fail('No measurements came back.');
|
|
394
412
|
const outDir = join(ctx.dir, META, 'measures');
|
|
@@ -417,6 +435,39 @@ async function measure(ctx) {
|
|
|
417
435
|
}
|
|
418
436
|
}
|
|
419
437
|
|
|
438
|
+
/** PageSpeed Insights for a page of the website (or any address), and the old page beside it with --against. */
|
|
439
|
+
async function speed(ctx) {
|
|
440
|
+
const target = positional[0] ?? '/';
|
|
441
|
+
const isUrl = /^https?:\/\//.test(target);
|
|
442
|
+
const strategy = flags.mobile && !flags.desktop ? 'mobile' : flags.desktop && !flags.mobile ? 'desktop' : 'both';
|
|
443
|
+
const args = { site_id: ctx.cfg.site, strategy, ...(isUrl ? { url: target } : { path: target, site: flags.live ? 'live' : 'preview' }), ...(flags.against ? { compare_url: String(flags.against) } : {}) };
|
|
444
|
+
say(`Testing${flags.against ? ' both pages' : ''} with PageSpeed Insights (a minute or two)...`);
|
|
445
|
+
const { json } = await tool(ctx.cfg, 'speed_website', args);
|
|
446
|
+
if (!json?.page) fail('No result came back.');
|
|
447
|
+
const outDir = join(ctx.dir, META, 'speed');
|
|
448
|
+
mkdirSync(outDir, { recursive: true });
|
|
449
|
+
const stamp = new Date().toISOString().replace(/[-:.]/g, '').slice(4, 18);
|
|
450
|
+
const base = (isUrl ? new URL(target).hostname + new URL(target).pathname : target).replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '') || 'home';
|
|
451
|
+
const file = join(outDir, `${base}-${stamp}.json`);
|
|
452
|
+
writeFileSync(file, JSON.stringify(json, null, '\t'));
|
|
453
|
+
const n = (v, unit = '') => (v === null || v === undefined ? '—' : `${v}${unit}`);
|
|
454
|
+
const line = (r) => r.error ? ` ${r.strategy.padEnd(8)} ${r.error}` : ` ${r.strategy.padEnd(8)} perf ${n(r.scores.performance)} a11y ${n(r.scores.accessibility)} best ${n(r.scores.best_practices)} seo ${n(r.scores.seo)} LCP ${n(r.lab.lcp_ms, 'ms')} FCP ${n(r.lab.fcp_ms, 'ms')} TBT ${n(r.lab.tbt_ms, 'ms')} CLS ${n(r.lab.cls)} ${r.lab.total_bytes ? Math.round(r.lab.total_bytes / 1024) + ' KB' : '—'} in ${n(r.lab.requests)} requests${r.field ? ` field: LCP ${n(r.field.lcp_ms, 'ms')} INP ${n(r.field.inp_ms, 'ms')} CLS ${n(r.field.cls)} (${r.field.overall})` : ''}`;
|
|
455
|
+
say(`\n${json.target}`);
|
|
456
|
+
for (const r of json.page) say(line(r));
|
|
457
|
+
if (json.compare) { say(`\n${json.compare_url} (the page it is compared with)`); for (const r of json.compare) say(line(r)); }
|
|
458
|
+
for (const r of json.page) {
|
|
459
|
+
if (r.error) continue;
|
|
460
|
+
say(`\n ${r.strategy}: what to change`);
|
|
461
|
+
if (r.lcp_element) say(` largest paint: ${r.lcp_element.replace(/\s+/g, ' ').slice(0, 160)}`);
|
|
462
|
+
for (const o of r.opportunities.slice(0, 8)) {
|
|
463
|
+
say(` • ${o.title}${o.savings_ms ? ` (${o.savings_ms} ms)` : ''}${o.savings_kb ? ` (${o.savings_kb} KB)` : ''}`);
|
|
464
|
+
for (const it of o.items.slice(0, 4)) say(` ${it}`);
|
|
465
|
+
}
|
|
466
|
+
for (const d of r.diagnostics.slice(0, 8)) say(` · ${d}`);
|
|
467
|
+
}
|
|
468
|
+
say(`\nSaved to ${relative(process.cwd(), file)}. Full reports: ${json.page.filter((r) => !r.error).map((r) => r.report_url).join(' ')}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
420
471
|
/** The website's redirects: list them, add one, remove one. They ship with the next publish. */
|
|
421
472
|
async function redirect(ctx) {
|
|
422
473
|
const sub = positional[0];
|
|
@@ -556,12 +607,21 @@ function rmCmd({ dir }) {
|
|
|
556
607
|
const f = join(colDir, `${sl}.${ext}`);
|
|
557
608
|
if (existsSync(f)) { rmSync(f); hit = true; }
|
|
558
609
|
}
|
|
559
|
-
if (hit) { removed++; say(`- ${collection}/${sl}`); } else say(`${collection}/${sl}: no such entry file`);
|
|
610
|
+
if (hit) { removed++; say(`- ${collection}/${sl}`); rememberRemoved(dir, `${collection}/${sl}`); } else say(`${collection}/${sl}: no such entry file`);
|
|
560
611
|
}
|
|
561
612
|
}
|
|
562
613
|
say(`${removed} entr${removed === 1 ? 'y' : 'ies'} removed from the folder; \`tidysites push --prune\` removes them from the website.`);
|
|
563
614
|
}
|
|
564
615
|
|
|
616
|
+
/**
|
|
617
|
+
* Entries removed here and not yet pruned from the website. A pull (the one after a failed push included) does not
|
|
618
|
+
* write them back; `push --prune` removes them from the website and forgets them. The list lives beside the snapshot.
|
|
619
|
+
*/
|
|
620
|
+
const REMOVED_FILE = 'removed.json';
|
|
621
|
+
function readRemoved(dir) { return new Set(readJson(join(dir, META, REMOVED_FILE)) ?? []); }
|
|
622
|
+
function writeRemoved(dir, set) { writeFileSync(join(dir, META, REMOVED_FILE), JSON.stringify([...set].sort(), null, '\t')); }
|
|
623
|
+
function rememberRemoved(dir, key) { const s = readRemoved(dir); s.add(key); writeRemoved(dir, s); }
|
|
624
|
+
|
|
565
625
|
async function publish(ctx) {
|
|
566
626
|
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
627
|
// A website live on the client's own domain is never published from here: the website refuses (409 live_domain) and says who may.
|
|
@@ -616,6 +676,8 @@ async function main() {
|
|
|
616
676
|
}
|
|
617
677
|
case 'measure':
|
|
618
678
|
return measure(config());
|
|
679
|
+
case 'speed':
|
|
680
|
+
return speed(config());
|
|
619
681
|
case 'redirect':
|
|
620
682
|
case 'redirects':
|
|
621
683
|
return redirect(config());
|
package/package.json
CHANGED