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

affected-ci

v1.0.2

Published

Run lint, typecheck, tests and Playwright specs on only what your diff can reach — from your import graph, with no workspaces required

Readme

affected-ci

Run lint, typecheck, tests and Playwright specs on only what your diff can reach — derived from your import graph, with no workspaces required.

Change one line in Button.tsx and you want Button's tests, plus the tests of everything that imports Button however deep the chain goes. You want the same for lint and typecheck. Billing never imports Button, so Billing should not be touched at all. And you want that list computed from your code every time, so it is never wrong and you never maintain it.

Turborepo and Nx do this by reading workspace manifests. This does it by reading your imports, which means it works in a single app in a single repo with no package boundaries at all.

Install

npm install --save-dev affected-ci
# recommended, and the default engine when present:
npm install --save-dev dependency-cruiser

What each tier needs

Every tier shells out to a tool you already have. Nothing here is bundled, and nothing is a hard dependency — you only need the tool for the tier you actually run. affected list and affected specs need nothing but git.

| Tier | Runs | Needs | | ----------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | list | nothing | git | | specs | nothing | git, and route files (see below) | | lint | eslint --cache --cache-strategy content --cache-location .eslintcache <files> | ESLint 7.21+ (when --cache-strategy landed) | | typecheck | tsc -p tsconfig.affected.json | TypeScript, and a tsconfig.json for the generated one to extend | | test | vitest related --run --passWithNoTests <files> | Vitest with the related subcommand | | e2e | playwright test <specs> | Playwright |

dependency-cruiser and typescript are the only declared peer dependencies, both optional, and they are for the engine, not the tiers. The tier tools are deliberately not declared: a repo that only wants affected test should not be told it needs ESLint.

--cache-strategy content matters: ESLint's default is mtime, and a CI checkout rewrites every mtime, so a restored cache would miss on every file.

Using different tools

Every tier is just a command and its arguments, so swap them in affected.config.mjs. Jest has a direct equivalent of vitest related:

export default {
  tiers: {
    test: {
      command: 'npx',
      args: ['jest', '--findRelatedTests', '--passWithNoTests'],
      allArgs: ['jest'], // the "global change, run everything" branch
    },
    lint: {
      command: 'npx',
      args: ['biome', 'check'],
      cache: [], // biome has its own; do not pass ESLint's flags
      all: ['src'],
      extensions: ['.ts', '.tsx', '.js', '.jsx'],
      maxArgs: 2000,
    },
  },
};

The contract a tier's tool has to satisfy is small: accept a list of file paths as trailing arguments, and exit non-zero on failure. Anything that does both works.

What specs assumes

The specs and e2e tiers are the only ones with a structural requirement: routes have to be files you can name, so a changed file can be traced to a URL. The default is the Next.js app router convention, src/app/**/page.tsx/**. If your routes come from a config object instead, override routes and map it yourself. The other four tiers do not care how you route.

Use it locally

npx affected list                # print the affected files, or ALL
npx affected specs               # print the affected e2e specs, or ALL

npx affected verify              # lint, typecheck, test — the whole local gate
npx affected lint
npx affected typecheck
npx affected test
npx affected e2e -- --shard=1/4  # everything after -- goes to the tool

BASE_REF defaults to origin/main; --base-ref overrides it.

In package.json:

{
  "scripts": {
    "verify": "affected verify"
  }
}

and in lefthook.yml:

pre-push:
  commands:
    verify:
      run: npm run verify

Use it in CI

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # the graph needs history to diff against
          filter: blob:none
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci

      - uses: Cst2989/affected-ci@v1
        with:
          tier: lint
          base-ref: origin/${{ github.base_ref }}

Run every job on every PR. Each one decides internally how much work to do, so every required check reports a result and nothing sits pending forever.

tier: list and tier: specs also set outputs — files, specs, is-global.

The action has no runtime dependencies, so consumers install nothing for it.

Adopting in a repo that already has CI

You do not delete or replace your workflows. This changes the command inside a step, not the step, the job, or the workflow.

   - uses: actions/checkout@v4
+    with:
+      fetch-depth: 0        # the graph needs history to diff against
+      filter: blob:none
   - uses: actions/setup-node@v4
   - run: npm ci
-  - run: npm run lint
+  - run: npx affected lint
+    env:
+      BASE_REF: origin/${{ github.base_ref }}

Your checkout, caching, matrix and job names stay exactly as they are.

fetch-depth: 0 is the one thing you must change. actions/checkout clones with fetch-depth: 1, so the runner holds a single commit and has no base branch to compare against.

Your required checks keep working, and that is the point. Because the job names do not move, every check still reports on every PR. The tempting alternative — a paths: filter on the workflow trigger — breaks branch protection permanently: a workflow that never starts never reports, GitHub does not read that silence as success, and the PR sits on Expected — Waiting for status to be reported forever. Here every job runs and each one decides internally how much work to do.

Convert one tier at a time. They are independent. Do lint first, watch a few PRs, then typecheck, then test.

Keep running everything on main. Narrowing is a PR optimisation. Your post-merge workflow, and your merge queue if you have one, should keep running the full suite — a green baseline is the assumption all of this rests on.

Check that it is selecting, not skipping. The failure mode worth fearing is a PR going green because nothing ran. On the first tier you convert, deliberately break something and confirm CI catches it. npx affected list prints exactly what was selected, so you can read it before trusting it. If the config is wrong in a way that would under-select, add the offending path to globalPaths and you are back to running everything — the safe direction.

Configure

affected.config.mjs at your repo root:

export { default } from 'affected-ci/presets/vite-react';

or spell it out:

export default {
  sourceDirs: ['src', 'e2e'],
  aliases: { '@/': 'src/' }, // builtin engine only; depcruise reads tsconfig
  globalPaths: ['package.json', 'tsconfig.json', '.github/', 'src/types/'],
  engine: 'auto', // auto | dependency-cruiser | builtin
};

Presets ship for next-app-router and vite-react.

The two engines

| | dependency-cruiser (default) | builtin | | ----------------------- | ---------------------------------------------------------- | ------------------------------------------- | | Resolution | real TypeScript parsing, tsconfig paths, dynamic imports | regex over import statements, one alias map | | Dependencies | needs dependency-cruiser + typescript | none | | Cold run on ~70 modules | ~150ms | ~50ms | | Warm (content cache) | ~5ms | n/a |

auto uses dependency-cruiser when it resolves and falls back to the builtin resolver, saying why. Naming an engine explicitly is a hard requirement — if you asked for dependency-cruiser and it cannot run correctly, that is an error rather than a silent downgrade to a resolver with different blind spots.

Both engines run against the same test suite, with the differences pinned in test/engine-differences.test.mjs. The one that matters: a tsconfig paths mapping you did not mirror in aliases is followed by dependency-cruiser and missed by the builtin resolver.

What null / ALL means

Some changes no import graph can model: a lockfile bump, a lint config, an ambient .d.ts, the CI workflow itself. Those are globalPaths, and they return null from the API and print ALL from the CLI, meaning run everything. An empty list means the graph genuinely reached nothing.

Failing toward over-selection

Every judgement call errs on the side of running too much, because the failure mode of running too little is a broken change that merges green.

  • A file whose internal import does not resolve — a typo, a half-finished rename, a computed import() — produces no edge and would silently drop out of the graph. Those files are always treated as affected.
  • A layout is not a page, so no route regex reaches it, but it renders on every route beneath it. src/app/**/layout.tsx expands into those routes.
  • A spec is selected on any route-shaped literal it contains, or that any helper it imports contains — not just a literal in page.goto()'s first argument.
  • Deleted paths still drag their dependents into the walk, then drop out of the result, because handing a missing path to ESLint or tsc is a hard error.
  • A cruise that returns zero modules is an error, not "nothing affected".

Why not depcruise --affected?

It is close, and it is where this started. Two reasons it is not enough:

  1. It compares with two dots. --affected is sugar over --reaches "$(watskeburt <rev>)", and watskeburt shells out to git diff <rev> --name-status. Against a base branch that has moved ahead, every file that branch touched looks like yours. This package computes the changed set from the merge base — three dots — and hands the result to the engine.
  2. It cannot map routes to specs. A Playwright spec imports nothing from your app; it navigates to a URL. Any import-based selector skips it, which is also why playwright --only-changed reports green on a broken page. The specs tier connects them through the route instead.

Known limits

  • Static imports only. A module loaded from a runtime-computed path is invisible, though the file doing the loading is force-selected.
  • page.goto() with an absolute URL or one assembled entirely by interpolation has no path literal to read.
  • Typecheck precision is partly illusory: your app entry is in most affected sets and tsc follows its imports forward into everything.

License

MIT