@tideworthy/tidysites 0.1.0 → 0.1.2
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 +14 -1
- package/bin/tidysites.mjs +38 -7
- package/lib/fetch.mjs +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ nothing else; it lives in `.tidysites/key`, which is kept out of git.
|
|
|
18
18
|
| | |
|
|
19
19
|
|---|---|
|
|
20
20
|
| `tidysites init <site_id> --host <url> --key <key>` | connect this folder to a website and pull it |
|
|
21
|
-
| `tidysites pull [--collections pages,services]` | the website's content to files; files for entries the website no longer has are removed |
|
|
21
|
+
| `tidysites pull [--collections pages,services,menus]` | the website's content to files; files for entries the website no longer has are removed |
|
|
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 |
|
|
@@ -30,6 +30,10 @@ nothing else; it lives in `.tidysites/key`, which is kept out of git.
|
|
|
30
30
|
| `tidysites fetch <url> [--out dir]` | a page of the site being ported: cleaned HTML, its outline (headings, navigation, images, phones, fonts, colours) and its stylesheets |
|
|
31
31
|
| `tidysites publish --yes` | the live site becomes a copy of the preview site |
|
|
32
32
|
|
|
33
|
+
A section keeps its type with its key: to replace an intro with a row, give the row its own `_key`
|
|
34
|
+
(`validate` says so; the website would only say "internal error"). After a push the pushed files take the
|
|
35
|
+
website's version (a `$media` becomes the stored image, `_version` appears); every other file stays as it is.
|
|
36
|
+
|
|
33
37
|
A push that would overwrite an entry changed on the website since the pull
|
|
34
38
|
is refused with the list; pull again (your files are overwritten, so copy
|
|
35
39
|
what you need first) or push `--force`.
|
|
@@ -62,6 +66,15 @@ References: a record names another by its id (what the pull shows), or by
|
|
|
62
66
|
|
|
63
67
|
Rich text (a section's `copy`, a post's `content`) is markdown.
|
|
64
68
|
|
|
69
|
+
Custom HTML (a row's `.html` file, a Columns HTML slot) reads the records:
|
|
70
|
+
`{{ business.phone }}`, `{{ page.title }}`, `{{#each services as s}}…{{ s.url }}…{{/each}}`,
|
|
71
|
+
`{{#if page.collection == "services"}}…{{/if}}`, and the drop-ins `{{> menu "primary"}}`,
|
|
72
|
+
`{{> logo}}`, `{{> phone}}`, `{{> address}}`, `{{> hours}}`, `{{> social}}`, `{{> year}}`,
|
|
73
|
+
`{{> button "Label" "/url" style=outline}}`, `{{> form "Name"}}`. A link to `#form:<Name>`
|
|
74
|
+
opens that form in a dialog. So a custom header is written once against `page.*` and
|
|
75
|
+
`business.*` and fits every page; `frame/frame.json` names it as `header_row`, or a page
|
|
76
|
+
names its own in `header_row`.
|
|
77
|
+
|
|
65
78
|
## Building a site from these files
|
|
66
79
|
|
|
67
80
|
1. `tidysites schema` for the section types, `tidysites gallery` to see them in this theme.
|
package/bin/tidysites.mjs
CHANGED
|
@@ -165,8 +165,15 @@ function readLocalMenus(dir) {
|
|
|
165
165
|
return out;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
async function pull({ dir, cfg }, collections = null) {
|
|
169
|
-
|
|
168
|
+
async function pull({ dir, cfg }, collections = null, { overwrite = new Set(), menus: wantMenus = null } = {}) {
|
|
169
|
+
let want = collections ?? (flags.collections ? String(flags.collections).split(',').map((s) => s.trim()).filter(Boolean) : null);
|
|
170
|
+
// `menus` is not a collection on the website; it is pulled through the site's own menu tools.
|
|
171
|
+
let refreshMenus = wantMenus ?? (want === null || want.includes('menus'));
|
|
172
|
+
if (want) want = want.filter((c) => c !== 'menus');
|
|
173
|
+
if (want && !want.length) {
|
|
174
|
+
await pullMenus({ dir, cfg });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
170
177
|
const [{ json: doc }, { json: schema }] = await Promise.all([
|
|
171
178
|
tool(cfg, 'pull_website', { site_id: cfg.site, ...(want ? { collections: want.join(',') } : {}) }),
|
|
172
179
|
tool(cfg, 'get_website_schema', { site_id: cfg.site })
|
|
@@ -184,7 +191,9 @@ async function pull({ dir, cfg }, collections = null) {
|
|
|
184
191
|
const key = `${collection}/${e.slug ?? e.id}`;
|
|
185
192
|
const mine = local[key];
|
|
186
193
|
const was = prev?.entries?.[key];
|
|
187
|
-
|
|
194
|
+
// An entry this push just wrote is the website's now: its file takes the website's version ($media
|
|
195
|
+
// becomes the stored image, _version appears), whatever the file said a moment ago.
|
|
196
|
+
if (mine && was && !overwrite.has(key) && !sameEntry(mine, was) && !sameEntry(mine, { status: e.status, data: e.data })) {
|
|
188
197
|
kept.push(key);
|
|
189
198
|
continue;
|
|
190
199
|
}
|
|
@@ -205,14 +214,14 @@ async function pull({ dir, cfg }, collections = null) {
|
|
|
205
214
|
}
|
|
206
215
|
writeFiles(dir, files);
|
|
207
216
|
const snap = snapshotOf(doc);
|
|
208
|
-
if (
|
|
217
|
+
if (refreshMenus) {
|
|
209
218
|
const menus = await readMenus(cfg);
|
|
210
219
|
snap.menus = menus;
|
|
211
220
|
const localMenus = readLocalMenus(dir);
|
|
212
221
|
for (const [name, menu] of Object.entries(menus)) {
|
|
213
222
|
const mine = localMenus[name];
|
|
214
223
|
const wasMenu = prev?.menus?.[name];
|
|
215
|
-
if (mine && !mine.error && wasMenu && JSON.stringify(mine.items) !== JSON.stringify(wasMenu.items) && JSON.stringify(mine.items) !== JSON.stringify(menu.items)) {
|
|
224
|
+
if (mine && !mine.error && wasMenu && !overwrite.has(`menus/${name}`) && JSON.stringify(mine.items) !== JSON.stringify(wasMenu.items) && JSON.stringify(mine.items) !== JSON.stringify(menu.items)) {
|
|
216
225
|
kept.push(`menus/${name}`);
|
|
217
226
|
continue;
|
|
218
227
|
}
|
|
@@ -234,6 +243,17 @@ async function pull({ dir, cfg }, collections = null) {
|
|
|
234
243
|
for (const k of kept) say(` kept your unpushed changes in ${k}`);
|
|
235
244
|
}
|
|
236
245
|
|
|
246
|
+
/** The menus alone: files and the snapshot's menus, the entries untouched. */
|
|
247
|
+
async function pullMenus({ dir, cfg }) {
|
|
248
|
+
const snap = readJson(join(dir, META, 'snapshot.json'));
|
|
249
|
+
if (!snap) fail('Nothing pulled yet. Run `tidysites pull`.');
|
|
250
|
+
const menus = await readMenus(cfg);
|
|
251
|
+
for (const [name, menu] of Object.entries(menus)) writeFiles(dir, { [`menus/${name}.json`]: `${JSON.stringify({ label: menu.label, items: menu.items }, null, '\t')}\n` });
|
|
252
|
+
snap.menus = menus;
|
|
253
|
+
writeFileSync(join(dir, META, 'snapshot.json'), JSON.stringify(snap, null, '\t'));
|
|
254
|
+
say(`Pulled ${Object.keys(menus).length} menu${Object.keys(menus).length === 1 ? '' : 's'}.`);
|
|
255
|
+
}
|
|
256
|
+
|
|
237
257
|
function status({ dir }) {
|
|
238
258
|
const snap = readJson(join(dir, META, 'snapshot.json'));
|
|
239
259
|
if (!snap) fail('Nothing pulled yet. Run `tidysites pull`.');
|
|
@@ -264,6 +284,15 @@ function validate({ dir }, local = null) {
|
|
|
264
284
|
if (!schema) fail('No content model here yet. Run `tidysites pull`.');
|
|
265
285
|
const read = local ? { local, problems: [] } : readLocal(dir);
|
|
266
286
|
const errors = [...read.problems, ...validateAll(read.local, schema)];
|
|
287
|
+
// The website keeps a section's type with its key: a key that changes type is refused with a bare "internal error", so say it here.
|
|
288
|
+
const snap = readJson(join(dir, META, 'snapshot.json'));
|
|
289
|
+
for (const [key, entry] of Object.entries(read.local)) {
|
|
290
|
+
const before = snap?.entries?.[key]?.data?.sections;
|
|
291
|
+
const after = entry.data?.sections;
|
|
292
|
+
if (!Array.isArray(before) || !Array.isArray(after)) continue;
|
|
293
|
+
const types = new Map(before.filter((b) => b && b._key).map((b) => [b._key, b._type]));
|
|
294
|
+
for (const b of after) if (b && b._key && types.has(b._key) && types.get(b._key) !== b._type) errors.push(`${key}.json: section "${b._key}" was a ${types.get(b._key)} and is now a ${b._type}; a section keeps its type, so give the new one its own _key`);
|
|
295
|
+
}
|
|
267
296
|
return errors;
|
|
268
297
|
}
|
|
269
298
|
|
|
@@ -320,8 +349,10 @@ async function push(ctx) {
|
|
|
320
349
|
await tool(ctx.cfg, 'call_website_site_tool', { site_id: ctx.cfg.site, tool: 'menu_set_items', arguments: { name, items } });
|
|
321
350
|
say(`Menu ${name}: ${items.length} item${items.length === 1 ? '' : 's'} written.`);
|
|
322
351
|
}
|
|
323
|
-
// Refresh only what went
|
|
324
|
-
|
|
352
|
+
// Refresh only what went, and take the website's version of exactly those entries; every other file stays as it is.
|
|
353
|
+
const pushed = new Set([...keys, ...menus.map((m) => `menus/${m}`)]);
|
|
354
|
+
if (touched.length) await pull(ctx, touched, { overwrite: pushed, menus: menus.length > 0 });
|
|
355
|
+
else if (menus.length) await pullMenus(ctx);
|
|
325
356
|
}
|
|
326
357
|
|
|
327
358
|
async function shot(ctx, target = positional[0] ?? '/', opts = {}) {
|
package/lib/fetch.mjs
CHANGED
|
@@ -108,8 +108,14 @@ export function cssSummary(css) {
|
|
|
108
108
|
const f = m[1].split(',')[0].replace(/["']/g, '').trim();
|
|
109
109
|
if (f && !/^(inherit|initial|unset|var\()/.test(f)) fonts.set(f, (fonts.get(f) ?? 0) + 1);
|
|
110
110
|
}
|
|
111
|
+
// The site's own names for its colours and fonts, when its stylesheet defines them; a framework's defaults sit below these.
|
|
112
|
+
const variables = {};
|
|
113
|
+
for (const m of css.matchAll(/--([a-zA-Z][\w-]*)\s*:\s*([^;}]+)/g)) {
|
|
114
|
+
const v = m[2].trim();
|
|
115
|
+
if (/#[0-9a-f]{3,8}\b|rgba?\(|hsla?\(|font|["'][A-Z][\w ]+["']/i.test(v) && !/^var\(/.test(v)) variables[m[1]] = v;
|
|
116
|
+
}
|
|
111
117
|
const top = (map, n) => [...map.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([value, count]) => ({ value, count }));
|
|
112
|
-
return { colors: top(colors, 12), fonts: top(fonts, 6) };
|
|
118
|
+
return { colors: top(colors, 12), fonts: top(fonts, 6), variables };
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
export function outlineText(o) {
|
|
@@ -133,6 +139,8 @@ export function outlineText(o) {
|
|
|
133
139
|
if (o.emails.length) lines.push(`Emails: ${o.emails.join(', ')}`);
|
|
134
140
|
if (o.fonts.length) lines.push(`Google fonts: ${o.fonts.join(', ')}`);
|
|
135
141
|
if (o.css) {
|
|
142
|
+
const vars = Object.entries(o.css.variables ?? {});
|
|
143
|
+
if (vars.length) { lines.push('', 'Stylesheet variables (the site\'s own names; trust these over the counts below):'); for (const [k, v] of vars.slice(0, 40)) lines.push(` --${k}: ${v}`); }
|
|
136
144
|
lines.push('', 'Stylesheet colours (most used first):', ` ${o.css.colors.map((c) => `${c.value} ×${c.count}`).join(' ')}`);
|
|
137
145
|
lines.push('Stylesheet fonts:', ` ${o.css.fonts.map((c) => `${c.value} ×${c.count}`).join(' ')}`);
|
|
138
146
|
}
|
package/package.json
CHANGED