Skip to content

Core: Invalidate cache on Storybook version upgrade#33717

Merged
valentinpalkovic merged 6 commits into
nextfrom
copilot/fix-cache-invalidation-on-upgrade
Feb 3, 2026
Merged

Core: Invalidate cache on Storybook version upgrade#33717
valentinpalkovic merged 6 commits into
nextfrom
copilot/fix-cache-invalidation-on-upgrade

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 30, 2026

Closes #33711

Problem

Users encounter build errors and runtime issues after upgrading Storybook due to stale cache. Currently requires manual deletion of node_modules/.cache/storybook.

Changes

Cache path now includes version

- node_modules/.cache/storybook/{sub}/{file}
+ node_modules/.cache/storybook/{version}/{sub}/{file}

Modified resolvePathInStorybookCache() to inject version from versions.storybook:

const version = versions.storybook || 'unknown';
return join(cacheDirectory, version, sub, fileOrDirectoryName);

Tests added

  • Version inclusion in path
  • Fallback to 'unknown' when version unavailable
  • Cross-platform path handling via path.join()

Impact

  • Automatic cache invalidation on upgrade
  • Old cache directories remain but unused
  • No API changes or migration required
Original prompt

This section details on the original issue you should resolve

<issue_title>[Bug]: Automatically invalidate or version-key the .cache directory on Storybook upgrades</issue_title>
<issue_description>### Describe the bug

When upgrading Storybook versions, users frequently encounter build errors or unexpected runtime behavior in the manager/preview UI. These issues are often resolved by manually deleting the node_modules/.cache/storybook directory.

Currently, the cache does not seem to be strictly tied to the specific version of Storybook installed, leading to "stale" cache issues that are difficult for users to debug.

Reproduction link

Reproduction steps

System

-

Additional context

No response</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Summary by CodeRabbit

  • Tests

    • Added comprehensive tests validating cache path construction across scenarios (mocked cache, fallback to project cache, default sub-directory, empty names, consistent per-version behavior, and unknown-version fallback).
  • Chores

    • Updated cache path structure to include Storybook version and an optional sub-directory (defaults to "default") to improve cache isolation and organization.

Copilot AI and others added 3 commits January 30, 2026 10:24
- Modified resolvePathInStorybookCache to include storybook version in cache path
- Added comprehensive unit tests for the new versioned cache functionality
- Cache path now includes version: node_modules/.cache/storybook/{version}/{sub}/{file}
- This ensures automatic cache invalidation when upgrading Storybook versions

Co-authored-by: valentinpalkovic <5889929+valentinpalkovic@users.noreply.github.com>
Co-authored-by: valentinpalkovic <5889929+valentinpalkovic@users.noreply.github.com>
…allback test

- Use path.join() for all expected path assertions to ensure cross-platform compatibility
- Add test for 'unknown' fallback when storybook version is not available
- Improve path comparison logic to be platform-agnostic
- All 7 tests now pass

Co-authored-by: valentinpalkovic <5889929+valentinpalkovic@users.noreply.github.com>
Copilot AI changed the title [WIP] Automatically invalidate cache directory on Storybook upgrades Invalidate cache on Storybook version upgrade Jan 30, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jan 30, 2026

Fails
🚫 PR description is missing the mandatory "#### Manual testing" section. Please add it so that reviewers know how to manually test your changes.

Generated by 🚫 dangerJS against 22690a6

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Jan 30, 2026

View your CI Pipeline Execution ↗ for commit 22690a6

Command Status Duration Result
nx run-many -t compile,check,knip,test,pretty-d... ✅ Succeeded 6m 24s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-03 10:36:50 UTC

@valentinpalkovic valentinpalkovic changed the title Invalidate cache on Storybook version upgrade Core: Invalidate cache on Storybook version upgrade Jan 30, 2026
@valentinpalkovic valentinpalkovic marked this pull request as ready for review January 30, 2026 15:02
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 30, 2026

📝 Walkthrough

Walkthrough

Utility updated to include the Storybook version in computed cache paths and accept an optional sub-directory; tests added to validate behaviors including empathic cache usage, cwd fallback, default subdir, empty names, version consistency, and unknown-version fallback.

Changes

Cohort / File(s) Summary
Cache path resolution & tests
code/core/src/common/utils/resolve-path-in-sb-cache.ts, code/core/src/common/utils/resolve-path-in-sb-cache.test.ts
Added import of versions, changed cache path layout to cacheDirectory/{version}/{sub}/{fileOrDirectoryName} with sub defaulting to "default" and version fallback to "unknown". Added comprehensive Vitest tests mocking empathic/package and versions to validate multiple scenarios.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@code/core/src/common/utils/resolve-path-in-sb-cache.test.ts`:
- Around line 20-105: The tests for resolvePathInStorybookCache currently set
mock return values inline; move all vi.mocked(pkg.cache).mockReturnValue(...)
calls and any temporary assignments to versions.storybook into beforeEach hooks
(use nested describe blocks for scenarios: e.g., "when cache available", "when
cache unavailable", "when version missing") so test bodies only assert behavior;
ensure each beforeEach sets the appropriate pkg.cache return and/or
versions.storybook value and use vi.clearAllMocks() (and restore original
versions.storybook value in an afterEach if modified) to avoid cross-test
pollution.

Comment on lines +20 to +105
describe('resolvePathInStorybookCache', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should include version in the cache path when using empathic cache', () => {
const mockCacheDir = '/mock/node_modules/.cache/storybook';
vi.mocked(pkg.cache).mockReturnValue(mockCacheDir);

const result = resolvePathInStorybookCache('test-file', 'test-sub');

expect(result).toContain(versions.storybook);
expect(result).toBe(join(mockCacheDir, versions.storybook, 'test-sub', 'test-file'));
});

it('should include version in the cache path when falling back to cwd', () => {
vi.mocked(pkg.cache).mockReturnValue(undefined);
const cwd = process.cwd();

const result = resolvePathInStorybookCache('test-file', 'test-sub');

expect(result).toContain(versions.storybook);
expect(result).toBe(
join(cwd, 'node_modules', '.cache', 'storybook', versions.storybook, 'test-sub', 'test-file')
);
});

it('should use default sub directory when not provided', () => {
const mockCacheDir = '/mock/node_modules/.cache/storybook';
vi.mocked(pkg.cache).mockReturnValue(mockCacheDir);

const result = resolvePathInStorybookCache('test-file');

expect(result).toBe(join(mockCacheDir, versions.storybook, 'default', 'test-file'));
});

it('should handle empty file or directory name', () => {
const mockCacheDir = '/mock/node_modules/.cache/storybook';
vi.mocked(pkg.cache).mockReturnValue(mockCacheDir);

const result = resolvePathInStorybookCache('', 'test-sub');

// Note: path.join() normalizes away the trailing slash for empty strings
expect(result).toBe(join(mockCacheDir, versions.storybook, 'test-sub'));
});

it('should create consistent paths for the same version', () => {
const mockCacheDir = '/mock/node_modules/.cache/storybook';
vi.mocked(pkg.cache).mockReturnValue(mockCacheDir);

const result1 = resolvePathInStorybookCache('file1', 'sub1');
const result2 = resolvePathInStorybookCache('file2', 'sub1');

expect(result1).toContain(versions.storybook);
expect(result2).toContain(versions.storybook);
// Verify both paths share the same base directory by comparing parent directories
const parent1 = result1.substring(0, result1.lastIndexOf(join('sub1', 'file1')));
const parent2 = result2.substring(0, result2.lastIndexOf(join('sub1', 'file2')));
expect(parent1).toBe(parent2);
});

it('should handle different subdirectories', () => {
const mockCacheDir = '/mock/node_modules/.cache/storybook';
vi.mocked(pkg.cache).mockReturnValue(mockCacheDir);

const result1 = resolvePathInStorybookCache('test-file', 'dev-server');
const result2 = resolvePathInStorybookCache('test-file', 'telemetry');

expect(result1).toBe(join(mockCacheDir, versions.storybook, 'dev-server', 'test-file'));
expect(result2).toBe(join(mockCacheDir, versions.storybook, 'telemetry', 'test-file'));
});

it('should use "unknown" as version when storybook version is not available', () => {
const mockCacheDir = '/mock/node_modules/.cache/storybook';
vi.mocked(pkg.cache).mockReturnValue(mockCacheDir);

// Mock the versions module to return a falsy value
vi.mocked(versions).storybook = '' as any;

const result = resolvePathInStorybookCache('test-file', 'test-sub');

expect(result).toContain('unknown');
expect(result).toBe(join(mockCacheDir, 'unknown', 'test-sub', 'test-file'));

// Reset the mock
vi.mocked(versions).storybook = '10.3.0-alpha.1';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Vitest beforeEach mock setup best practices organizing test mocks

💡 Result:

Vitest mock setup best practices (esp. around beforeEach)

1) Put module mocks (vi.mock) at top-level, not in beforeEach

vi.mock() is hoisted and runs before imports, so using it inside beforeEach usually doesn’t do what you expect. If you need shared variables for a hoisted mock factory, define them with vi.hoisted. Also note: modules imported in a setupFiles file may already be cached by the time a test runs, making later mocking ineffective (use vi.resetModules() if you must). [1]

2) Use beforeEach for per-test state, not for “declaring mocks”

Good beforeEach usage:

  • reset counters / data fixtures
  • set default return values for already-created mocks/spies
  • re-stub globals that tests mutate (or do it in afterEach cleanup)

Vitest explicitly warns to clear/restore mocks between tests to avoid state leaking. [2]

3) Clean up consistently: prefer afterEach + the right reset function

Pick the cleanup level you actually need:

  • vi.clearAllMocks() → clears call history only (keeps implementations) [3]
  • vi.resetAllMocks() → clears history and resets implementations [3]
  • vi.restoreAllMocks() → restores original implementations for vi.spyOn spies (and in newer Vitest behavior, it doesn’t “reset everything” the way people often assume) [3][4]

Vitest docs even suggest calling vi.restoreAllMocks() in afterEach (or enabling the equivalent config) when you rely on spies. [3]

4) Use setupFiles for global test environment defaults, not heavy mocking

setupFiles runs before each test file (in that worker process). It’s ideal for things like registering custom matchers, test-library config, global polyfills/stubs—but be careful: if isolation is off, you can end up doing repeated work in the same global scope. [5]

5) Organize mocks by intent

A common, maintainable layout:

  • Per-test-file mocks: live next to the test (easy to see what’s mocked)
  • Reusable mock builders/factories: test/factories/* (pure functions returning data / mock objects)
  • Shared environment stubs (global APIs): in setupFiles (e.g., vi.stubGlobal(...)) [6]

6) Prefer dependency boundaries you can swap

If you find yourself re-mocking deep imports in beforeEach, it’s often simpler to:

  • inject dependencies (pass collaborators in)
  • wrap third-party modules behind your own small “adapter” module, and only mock that adapter

This reduces hoisting/caching surprises and makes mocks more local and readable. [1]


Sources: Vitest docs on vi.mock hoisting/caching and vi.hoisted [1], mocking guide warning about clearing/restoring [2], vi.clearAllMocks/resetAllMocks/restoreAllMocks semantics and afterEach suggestion [3], migration note on changed restoreAllMocks behavior [4], setupFiles behavior/constraints [5], and vi.stubGlobal example [6].


🏁 Script executed:

head -20 code/core/src/common/utils/resolve-path-in-sb-cache.test.ts

Repository: storybookjs/storybook

Length of output: 514


Move mock behavior configuration into beforeEach hooks; avoid setting return values inline in test cases.

Mock behavior (via .mockReturnValue() and property assignment) should live in beforeEach blocks to keep state management consistent and separate from test logic. Use nested describe blocks with their own beforeEach to organize scenarios with different mock configurations (e.g., when cache is unavailable, or version is missing).

♻️ Suggested refactor sketch
 describe('resolvePathInStorybookCache', () => {
+  const mockCacheDir = '/mock/node_modules/.cache/storybook';
+
   beforeEach(() => {
     vi.clearAllMocks();
+    vi.mocked(pkg.cache).mockReturnValue(mockCacheDir);
+    vi.mocked(versions).storybook = '10.3.0-alpha.1';
   });

   it('should include version in the cache path when using empathic cache', () => {
-    const mockCacheDir = '/mock/node_modules/.cache/storybook';
-    vi.mocked(pkg.cache).mockReturnValue(mockCacheDir);
     const result = resolvePathInStorybookCache('test-file', 'test-sub');
     ...
   });

-  it('should include version in the cache path when falling back to cwd', () => {
-    vi.mocked(pkg.cache).mockReturnValue(undefined);
-    const cwd = process.cwd();
-    ...
-  });
+  describe('when empathic cache is unavailable', () => {
+    beforeEach(() => {
+      vi.mocked(pkg.cache).mockReturnValue(undefined);
+    });
+
+    it('should include version in the cache path when falling back to cwd', () => {
+      const cwd = process.cwd();
+      ...
+    });
+  });

-  it('should use "unknown" as version when storybook version is not available', () => {
-    const mockCacheDir = '/mock/node_modules/.cache/storybook';
-    vi.mocked(pkg.cache).mockReturnValue(mockCacheDir);
-    vi.mocked(versions).storybook = '' as any;
+  describe('when storybook version is not available', () => {
+    beforeEach(() => {
+      vi.mocked(versions).storybook = '' as any;
+    });
+
+    it('should use "unknown" as version when storybook version is not available', () => {
       const result = resolvePathInStorybookCache('test-file', 'test-sub');
       ...
-    vi.mocked(versions).storybook = '10.3.0-alpha.1';
-  });
+    });
+  });
 });
🤖 Prompt for AI Agents
In `@code/core/src/common/utils/resolve-path-in-sb-cache.test.ts` around lines 20
- 105, The tests for resolvePathInStorybookCache currently set mock return
values inline; move all vi.mocked(pkg.cache).mockReturnValue(...) calls and any
temporary assignments to versions.storybook into beforeEach hooks (use nested
describe blocks for scenarios: e.g., "when cache available", "when cache
unavailable", "when version missing") so test bodies only assert behavior;
ensure each beforeEach sets the appropriate pkg.cache return and/or
versions.storybook value and use vi.clearAllMocks() (and restore original
versions.storybook value in an afterEach if modified) to avoid cross-test
pollution.

@valentinpalkovic valentinpalkovic added the patch:yes Bugfix & documentation PR that need to be picked to main branch label Feb 3, 2026
@valentinpalkovic valentinpalkovic merged commit 5176cb7 into next Feb 3, 2026
125 of 128 checks passed
@valentinpalkovic valentinpalkovic deleted the copilot/fix-cache-invalidation-on-upgrade branch February 3, 2026 10:28
valentinpalkovic added a commit that referenced this pull request Feb 4, 2026
…tion-on-upgrade

Core: Invalidate cache on Storybook version upgrade
(cherry picked from commit 5176cb7)
@github-actions github-actions Bot added the patch:done Patch/release PRs already cherry-picked to main/release branch label Feb 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug ci:normal patch:done Patch/release PRs already cherry-picked to main/release branch patch:yes Bugfix & documentation PR that need to be picked to main branch

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Bug]: Automatically invalidate or version-key the .cache directory on Storybook upgrades

2 participants