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

@devfellowship/ux-paths-capture

v0.1.1

Published

The DFL UX Paths capture producer: a build-time data-source stamp, a deterministic browser, and the walk that turns a rendered screen into the regions.json that @devfellowship/ux-paths-spec describes.

Downloads

443

Readme

@devfellowship/ux-paths-capture

The producer half of the UX map: it stamps a build with the source file and line that wrote each element, walks the rendered page, and writes the regions.json that @devfellowship/ux-paths-spec describes.

Together they let a designer click a region of a screenshot and get an exact file — with no model call, and with no clone of the application's repository on the reader's side.

npm install --save-dev @devfellowship/ux-paths-capture@^0.1.1

Why this is a package

Two applications capture themselves this way, and they live in different GitHub organizations, so no token in either one can read the other's source. A copy would have been the third implementation of one contract — the reader half already recorded the second as debt in its own header.

Two producers drift the way two readers already did, and the drift is invisible: both stay green while a click resolves to the wrong file. npm crosses the boundary that a repository cannot.

🚨 The stamp is build-time and capture-only

It must never reach production. Two independent reasons:

  1. Weight. One attribute per host element, on every element of every screen. It is dead payload for an end user.
  2. Disclosure. The stamp publishes an application's internal file layout into the DOM, where anyone who opens dev tools reads it. For a closed-source application that is a disclosure control, not a size concern.

The gate is an environment variable that is absent by default. It is not the bundler mode: a capture build is a production-mode build, because the artifact under capture has to be the artifact that ships, minus the stamp — so a test on the mode is true exactly when the stamp is wanted and true for the real release.

Prove the default over the artifact, not over the config, with the bin this package ships:

# the release build — expect ZERO
npx ux-paths-assert-no-stamp dist

# the capture build — expect MORE THAN ZERO
npx ux-paths-assert-no-stamp dist --expect-present

Run both in CI. The second is what keeps the first honest: a grep for a string that is normally absent passes for free, and would keep passing if the detector broke. The bin also self-tests its own detector on every run, and refuses an empty or missing directory.

⚠️ A leak is the attribute BOUND TO A FILE AND LINE, not the words data-source appearing somewhere. An application that explains its own capture pipeline puts that name into user-facing copy, and the copy is bundled — the first version of this detector counted the name alone and failed a perfectly clean build. A guard that fires on the word for the thing is one an engineer learns to override, and an overridden disclosure control is worse than none.

The three entries

| import | needs | holds | |---|---|---| | @devfellowship/ux-paths-capture | nothing | the stamp transform, the component namer, the region builder, the artifact detector | | …/vite | vite (optional peer, types only) | the stamp as a Vite plugin | | …/playwright | @playwright/test (optional peer, types only) | the deterministic browser, the DOM walk, the pair writer |

The split keeps the root importable by something with no bundler and no browser — a CI guard, an MCP tool, a plain Node script.

Stamp a build

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import { sourceStamp } from "@devfellowship/ux-paths-capture/vite";

export default defineConfig({
  plugins: [
    react(),
    // Returns `undefined` when the switch is off, and `undefined` is a valid
    // member of `plugins` — so there is one gate, in one place.
    sourceStamp({ root: __dirname }),
  ],
});

The default switch is UX_PATHS_SOURCE_STAMP. An application that already has its own spelling keeps it:

sourceStamp({ root: __dirname, envVar: "MY_APP_SOURCE_STAMP" });

The shared thing is the gate, not the string. Renaming a switch that a working capture job already sets would disable that job silently.

Capture a screen

The package owns everything that is the same for every application: the deterministic browser, the DOM walk, and the geometry. It deliberately does not own navigation or the settle policy — one app waits on a data-testid, another shoots until two consecutive images hash identically, a third stubs an auth gate first. Those are properties of the app, not of the map.

import { chromium } from "@playwright/test";
import {
  CAPTURE_CONTEXT_OPTIONS,
  CAPTURE_LAUNCH_ARGS,
  applyFreezeStylesheet,
  installDeterminism,
  writeCapturePair,
} from "@devfellowship/ux-paths-capture/playwright";

const browser = await chromium.launch({ args: [...CAPTURE_LAUNCH_ARGS] });
const context = await browser.newContext(CAPTURE_CONTEXT_OPTIONS);
const page = await context.newPage();

await installDeterminism(page); // BEFORE the first navigation
await page.goto("/login");
await applyFreezeStylesheet(page); // AFTER every navigation
// …your own settle policy here…

const png = await page.screenshot({
  fullPage: false,
  animations: "disabled",
  caret: "hide",
  scale: "css",
});

await writeCapturePair(page, {
  outDir: "capture/login",
  screen: "login",
  root: process.cwd(), // where a stamped path is resolved from
  png,
});

That writes screenshot.png and regions.json side by side.

The two files are a PAIR

regions.json describes the geometry of exactly one image, in that image's own pixel space — CSS pixels, deviceScaleFactor: 1, viewport-clipped. A rect placed next to a different image is worse than no rect at all.

Viewport-clipped and not fullPage for a concrete reason: getBoundingClientRect() speaks viewport coordinates, and Chromium resizes the viewport to take a full-page shot — which moves fixed/sticky elements and can mount content that was not mounted when the rects were read.

🧊 Pixel stability is a one-host promise

Everything downstream compares two screenshots and asks whether the diff reached zero, so the freezing is part of the contract. But glyph rasterisation belongs to the machine: a different FreeType or fontconfig renders the same text with different anti-aliasing. Two captures on one host are byte-identical; two captures on different hosts are not promised. A comparison across host classes must run in one container to mean anything.

performance.now() is left real on purpose — freezing it stalls React's scheduler and every requestAnimationFrame loop, trading a cosmetic wobble for a hang.

What is not attributed, and why that is correct

  • Third-party components emit their own DOM from their own compiled JS. The bundler never sees their JSX.
  • Pre-compiled workspace packages have already turned their JSX into jsx() calls before the bundler runs. First-party code, still unattributed.
  • React.createElement and dangerouslySetInnerHTML — no JSX tag, no stamp.

A region that lands on unattributed DOM resolves through its nearest stamped ancestor, which is the file that mounted the third-party component. That is the honest answer, and it is the one a designer can act on.

Reading a map back

resolveRegionAt is re-exported here, but it is defined in @devfellowship/ux-paths-spec. A viewer should import it from there and skip this package entirely — the reader half has zero runtime dependencies and is meant for a browser.

License

MIT