@decocms/parity 0.11.5 → 0.11.8
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 +27 -0
- package/README.md +5 -1
- package/dist/cli.js +263 -11
- package/package.json +4 -3
- package/scripts/postinstall.cjs +48 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [0.11.8](https://github.com/decocms/parity/compare/v0.11.7...v0.11.8) (2026-06-17)
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* **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.
|
|
13
|
+
* **`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.
|
|
14
|
+
* **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).
|
|
15
|
+
|
|
16
|
+
## [0.11.7](https://github.com/decocms/parity/compare/v0.11.6...v0.11.7) (2026-06-17)
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
* **First-run UX after `npm install -g`.** Two papercuts:
|
|
21
|
+
1. **Playwright Chromium wasn't auto-installed.** A fresh global install left users with `browserType.launch: Executable doesn't exist at ...` plus a stack trace on the first `parity run`. Now there's a `postinstall` script that runs `npx playwright install chromium` automatically (one-time, ~140 MB). Skippable via `PARITY_SKIP_PLAYWRIGHT_INSTALL=1` for CI / Docker / monorepos that manage browsers separately. Failures during postinstall (offline, corp proxy) are downgraded to a warning so the global install itself still completes.
|
|
22
|
+
2. **`launchBrowser` now catches the missing-browser error** and prints a single clear instruction (`npx playwright install chromium`) instead of Playwright's ASCII banner + stack trace. Original error is preserved on `.cause` for debugging.
|
|
23
|
+
|
|
24
|
+
### Known cosmetic warning
|
|
25
|
+
|
|
26
|
+
The `@anthropic-ai/claude-agent-sdk` peer-dep on `zod@^4.0.0` clashes with parity's `zod@^3.x`. The warning is harmless — npm nests the two versions and everything works. A migration to zod 4 is tracked separately (lots of schema API differences).
|
|
27
|
+
|
|
28
|
+
## [0.11.6](https://github.com/decocms/parity/compare/v0.11.5...v0.11.6) (2026-06-17)
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
* **New `plp-pagination` check.** Tests that `?page=2` and `?page=3` of every captured PLP return 200 AND show different products than page 1. Catches the classic migration regression: the new site silently ignores `?page=N` and returns the first page on every request (or caps pagination). Live tested against bagaggio TanStack — surfaces a real critical bug (`page=2` shows the same 10 products as `page=1`, 100% overlap). Falls back to scraping the home page for a category link when running standalone (`parity check plp-pagination`).
|
|
33
|
+
* **Cross-side count divergence detection.** When prod and cand both serve a paginated PLP but cand's `?page=2` product count differs from prod's by more than 30%, flag as medium-severity (likely sort-order or index pruning regression).
|
|
34
|
+
|
|
8
35
|
## [0.11.5](https://github.com/decocms/parity/compare/v0.11.4...v0.11.5) (2026-06-17)
|
|
9
36
|
|
|
10
37
|
### Fixed
|
package/README.md
CHANGED
|
@@ -26,11 +26,15 @@ Three use cases drive the design:
|
|
|
26
26
|
## Quickstart
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
# Install
|
|
29
|
+
# Install — auto-installs Playwright Chromium on postinstall (~140 MB, one-time)
|
|
30
30
|
npm install -g @decocms/parity
|
|
31
31
|
# or run without install
|
|
32
32
|
npx @decocms/parity run --prod ... --cand ...
|
|
33
33
|
|
|
34
|
+
# In CI / behind a corp proxy? Skip the auto-install and do it later:
|
|
35
|
+
# PARITY_SKIP_PLAYWRIGHT_INSTALL=1 npm install -g @decocms/parity
|
|
36
|
+
# npx playwright install chromium
|
|
37
|
+
|
|
34
38
|
# First-time smoke run (~30s, no LLM needed)
|
|
35
39
|
parity run --prod https://oldsite.com --cand https://newsite.example.dev --preset smoke --open
|
|
36
40
|
|
package/dist/cli.js
CHANGED
|
@@ -1790,11 +1790,29 @@ var VIEWPORT_PRESETS = {
|
|
|
1790
1790
|
}
|
|
1791
1791
|
};
|
|
1792
1792
|
async function launchBrowser(opts = {}) {
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1793
|
+
try {
|
|
1794
|
+
return await chromium.launch({
|
|
1795
|
+
headless: opts.headless ?? true,
|
|
1796
|
+
slowMo: opts.slowMo ?? 0,
|
|
1797
|
+
args: ["--disable-blink-features=AutomationControlled"]
|
|
1798
|
+
});
|
|
1799
|
+
} catch (err) {
|
|
1800
|
+
const msg = err.message ?? "";
|
|
1801
|
+
if (msg.includes("Executable doesn't exist")) {
|
|
1802
|
+
const friendly = new Error([
|
|
1803
|
+
"Playwright's Chromium binary is not installed yet. Run:",
|
|
1804
|
+
"",
|
|
1805
|
+
" npx playwright install chromium",
|
|
1806
|
+
"",
|
|
1807
|
+
"(one-time, ~140 MB download). Then re-run parity. This usually happens",
|
|
1808
|
+
"after a fresh `npm install -g @decocms/parity`."
|
|
1809
|
+
].join(`
|
|
1810
|
+
`));
|
|
1811
|
+
friendly.cause = err;
|
|
1812
|
+
throw friendly;
|
|
1813
|
+
}
|
|
1814
|
+
throw err;
|
|
1815
|
+
}
|
|
1798
1816
|
}
|
|
1799
1817
|
async function newContext(browser, opts) {
|
|
1800
1818
|
const baseContext = VIEWPORT_PRESETS[opts.viewport];
|
|
@@ -8534,6 +8552,232 @@ function hasReservedSpace($img) {
|
|
|
8534
8552
|
return false;
|
|
8535
8553
|
}
|
|
8536
8554
|
|
|
8555
|
+
// src/checks/plp-pagination.ts
|
|
8556
|
+
async function plpPagination(ctx) {
|
|
8557
|
+
const start = Date.now();
|
|
8558
|
+
const issues = [];
|
|
8559
|
+
let prodPlp = pickPlpUrl(ctx.prodFlows);
|
|
8560
|
+
let candPlp = pickPlpUrl(ctx.candFlows);
|
|
8561
|
+
if (!prodPlp && ctx.prodPages.length > 0) {
|
|
8562
|
+
prodPlp = await discoverPlpFromHome(ctx.prodPages[0].url);
|
|
8563
|
+
}
|
|
8564
|
+
if (!candPlp && ctx.candPages.length > 0) {
|
|
8565
|
+
candPlp = await discoverPlpFromHome(ctx.candPages[0].url);
|
|
8566
|
+
}
|
|
8567
|
+
if (!prodPlp && !candPlp) {
|
|
8568
|
+
return {
|
|
8569
|
+
name: "plp-pagination",
|
|
8570
|
+
status: "skipped",
|
|
8571
|
+
severity: "medium",
|
|
8572
|
+
durationMs: Date.now() - start,
|
|
8573
|
+
summary: "no PLP captured by purchase-journey step 2 AND no home page to discover one from — skipping",
|
|
8574
|
+
issues: []
|
|
8575
|
+
};
|
|
8576
|
+
}
|
|
8577
|
+
const data = {};
|
|
8578
|
+
for (const [side, plp] of [
|
|
8579
|
+
["prod", prodPlp],
|
|
8580
|
+
["cand", candPlp]
|
|
8581
|
+
]) {
|
|
8582
|
+
if (!plp)
|
|
8583
|
+
continue;
|
|
8584
|
+
const page1 = await fetchPlpProducts(plp);
|
|
8585
|
+
const page2 = await fetchPlpProducts(withPage(plp, 2));
|
|
8586
|
+
const page3 = await fetchPlpProducts(withPage(plp, 3));
|
|
8587
|
+
data[`${side}_page_counts`] = {
|
|
8588
|
+
page1: { status: page1.status, products: page1.productPaths.length },
|
|
8589
|
+
page2: { status: page2.status, products: page2.productPaths.length },
|
|
8590
|
+
page3: { status: page3.status, products: page3.productPaths.length }
|
|
8591
|
+
};
|
|
8592
|
+
for (const [n, result] of [
|
|
8593
|
+
[2, page2],
|
|
8594
|
+
[3, page3]
|
|
8595
|
+
]) {
|
|
8596
|
+
if (result.status !== 200) {
|
|
8597
|
+
issues.push({
|
|
8598
|
+
id: `plp-pagination:${side}:page${n}:status-${result.status}`,
|
|
8599
|
+
severity: "high",
|
|
8600
|
+
category: "functional",
|
|
8601
|
+
check: "plp-pagination",
|
|
8602
|
+
summary: `[${side}] page=${n} returned HTTP ${result.status} (expected 200) — pagination breaks under ?page=${n}`,
|
|
8603
|
+
page: withPage(plp, n)
|
|
8604
|
+
});
|
|
8605
|
+
}
|
|
8606
|
+
}
|
|
8607
|
+
if (page1.productPaths.length > 0 && page2.status === 200) {
|
|
8608
|
+
const overlap = setOverlap(page1.productPaths, page2.productPaths);
|
|
8609
|
+
if (overlap > 0.9) {
|
|
8610
|
+
issues.push({
|
|
8611
|
+
id: `plp-pagination:${side}:page2-identical`,
|
|
8612
|
+
severity: "critical",
|
|
8613
|
+
category: "functional",
|
|
8614
|
+
check: "plp-pagination",
|
|
8615
|
+
summary: `[${side}] page=2 shows the same ${page1.productPaths.length} products as page=1 (${(overlap * 100).toFixed(0)}% overlap) — ?page=N is being ignored`,
|
|
8616
|
+
page: withPage(plp, 2),
|
|
8617
|
+
details: `Page 1 first product: ${page1.productPaths[0] ?? "(none)"}
|
|
8618
|
+
Page 2 first product: ${page2.productPaths[0] ?? "(none)"}`
|
|
8619
|
+
});
|
|
8620
|
+
}
|
|
8621
|
+
}
|
|
8622
|
+
if (page2.productPaths.length > 0 && page3.status === 200) {
|
|
8623
|
+
const overlap = setOverlap(page2.productPaths, page3.productPaths);
|
|
8624
|
+
if (overlap > 0.9) {
|
|
8625
|
+
issues.push({
|
|
8626
|
+
id: `plp-pagination:${side}:page3-identical`,
|
|
8627
|
+
severity: "critical",
|
|
8628
|
+
category: "functional",
|
|
8629
|
+
check: "plp-pagination",
|
|
8630
|
+
summary: `[${side}] page=3 shows the same products as page=2 (${(overlap * 100).toFixed(0)}% overlap) — pagination capped or broken`,
|
|
8631
|
+
page: withPage(plp, 3)
|
|
8632
|
+
});
|
|
8633
|
+
}
|
|
8634
|
+
}
|
|
8635
|
+
}
|
|
8636
|
+
if (prodPlp && candPlp) {
|
|
8637
|
+
const prodCounts = data.prod_page_counts;
|
|
8638
|
+
const candCounts = data.cand_page_counts;
|
|
8639
|
+
if (prodCounts && candCounts) {
|
|
8640
|
+
const p = prodCounts.page2.products;
|
|
8641
|
+
const c = candCounts.page2.products;
|
|
8642
|
+
if (p > 0 && Math.abs(p - c) / p > 0.3) {
|
|
8643
|
+
issues.push({
|
|
8644
|
+
id: "plp-pagination:cross-side-count-divergence",
|
|
8645
|
+
severity: "medium",
|
|
8646
|
+
category: "functional",
|
|
8647
|
+
check: "plp-pagination",
|
|
8648
|
+
summary: `page=2 product count diverges: prod=${p}, cand=${c} (Δ ${Math.abs(p - c)}, >30%)`
|
|
8649
|
+
});
|
|
8650
|
+
}
|
|
8651
|
+
}
|
|
8652
|
+
}
|
|
8653
|
+
const status = issues.some((i) => i.severity === "critical") ? "fail" : issues.length > 0 ? "fail" : "pass";
|
|
8654
|
+
return {
|
|
8655
|
+
name: "plp-pagination",
|
|
8656
|
+
status,
|
|
8657
|
+
severity: "high",
|
|
8658
|
+
durationMs: Date.now() - start,
|
|
8659
|
+
summary: issues.length === 0 ? "PLP pagination working on both sides (tested page=2 + page=3)" : `${issues.length} pagination issue(s) — see details`,
|
|
8660
|
+
issues,
|
|
8661
|
+
data
|
|
8662
|
+
};
|
|
8663
|
+
}
|
|
8664
|
+
async function fetchPlpProducts(url) {
|
|
8665
|
+
try {
|
|
8666
|
+
const res = await fetch(url, {
|
|
8667
|
+
headers: {
|
|
8668
|
+
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"
|
|
8669
|
+
},
|
|
8670
|
+
signal: AbortSignal.timeout(15000)
|
|
8671
|
+
});
|
|
8672
|
+
if (res.status !== 200) {
|
|
8673
|
+
return { status: res.status, productPaths: [] };
|
|
8674
|
+
}
|
|
8675
|
+
const html = await res.text();
|
|
8676
|
+
const paths = new Set;
|
|
8677
|
+
const re = /href="([^"]+\/p(?:\?[^"]*|\/[^"]+|))"/gi;
|
|
8678
|
+
for (;; ) {
|
|
8679
|
+
const m = re.exec(html);
|
|
8680
|
+
if (!m)
|
|
8681
|
+
break;
|
|
8682
|
+
const path = m[1];
|
|
8683
|
+
if (!path)
|
|
8684
|
+
continue;
|
|
8685
|
+
const normalized = path.split("?")[0].replace(/\/$/, "");
|
|
8686
|
+
paths.add(normalized);
|
|
8687
|
+
}
|
|
8688
|
+
const re2 = /href="([^"]+\/products\/[^"]+)"/gi;
|
|
8689
|
+
for (;; ) {
|
|
8690
|
+
const m = re2.exec(html);
|
|
8691
|
+
if (!m)
|
|
8692
|
+
break;
|
|
8693
|
+
const path = m[1];
|
|
8694
|
+
if (path)
|
|
8695
|
+
paths.add(path.split("?")[0].replace(/\/$/, ""));
|
|
8696
|
+
}
|
|
8697
|
+
return { status: 200, productPaths: Array.from(paths) };
|
|
8698
|
+
} catch (err) {
|
|
8699
|
+
return { status: 0, productPaths: [] };
|
|
8700
|
+
}
|
|
8701
|
+
}
|
|
8702
|
+
function withPage(url, n) {
|
|
8703
|
+
try {
|
|
8704
|
+
const u = new URL(url);
|
|
8705
|
+
u.searchParams.set("page", String(n));
|
|
8706
|
+
return u.toString();
|
|
8707
|
+
} catch {
|
|
8708
|
+
const sep = url.includes("?") ? "&" : "?";
|
|
8709
|
+
return `${url}${sep}page=${n}`;
|
|
8710
|
+
}
|
|
8711
|
+
}
|
|
8712
|
+
async function discoverPlpFromHome(homeUrl) {
|
|
8713
|
+
try {
|
|
8714
|
+
const res = await fetch(homeUrl, {
|
|
8715
|
+
headers: {
|
|
8716
|
+
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"
|
|
8717
|
+
},
|
|
8718
|
+
signal: AbortSignal.timeout(15000)
|
|
8719
|
+
});
|
|
8720
|
+
if (!res.ok)
|
|
8721
|
+
return null;
|
|
8722
|
+
const html = await res.text();
|
|
8723
|
+
const candidates = new Set;
|
|
8724
|
+
const re = /href="([^"]+)"/gi;
|
|
8725
|
+
for (;; ) {
|
|
8726
|
+
const m = re.exec(html);
|
|
8727
|
+
if (!m)
|
|
8728
|
+
break;
|
|
8729
|
+
const href = m[1];
|
|
8730
|
+
if (!href)
|
|
8731
|
+
continue;
|
|
8732
|
+
if (/^https?:\/\//.test(href) || href.startsWith("#") || href === "/" || /\/(p|cart|carrinho|checkout|account|conta|login|wishlist|favoritos)(\/|$|\?)/i.test(href) || /\.(jpg|png|webp|css|js|svg|ico|woff)/i.test(href)) {
|
|
8733
|
+
continue;
|
|
8734
|
+
}
|
|
8735
|
+
const segments = href.split("?")[0].split("/").filter(Boolean);
|
|
8736
|
+
if (segments.length === 0 || segments.length > 3)
|
|
8737
|
+
continue;
|
|
8738
|
+
candidates.add(href.split("?")[0]);
|
|
8739
|
+
}
|
|
8740
|
+
const sorted = Array.from(candidates).sort((a, b) => a.length - b.length);
|
|
8741
|
+
const home = new URL(homeUrl);
|
|
8742
|
+
const pick = sorted[0];
|
|
8743
|
+
if (!pick)
|
|
8744
|
+
return null;
|
|
8745
|
+
return new URL(pick, home).toString();
|
|
8746
|
+
} catch {
|
|
8747
|
+
return null;
|
|
8748
|
+
}
|
|
8749
|
+
}
|
|
8750
|
+
function pickPlpUrl(flows) {
|
|
8751
|
+
for (const fc of flows) {
|
|
8752
|
+
if (fc.flow !== "purchase-journey")
|
|
8753
|
+
continue;
|
|
8754
|
+
const plpStep = fc.steps?.find((s) => s.name === "navigate-plp" && s.status === "ok");
|
|
8755
|
+
if (plpStep?.url)
|
|
8756
|
+
return plpStep.url;
|
|
8757
|
+
const plpPage = fc.pages.find((p) => {
|
|
8758
|
+
try {
|
|
8759
|
+
const u = new URL(p.url);
|
|
8760
|
+
return u.pathname.split("/").filter(Boolean).length <= 2 && !/\/p$|\/products\//.test(u.pathname);
|
|
8761
|
+
} catch {
|
|
8762
|
+
return false;
|
|
8763
|
+
}
|
|
8764
|
+
});
|
|
8765
|
+
if (plpPage)
|
|
8766
|
+
return plpPage.url;
|
|
8767
|
+
}
|
|
8768
|
+
return null;
|
|
8769
|
+
}
|
|
8770
|
+
function setOverlap(a, b) {
|
|
8771
|
+
if (a.length === 0 || b.length === 0)
|
|
8772
|
+
return 0;
|
|
8773
|
+
const sa = new Set(a);
|
|
8774
|
+
let common = 0;
|
|
8775
|
+
for (const x of b)
|
|
8776
|
+
if (sa.has(x))
|
|
8777
|
+
common++;
|
|
8778
|
+
return common / Math.max(a.length, b.length);
|
|
8779
|
+
}
|
|
8780
|
+
|
|
8537
8781
|
// src/checks/purchase-journey-flow.ts
|
|
8538
8782
|
var STEP_LABELS3 = {
|
|
8539
8783
|
"visit-home": "Visitar home",
|
|
@@ -10515,7 +10759,8 @@ var ALL_CHECKS = [
|
|
|
10515
10759
|
pdpGalleryRelated,
|
|
10516
10760
|
footerLinksHealth,
|
|
10517
10761
|
loginFlow,
|
|
10518
|
-
pictureMissingDims
|
|
10762
|
+
pictureMissingDims,
|
|
10763
|
+
plpPagination
|
|
10519
10764
|
];
|
|
10520
10765
|
var ALL_CHECKS_BY_NAME = {
|
|
10521
10766
|
"http-status-parity": httpStatusParity,
|
|
@@ -10542,7 +10787,8 @@ var ALL_CHECKS_BY_NAME = {
|
|
|
10542
10787
|
"pdp-gallery-related": pdpGalleryRelated,
|
|
10543
10788
|
"footer-links-health": footerLinksHealth,
|
|
10544
10789
|
"login-flow": loginFlow,
|
|
10545
|
-
"picture-missing-dims": pictureMissingDims
|
|
10790
|
+
"picture-missing-dims": pictureMissingDims,
|
|
10791
|
+
"plp-pagination": plpPagination
|
|
10546
10792
|
};
|
|
10547
10793
|
var FLOW_DEPENDENT_CHECKS = new Set([
|
|
10548
10794
|
"purchase-journey-flow",
|
|
@@ -11262,9 +11508,12 @@ function compactHtmlForRecovery(html, maxChars = 12000) {
|
|
|
11262
11508
|
if (attrs.class) {
|
|
11263
11509
|
const tokens = attrs.class.split(/\s+/).filter(Boolean);
|
|
11264
11510
|
const kept = tokens.filter((t) => isSemanticClassToken(t));
|
|
11265
|
-
|
|
11266
|
-
if (
|
|
11511
|
+
const joined = kept.join(" ");
|
|
11512
|
+
if (joined) {
|
|
11513
|
+
attrs.class = joined;
|
|
11514
|
+
} else {
|
|
11267
11515
|
delete attrs.class;
|
|
11516
|
+
}
|
|
11268
11517
|
}
|
|
11269
11518
|
});
|
|
11270
11519
|
const allowed = $("header, nav, main, footer, dialog, [role='dialog'], form, button, a, input, [data-buy-button], [data-checkout], [data-minicart], [data-product-list], [role='button'], [aria-label]").map((_, el) => $.html(el)).get().join(`
|
|
@@ -11551,9 +11800,12 @@ function compactHtmlForSelectors(html, maxChars = 30000) {
|
|
|
11551
11800
|
if (attrs.class) {
|
|
11552
11801
|
const tokens = attrs.class.split(/\s+/).filter(Boolean);
|
|
11553
11802
|
const kept = tokens.filter((t) => isSemanticClass(t));
|
|
11554
|
-
|
|
11555
|
-
if (
|
|
11803
|
+
const joined2 = kept.join(" ");
|
|
11804
|
+
if (joined2) {
|
|
11805
|
+
attrs.class = joined2;
|
|
11806
|
+
} else {
|
|
11556
11807
|
delete attrs.class;
|
|
11808
|
+
}
|
|
11557
11809
|
}
|
|
11558
11810
|
});
|
|
11559
11811
|
const sections = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/parity",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.8",
|
|
4
4
|
"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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"bin": {
|
|
33
33
|
"parity": "dist/cli.js"
|
|
34
34
|
},
|
|
35
|
-
"files": ["dist", "README.md", "AGENTS.md", "CHANGELOG.md", "LICENSE"],
|
|
35
|
+
"files": ["dist", "scripts/postinstall.cjs", "README.md", "AGENTS.md", "CHANGELOG.md", "LICENSE"],
|
|
36
36
|
"scripts": {
|
|
37
37
|
"dev": "bun run --hot src/cli.ts",
|
|
38
38
|
"build": "bun build src/cli.ts --target=node --outfile=dist/cli.js --packages=external && bun run scripts/postbuild.ts",
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
"deadcode": "knip",
|
|
45
45
|
"lint": "biome lint .",
|
|
46
46
|
"fmt": "biome format --write .",
|
|
47
|
-
"prepublishOnly": "bun run check && bun run build"
|
|
47
|
+
"prepublishOnly": "bun run check && bun run build",
|
|
48
|
+
"postinstall": "node scripts/postinstall.cjs"
|
|
48
49
|
},
|
|
49
50
|
"engines": {
|
|
50
51
|
"node": ">=20",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Best-effort Playwright Chromium install on first `npm install` of
|
|
4
|
+
* `@decocms/parity`. Skipped when:
|
|
5
|
+
* - `PARITY_SKIP_PLAYWRIGHT_INSTALL=1` is set (CI / Docker / monorepos
|
|
6
|
+
* that manage the browser separately).
|
|
7
|
+
* - The binary is already present.
|
|
8
|
+
*
|
|
9
|
+
* Failures are downgraded to a warning so a `npm install -g` that
|
|
10
|
+
* happens to be offline / behind a corp proxy doesn't block the whole
|
|
11
|
+
* install. The runtime path in `engine/browser.ts:launchBrowser` catches
|
|
12
|
+
* the missing-browser case and tells the user exactly what to run.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
if (process.env.PARITY_SKIP_PLAYWRIGHT_INSTALL === "1") {
|
|
16
|
+
console.log("[parity] PARITY_SKIP_PLAYWRIGHT_INSTALL=1 set — skipping Chromium install");
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { spawnSync } = require("node:child_process");
|
|
21
|
+
|
|
22
|
+
// Probe Playwright to find out if Chromium is already extracted. The cheap
|
|
23
|
+
// way: try to resolve the binary via `npx playwright install --dry-run`.
|
|
24
|
+
// `--dry-run` exits 0 when everything is already installed and non-zero
|
|
25
|
+
// when downloads are needed. We don't actually rely on the exit code; we
|
|
26
|
+
// always attempt `install chromium` (idempotent) and let Playwright print
|
|
27
|
+
// "already up to date" when there's nothing to do.
|
|
28
|
+
try {
|
|
29
|
+
console.log("[parity] Installing Playwright Chromium (one-time, ~140 MB)…");
|
|
30
|
+
const result = spawnSync("npx", ["--yes", "playwright", "install", "chromium"], {
|
|
31
|
+
stdio: "inherit",
|
|
32
|
+
env: process.env,
|
|
33
|
+
});
|
|
34
|
+
if (result.status === 0) {
|
|
35
|
+
console.log("[parity] Chromium ready.");
|
|
36
|
+
} else {
|
|
37
|
+
console.log(
|
|
38
|
+
"[parity] Chromium install returned a non-zero exit. " +
|
|
39
|
+
"If `parity run` later fails with 'Executable doesn't exist', " +
|
|
40
|
+
"run `npx playwright install chromium` manually.",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
const reason = err?.message ?? "unknown error";
|
|
45
|
+
console.log(
|
|
46
|
+
`[parity] Playwright install skipped (${reason}). Run \`npx playwright install chromium\` before your first \`parity run\`.`,
|
|
47
|
+
);
|
|
48
|
+
}
|