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

cypress-only-changed

v0.1.0

Published

[![Test](https://github.com/alirezamirian/cypress-only-changed/actions/workflows/test.yml/badge.svg)](https://github.com/alirezamirian/cypress-only-changed/actions/workflows/test.yml)

Readme

cypress-only-changed

Test

A webpack plugin for Cypress component testing that skips specs whose transitive dependency tree doesn't include any changed file. Similar to --only-changed option in jest or playwright.

Useful in CI to avoid running the full test suite on every commit: only specs that could be affected by the diff are executed.

How it works

During webpack's finishModules phase the plugin walks the dependency graph from each spec file. If none of the files in that spec's transitive dep tree appear in the changed-files list, the spec's source is replaced with a stub:

describe('__skipped__', () => {
  it.skip('no changed dependencies — N files checked', () => {});
});

The walk is tree-shaking–aware: barrel re-exports (export { X } from './Y') are followed only for the names that are actually imported, so an unrelated module re-exported from the same index file doesn't pull the spec in.

Two modes

There are two ways to use this, sharing the exact same dependency analysis:

| Mode | How | Unaffected specs | |---|---|---| | Stubbing (the plugin) | new CypressOnlyChangedPlugin() in your webpack config | Still appear in the run, but their body is replaced with a pending it.skip | | Filtering (filterOnlyChangedSpecs) | Restrict Cypress specPattern in setupNodeEvents to the affected specs | Excluded from the run — the browser is never launched for them |

Stubbing is the simplest drop-in. Filtering is faster when most specs are unaffected, because Cypress's fixed per-spec browser orchestration (navigating, loading, tearing down a session for every spec) dominates the run time even when a spec's body is a no-op it.skip. See Spec-level filtering.

Requirements

The tree-shaking analysis relies on webpack's harmony (ESM) module graph. TypeScript source files must be compiled with "module": "ESNext" (or "preserve") in the tsconfig used by ts-loader. With "module": "commonjs", every import is downleveled to require(), webpack sees only opaque CJS connections, and the plugin cannot prune barrel re-exports — any spec that imports from a shared barrel will be considered affected by any change to any file that barrel transitively re-exports. The plugin emits a webpack warning when it detects this situation.

Configuring ts-loader for ESM output

If ts-loader has no explicit configFile, it picks up the nearest tsconfig.json. If that file has "module": "commonjs" (a common default), create a webpack-specific override and point ts-loader at it:

// tsconfig.cypress-webpack.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler"
  }
}

Limitation: static imports only

The analysis is purely static — it follows import/export edges in the webpack module graph. A spec is only linked to the code it exercises if that code is reachable through those edges. Anything referenced only at runtime is invisible to the analysis, e.g.:

  • components rendered from an HTML/template string rather than imported (common when a shared, framework-agnostic test suite mounts a component by markup instead of importing it);
  • modules pulled in by string-based dynamic resolution the bundler can't see.

If a spec exercises a component it doesn't (transitively) import, a change to that component won't mark the spec as affected — a false negative (the spec is wrongly skipped). This applies to both the plugin and filterOnlyChangedSpecs.

Guidance: make sure each spec statically imports the subject it tests (directly, or via an examples/index module that imports it) — which is also what makes the spec type-check. If a project's specs fundamentally can't do this (e.g. an Angular suite that mounts components via template strings), spec-level filtering isn't safe for it; prefer running the full suite there.

Installation

npm install --save-dev cypress-only-changed

Setup

Add the plugin to your Cypress webpack configuration:

// cypress.config.ts
import { defineConfig } from 'cypress';
import { CypressOnlyChangedPlugin } from 'cypress-only-changed';

export default defineConfig({
  component: {
    devServer: {
      framework: 'react',
      bundler: 'webpack',
      webpackConfig: {
        plugins: [new CypressOnlyChangedPlugin()],
      },
    },
  },
});

The plugin is a no-op by default — all specs run normally unless ONLY_CHANGED is set.

Spec-level filtering with filterOnlyChangedSpecs

The stubbing plugin still lets Cypress start a browser session for every spec — even the skipped ones. When most specs are unaffected, that per-spec overhead is the bottleneck. Spec-level filtering runs the same dependency analysis once up front and hands Cypress only the affected specs, so it never launches a browser for the rest.

filterOnlyChangedSpecs is the batteries-included way to do this. Call it from setupNodeEvents and return what it gives you — it reads everything it needs from the Cypress config (spec globs and the webpack config), rewrites config.specPattern, and manages the "nothing affected" placeholder internally:

// cypress.config.ts
import { defineConfig } from 'cypress';
import { filterOnlyChangedSpecs } from 'cypress-only-changed';

import webpackConfig from './cypress/webpack.config';

export default defineConfig({
  component: {
    devServer: { framework: 'react', bundler: 'webpack', webpackConfig },
    setupNodeEvents(on, config) {
      // No spec list, no placeholder file, no null-handling — the package
      // reads config.specPattern / config.excludeSpecPattern and
      // config.devServer.webpackConfig for you.
      return filterOnlyChangedSpecs(config);
    },
  },
});

Run it with the same ONLY_CHANGED variable as the plugin (see below):

ONLY_CHANGED=origin/main cypress run --component

You don't need the CypressOnlyChangedPlugin in your webpack config when you filter — the analysis is done for you.

filterOnlyChangedSpecs(config, options?)

config is the Cypress config passed to setupNodeEvents. It reads projectRoot, specPattern, excludeSpecPattern, and devServer.webpackConfig from it. Behavior:

| ONLY_CHANGED / changes | Effect on config.specPattern | |---|---| | unset | untouched — every spec runs (no-op) | | set, some specs affected | replaced with the affected spec paths | | set, nothing affected | replaced with an internal placeholder spec (run exits 0) |

All options are optional:

| Option | Type | Default | Description | |---|---|---|---| | webpackConfig | webpack.Configuration | config.devServer.webpackConfig | Override the webpack config used for the graph pass. | | excludedPaths | string[] | ['node_modules'] | Directory names excluded from the dependency walk. | | changedFiles | string[] | (git) | Explicit changed files (absolute paths). When omitted, the same ONLY_CHANGED git resolution as the plugin is used. | | placeholderSpecName | string | 'no-affected-specs.cy.js' | File name of the generated placeholder spec. | | log | false \| LoggerEntry \| LoggerEntry[] | false | Per-spec logging — the same options as the plugin's log ('minimal', 'verbose', 'github-actions', or a custom function). See below. |

Logging ('minimal', 'verbose', …)

filterOnlyChangedSpecs and computeAffectedSpecs accept the exact same log option as the CypressOnlyChangedPlugin — including the 'verbose' reporter that prints, per affected spec, the dependency tree of changed files that pulled it in:

setupNodeEvents(on, config) {
  return filterOnlyChangedSpecs(config, { log: 'verbose' });
}
[cypress-only-changed] SKIP  Button.cy.tsx
[cypress-only-changed] RUN   Form.cy.tsx
  └── Form.tsx
      └── Input.tsx
[cypress-only-changed] RUN   Input.cy.tsx
  └── Input.tsx

Unlike the plugin (which defaults to 'minimal'), these functions default to false (silent) — filterOnlyChangedSpecs already prints a one-line summary, so per-spec logging is opt-in. See the Logging section for the full list of reporters, the github-actions summary, and custom reporters.

Trade-off: filtered-out specs don't appear in the Cypress report at all (unlike stubbing, where they show as pending). Affected-spec detection is identical to the plugin — it reuses the same tree-shaking-aware walk.

Works with cypress open too

Because filtering happens in setupNodeEvents (not inside the webpack build), it applies to both cypress run and cypress open. Opening the interactive runner with ONLY_CHANGED set shows only the affected specs in the spec list — handy for local development, since you can iterate on just the specs your change touches instead of scrolling past the whole suite. (The in-webpack CypressOnlyChangedPlugin also technically runs in open, but it only stubs spec bodies — every spec still shows up in the list.)

Caveat: the affected-spec list is computed once, when setupNodeEvents runs at startup. It does not react to files you edit during an open session — a spec that becomes affected (or unaffected) after you start won't appear or disappear until you restart Cypress. For a live, always-current list, run cypress open without ONLY_CHANGED (all specs) and rely on filtering only in CI / cypress run.

Lower-level: computeAffectedSpecs / discoverSpecs

If you need more control (e.g. your dev server doesn't expose a plain webpack config, or you want to decide what to do with the result yourself), filterOnlyChangedSpecs is built from two smaller exports:

  • discoverSpecs(config)string[] — globs config.specPattern from config.projectRoot, honoring config.excludeSpecPattern, matching files only (a directory named like a spec — e.g. an image-snapshot folder — is ignored) and always excluding node_modules.
  • computeAffectedSpecs({ webpackConfig, specs, excludedPaths?, changedFiles?, log? })Promise<string[] | null> — runs the single webpack graph pass and returns the affected specs (null when ONLY_CHANGED is unset → run all; [] when nothing is affected). Accepts the same log reporters as above.
import { computeAffectedSpecs, discoverSpecs } from 'cypress-only-changed';

const specs = discoverSpecs(config);
const affected = await computeAffectedSpecs({ webpackConfig, specs });
// affected: null → run all, [] → run none, string[] → run these

Usage

Add scripts to package.json for the scenarios you need:

{
  "scripts": {
    "test": "cypress run --component",
    "test:affected": "ONLY_CHANGED=origin/main npm run test",
    "test:uncommitted": "ONLY_CHANGED= npm run test"
  }
}

| Script | What it runs | |---|---| | test | All specs (plugin is a no-op) | | test:affected | Specs affected by commits on the current branch vs origin/main | | test:uncommitted | Specs affected by uncommitted working-tree changes |

ONLY_CHANGED environment variable

| Value | Changed files source | |---|---| | (unset) | Plugin is a no-op; all specs run | | Empty (ONLY_CHANGED=) | git diff --name-only HEAD — uncommitted changes | | A ref (ONLY_CHANGED=origin/main) | git diff --name-only origin/main...HEAD — branch diff |

Paths are resolved relative to the git repository root.

Plugin options

new CypressOnlyChangedPlugin(options?)

| Option | Type | Default | Description | |---|---|---|---| | log | false \| ReporterEntry \| ReporterEntry[] | 'minimal' | Reporter(s) to call per spec. false silences all per-spec logging. | | excludedPaths | string[] | ['node_modules'] | Directory names to exclude from the dependency walk. |

Logging

Default: 'minimal'

When log is not set the built-in minimal reporter prints one line per spec — no dependency tree:

[cypress-only-changed] SKIP  Button.cy.ts
[cypress-only-changed] RUN   Form.cy.ts  (2 changed deps)

Verbose reporter

The 'verbose' reporter prints the same SKIP/RUN line plus an ASCII tree of the dependency paths that led to the decision (changed files are highlighted; unchanged deps are dimmed):

new CypressOnlyChangedPlugin({ log: 'verbose' })
[cypress-only-changed] SKIP  Button.cy.ts
[cypress-only-changed] RUN   Form.cy.ts
  ├── components/Form.tsx
  │   └── utils/validation.ts
  └── styles/form.css

GitHub Actions summary

The 'github-actions' reporter writes a Markdown table to $GITHUB_STEP_SUMMARY — the panel shown at the bottom of each GitHub Actions job run. It is a no-op when GITHUB_STEP_SUMMARY is not set (i.e. outside of GitHub Actions).

new CypressOnlyChangedPlugin({ log: ['minimal', 'github-actions'] })

The summary looks like this:

| Spec | Status | Changed dependencies | |---|:---:|---| | Button.cy.ts | ⏭ skip | — | | Form.cy.ts | ▶ run | validation.ts and 2 more | | Login.cy.ts | ▶ run | auth.ts |

Custom reporter

Pass a function, or an array to combine reporters:

import { CypressOnlyChangedPlugin, SpecReport } from 'cypress-only-changed';

new CypressOnlyChangedPlugin({
  log: ({ specPath, changedDeps }: SpecReport) => {
    console.log(specPath, changedDeps.length > 0 ? 'RUN' : 'SKIP');
  },
})

// combine with a builtin:
new CypressOnlyChangedPlugin({ log: ['verbose', myCustomReporter] })

SpecReport fields:

| Field | Type | Description | |---|---|---| | specPath | string | Absolute path to the spec file | | deps | string[] | All transitive dependency paths | | changedDeps | string[] | Subset of deps that appear in the changed-files list | | directDeps | Map<string, string[]> | Adjacency map for tree rendering (path → direct dep paths) |