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-afterUsage
storybook-diff <storybook-a-dir> <storybook-b-dir> [options]Arguments
storybook-a-dir: path to a compiled static Storybook directory (must containindex.jsonorstories.jsonand*.iframe.bundle.jsfiles)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:
.gitattributesauto-detection (on by default): If the storybook directory is inside a git repo, patterns markedlinguist-generated=truein.gitattributesare automatically ignored.--ignore-modules: Manually specify glob patterns. Supports*(single segment) and**(any depth). Examples:'*/generated/*','**/*.graphql'.--auto-ignore-threshold: Changed modules that affect more than n stories are automatically ignored (default: 50). Set to0to 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
- Load story metadata from
index.jsonorstories.jsonin each build. Docs-only entries are excluded. - Build a module index by parsing all
*.iframe.bundle.jswebpack bundles. Each module's path, content hash, and imports are extracted. - 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).
- Compute changed modules by comparing module content hashes between builds, then filtering out ignored modules.
- 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 testsTest fixtures live in test/fixtures/.
Real-world test samples can be placed in test-samples/ or test-samples-<group>/.
