create-website-build-kit 0.1.5 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-website-build-kit",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Scaffold a production marketing site \u2014 Astro on Cloudflare Workers, with the gates, the migration playbook and the accessibility work already wired.",
5
5
  "keywords": [
6
6
  "astro",
@@ -31,12 +31,15 @@
31
31
  */
32
32
 
33
33
  import { execFileSync } from 'node:child_process';
34
- import { existsSync, readdirSync, writeFileSync } from 'node:fs';
34
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
35
35
 
36
36
  const OUT = 'src/data/lastmod.json';
37
37
 
38
- const git = (args) =>
39
- execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
38
+ const gitRaw = (args) =>
39
+ execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
40
+
41
+ /** Trimmed — correct for a single scalar like `--format=%cI` or a rev-parse answer. */
42
+ const git = (args) => gitRaw(args).trim();
40
43
 
41
44
  if (git(['rev-parse', '--is-shallow-repository']) === 'true') {
42
45
  console.error(
@@ -47,8 +50,22 @@ if (git(['rev-parse', '--is-shallow-repository']) === 'true') {
47
50
  }
48
51
 
49
52
  /** Files with uncommitted changes. Their real "last modified" is now, not their last commit. */
53
+ /*
54
+ * ⚠ gitRaw, NOT git — `--porcelain` LINES BEGIN WITH A SIGNIFICANT SPACE.
55
+ *
56
+ * The status format is two columns then a space: ` M path` for an unstaged
57
+ * modification. Trimming the whole output eats the leading space of the FIRST
58
+ * line only, so `slice(3)` then cut one character too far and produced
59
+ * "rc/pages/about.astro". That path matched nothing, so the first uncommitted
60
+ * file was never treated as dirty and kept its old commit date — while the
61
+ * script still printed "1 uncommitted file(s) dated today".
62
+ *
63
+ * Only the first entry, and only when it starts with a space, which is the
64
+ * ordinary case of having edited a page without committing it: the page most
65
+ * worth recrawling is the one that silently keeps a stale date.
66
+ */
50
67
  const dirty = new Set(
51
- git(['status', '--porcelain'])
68
+ gitRaw(['status', '--porcelain'])
52
69
  .split('\n')
53
70
  .filter(Boolean)
54
71
  .map((l) => l.slice(3).trim()),
@@ -139,6 +156,9 @@ for (const [route, files] of [...sources].sort()) {
139
156
  if (date) out[route] = date;
140
157
  }
141
158
 
159
+ /* src/data/ exists in the template, but not in a bare checkout or a fixture —
160
+ and an ENOENT stack is not a diagnosis. */
161
+ mkdirSync(OUT.slice(0, OUT.lastIndexOf('/')), { recursive: true });
142
162
  writeFileSync(OUT, JSON.stringify(out, null, 2) + '\n');
143
163
 
144
164
  const spread = Object.values(out).reduce((m, d) => m.set(d, (m.get(d) ?? 0) + 1), new Map());
@@ -185,7 +185,18 @@ function worstContrast(image, fgHex, [x, y, w, h]) {
185
185
 
186
186
  /* ── Drawing ───────────────────────────────────────────────────────────── */
187
187
 
188
- const manifest = JSON.parse(readFileSync('src/data/image-manifest.json', 'utf8'));
188
+ /*
189
+ * ⚠ LAZY, BECAUSE A TOP-LEVEL READ RUNS BEFORE preflight().
190
+ *
191
+ * As a `const` at module scope this executed at import time — before main()
192
+ * called preflight() — so a project without an image manifest died on a raw
193
+ * ENOENT stack instead of being told its config was still the stub. The
194
+ * preflight exists precisely to name what is missing, and it was unreachable
195
+ * for the most common way to be missing something.
196
+ */
197
+ let manifestCache = null;
198
+ const manifest = () =>
199
+ (manifestCache ??= JSON.parse(readFileSync('src/data/image-manifest.json', 'utf8')));
189
200
 
190
201
  /**
191
202
  * Render one text run as its own transparent layer.
@@ -225,7 +236,7 @@ function buildCard(card, fonts, mark) {
225
236
 
226
237
  /* 1. Background. */
227
238
  if (card.photo) {
228
- const entry = manifest[card.photo];
239
+ const entry = manifest()[card.photo];
229
240
  if (!entry) throw new Error(`${card.route ?? slug}: no manifest entry for "${card.photo}"`);
230
241
  const file = join('public', entry.src);
231
242
  if (!existsSync(file)) throw new Error(`${card.route ?? slug}: missing file ${file}`);
@@ -238,6 +238,81 @@ tell(
238
238
  );
239
239
  }
240
240
 
241
+ /*
242
+ * ── THE TELLS OF A GENERATED SITE, NOT A TEMPLATED ONE ─────────────────────
243
+ *
244
+ * Everything above catches the 2015 agency template: three equal cards, body
245
+ * text at container width, a headline at 96px. This block catches a newer and
246
+ * closer failure — the house style of the thing writing the code.
247
+ *
248
+ * These are counts with generous thresholds, deliberately. One frosted header
249
+ * is a decision; three glass surfaces is an aesthetic nobody chose. A gate that
250
+ * fires on a single legitimate use is one people learn to switch off, which is
251
+ * how the first version of `check:refs` shipped with seven false positives on a
252
+ * clean tree.
253
+ */
254
+
255
+ /*
256
+ * "glass everywhere" — backdrop-filter as a look rather than a decision.
257
+ *
258
+ * ⚠ THE CHARACTER CLASS EXCLUDES `}` AS WELL AS `;`, AND NONE OF THESE THREE
259
+ * REQUIRE A TRAILING SEMICOLON. The last declaration in a block may legally
260
+ * omit it. Written as `[^;]+;` these matched nothing there; written greedily
261
+ * as `[^;]*` one match ran across two whole declarations and counted them as
262
+ * one. Both were caught by fixtures asserting the check FIRES, never by the
263
+ * clean template, where all three read as passing.
264
+ */
265
+ {
266
+ const glass = [...sourceCss.matchAll(/backdrop-filter:[^;}]*blur/g)].length;
267
+ tell(
268
+ 'frosted glass on more than one surface',
269
+ glass > 1,
270
+ `${glass} backdrop-filter blurs. One translucent header is a choice; a page of them is the default look of generated UI, and each one costs a paint.`,
271
+ );
272
+ }
273
+
274
+ /*
275
+ * "giant border radii" — 24px and up.
276
+ *
277
+ * ⚠ A pill and a circle are NOT this. `9999px`, `50%` and `100%` are how you
278
+ * write "fully round" for a badge or an avatar, and flagging those would make
279
+ * the check useless on any correct design.
280
+ */
281
+ {
282
+ const radii = [...sourceCss.matchAll(/border-radius:\s*([^;}]+)/g)]
283
+ .flatMap((m) => [...m[1].matchAll(/([\d.]+)(px|rem)/g)])
284
+ .map((m) => (m[2] === 'rem' ? Number(m[1]) * 16 : Number(m[1])))
285
+ .filter((px) => px >= 24 && px < 200);
286
+ tell(
287
+ 'border radii of 24px and up, repeatedly',
288
+ radii.length > 2,
289
+ `${radii.length} radii at 24px or more (${[...new Set(radii)].slice(0, 4).join(', ')}px). Softness at that scale reads as a default rather than a decision. Pills and circles are excluded.`,
290
+ );
291
+ }
292
+
293
+ /*
294
+ * "glow" — a shadow with no offset and a real blur. `0 0 40px <colour>` is
295
+ * decoration; nothing in the physical world lights up from behind.
296
+ *
297
+ * ⚠ A focus ring is `0 0 0 3px` — zero blur. Requiring blur ≥ 16px is what
298
+ * keeps this from flagging the one shadow every accessible site needs.
299
+ */
300
+ {
301
+ const glows = [...sourceCss.matchAll(/box-shadow:\s*([^;}]+)/g)]
302
+ .flatMap((m) => m[1].split(','))
303
+ .filter((sh) => /(^|\s)0\s+0\s+([\d.]+)(px|rem)/.test(sh))
304
+ .filter((sh) => {
305
+ const m = /(^|\s)0\s+0\s+([\d.]+)(px|rem)/.exec(sh);
306
+ const blur = m[3] === 'rem' ? Number(m[2]) * 16 : Number(m[2]);
307
+ return blur >= 16;
308
+ });
309
+ tell(
310
+ 'glow shadows',
311
+ glows.length > 0,
312
+ `${glows.length} zero-offset shadow(s) with a large blur. A glow is decoration with no physical referent; a shadow with offset reads as light. Focus rings (0 0 0 3px) are excluded.`,
313
+ );
314
+ }
315
+
241
316
  // "the 404, the empty state or the form's invalid state was never designed"
242
317
  tell(
243
318
  'no invalid / busy form state',