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

storybook-diff

v0.1.0

Published

Compare two compiled Storybook builds and output a JSON diff of created, deleted, and modified stories

Readme

storybook-diff

A TypeScript CLI that compares two compiled Storybook directories and outputs JSON describing which stories were created, deleted, or modified.

Change detection works by parsing the webpack bundles in each Storybook build, hashing each story's export snippet, and walking the module dependency graph to detect transitive changes.

Quick start

npm install
npm run build

node dist/cli.js \
  test-samples/storybook-before \
  test-samples/storybook-after

Usage

storybook-diff <storybook-a-dir> <storybook-b-dir> [options]

Arguments

  • storybook-a-dir: path to a compiled static Storybook directory (must contain index.json or stories.json and *.iframe.bundle.js files)
  • storybook-b-dir: path to a second compiled static Storybook directory

Options

| Option | Description | |---|---| | --max-depth <n> | Max import depth to walk from each story (default: 5) | | --no-depth-limit | Walk the full transitive dependency graph | | --ignore-modules <patterns> | Comma-separated glob patterns of modules to ignore (repeatable) | | --auto-ignore-threshold <n> | Auto-ignore changed modules affecting more than n stories (default: 50) | | --no-gitattributes | Disable automatic .gitattributes linguist-generated parsing |

Filtering noisy modules

Generated files (GraphQL schemas, i18n JSON, etc.) often cause massive false positives because they are transitively imported by many stories. Three mechanisms help:

  1. .gitattributes auto-detection (on by default): If the storybook directory is inside a git repo, patterns marked linguist-generated=true in .gitattributes are automatically ignored.
  2. --ignore-modules: Manually specify glob patterns. Supports * (single segment) and ** (any depth). Examples: '*/generated/*', '**/*.graphql'.
  3. --auto-ignore-threshold: Changed modules that affect more than n stories are automatically ignored (default: 50). Set to 0 to disable.

Output

The CLI prints JSON to stdout.

Example output

{
  "summary": {
    "totalA": 350,
    "totalB": 352,
    "created": 2,
    "deleted": 0,
    "modified": 1,
    "unchanged": 349,
    "contentChanged": 0,
    "dependencyChanged": 1
  },
  "created": [
    { "id": "sidebar-nav--collapsed", "title": "Sidebar/Nav", "name": "Collapsed" },
    { "id": "sidebar-nav--expanded", "title": "Sidebar/Nav", "name": "Expanded" }
  ],
  "deleted": [],
  "modified": [
    {
      "id": "button--primary",
      "title": "Button",
      "name": "Primary",
      "hashA": "abc123...",
      "hashB": "def456...",
      "changeType": "dependency",
      "triggeringModules": [
        "./src/components/Button/Button.tsx"
      ]
    }
  ],
  "changeSources": [
    {
      "modulePath": "./src/components/Button/Button.tsx",
      "affectedStoryCount": 1,
      "ignored": false
    },
    {
      "modulePath": "./src/generated/graphql-types.tsx",
      "affectedStoryCount": 200,
      "ignored": true,
      "ignoreReason": "auto-threshold"
    }
  ],
  "ignoredModules": [
    "./src/generated/graphql-types.tsx"
  ]
}

Output schema

summary

| Field | Type | Description | |---|---|---| | totalA | number | Total stories in build A | | totalB | number | Total stories in build B | | created | number | Stories present only in B | | deleted | number | Stories present only in A | | modified | number | Stories present in both with changes detected | | unchanged | number | Stories present in both with no changes | | contentChanged | number | Modified stories where the story export itself changed | | dependencyChanged | number | Modified stories where a transitive dependency changed |

created / deleted

Array of { id, title, name }.

modified

Array of objects:

| Field | Type | Description | |---|---|---| | id | string | Story ID | | title | string | Story title | | name | string | Story name | | hashA | string | Content hash from build A | | hashB | string | Content hash from build B | | changeType | "content" | "dependency" | "both" | What triggered the change | | triggeringModules | string[] | Module paths that caused a dependency change (only present when changeType is "dependency" or "both") |

changeSources

Array of all changed modules with their blast radius:

| Field | Type | Description | |---|---|---| | modulePath | string | The webpack module path that changed | | affectedStoryCount | number | How many stories transitively depend on this module | | ignored | boolean | Whether this module was filtered out | | ignoreReason | string? | "manual-pattern", "auto-threshold", or "gitattributes" |

ignoredModules

Array of module path strings that were filtered out. Only present when modules were ignored.

Exit codes

| Code | Meaning | |---|---| | 0 | Success | | 1 | Invalid arguments | | 2 | Input directory or Storybook metadata could not be read | | 3 | Unexpected processing failure |

How it works

  1. Load story metadata from index.json or stories.json in each build. Docs-only entries are excluded.
  2. Build a module index by parsing all *.iframe.bundle.js webpack bundles. Each module's path, content hash, and imports are extracted.
  3. Capture story snapshots by computing a content hash (from the story's export snippet and metadata) and collecting the set of transitively reachable module paths via BFS (depth-limited).
  4. Compute changed modules by comparing module content hashes between builds, then filtering out ignored modules.
  5. Diff snapshots by comparing stories by ID: created (only in B), deleted (only in A), modified (content hash differs or a reachable module changed), unchanged.

Development

npm install
npm run build        # compile TypeScript
npm test             # build + run tests

Test fixtures live in test/fixtures/.

Real-world test samples can be placed in test-samples/ or test-samples-<group>/.