Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions scripts/postinstall.js → scripts/postinstall.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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\`.`,
);
}
23 changes: 14 additions & 9 deletions src/checks/plp-pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export async function plpPagination(ctx: CheckContext): Promise<CheckResult> {
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,
Expand Down Expand Up @@ -179,8 +179,9 @@ async function fetchPlpProducts(url: string): Promise<PlpFetchResult> {
// OR containing `/p/` OR `/products/`.
const paths = new Set<string>();
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
Expand All @@ -190,7 +191,9 @@ async function fetchPlpProducts(url: string): Promise<PlpFetchResult> {
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(/\/$/, ""));
}
Expand Down Expand Up @@ -230,16 +233,19 @@ async function discoverPlpFromHome(homeUrl: string): Promise<string | null> {
const html = await res.text();
const candidates = new Set<string>();
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
if (
/^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;
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 10 additions & 2 deletions src/llm/discover-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>` 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;
}
}
});

Expand Down
9 changes: 7 additions & 2 deletions src/llm/recover-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/report/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { prod?: typeof captures[0]; cand?: typeof captures[0] }>();
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;
Expand Down
Loading