npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@juangadm/pre-post

v1.2.0

Published

Before/after screenshots for pull requests: detect changed routes, capture, pixel-diff, and post a sticky PR comment

Readme

pre-post

Before/after screenshots for pull requests. One command detects the routes your branch changed, captures each one before and after, uploads the images to GitHub, and puts them at the top of the PR description where a reviewer can judge them at a glance.

It works out both sides itself, cheapest option first:

| | Pre (the baseline) | Post (this branch) | |---|---|---| | 1 | --before | --after | | 2 | before in .pre-post.json | the preview deployment for this commit | | 3 | the production deployment for the base commit | a local dev server | | 4 | whatever is on production now | | | 5 | the base commit, served locally | |

Rows 2 to 4 need no dev environment at all, which is the point: a preview deployment and a production URL are enough for anyone who can open the PR. Deployments come from the GitHub Deployments API, so Vercel, Cloudflare Pages, Netlify and Render all work with no extra token and nothing to configure; a host that records only a commit status is read from the deployment bot's own PR comment instead. Row 4 covers repositories that do not deploy every push to their default branch, so nothing is deployed at the fork point — it prints which commit Pre actually came from rather than implying the base.

pre-post never guesses a baseline. If no deployment can be found it says so and tells you the one flag that fixes it, because a baseline that is quietly the wrong site reads as 100% changed on every route and looks exactly like a real result.

The last baseline needs no network at all: it checks the base commit into a throwaway worktree and boots its dev script. That keeps pre-post working inside a sandboxed agent container, a CI job, or behind an egress allowlist — and it compares against exactly what the branch forked from, rendered in the same browser as the Post side.

Originally forked from before-and-after by James Clements / Vercel Labs.

$ npx -y @juangadm/pre-post@latest pr

Routes (nextjs-app, 41ms): /, /pricing
  /                           medium src/app/page.tsx imports src/components/ui/button.tsx (2 hops)
  /pricing                    medium src/app/pricing/page.tsx imports src/components/ui/button.tsx (2 hops)
Capturing 8 screenshots (2 route(s) × 2 viewport(s)) ...
  changed  /pricing @ mobile (1.42%, 1811ms)
  changed  /pricing @ desktop (0.64%, 2036ms)
  same     / @ mobile (0.00%, 1990ms)
  same     / @ desktop (0.00%, 2211ms)
Publishing 10 image(s) to acme/web@pre-post-assets ...
Updated PR description: https://github.com/acme/web/pull/42
pre-post · PR #42 · 2 route(s) · 2 viewport(s) · 6.8s
  /         desktop  no change
  /         mobile   no change
  /pricing  desktop  0.64% changed
  /pricing  mobile   1.4% changed
Comment: https://github.com/acme/web/pull/42

How it works

  1. Routes. Diffs the branch against the merge base with main — fetching that branch first when the checkout does not have it, which is the normal shape in CI and in the web/sandbox editors. When no shared history can be established it stops with one sentence rather than reporting an empty diff, and --base <ref> names the commit directly. Then it follows the import graph: a change to components/ui/button.tsx marks every page that imports it. Next.js App Router and Pages Router, Vite apps (React Router, file-based src/pages), and a generic fallback. Monorepos are handled by picking the app that owns the changed files.
  2. Capture. Playwright + Chromium headless shell. The page's own clock is held still while it loads and then run forward by a fixed budget, so a page that animates on a timer is photographed at the same frame on both sides however fast each host answered. Reduced motion, animations finished, caret hidden, fonts and images settled, layout stable, lazy content primed. 2x device scale, full page (capped at 2400 CSS px), desktop (add mobile with -r). All routes and viewports run concurrently.
  3. Diff. Pure-JS pixel comparison in worker threads. Reports the percentage changed, the bounding box, and a tight crop of the changed region. A route counts as changed when the painted difference covers at least minChangedArea CSS pixels² (default 100, roughly a third of a 16px icon) or at least threshold of the canvas — the first rule is what decides on a page, the second on a small image.
  4. Layout shift. A padding change near the top of a page moves everything below it, and pixel diffing counts every moved pixel as changed: a change a designer would call "slightly roomier" reads as most of the page repainted, and the crop is suppressed just when it would be most useful. So the two sides are checked for a single vertical offset first — how far the content moved, and from which row. When one is found, Pre is re-spaced into Post's layout and the two are compared there: the crop comes from that pair, and the comment says Content shifted down 48px instead of quoting a percentage. Rows Post gained are left as background rather than skipped, so content inserted above the shift — the banner that caused it — still reads as new rather than as nothing. The offset is only accepted when it accounts for most of the difference across the rows both sides share. The raw numbers are left alone: a move is a visual change, and reporting less of one would hide it. Reflow, where content moves both across and down, has no single offset to find, so it is reported the way it always was.
  5. Publish. Images go to a pre-post-assets branch in the same repository via the GitHub API, as one commit per run. Nothing is committed to the PR branch, no CI is triggered, and the blob URLs render on private repos — a screenshot is visible to exactly whoever can see the repository. pre-post prune removes images for PRs closed more than 90 days ago, but note that it commits a deletion rather than rewriting history: the older commits still hold the blobs, so a link handed out earlier keeps working. Treat anything captured as permanent, and think twice before pointing pre-post at a preview holding real data.
  6. Describe. The images go in a delimited block at the top of the PR description, replaced in place on every run and leaving your own text untouched. Changed routes show a Pre/Post crop with the full page collapsed underneath; unchanged routes fold into a single line. If the PR cannot be edited — a fork, a read-only token — it falls back to one sticky comment.

Install

Nothing to install. npx -y @juangadm/pre-post@latest pr downloads the CLI and, on first use, the Chromium headless shell (~80 MB). You need Node 20+ and a GitHub token: either gh auth login or GH_TOKEN.

As a Claude Code skill:

npx skills add juangadm/pre-post -y

Then say /pre-post after making UI changes.

Usage

pre-post pr                                  # everything, on the current branch's PR
pre-post pr --before https://acme.com        # pin the baseline (saved to .pre-post.json)
pre-post pr --no-local-baseline              # never build the base commit locally
pre-post pr --routes /pricing,/docs          # explicit routes
pre-post pr --viewports desktop,1440x900     # custom viewports
pre-post pr --dry-run                        # capture + diff locally, post nothing
pre-post pr --json                           # machine-readable output

pre-post https://acme.com http://localhost:3000 --routes /pricing   # ad-hoc comparison
pre-post before.png after.png                # diff two images
pre-post detect                              # which routes does this branch touch?
pre-post login https://staging.acme.com      # sign in once; the session is reused
pre-post prune --days 90                     # clean up the assets branch
pre-post doctor                              # browser, token, dev server, config

When something needs a human, the CLI exits with code 3 and one sentence saying what to do (log in, start the dev server, pass --before). Re-running picks up where it left off.

Exit codes, so a script or an agent can branch on them:

| code | meaning | |---|---| | 0 | done; for doctor, pre-post pr can run | | 1 | the run failed — every capture errored, or an unexpected error; for doctor, a required check failed | | 2 | the arguments could not be parsed | | 3 | something needs a human; the message says what |

doctor marks a check FAIL only when pre-post pr has no way to proceed without it — the browser, a GitHub token, and being inside a git repository. Everything else prints as note: no dev server running, or no --before saved, narrows which strategy a run picks rather than stopping it, so those never change the exit code.

Results go into a delimited block at the top of the PR description, which re-runs replace in place, leaving your own text untouched. If the PR cannot be edited — a fork, a read-only token — it falls back to a single sticky comment.

Configuration

Optional .pre-post.json in the repo root:

{
  "before": "https://acme.com",
  "routes": ["/"],
  "samples": { "/blog/[slug]": "/blog/hello-world" },
  "viewports": ["desktop"],
  "fullPage": true,
  "maxHeight": 2400,
  "scale": 2,
  "threshold": 0.001,
  "minChangedArea": 100,
  "maxRoutes": 6,
  "ignore": ["apps/docs"],
  "headers": {},
  "assetsBranch": "pre-post-assets"
}

Environment:

| Variable | Purpose | |---|---| | GH_TOKEN / GITHUB_TOKEN | GitHub token (default: gh auth token) | | VERCEL_AUTOMATION_BYPASS_SECRET | Bypass Vercel Deployment Protection on preview and production URLs | | PRE_POST_CONCURRENCY | Parallel pages (default 6) | | PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH | Use a specific Chromium binary | | GH_REPO | owner/repo when the remote URL cannot be parsed |

Library

import { runPr, captureScreenshot, diffImages, detectRoutesForRepo } from '@juangadm/pre-post';

Development

pnpm install
pnpm build
pnpm test:unit
TEST_BROWSER=true pnpm test        # needs a Chromium; npx playwright-core install chromium-headless-shell

pnpm and npm cannot share a node_modules. pnpm builds a symlinked tree that npm cannot read, so npm install over it fails with Cannot read properties of null (reading 'edgesOut'), which names none of that. Run rm -rf node_modules site/node_modules before switching either way. npm also installs the CLI only — site/ is a pnpm workspace member, so use pnpm to work on the site.

License

MIT