@decocms/blocks-cli 7.37.6 → 7.38.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/blocks-cli",
3
- "version": "7.37.6",
3
+ "version": "7.38.0",
4
4
  "type": "module",
5
5
  "description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  "lint:unused": "knip"
32
32
  },
33
33
  "dependencies": {
34
- "@decocms/blocks": "7.37.6",
34
+ "@decocms/blocks": "7.38.0",
35
35
  "ts-morph": "^27.0.0",
36
36
  "tsx": "^4.22.5"
37
37
  },
@@ -3,14 +3,16 @@
3
3
  * .github/workflows/perf.yml — GitHub Actions workflow
4
4
  * tools/perf/changed-paths.sh — maps edited sections → CMS page paths
5
5
  * tools/perf/compare.mjs — compares two LHCI result dirs, emits markdown
6
- * lighthouserc.json — LHCI collect settings (3 runs, mobile UA)
6
+ * lighthouserc.json — LHCI collect settings (3 runs, mobile UA, 4 categories)
7
7
  *
8
8
  * Design rationale:
9
9
  * - CF Workers Builds generates a per-PR preview URL (workers.dev). Both the
10
10
  * PR and main previews are workers.dev — no edge cache — so the delta is
11
11
  * purely code, not cache state.
12
- * - Gate signal = CLS + TBT (layout stability / JS work; cache-insensitive).
13
- * LCP and score are informational only (noisy on cold SSR).
12
+ * - Gate signal = CLS + TBT + a11y/best-practices/SEO scores. CLS/TBT are
13
+ * layout stability / JS work; the three category scores are static-analysis
14
+ * audits. All cache-insensitive, so a regression is real code, not cold SSR.
15
+ * LCP and performance score are informational only (noisy on cold SSR).
14
16
  * - Advisory: `continue-on-error: true`. Remove it once the site stabilises.
15
17
  * - LHCI 0.15 does not write manifest.json; compare.mjs reads lhr-*.json
16
18
  * directly and selects the median run by performance score.
@@ -40,7 +42,7 @@ function generatePerfYml(workerName: string): string {
40
42
  # preview URL do output — sem token CF nem build local.
41
43
  #
42
44
  # Baseline: URL fixa da preview do main (https://${workerName}.deco-cx.workers.dev).
43
- # Gate: CLS + TBT (estrutura/JS, insensíveis a cache). LCP/score: informativos.
45
+ # Gate: CLS + TBT + A11y/BP/SEO (estrutura/JS + auditorias estáticas, insensíveis a cache). LCP/score: informativos.
44
46
 
45
47
  on:
46
48
  pull_request:
@@ -297,8 +299,10 @@ const COMPARE_MJS = `#!/usr/bin/env node
297
299
  // cache, same runner/region, same mobile UA — so the delta is the PR's code
298
300
  // effect, not cache warm/cold.
299
301
  //
300
- // Gate signal = CLS + TBT (page structure / JS work — cache-insensitive).
301
- // LCP / score are shown but informative only (noisy on cold workers.dev SSR).
302
+ // Gate signal = CLS + TBT + a11y/best-practices/SEO scores. CLS/TBT are page
303
+ // structure / JS work; the three category scores are static-analysis audits.
304
+ // All are cache-insensitive, so a regression is real code, not cold SSR.
305
+ // LCP / performance score are shown but informative only (noisy on cold workers.dev SSR).
302
306
  //
303
307
  // LHCI 0.15 does not write manifest.json; we read lhr-*.json directly.
304
308
  //
@@ -312,7 +316,12 @@ const TBT_ABS = 50; // ms — deltas below this are noise
312
316
  const TBT_REL = 0.15; // 15% relative change
313
317
  const CLS_ABS = 0.02; // absolute CLS delta that matters
314
318
  const CLS_FLOOR = 0.1; // only flag CLS regression when PR is above "good"
319
+ const SCORE_DROP = 3; // pts — a11y/bp/seo score drop that gates (tolerates 1–2pt audit jitter)
320
+ const SCORES = ["a11y", "bp", "seo"]; // gated category scores (0–100)
315
321
 
322
+ // base/pr = { tbt, cls, a11y, bp, seo }. Called from selftest with only
323
+ // { tbt, cls }: the SCORES deltas become NaN and NaN comparisons are false,
324
+ // so score gating is simply inert there.
316
325
  function verdict(base, pr) {
317
326
  const tbtUp = pr.tbt - base.tbt;
318
327
  const tbtBad = tbtUp > TBT_ABS && tbtUp > base.tbt * TBT_REL;
@@ -320,8 +329,10 @@ function verdict(base, pr) {
320
329
  const clsUp = pr.cls - base.cls;
321
330
  const clsBad = clsUp > CLS_ABS && pr.cls > CLS_FLOOR;
322
331
  const clsGood = -clsUp > CLS_ABS && base.cls > CLS_FLOOR;
323
- if (tbtBad || clsBad) return { icon: "🔴", gate: true };
324
- if (tbtGood || clsGood) return { icon: "🟢", gate: false };
332
+ const scoreBad = SCORES.some((k) => pr[k] - base[k] <= -SCORE_DROP);
333
+ const scoreGood = !scoreBad && SCORES.some((k) => pr[k] - base[k] >= SCORE_DROP);
334
+ if (tbtBad || clsBad || scoreBad) return { icon: "🔴", gate: true };
335
+ if (tbtGood || clsGood || scoreGood) return { icon: "🟢", gate: false };
325
336
  return { icon: "⚪", gate: false };
326
337
  }
327
338
 
@@ -340,7 +351,10 @@ function loadDir(dir) {
340
351
  const lhr = runs[Math.floor(runs.length / 2)];
341
352
  const a = lhr.audits;
342
353
  byPath[new URL(url).pathname] = {
343
- score: Math.round((lhr.categories.performance.score ?? 0) * 100),
354
+ score: Math.round((lhr.categories.performance?.score ?? 0) * 100),
355
+ a11y: Math.round((lhr.categories.accessibility?.score ?? 0) * 100),
356
+ bp: Math.round((lhr.categories["best-practices"]?.score ?? 0) * 100),
357
+ seo: Math.round((lhr.categories.seo?.score ?? 0) * 100),
344
358
  lcp: a["largest-contentful-paint"].numericValue,
345
359
  cls: a["cumulative-layout-shift"].numericValue,
346
360
  tbt: a["total-blocking-time"].numericValue,
@@ -351,11 +365,14 @@ function loadDir(dir) {
351
365
 
352
366
  const ms = (v) => \`\${Math.round(v)}ms\`;
353
367
  const cls = (v) => v.toFixed(3);
368
+ const pts = (v) => \`\${Math.round(v)}\`;
354
369
  function delta(base, pr, fmt) {
355
370
  const d = pr - base;
356
371
  const arrow = d < 0 ? "↓" : d > 0 ? "↑" : "";
357
372
  return \`\${d > 0 ? "+" : ""}\${fmt(d)} \${arrow}\`.trim();
358
373
  }
374
+ // base→PR (Δ) for a 0–100 category score.
375
+ const scoreCell = (b, p) => \`\${b}→\${p} (\${delta(b, p, pts)})\`;
359
376
 
360
377
  function render(base, pr) {
361
378
  const paths = [...new Set([...Object.keys(base), ...Object.keys(pr)])].sort();
@@ -365,29 +382,29 @@ function render(base, pr) {
365
382
  "",
366
383
  "Ambas as previews rodam em \`*.workers.dev\` (sem edge cache), mesmo runner e UA mobile — o delta é efeito do código, não de cache quente/frio.",
367
384
  "",
368
- "**Gate:** CLS + TBT (estrutura / JS). LCP e score são informativos (ruído esperado em SSR frio).",
385
+ "**Gate:** CLS + TBT + A11y/BP/SEO (estrutura/JS + auditorias estáticas). LCP e score de performance são informativos (ruído esperado em SSR frio).",
369
386
  "",
370
- "| Página | | CLS (base→PR) | TBT (base→PR) | LCP | Score |",
371
- "|---|:--:|---|---|---|---|",
387
+ "| Página | | CLS (base→PR) | TBT (base→PR) | A11y | BP | SEO | LCP | Score |",
388
+ "|---|:--:|---|---|---|---|---|---|---|",
372
389
  ];
373
390
  for (const p of paths) {
374
391
  const b = base[p];
375
392
  const r = pr[p];
376
393
  if (!b || !r) {
377
- lines.push(\`| \\\`\${p}\\\` | ⚠️ | \${b ? "faltou PR" : "faltou base"} | | | |\`);
394
+ lines.push(\`| \\\`\${p}\\\` | ⚠️ | \${b ? "faltou PR" : "faltou base"} | | | | | | |\`);
378
395
  continue;
379
396
  }
380
397
  const v = verdict(b, r);
381
398
  anyGate = anyGate || v.gate;
382
399
  lines.push(
383
- \`| \\\`\${p}\\\` | \${v.icon} | \${cls(b.cls)}→\${cls(r.cls)} (\${delta(b.cls, r.cls, cls)}) | \${ms(b.tbt)}→\${ms(r.tbt)} (\${delta(b.tbt, r.tbt, ms)}) | \${ms(b.lcp)}→\${ms(r.lcp)} | \${b.score}→\${r.score} |\`,
400
+ \`| \\\`\${p}\\\` | \${v.icon} | \${cls(b.cls)}→\${cls(r.cls)} (\${delta(b.cls, r.cls, cls)}) | \${ms(b.tbt)}→\${ms(r.tbt)} (\${delta(b.tbt, r.tbt, ms)}) | \${scoreCell(b.a11y, r.a11y)} | \${scoreCell(b.bp, r.bp)} | \${scoreCell(b.seo, r.seo)} | \${ms(b.lcp)}→\${ms(r.lcp)} | \${b.score}→\${r.score} |\`,
384
401
  );
385
402
  }
386
403
  lines.push("");
387
404
  lines.push(
388
405
  anyGate
389
- ? "🔴 **Regressão de CLS/TBT detectada** (advisory — não trava o merge)."
390
- : "🟢 Sem regressão de CLS/TBT.",
406
+ ? "🔴 **Regressão detectada em CLS/TBT/A11y/BP/SEO** (advisory — não trava o merge)."
407
+ : "🟢 Sem regressão nas métricas gateadas (CLS/TBT/A11y/BP/SEO).",
391
408
  );
392
409
  return { md: lines.join("\\n"), anyGate };
393
410
  }
@@ -400,6 +417,13 @@ function selftest() {
400
417
  ok(!verdict({ tbt: 100, cls: 0.02 }, { tbt: 100, cls: 0.08 }).gate, "cls under 0.1 floor no gate");
401
418
  ok(verdict({ tbt: 300, cls: 0.01 }, { tbt: 100, cls: 0.01 }).icon === "🟢", "tbt drop is green");
402
419
  ok(verdict({ tbt: 100, cls: 0.01 }, { tbt: 105, cls: 0.01 }).icon === "⚪", "tiny change is neutral");
420
+ // Category-score gating. S() supplies a neutral CLS/TBT baseline plus perfect scores.
421
+ const S = (o) => ({ tbt: 100, cls: 0.01, a11y: 100, bp: 100, seo: 100, ...o });
422
+ ok(verdict(S({}), S({ seo: 96 })).gate, "seo 100→96 (-4) should gate");
423
+ ok(verdict(S({ a11y: 90 }), S({ a11y: 87 })).gate, "a11y -3 should gate");
424
+ ok(!verdict(S({}), S({ bp: 98 })).gate, "bp -2 is jitter, no gate");
425
+ ok(verdict(S({ a11y: 90 }), S({ a11y: 95 })).icon === "🟢", "a11y +5 is green");
426
+ ok(verdict(S({}), S({ a11y: 99, seo: 97 })).icon === "🔴", "seo -3 gates even with a11y -1");
403
427
  console.log("compare.mjs selftest: OK");
404
428
  }
405
429
 
@@ -411,12 +435,12 @@ process.stdout.write(render(loadDir(baseDir), loadDir(prDir)).md + "\\n");
411
435
  `;
412
436
 
413
437
  const LIGHTHOUSERC_JSON = `{
414
- "//": "Lighthouse CI config for the per-PR perf workflow (.github/workflows/perf.yml). Mobile UA matches worker-entry.ts buildSegment (MOBILE_RE) so warmup and measurement hit the same device cache key.",
438
+ "//": "Lighthouse CI config for the per-PR perf workflow (.github/workflows/perf.yml). Mobile UA matches worker-entry.ts buildSegment (MOBILE_RE) so warmup and measurement hit the same device cache key. accessibility/best-practices/seo são análise estática determinística (não sofrem ruído de SSR frio como LCP/score), então compare.mjs os gateia por regressão de score.",
415
439
  "ci": {
416
440
  "collect": {
417
441
  "numberOfRuns": 3,
418
442
  "settings": {
419
- "onlyCategories": ["performance"],
443
+ "onlyCategories": ["performance", "accessibility", "best-practices", "seo"],
420
444
  "emulatedUserAgent": "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
421
445
  }
422
446
  }