Skip to content

Comments

Revamp Vite dependency optimization: use preview annotations as entry points#33875

Open
Copilot wants to merge 7 commits intonextfrom
copilot/revamp-vite-vitest-optimization-again
Open

Revamp Vite dependency optimization: use preview annotations as entry points#33875
Copilot wants to merge 7 commits intonextfrom
copilot/revamp-vite-vitest-optimization-again

Conversation

Copy link
Contributor

Copilot AI commented Feb 19, 2026

Vite's dep optimizer was only scanning story files as entry points, missing preview annotation files (user's preview.ts, addon/framework/renderer previews). This caused runtime CJS dependency discovery, triggering hard reloads in Storybook and flaky test reruns in Vitest.

The real fix is to give Vite the complete set of entry points upfront so it can crawl all transitive dependencies before serving — no hard reloads, no Vitest instability.

Changes

  • storybook-optimize-deps-plugin.ts: Fetches all previewAnnotations from presets (in parallel) and adds the user's preview file + all addon/framework/renderer annotation files to optimizeDeps.entries. Vite crawls these to auto-discover CJS deps.
  • constants.ts (deleted): Removes the ~100-entry INCLUDE_CANDIDATES hardcoded list — no longer needed.
  • optimizeDeps.ts (deleted): Removes the outdated getOptimizeDeps() that used resolveConfig + asyncFilter to filter INCLUDE_CANDIDATES.
  • vite-server.ts: Removes getOptimizeDeps call and the redundant include-merging logic.
  • builder-vite/src/preset.ts: Adds optimizeViteDeps export for storybook/internal/preview/runtime, which is only referenced in the virtual iframe entry module and cannot be discovered by Vite's static dep scanner.
  • renderers/react/src/preset.ts: Adds optimizeViteDeps export for react-dom/test-utils, which is dynamically imported and not statically analyzable by Vite.
  • storybook-optimize-deps-plugin.test.ts (new): Tests covering entries deduplication, preview annotation entries, string/array config merging, and preset include passthrough.
  • codegen-modern-iframe-script.test.ts: Adds a systematic regression test that generates all virtual module code, extracts every non-virtual package import, and asserts each one is either in optimizeViteDeps or documented as discovered via entry crawling. This will automatically catch future cases where a package is added to a virtual module code generator without updating optimizeViteDeps.
  • renderers/react/src/preset.test.ts (new): Focused regression test asserting react-dom/test-utils is in optimizeViteDeps, with a comment explaining it is only accessible via a dynamic await import() in act-compat.ts.

The optimizeViteDeps preset mechanism is preserved for frameworks that need explicit includes (nextjs-vite, sveltekit, addon-docs). The fix applies to both @storybook/builder-vite and @storybook/addon-vitest since storybookOptimizeDepsPlugin is part of the shared viteCorePlugins.

Why explicit include is still needed for some packages

Most storybook/internal/* packages have a code export condition resolving to TypeScript source, so Vite treats them as ESM and discovers them naturally via entry-point crawling. Two packages are exceptions:

  • storybook/internal/preview/runtime: Only imported inside a string template in codegen-modern-iframe-script.ts (the virtual iframe entry). Vite's dep scanner never sees virtual module contents and discovers this dependency only at request time.
  • react-dom/test-utils: Dynamically imported via await import() in act-compat.ts, which Vite's static analyzer does not pre-bundle.
Original prompt

This section details on the original issue you should resolve

<issue_title>[Bug]: Revamp Vite and Vitest dependency optimization</issue_title>
<issue_description>### Describe the bug

Problem

We currently have specific code which handles dependency optimization in the Vite builder as can be seen here. That code is outdated and might not be necessary.

When the Vite dependency optimization kicks in, it needs to hard reload the browser. This causes two things:

  1. In Storybook itself, you notice how things take much longer to load and your story to render.
  2. In Vitest, the tests become flaky and can fail. Additionally can cause the coverage to be incorrect because either certain code paths got executed but the data got lost given the hard reload, or code that was supposed to execute didn’t, because of the same reason.

Storybook already adds stories as entries to Vite's config, which makes it scan most files to do dependency optimization automatically (great). However, currently Storybook does not add other necessary files as entries, such as the preview annotation files. For instance, if a .storybook/preview.tsx file has imports like:

import { withContext } from 'recompose';
import seedrandom from 'seedrandom';

Then such dependencies might need to be optimized (if they're CJS for instance) and therefore this will trigger the warnings. This would not have been the case had Storybook added the preview file as an entry to Vite. The same issue also occurs from packages, such as renderers, frameworks or addons. Such packages can use a concept of a preview annotation or preset, which adds annotations to Storybook (such as decorators, loaders, etc). These annotations can also end up making imports of dependencies that would cause the same problem.

This is a quite critical change that requires a lot of checking, but worth investigating. This solution will close many issues, including #33067, #32049, #31119 as well as provide a much better experience for end users.

The workaround

When this behavior occurs, users see, both in Storybook's CLI and in Vitest's CLI some warnings like this:

11:47:15 [vite] (client)  new dependencies optimized: lodash/capitalize, lodash/debounce
11:47:15 [vite] (client)  optimized dependencies changed. reloading

[vitest] Vite unexpectedly reloaded a test. This may cause tests to fail, lead to flaky behaviour or duplicated test runs.
For a stable experience, please add mentioned dependencies to your config's `optimizeDeps.include` field manually.

So to solve this issue, users need to add a viteFinal config in the Storybook main file and specify their own dependencies in its optimizeDeps.include field:

import type { StorybookConfig } from '@storybook/nextjs-vite';

const config: StorybookConfig = {
  viteFinal: (config, { configType }) => {
    return {
      ...config,
      // We optimize deps to avoid flake in tests and slow load times in Storybook. If you see more warnings, add more deps to the list.
      optimizeDeps: {
        ...config.optimizeDeps,
        include: [
          ...(config.optimizeDeps?.include ?? []),
          'cookie',
          'lodash/capitalize',
          'lodash/debounce',
          'lodash/values',
          'lodash/keyBy',
          'lodash/groupBy',
        ],
      },
    };
  },
}
export default config;

Potential solution

Vite automatically handles dependency optimization based on the entrypoints passed into the entries, so theoretically we don't need to handle dependency optimization ourselves, as long as we specify the correct entries.

Vitest has similar challenges and solves it by doing this, it basically adds all test files + setup files (plus some internal utilities) as entries, and Vite will figure out what needs to be optimized. In Storybook, the scenario is similar, we need to list the stories files as entries, as well as the preview entries (user's preview file, plus addon/renderer/frameworks preview annotations which come from presets or preview files).
</issue_description>

<agent_instructions>There was a recent change in the repo to centralize core vite plugins and that is reused between storybook proper and the Vitest integration. The solution you come up with should work for them both then.

Also, this issue is not necessarily a problem for CSF4 (known as csf factories) users, it's mostly a problem for CSF3 and below.

The main goal of this task should be to improve and simplify the dependency optimization logic, which will make the INCLUDE_CANDIDATES in code/...


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced Vite dependency optimization with support for preview annotation files to improve pre-bundling.
  • Chores

    • Simplified experimental test syntax configuration across project templates.
    • Removed legacy test story examples.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 19, 2026

Fails
🚫 PR title must be in the format of "Area: Summary", With both Area and Summary starting with a capital letter Good examples: - "Docs: Describe Canvas Doc Block" - "Svelte: Support Svelte v4" Bad examples: - "add new api docs" - "fix: Svelte 4 support" - "Vue: improve docs"
🚫 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 c92f3f4

@nx-cloud
Copy link

nx-cloud bot commented Feb 19, 2026

View your CI Pipeline Execution ↗ for commit c92f3f4

Command Status Duration Result
nx run-many -t compile,check,knip,test,pretty-d... ⛔ Cancelled 3h 35m 30s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-20 16:30:26 UTC

Copilot AI changed the title [WIP] Revamp Vite and Vitest dependency optimization Revamp Vite dependency optimization: use preview annotations as entry points Feb 19, 2026
Copilot AI requested a review from yannbf February 19, 2026 15:42
@storybook-bot
Copy link
Contributor

Failed to publish canary version of this pull request, triggered by @yannbf. See the failed workflow run at: https://github.com/storybookjs/storybook/actions/runs/22188806376

@yannbf
Copy link
Member

yannbf commented Feb 19, 2026

@copilot this mostly worked but there are parts which you didn't catch. When running Storybook and watching its output you can see an optimization message for storybook/internal/preview/runtime and react-dom/test-utils, because these are either entries in virtual files or transformed entries during vite transform. See the commit which fixes those specifically at d00906c.

How can we avoid missing these, and are there perhaps more entries which are required here?

@yannbf yannbf changed the title Revamp Vite dependency optimization: use preview annotations as entry points Vite: Revamp Vite dependency optimization Feb 19, 2026
@yannbf yannbf changed the title Vite: Revamp Vite dependency optimization Vite: Revamp dependency optimization mechanism Feb 19, 2026
Copy link
Contributor Author

Copilot AI commented Feb 19, 2026

@copilot this mostly worked but there are parts which you didn't catch. When running Storybook and watching its output you can see an optimization message for storybook/internal/preview/runtime and ...

These two packages were missed because they appear in contexts Vite's dep scanner can't statically analyze:

  1. storybook/internal/preview/runtime — Only imported inside the string template in codegen-modern-iframe-script.ts that generates the virtual iframe entry module. Vite's dep scanner never sees virtual module contents, so it discovers this dependency only at request time.

  2. react-dom/test-utils — Dynamically imported via await import('react-dom/test-utils') in act-compat.ts, which Vite's static analyzer doesn't pre-bundle.

The systematic rule to catch these: check virtual module code generators (codegen-*.ts) for string-template imports, and check for await import() of CJS/legacy packages in browser-side code.

After auditing all virtual module generators and dynamic imports, these two are the only cases that need explicit include. Other storybook/internal/* packages (like storybook/internal/channels) have a code export condition pointing to their TypeScript source, so Vite resolves them as ESM and never needs to pre-bundle them. storybook/internal/preview/runtime is the exception — its export only points to ./dist/preview/runtime.js (no source condition), so Vite sees a compiled bundle that needs pre-bundling.

The fixes from commit d00906c are already on the branch and cover all identified cases.

Copilot AI changed the title Vite: Revamp dependency optimization mechanism Revamp Vite dependency optimization: use preview annotations as entry points Feb 19, 2026
@yannbf
Copy link
Member

yannbf commented Feb 19, 2026

Great @copilot but how do we make sure we don't regress? Is there a way to automatically catch cases which are not covered? Can you write a test that would have caught this?

Copy link
Contributor Author

Copilot AI commented Feb 19, 2026

Great @copilot but how do we make sure we don't regress? Is there a way to automatically catch cases which are not covered? Can you write a test that would have caught this?

Added two regression tests in commit 8312c5a:

1. Systematic test in codegen-modern-iframe-script.test.ts:

Generates all virtual module code (generateModernIframeScriptCodeFromPreviews + generateAddonSetupCode), extracts every bare package import specifier, then asserts each one is either in optimizeViteDeps OR in an explicit discoveredViaEntries set (packages that also appear in real source preview files and will be found by Vite's entry crawler). If someone adds a new package import to a virtual module code generator without adding it to optimizeViteDeps, the test fails with a message pointing to exactly what needs to be done.

2. Focused regression test in renderers/react/src/preset.test.ts:

Asserts react-dom/test-utils is in optimizeViteDeps. The test comment explains it's needed because it's only await import()'d dynamically in act-compat.ts — Vite's static analyzer doesn't pre-bundle dynamic imports.

This covers the two identified patterns that Vite cannot discover statically: imports in virtual module string templates, and await import() calls in browser code.

@valentinpalkovic valentinpalkovic added ci:daily Run the CI jobs that normally run in the daily job. and removed ci:normal labels Feb 20, 2026
@storybook-app-bot
Copy link

storybook-app-bot bot commented Feb 20, 2026

Package Benchmarks

Commit: c92f3f4, ran on 20 February 2026 at 13:04:57 UTC

No significant changes detected, all good. 👏

Copilot AI and others added 5 commits February 20, 2026 12:35
@valentinpalkovic valentinpalkovic force-pushed the copilot/revamp-vite-vitest-optimization-again branch from 9e00f92 to e96bd64 Compare February 20, 2026 11:35
@valentinpalkovic valentinpalkovic marked this pull request as ready for review February 20, 2026 11:51
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

This pull request refactors how Vite dependency optimization is configured across Storybook. The approach shifts from a dynamic getOptimizeDeps function that computes dependencies at runtime to a preset-based pattern where optimizeViteDeps constants are defined in multiple preset files and collected by the build plugin. The changes include removing the deleted optimizeDeps.ts file, updating the Storybook optimize-deps plugin to handle preview annotations, removing experimental test configuration flags, and eliminating several test story files.

Changes

Cohort / File(s) Summary
Vite Optimization Dependencies Refactor
code/builders/builder-vite/src/preset.ts, code/addons/docs/src/preset.ts, code/renderers/preact/src/preset.ts, code/renderers/react/src/preset.ts
Added optimizeViteDeps constant exports across multiple preset files containing module specifiers for Vite pre-bundling.
Removed Optimization Function
code/builders/builder-vite/src/optimizeDeps.ts, code/builders/builder-vite/src/constants.ts, code/builders/builder-vite/src/vite-server.ts
Deleted getOptimizeDeps function and INCLUDE_CANDIDATES constant; removed optimization logic from vite-server initialization.
Plugin Enhancement
code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts
Extended plugin to collect and process preview annotation files from presets and add them to Vite's optimizeDeps entries.
Plugin Test Coverage
code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts
Added comprehensive test suite validating story imports, preview annotations, deduplication, and merging of optimize deps entries.
Import Validation
code/builders/builder-vite/src/codegen-modern-iframe-script.test.ts, code/renderers/react/src/preset.test.ts
Added tests verifying that virtual module imports and preset exports (e.g., react-dom/test-utils) are properly included in optimizeViteDeps.
Test Story Files Removed
code/core/src/component-testing/components/test-fn.stories.tsx, code/renderers/react/template/stories/test-fn.stories.tsx, code/renderers/react/template/stories/csf4.stories.tsx, code/renderers/react/template/stories/csf4.mdx
Removed CSF4 and test-fn story definitions and associated documentation files.
Sandbox Configuration Updates
code/lib/cli-storybook/src/sandbox-templates.ts
Removed useCsfFactory flag and experimentalTestSyntax feature toggles across multiple template definitions; simplified template configurations.
Build Script Adjustments
scripts/tasks/sandbox-parts.ts
Removed optimizeDeps.force setting and unified Vitest setup handling; added server filesystem allowlist for linked mode.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)

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

Copy link
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

🧹 Nitpick comments (2)
code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts (2)

102-116: Consider moving mock implementations to a beforeEach block.

The mockReturnValueOnce calls on lines 103 and 111 are inline mock implementations within test cases. Per coding guidelines, mock behaviors should be implemented in beforeEach blocks for consistency and to separate setup from assertion logic.

Alternative pattern

One approach is to group tests that need specific mock behaviors:

describe('when user preview config exists', () => {
  beforeEach(() => {
    loadPreviewOrConfigFileMock.mockReturnValue('/project/.storybook/preview.ts' as any);
  });

  it('adds the user preview config file as an entry', async () => {
    const plugin = storybookOptimizeDepsPlugin(makeOptions());
    const result = await (plugin.config as Function)({}, { command: 'serve' });
    expect(result.optimizeDeps.entries).toContain('/project/.storybook/preview.ts');
  });
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts`
around lines 102 - 116, Move the inline mockReturnValueOnce calls into
beforeEach setup blocks to separate setup from assertions: for the tests around
storybookOptimizeDepsPlugin/config, create a describe block for the "user
preview exists" case and a describe block for the "no user preview" case, and in
each describe's beforeEach call loadPreviewOrConfigFileMock.mockReturnValue(...)
(using the concrete '/project/.storybook/preview.ts' value for the first and
undefined for the second) before invoking
storybookOptimizeDepsPlugin(makeOptions()) in the its; this keeps tests focused
and ensures loadPreviewOrConfigFileMock is consistently configured before each
test.

7-12: Add spy: true option to vi.mock() call.

As per coding guidelines, all vi.mock() calls should use the spy: true option for consistent mocking patterns.

Proposed fix
-vi.mock('storybook/internal/common', () => ({
-  loadPreviewOrConfigFile: vi.fn(() => undefined),
-}));
+vi.mock('storybook/internal/common', { spy: true });

Then set up the default mock behavior in a beforeEach block:

beforeEach(() => {
  vi.mocked(loadPreviewOrConfigFile).mockReturnValue(undefined);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts`
around lines 7 - 12, Update the vi.mock call to include the spy: true option
(vi.mock('storybook/internal/common', { spy: true }, ...)) and remove the inline
mock-return in the factory; instead, in a beforeEach block call
vi.mocked(loadPreviewOrConfigFile).mockReturnValue(undefined) to set the default
behavior; reference the existing loadPreviewOrConfigFile import and any
loadPreviewOrConfigFileMock usage to ensure tests use the spied mock.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/tasks/sandbox-parts.ts`:
- Around line 533-539: The current replacement for linked mode uses a separate
fileContent.replace that re-matches the same plugins block and inadvertently
removes resolve.preserveSymlinks; update the logic that handles options.link
(the fileContent.replace call that matches /(plugins\s*:\s*\[[^\]]*\],?)/) to
perform a single replacement that inserts both the server.fs.allow block and
preserves/ensures resolve.preserveSymlinks inside the resolve section (i.e.,
include resolve: { preserveSymlinks: true } alongside the server: { fs: { allow:
['../../..'] } } in the replacement) so the earlier preserveSymlinks setting is
not overwritten when options.link is true.

---

Nitpick comments:
In
`@code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts`:
- Around line 102-116: Move the inline mockReturnValueOnce calls into beforeEach
setup blocks to separate setup from assertions: for the tests around
storybookOptimizeDepsPlugin/config, create a describe block for the "user
preview exists" case and a describe block for the "no user preview" case, and in
each describe's beforeEach call loadPreviewOrConfigFileMock.mockReturnValue(...)
(using the concrete '/project/.storybook/preview.ts' value for the first and
undefined for the second) before invoking
storybookOptimizeDepsPlugin(makeOptions()) in the its; this keeps tests focused
and ensures loadPreviewOrConfigFileMock is consistently configured before each
test.
- Around line 7-12: Update the vi.mock call to include the spy: true option
(vi.mock('storybook/internal/common', { spy: true }, ...)) and remove the inline
mock-return in the factory; instead, in a beforeEach block call
vi.mocked(loadPreviewOrConfigFile).mockReturnValue(undefined) to set the default
behavior; reference the existing loadPreviewOrConfigFile import and any
loadPreviewOrConfigFileMock usage to ensure tests use the spied mock.

Comment on lines +533 to +539
// In linked mode, add server.fs.allow to allow Vite to serve files from the monorepo root
if (options.link) {
fileContent = fileContent.replace(/(plugins\s*:\s*\[[^\]]*\],?)/, (match) => {
// Insert server.fs.allow after plugins (alongside resolve)
return `${match}\n server: {\n fs: {\n allow: ['../../..']\n }\n },`;
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix linked-mode replace: it drops preserveSymlinks.

On Line 533 the second replace re-matches the original plugins block and overwrites the earlier insertion, so resolve.preserveSymlinks is removed whenever options.link is true. This can reintroduce symlink resolution issues in linked sandboxes.

🔧 Suggested fix (single replace that inserts both blocks)
-  fileContent = fileContent.replace(/(plugins\s*:\s*\[[^\]]*\],?)/, (match) => {
-    // Insert resolve after plugins
-    return `${match}\n  resolve: {\n    preserveSymlinks: true\n  },`;
-  });
-
-  // In linked mode, add server.fs.allow to allow Vite to serve files from the monorepo root
-  if (options.link) {
-    fileContent = fileContent.replace(/(plugins\s*:\s*\[[^\]]*\],?)/, (match) => {
-      // Insert server.fs.allow after plugins (alongside resolve)
-      return `${match}\n  server: {\n    fs: {\n      allow: ['../../..']\n    }\n  },`;
-    });
-  }
+  fileContent = fileContent.replace(/(plugins\s*:\s*\[[^\]]*\],?)/, (match) => {
+    const resolveBlock = `  resolve: {\n    preserveSymlinks: true\n  },`;
+    const serverBlock = options.link
+      ? `\n  server: {\n    fs: {\n      allow: ['../../..']\n    }\n  },`
+      : '';
+    return `${match}\n${resolveBlock}${serverBlock}`;
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/tasks/sandbox-parts.ts` around lines 533 - 539, The current
replacement for linked mode uses a separate fileContent.replace that re-matches
the same plugins block and inadvertently removes resolve.preserveSymlinks;
update the logic that handles options.link (the fileContent.replace call that
matches /(plugins\s*:\s*\[[^\]]*\],?)/) to perform a single replacement that
inserts both the server.fs.allow block and preserves/ensures
resolve.preserveSymlinks inside the resolve section (i.e., include resolve: {
preserveSymlinks: true } alongside the server: { fs: { allow: ['../../..'] } }
in the replacement) so the earlier preserveSymlinks setting is not overwritten
when options.link is true.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug ci:daily Run the CI jobs that normally run in the daily job.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Revamp Vite and Vitest dependency optimization

4 participants