From 5caf528530a269f97c0f32b2ae7f1b2a251d3641 Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Wed, 17 Jun 2026 00:02:48 -0300 Subject: [PATCH] =?UTF-8?q?fix(ci):=20repair=20main=20=E2=80=94=20postinst?= =?UTF-8?q?all=20ESM/CJS=20+=20lint=20sweep=20on=20landed=20PRs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI was broken on every main commit since 0.11.0 because PRs were merged with --admin (bypassing CI) and accumulated three classes of issues: 1. **postinstall.js used `require()` in an ESM package.** `package.json` has `"type": "module"`, so the postinstall throws `ReferenceError: require is not defined` on every `bun install` (including the CI install step). Renamed to `postinstall.cjs` and updated `package.json` `files` + `postinstall` script accordingly. 2. **plp-pagination.ts: noAssignInExpressions.** Three `while ((m = re.exec(html)))` loops rewritten to `for (;;)` + early-break. `useTemplate` fixed on the no-issues summary line. 3. **discover-selectors.ts / recover-step.ts: noDelete.** Was already present in 0.11.4. The biome auto-fix replaced `delete attrs.class` with `attrs.class = undefined`, but cheerio's `attribs` type is `Record` so TS rejected it. Restructured to assign the joined string when non-empty and `delete` only when empty. The `delete attrs[name]` for style uses computed access which biome's `noDelete` doesn't flag. 4. **postinstall.cjs:** internal cleanup — optional chain for `err?.message`, template literal for the multi-line message. Verified locally: `bun run lint` clean, `bunx tsc --noEmit` clean, 701 tests pass. Bumps 0.11.8. --- CHANGELOG.md | 8 +++++++ package.json | 6 +++--- scripts/{postinstall.js => postinstall.cjs} | 4 ++-- src/checks/plp-pagination.ts | 23 +++++++++++++-------- src/llm/discover-selectors.ts | 12 +++++++++-- src/llm/recover-step.ts | 9 ++++++-- src/report/render.ts | 5 ++++- 7 files changed, 48 insertions(+), 19 deletions(-) rename scripts/{postinstall.js => postinstall.cjs} (90%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01ce6ea..1258714 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [0.11.8](https://github.com/decocms/parity/compare/v0.11.7...v0.11.8) (2026-06-17) + +### Fixed + +* **CI on `main` was broken across the 0.11.x patch series.** Three issues converged: (a) the new `postinstall` script in 0.11.7 used CommonJS `require()` but the package is `"type": "module"`, so every fresh `bun install --frozen-lockfile` (including the CI install step) threw `ReferenceError: require is not defined`. (b) Several lint errors carried over from PRs that used `--admin` to bypass CI (template-literal-as-string, assign-in-while-conditions in the new `plp-pagination` check, `delete attrs[name]` flagged by `noDelete`). (c) The earlier `0.11.4` HTML-compaction code in `discover-selectors`/`recover-step` triggered the same `noDelete` rule. +* **`postinstall.js` renamed to `postinstall.cjs`** so it runs as CommonJS regardless of the package's `"type"`. `package.json` `files` list and the `postinstall` script invocation updated accordingly. +* **Lint clean across the repo.** `bun run lint` returns zero errors. Loop patterns rewritten from `while ((m = re.exec(s)))` to `for (;;)` + early-break (biome's `noAssignInExpressions`). Two intentional `delete` calls on cheerio attribs get a scoped `biome-ignore` with the rationale (cheerio serializes `undefined`-valued attrs as empty strings; only `delete` actually removes them). + ## [0.11.7](https://github.com/decocms/parity/compare/v0.11.6...v0.11.7) (2026-06-17) ### Fixed diff --git a/package.json b/package.json index 163e4f6..6c647fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@decocms/parity", - "version": "0.11.7", + "version": "0.11.8", "description": "E2E parity validator for site migrations. Compares prod vs cand and reports UI, functional, SEO, visual, and Web Vitals deltas with an LLM-ranked HTML report.", "type": "module", "license": "MIT", @@ -32,7 +32,7 @@ "bin": { "parity": "dist/cli.js" }, - "files": ["dist", "scripts/postinstall.js", "README.md", "AGENTS.md", "CHANGELOG.md", "LICENSE"], + "files": ["dist", "scripts/postinstall.cjs", "README.md", "AGENTS.md", "CHANGELOG.md", "LICENSE"], "scripts": { "dev": "bun run --hot src/cli.ts", "build": "bun build src/cli.ts --target=node --outfile=dist/cli.js --packages=external && bun run scripts/postbuild.ts", @@ -45,7 +45,7 @@ "lint": "biome lint .", "fmt": "biome format --write .", "prepublishOnly": "bun run check && bun run build", - "postinstall": "node scripts/postinstall.js" + "postinstall": "node scripts/postinstall.cjs" }, "engines": { "node": ">=20", diff --git a/scripts/postinstall.js b/scripts/postinstall.cjs similarity index 90% rename from scripts/postinstall.js rename to scripts/postinstall.cjs index 04e2408..376ddcf 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.cjs @@ -41,8 +41,8 @@ try { ); } } catch (err) { + const reason = err?.message ?? "unknown error"; console.log( - `[parity] Playwright install skipped (${err && err.message ? err.message : "unknown error"}). ` + - "Run `npx playwright install chromium` before your first `parity run`.", + `[parity] Playwright install skipped (${reason}). Run \`npx playwright install chromium\` before your first \`parity run\`.`, ); } diff --git a/src/checks/plp-pagination.ts b/src/checks/plp-pagination.ts index 7e14a01..f255f7a 100644 --- a/src/checks/plp-pagination.ts +++ b/src/checks/plp-pagination.ts @@ -147,7 +147,7 @@ export async function plpPagination(ctx: CheckContext): Promise { durationMs: Date.now() - start, summary: issues.length === 0 - ? `PLP pagination working on both sides (tested page=2 + page=3)` + ? "PLP pagination working on both sides (tested page=2 + page=3)" : `${issues.length} pagination issue(s) — see details`, issues, data, @@ -179,8 +179,9 @@ async function fetchPlpProducts(url: string): Promise { // OR containing `/p/` OR `/products/`. const paths = new Set(); const re = /href="([^"]+\/p(?:\?[^"]*|\/[^"]+|))"/gi; - let m: RegExpExecArray | null; - while ((m = re.exec(html))) { + for (;;) { + const m = re.exec(html); + if (!m) break; const path = m[1]; if (!path) continue; // Normalize: drop query, drop trailing slash. We're checking SET @@ -190,7 +191,9 @@ async function fetchPlpProducts(url: string): Promise { paths.add(normalized); } const re2 = /href="([^"]+\/products\/[^"]+)"/gi; - while ((m = re2.exec(html))) { + for (;;) { + const m = re2.exec(html); + if (!m) break; const path = m[1]; if (path) paths.add(path.split("?")[0]!.replace(/\/$/, "")); } @@ -230,8 +233,9 @@ async function discoverPlpFromHome(homeUrl: string): Promise { const html = await res.text(); const candidates = new Set(); const re = /href="([^"]+)"/gi; - let m: RegExpExecArray | null; - while ((m = re.exec(html))) { + for (;;) { + const m = re.exec(html); + if (!m) break; const href = m[1]; if (!href) continue; // Skip non-PLP hrefs @@ -239,7 +243,9 @@ async function discoverPlpFromHome(homeUrl: string): Promise { /^https?:\/\//.test(href) || href.startsWith("#") || href === "/" || - /\/(p|cart|carrinho|checkout|account|conta|login|wishlist|favoritos)(\/|$|\?)/i.test(href) || + /\/(p|cart|carrinho|checkout|account|conta|login|wishlist|favoritos)(\/|$|\?)/i.test( + href, + ) || /\.(jpg|png|webp|css|js|svg|ico|woff)/i.test(href) ) { continue; @@ -272,8 +278,7 @@ function pickPlpUrl(flows: CheckContext["prodFlows"]): string | null { const u = new URL(p.url); // PLP heuristic: depth-1 or depth-2 path, not /p or /products return ( - u.pathname.split("/").filter(Boolean).length <= 2 && - !/\/p$|\/products\//.test(u.pathname) + u.pathname.split("/").filter(Boolean).length <= 2 && !/\/p$|\/products\//.test(u.pathname) ); } catch { return false; diff --git a/src/llm/discover-selectors.ts b/src/llm/discover-selectors.ts index b8b3c2a..6a92337 100644 --- a/src/llm/discover-selectors.ts +++ b/src/llm/discover-selectors.ts @@ -233,8 +233,16 @@ export function compactHtmlForSelectors(html: string, maxChars = 30_000): string if (attrs.class) { const tokens = attrs.class.split(/\s+/).filter(Boolean); const kept = tokens.filter((t) => isSemanticClass(t)); - attrs.class = kept.join(" "); - if (!attrs.class) delete attrs.class; + const joined = kept.join(" "); + if (joined) { + attrs.class = joined; + } else { + // Drop the attribute entirely when nothing semantic remains — + // cheerio's attribs type is `Record` so we + // can't assign undefined; delete is the right tool here. + // biome-ignore lint/performance/noDelete: cheerio attribs are a plain object that needs the key gone, not undefined. + delete attrs.class; + } } }); diff --git a/src/llm/recover-step.ts b/src/llm/recover-step.ts index 158279f..5002a49 100644 --- a/src/llm/recover-step.ts +++ b/src/llm/recover-step.ts @@ -50,8 +50,13 @@ export function compactHtmlForRecovery(html: string, maxChars = 12_000): string if (attrs.class) { const tokens = attrs.class.split(/\s+/).filter(Boolean); const kept = tokens.filter((t) => isSemanticClassToken(t)); - attrs.class = kept.join(" "); - if (!attrs.class) delete attrs.class; + const joined = kept.join(" "); + if (joined) { + attrs.class = joined; + } else { + // biome-ignore lint/performance/noDelete: cheerio attribs need the key gone, not undefined. + delete attrs.class; + } } }); // Keep only elements likely to be interactive or contain them. Added diff --git a/src/report/render.ts b/src/report/render.ts index 85edfe4..a9441af 100644 --- a/src/report/render.ts +++ b/src/report/render.ts @@ -326,7 +326,10 @@ function renderFlowStepTimeline(checkName: string, run: Run, runDir: string): st if (captures.length === 0) return ""; // Pair prod + cand by viewport so the user sees side-by-side step state. - const byViewport = new Map(); + const byViewport = new Map< + string, + { prod?: (typeof captures)[0]; cand?: (typeof captures)[0] } + >(); for (const fc of captures) { const slot = byViewport.get(fc.viewport) ?? {}; if (fc.side === "prod") slot.prod = fc;