@getvitops/utils 8.2.0 → 8.3.0-alpha.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/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # @getvitops/utils
2
2
 
3
+ ## 8.3.0-alpha.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 161c7f3: A contact form that works end to end: `vitops setup turnstile`, `<wc-turnstile>`, the `vitops.form` EmDash block, and the route the emdash template ships to receive it.
8
+
9
+ **CSS, all formats** (no existing class changes): `forms.css` gains `.form-notice--target:not(:target)` (a message revealed only when the URL fragment names it, which is how a prerendered page shows a no-JS form's result) and `:where(wc-turnstile) { display: block }`.
10
+
11
+ **`@getvitops/core`:** new `<wc-turnstile sitekey="…">` in `elements.js`. It loads Cloudflare Turnstile on the first interaction with its form (no request to `challenges.cloudflare.com` for a visitor who only reads the page), holds an early submit until the token arrives, and leaves its slotted no-JS explanation in place until JS takes over. Turnstile is category `necessary`, so it is not behind the consent banner.
12
+
13
+ **`@getvitops/utils` / `@getvitops/cli`:** new `@getvitops/utils/turnstile` and `vitops setup turnstile [--dry] [--check] [--no-write] [--no-secret]`. It plans from the site's hostnames, creates a managed widget unless one already covers them (an existing widget is adopted, never edited, rotated or deleted), writes the public key to `site.security.turnstile.siteKey`, and pipes the one-time secret to `wrangler secret put VITOPS_TURNSTILE_SECRET` without writing it to any file. Needs `CLOUDFLARE_API_TOKEN` with **Account > Turnstile > Edit** and `CLOUDFLARE_ACCOUNT_ID`.
14
+
15
+ **`@getvitops/astro`:** `virtual:getvitops/head` gains `turnstileSiteKey` (read from `site.security.turnstile.siteKey`) so blocks can render the widget. No other change.
16
+
17
+ **`@getvitops/generator`:** `TIERS` gains a `turnstile` entry (wc-turnstile, claimed by `vitops.form`); `wc-multi-field`'s `emdashExempt` reason is reworded now the form block exists.
18
+
19
+ **`@getvitops/emdash`:** new `vitops.form` block: a field repeater (text, email, phone, long text, choose one, tick box), required flags, help text, success/error messages, optional Turnstile. It renders a semantic `<form method="post">` that works with no JavaScript, with a honeypot named per `createConversionRoute`'s defaults. A field named `phone` submits as `tel`, because the route reserves `phone` and `event`.
20
+
21
+ **`@getvitops/create`:** the emdash template ships `src/pages/api/track.ts` (`prerender = false`, `createConversionRoute()`), a seeded contact form, and `challenges.cloudflare.com` in both templates' Report-Only CSP (`script-src`, `frame-src`). The route refuses a post with nowhere to deliver and fails closed when a site key exists without `VITOPS_TURNSTILE_SECRET`. SETUP.html gains the Turnstile step. Existing sites: copy the route and the CSP change, then run `vitops setup turnstile`.
22
+
23
+ - 175d16e: **Added: `vitops.reviews` and `vitops.nav` EmDash blocks.** Non-breaking.
24
+
25
+ - **`vitops.reviews`** renders Google reviews from the committed `.vitops/reviews.json` cache (`vitops reviews`) as a grid, carousel or marquee of `<ReviewCard>`. Fields: location key, layout, columns, limit, lowest rating, order, featured ids, reviewer photos (off by default), tone/colour. Which reviews show is `selectReviews()`, the one shared decision. Review JSON-LD is not offered (Google treats self-marked-up reviews as ineligible for rich results). A new review reaches the page after `vitops reviews` is re-run, committed and the site rebuilt, not when an editor saves; with no cache file the block renders nothing (a note in `astro dev`).
26
+ - **`vitops.nav`** renders a named EmDash menu inside page content (site map, section links, footer-style columns), never the header. Fields: menu (picked from the site's menus via a new `menus/list` plugin route, with `footer-sitemap`/`footer-legal`/`primary` as the static fallback), accessible name, visible heading and level, layout `list | inline | columns | sitenav`, depth 1-3, tone/colour. It is a `<nav>` with a name that is never `Primary`, `Site map` or `Legal` (the layout's landmarks), `aria-current="page"`, no runtime JS; a missing or empty menu renders nothing.
27
+ - `@getvitops/astro`: **`virtual:getvitops/reviews`**, served by `vitops()`, is `.vitops/reviews.json` as a build-time import (an empty cache when absent or unreadable), because a Worker cannot read the file. `<ReviewCard>` now renders the review's source link (`.review__source`, `rel="nofollow noopener noreferrer"`), which the pattern already styled. **Fix:** `src/json-ld.ts` was missing from the published `files`, so any consumer importing `<ReviewCard>` (which composes `schemas/Review.astro`) failed to build with an unresolved import. `@getvitops/emdash` now needs this version of `@getvitops/astro` for the `vitops.reviews` block.
28
+ - `@getvitops/utils`: new Worker-safe subpath **`@getvitops/utils/reviews/select`** (`selectReviews` and the review types, no `node:fs`). `selectReviews` now takes only the display fields (`featured`, `minRating`, `limit`, `sort`), so a caller with no configured `locationId` can use it; existing calls are unchanged.
29
+ - `@getvitops/core`: new `.navlist` pattern in `patterns/sitenav.css` (`.navlist`, `__item`, `__link`, `__label`, `__sub`, `--inline`, `--columns`). **All formats** (css, bricks, tailwind). No existing class changes.
30
+ - `@getvitops/generator`: `TIERS.review` lists `vitops.reviews` and `TIERS.sitenav` lists `vitops.nav` (and `.navlist`), so `vitops docs components` and the docsite show them.
31
+
32
+ - 83d6f3c: Theming escape hatches, so a real palette builds without raw ramp steps. All additive; existing configs build unchanged.
33
+
34
+ - **Dark-only sites.** `designSystem.defaultColorScheme: "dark"` (accepted by the schema before, but ignored) now makes the dark tokens the `:root` appearance with `color-scheme: dark`, emits no light block or `data-theme` flip, and skips light-mode contrast checks. Reaches css, bricks and tailwind formats. Do not ship `<wc-color-scheme-toggle>` on such a site.
35
+ - **Role solid step and foreground.** `colors.roles.<role>` may be `{ hue, kind?, solid?, foreground? }`. `solid: 400` starts the solid family at step 400 (bold and x-bold walk one and two rungs darker); `foreground: "dark" | "light"` pins `text-on-<role>`. The APCA check (Lc 60) still runs on the pair emitted. The string shorthand is unchanged. All formats. The live editor's `roleTokens` still assume the default 600 family when re-pointing a role.
36
+ - **Split shadows.** `shadows.<name>` may be `{ box?, drop? }`: `box` (spread, inset, layers) is `--shadow-<name>`; `drop` is `--shadow-<name>-drop` and feeds `.drop-shadow-<name>` and the `shadow:` state shortcut. The string form is unchanged. css/bricks emit `.drop-shadow-*`; tailwind emits the `-drop` token only.
37
+ - **Layout tokens.** New optional `layout` block (`widths.{measure,breakout,spotlight,nav}`, `regionSpace.{min,preferred,max}`) emitted as `--width-*` / `--region-space*` on `:root` in css, bricks and tailwind formats, and exposed in `design-manifest.json` (`layout`, `reverseIndex`). Keys you omit keep the framework defaults.
38
+ - **`acknowledgeOffLadder: true`** on a palette hue silences that hue's off-ladder anchor warnings.
39
+ - **Typography:** `transform` and `decoration` are now accepted as aliases of `text-transform` and `text-decoration` (canonical wins when both are present) instead of warning and being dropped.
40
+ - **Docs strings** (`authoring.md`, JSON Schema hovers) state the Lc 60 muted-text requirement against the page plane and give a type-scale recipe for marketing headings.
41
+ - `@getvitops/utils`: `pickOn` takes an optional `prefer: 'light' | 'dark'`.
42
+
43
+ - b47df4d: **Added: `vitops audit` — Lighthouse + axe budgets over a built site.** Non-breaking, opt-in.
44
+
45
+ `vitops audit [--build] [--url <base>] [--pages /a,/b] [--warn] [--out <dir>]` serves the existing build with `astro preview` on a dedicated port (4398; it aborts rather than attach to something else already there), audits `/` plus `/privacy`, `/terms`, `/cookies`, `/contact` and every link in `seed/seed.json`'s `primary*`/`footer*` menus, and skips any route that 404s (a 404 on `/` fails the run). Per page it runs axe (tags wcag2a, wcag2aa, wcag21a, wcag21aa) and Lighthouse on the same Chromium. Default budgets: LCP < 2500 ms, CLS < 0.1, TBT < 200 ms (the INP proxy), no serious or critical axe violations; override with `--lcp`, `--cls`, `--tbt`, `--impact`. Writes `report.json`, `report.md` and `pages/<page>.{json,md}` to `.vitops/audit/`; exits non-zero on failure unless `--warn`. Every report states that a pass is **not** AODA certification and lists the manual checks.
46
+
47
+ `playwright`, `@axe-core/playwright` and `lighthouse` are **optional peer dependencies** of `@getvitops/utils`, loaded lazily — nothing is installed or loaded unless you run the command, and a missing one fails with the install command. Pure planner and report rendering are `@getvitops/utils/audit` (new subpath).
48
+
49
+ CI: the reusable `site-ci.yml` gains opt-in `run-audit` (default `false`) and `audit-args` inputs; callers pinned to `@v1` that pass neither are unaffected.
50
+
51
+ ### Patch Changes
52
+
53
+ - @getvitops/config@8.3.0-alpha.0
54
+
3
55
  ## 8.2.0
4
56
 
5
57
  ### Minor Changes
@@ -0,0 +1,180 @@
1
+ /** Shapes shared by the audit planner, the executor and the report renderers. */
2
+ type AxeImpact = 'minor' | 'moderate' | 'serious' | 'critical';
3
+ interface AuditBudgets {
4
+ /** Largest Contentful Paint ceiling, ms. */
5
+ lcpMs: number;
6
+ /** Cumulative Layout Shift ceiling (unitless). */
7
+ cls: number;
8
+ /** Total Blocking Time ceiling, ms — the lab proxy for INP. */
9
+ tbtMs: number;
10
+ /** axe impacts that fail the page. Default: serious + critical. */
11
+ blockingImpacts: AxeImpact[];
12
+ }
13
+ interface AxeViolation {
14
+ id: string;
15
+ /** axe reports `null` impact for some incomplete rules; normalised to `moderate`. */
16
+ impact: AxeImpact;
17
+ help: string;
18
+ helpUrl: string;
19
+ /** Total nodes the rule failed on. */
20
+ nodeCount: number;
21
+ /** First few CSS selectors, for the report. */
22
+ targets: string[];
23
+ }
24
+ interface PageMetrics {
25
+ /** `undefined` when Lighthouse could not compute it — which fails the page, not passes it. */
26
+ lcpMs?: number | undefined;
27
+ cls?: number | undefined;
28
+ tbtMs?: number | undefined;
29
+ /** Lighthouse category scores, 0-100. */
30
+ scores: {
31
+ performance?: number | undefined;
32
+ seo?: number | undefined;
33
+ bestPractices?: number | undefined;
34
+ };
35
+ }
36
+ interface BudgetFailure {
37
+ kind: 'lcp' | 'cls' | 'tbt' | 'axe' | 'metric-missing' | 'page-error' | 'required-page-missing';
38
+ message: string;
39
+ }
40
+ type PageStatus = 'audited' | 'skipped' | 'error';
41
+ interface PageResult {
42
+ path: string;
43
+ url: string;
44
+ status: PageStatus;
45
+ httpStatus?: number;
46
+ /** Why a page was skipped or errored. */
47
+ reason?: string;
48
+ metrics?: PageMetrics;
49
+ violations?: AxeViolation[];
50
+ failures: BudgetFailure[];
51
+ }
52
+ type FormFactor = 'mobile' | 'desktop';
53
+ interface AuditReport {
54
+ generatedAt: string;
55
+ baseUrl: string;
56
+ formFactor: FormFactor;
57
+ budgets: AuditBudgets;
58
+ axeTags: string[];
59
+ pages: PageResult[];
60
+ /** True when no page has a failure. */
61
+ passed: boolean;
62
+ /** Always present: what a pass does and does not mean. */
63
+ disclaimer: string;
64
+ manualChecks: string[];
65
+ }
66
+ export declare const DEFAULT_BUDGETS: AuditBudgets;
67
+ /**
68
+ * `wcag2a`/`wcag2aa` are what AODA (WCAG 2.0 AA) asks for; `wcag21a`/`wcag21aa` add the
69
+ * 2.1 criteria we target. `wcag21aa` alone covers only the *new AA* criteria, not the
70
+ * new level-A ones, so both 2.1 tags are listed.
71
+ */
72
+ export declare const AXE_TAGS: string[];
73
+ /** Pages tried on every run. `/` is required; the rest are skipped when they 404. */
74
+ export declare const DEFAULT_PAGES: string[];
75
+ export declare const DISCLAIMER: string;
76
+ export declare const MANUAL_CHECKS: string[];
77
+ /**
78
+ * A menu-ish thing: EmDash seed (`items`), `getMenu()` (`items`, items with `children`),
79
+ * or either shape nested. Only `name`, `url` and children are read.
80
+ */
81
+ interface MenuLike {
82
+ name?: string;
83
+ items?: MenuItemLike[];
84
+ }
85
+ interface MenuItemLike {
86
+ url?: string | null;
87
+ children?: MenuItemLike[];
88
+ items?: MenuItemLike[];
89
+ }
90
+ /**
91
+ * Normalise to a site-relative path, or `undefined` for anything that is not an
92
+ * auditable page of this site: external URLs, hash-only links, `mailto:`/`tel:`,
93
+ * protocol-relative URLs, file downloads (anything with an extension) and the CMS
94
+ * admin. Query and hash are dropped; a trailing slash is too (except the root).
95
+ */
96
+ export declare function normalizePath(url: string | null | undefined): string | undefined;
97
+ /** Is this one of the menus whose links are pages worth auditing? */
98
+ export declare function isAuditMenu(name: string | undefined): boolean;
99
+ /** Paths from the `primary*` / `footer*` menus (`footer-legal`, `primary-mobile`, …). */
100
+ export declare function pathsFromMenus(menus: MenuLike[]): string[];
101
+ /** Same, from an EmDash seed file's parsed JSON (`{ menus: [...] }`). */
102
+ export declare function pathsFromSeed(seed: unknown): string[];
103
+ interface PagePlanInput {
104
+ /** Paths found in menus. */
105
+ menuPaths?: string[];
106
+ /** Paths from `--pages`. */
107
+ extra?: string[];
108
+ /** Default true. `--no-defaults` for a site that wants an exact list. */
109
+ includeDefaults?: boolean;
110
+ }
111
+ /** The pages to try, `/` always first, unique, in a stable order. */
112
+ export declare function planPages(input?: PagePlanInput): string[];
113
+ /**
114
+ * What an HTTP status means for a candidate page. 404/410 → skipped (a single-page
115
+ * site has no `/contact`, and that is fine) — except `/`, which must exist. Any other
116
+ * non-2xx is an error: a 500 on `/privacy` is a defect, not an absent page.
117
+ */
118
+ export declare function classifyStatus(path: string, httpStatus: number | undefined): {
119
+ status: 'audited' | 'skipped' | 'error';
120
+ reason?: string;
121
+ };
122
+ /** axe can report a null impact; treat it as `moderate`, never silently drop it. */
123
+ export declare function normalizeImpact(impact: string | null | undefined): AxeImpact;
124
+ /** `serious,critical` → impacts, rejecting typos so a budget cannot silently be empty. */
125
+ export declare function parseImpacts(list: string): AxeImpact[];
126
+ /** Everything that makes one audited page fail. Empty = the page passes. */
127
+ export declare function evaluatePage(metrics: PageMetrics | undefined, violations: AxeViolation[] | undefined, budgets: AuditBudgets): BudgetFailure[];
128
+ /** Build a `PageResult` for a page that was not audited (skipped or error). */
129
+ export declare function unauditedPage(path: string, url: string, httpStatus: number | undefined, verdict: {
130
+ status: 'skipped' | 'error';
131
+ reason?: string;
132
+ }): PageResult;
133
+ export declare function auditedPage(path: string, url: string, httpStatus: number, metrics: PageMetrics, violations: AxeViolation[], budgets: AuditBudgets): PageResult;
134
+ export declare function buildReport(input: {
135
+ generatedAt: string;
136
+ baseUrl: string;
137
+ formFactor: FormFactor;
138
+ budgets: AuditBudgets;
139
+ pages: PageResult[];
140
+ }): AuditReport;
141
+ /** `/` → `home`, `/a/b` → `a-b`: a safe per-page filename. */
142
+ export declare function pageSlug(path: string): string;
143
+ export declare function renderPageMarkdown(page: PageResult, budgets: AuditBudgets): string;
144
+ export declare function renderMarkdown(report: AuditReport): string;
145
+ /** Files to write, relative to the output dir: JSON + markdown overall and per page. */
146
+ export declare function reportFiles(report: AuditReport): Record<string, string>;
147
+ /** One-line-per-failure console summary. */
148
+ export declare function formatSummary(report: AuditReport): string;
149
+ /** A free TCP port, or `true` for "this specific one is free". */
150
+ export declare function isPortFree(port: number): Promise<boolean>;
151
+ /** Run a command to completion, inheriting stdio. Rejects on a non-zero exit. */
152
+ export declare function runCommand(cmd: string, args: string[], cwd: string): Promise<void>;
153
+ interface PreviewServer {
154
+ baseUrl: string;
155
+ stop(): void;
156
+ }
157
+ /**
158
+ * `astro preview` on a dedicated port.
159
+ *
160
+ * Astro's preview server silently moves to the next port when its own is taken, and
161
+ * a readiness probe would then happily attach to whatever owns the original one — the
162
+ * failure `site/playwright.config.ts` documents. So the port is checked free first and
163
+ * the run aborts if it is not.
164
+ */
165
+ export declare function startPreview(opts: {
166
+ cwd: string;
167
+ port: number;
168
+ timeoutMs?: number;
169
+ }): Promise<PreviewServer>;
170
+ interface RunOptions {
171
+ baseUrl: string;
172
+ paths: string[];
173
+ budgets: AuditBudgets;
174
+ formFactor: FormFactor;
175
+ onProgress?: (message: string) => void;
176
+ }
177
+ /** Audit each path. Never throws for a bad PAGE — only for missing tooling/browser. */
178
+ export declare function runAudit(opts: RunOptions): Promise<PageResult[]>;
179
+ export type { AuditBudgets, AuditReport, AxeImpact, AxeViolation, BudgetFailure, FormFactor, MenuItemLike, MenuLike, PageMetrics, PagePlanInput, PageResult, PageStatus, PreviewServer, RunOptions };
180
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/audit/types.ts","../../src/audit/plan.ts","../../src/audit/run.ts"],"mappings":";KAEY;UAEK;;EAEf;;EAEA;;EAEA;;EAEA,iBAAiB;;UAGF;EACf;;EAEA,QAAQ;EACR;EACA;;EAEA;;EAEA;;UAGe;;EAEf;EACA;EACA;;EAEA;IACE;IACA;IACA;;;UAIa;EACf;EACA;;KAGU;UAEK;EACf;EACA;EACA,QAAQ;EACR;;EAEA;EACA,UAAU;EACV,aAAa;EACb,UAAU;;KAGA;UAEK;EACf;EACA;EACA,YAAY;EACZ,SAAS;EACT;EACA,OAAO;;EAEP;;EAEA;EACA;;qBCrDW,iBAAiB;;;;;;qBAYjB;;qBAGA;qBAEA;qBAKA;;;;;UAgBI;EACf;EACA,QAAQ;;UAEO;EACf;EACA,WAAW;EACX,QAAQ;;;;;;;;wBASM,cAAc;;wBAsBd,YAAY;;wBAKZ,eAAe,OAAO;;wBAOtB,cAAc;UAKb;;EAEf;;EAEA;;EAEA;;;wBAIc,UAAU,QAAO;;;;;;wBAqBjB,eACd,cACA;EACG;EAAyC;;;wBAgB9B,gBAAgB,oCAAoC;;wBAKpD,aAAa,eAAe;;wBAe5B,aACd,SAAS,yBACT,YAAY,4BACZ,SAAS,eACR;;wBAyCa,cACd,cACA,aACA,gCACA;EAAW;EAA6B;IACvC;wBAmBa,YACd,cACA,aACA,oBACA,SAAS,aACT,YAAY,gBACZ,SAAS,eACR;wBAYa,YAAY;EAC1B;EACA;EACA,YAAY;EACZ,SAAS;EACT,OAAO;IACL;;wBAiBY,SAAS;wBAOT,mBAAmB,MAAM,YAAY,SAAS;wBAyC9C,eAAe,QAAQ;;wBAiCvB,YAAY,QAAQ,cAAc;;wBAgBlC,cAAc,QAAQ;;wBC3VtB,WAAW,eAAe;;wBAoB1B,WAAW,aAAa,gBAAgB,cAAc;UAUrD;EACf;EACA;;;;;;;;;;wBAWoB,aAAa;EACjC;EACA;EACA;IACE,QAAQ;UA+FK;EACf;EACA;EACA,SAAS;EACT,YAAY;EACZ,cAAc;;;wBAIM,SAAS,MAAM,aAAa,QAAQ"}