Skip to content

Conversation

@valentinpalkovic
Copy link
Contributor

@valentinpalkovic valentinpalkovic commented Jan 29, 2026

Closes #33695

What I did

This PR refactors the @storybook/addon-vitest postinstall script to be more idempotent and user-friendly. Instead of failing when it encounters existing configurations or setup files, the script now intelligently detects existing setups and avoids redundant modifications.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

Caution

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This pull request has been released as version 0.0.0-pr-33712-sha-2b12f5bb. Try it out in a new sandbox by running npx storybook@0.0.0-pr-33712-sha-2b12f5bb sandbox or in an existing project with npx storybook@0.0.0-pr-33712-sha-2b12f5bb upgrade.

More information
Published version 0.0.0-pr-33712-sha-2b12f5bb
Triggered by @valentinpalkovic
Repository storybookjs/storybook
Branch valentin/addon-vitest-skip-existing-configs
Commit 2b12f5bb
Datetime Thu Jan 29 15:00:48 UTC 2026 (1769698848)
Workflow run 21483136852

To request a new release of this pull request, mention the @storybookjs/core team.

core team members can create a new canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=33712

Summary by CodeRabbit

  • New Features

    • Vitest addon postinstall now detects existing configurations and reuses setup files instead of failing.
  • Tests

    • Added comprehensive tests for Vitest configuration detection logic.

✏️ Tip: You can customize this high-level summary in your review settings.

@valentinpalkovic valentinpalkovic self-assigned this Jan 29, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Jan 29, 2026

Fails
🚫 The "#### Manual testing" section must be filled in. Please describe how to test the changes you've made, step by step, so that reviewers can confirm your PR works as intended.

Generated by 🚫 dangerJS against 2b12f5b

@nx-cloud
Copy link

nx-cloud bot commented Jan 29, 2026

View your CI Pipeline Execution ↗ for commit 2b12f5b

Command Status Duration Result
nx run-many -t compile,check,knip,test,pretty-d... ❌ Failed 10m 30s View ↗

☁️ Nx Cloud last updated this comment at 2026-01-29 15:10:04 UTC

@valentinpalkovic valentinpalkovic force-pushed the valentin/addon-vitest-skip-existing-configs branch from a03ec3f to 2b12f5b Compare January 29, 2026 14:57
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

📝 Walkthrough

Walkthrough

This PR adds AST-based configuration detection to the Vitest addon's postinstall flow. The new isConfigAlreadySetup helper function determines whether Storybook test plugins are already configured in a Vitest config file. The postinstall logic now reuses existing setup files and skips updates when prior configuration is detected. The associated error class is removed.

Changes

Cohort / File(s) Summary
Test Coverage
code/addons/vitest/src/postinstall.test.ts
New test suite validating isConfigAlreadySetup behavior when storybookTest addon is present or absent in Vitest config.
Core Implementation
code/addons/vitest/src/postinstall.ts
Introduces isConfigAlreadySetup function using Babel AST traversal to detect existing plugin configuration; updates postinstall logic to skip setup when config already exists, reuse existing setup files, and add conditional checks for workspace/root configs.
Error Handling
code/core/src/server-errors.ts
Removes exported AddonVitestPostinstallExistingSetupFileError class, eliminating the error previously thrown for existing setup files.

Sequence Diagram

sequenceDiagram
    actor User
    participant PostInstall as Postinstall Handler
    participant ConfigDetector as isConfigAlreadySetup
    participant AST as AST Parser<br/>(Traverse)
    participant VitestConfig as Vitest Config
    participant SetupFile as Setup File

    User->>PostInstall: Run postinstall
    PostInstall->>PostInstall: Check for vitest.workspace
    alt Workspace exists
        PostInstall->>ConfigDetector: Check if already configured
        ConfigDetector->>VitestConfig: Read config content
        ConfigDetector->>AST: Parse & traverse AST
        AST->>AST: Find storybookTest plugin
        AST-->>ConfigDetector: Plugin found or not found
        ConfigDetector-->>PostInstall: isConfigAlreadySetup result
        alt Already configured
            PostInstall->>PostInstall: Log success & exit
        else Not configured
            PostInstall->>SetupFile: Create setup file
            SetupFile-->>PostInstall: File created
        end
    else Root config exists
        PostInstall->>ConfigDetector: Check if already configured
        ConfigDetector->>VitestConfig: Read config content
        ConfigDetector->>AST: Parse & traverse AST
        AST-->>ConfigDetector: Plugin detection result
        ConfigDetector-->>PostInstall: isConfigAlreadySetup result
        alt Already configured
            PostInstall->>PostInstall: Skip update & log
        else Not configured
            PostInstall->>VitestConfig: Update config with plugin
            PostInstall->>SetupFile: Create or reuse setup file
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

✨ Finishing touches
  • 📝 Generate docstrings

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
code/addons/vitest/src/postinstall.ts (1)

241-287: Avoid returning early so downstream setup still runs.
The early return skips a11y automigration and the final success/error messaging for workspace users, while the root-config path continues. Consider logging and then continuing (or guarding only the update block).

🛠️ Suggested fix
-    if (alreadyConfigured) {
-      logger.step(
-        CLI_COLORS.success('Vitest for Storybook is already properly configured. Skipping setup.')
-      );
-      return;
-    }
-
-    const workspaceTemplate = await loadTemplate('vitest.workspace.template.ts', {
-      EXTENDS_WORKSPACE: viteConfigFile
-        ? relative(dirname(vitestWorkspaceFile), viteConfigFile)
-        : '',
-      CONFIG_DIR: options.configDir,
-      SETUP_FILE: relative(dirname(vitestWorkspaceFile), existingSetupFile ?? vitestSetupFile),
-    }).then((t) => t.replace(`\n  'ROOT_CONFIG',`, '').replace(/\s+extends: '',/, ''));
-    const source = babelParse(workspaceTemplate);
-    const target = babelParse(workspaceFileContent);
-
-    const updated = updateWorkspaceFile(source, target);
-    if (updated) {
-      logger.step(`Updating your Vitest workspace file...`);
-
-      logger.log(`${vitestWorkspaceFile}`);
-
-      const formattedContent = await formatFileContent(vitestWorkspaceFile, generate(target).code);
-      await writeFile(vitestWorkspaceFile, formattedContent);
-    } else {
-      logger.error(
-        dedent`
-          Could not update existing Vitest workspace file:
-          ${vitestWorkspaceFile}
-
-          I was able to configure most of the addon but could not safely extend
-          your existing workspace file automatically, you must do it yourself.
-
-          Please refer to the documentation to complete the setup manually:
-          https://storybook.js.org/docs/next/${DOCUMENTATION_LINK}#manual-setup-advanced
-        `
-      );
-      errors.push(
-        new AddonVitestPostinstallWorkspaceUpdateError({ filePath: vitestWorkspaceFile })
-      );
-    }
+    if (alreadyConfigured) {
+      logger.step(
+        CLI_COLORS.success('Vitest for Storybook is already properly configured. Skipping setup.')
+      );
+    } else {
+      const workspaceTemplate = await loadTemplate('vitest.workspace.template.ts', {
+        EXTENDS_WORKSPACE: viteConfigFile
+          ? relative(dirname(vitestWorkspaceFile), viteConfigFile)
+          : '',
+        CONFIG_DIR: options.configDir,
+        SETUP_FILE: relative(dirname(vitestWorkspaceFile), existingSetupFile ?? vitestSetupFile),
+      }).then((t) => t.replace(`\n  'ROOT_CONFIG',`, '').replace(/\s+extends: '',/, ''));
+      const source = babelParse(workspaceTemplate);
+      const target = babelParse(workspaceFileContent);
+
+      const updated = updateWorkspaceFile(source, target);
+      if (updated) {
+        logger.step(`Updating your Vitest workspace file...`);
+
+        logger.log(`${vitestWorkspaceFile}`);
+
+        const formattedContent = await formatFileContent(
+          vitestWorkspaceFile,
+          generate(target).code
+        );
+        await writeFile(vitestWorkspaceFile, formattedContent);
+      } else {
+        logger.error(
+          dedent`
+            Could not update existing Vitest workspace file:
+            ${vitestWorkspaceFile}
+
+            I was able to configure most of the addon but could not safely extend
+            your existing workspace file automatically, you must do it yourself.
+
+            Please refer to the documentation to complete the setup manually:
+            https://storybook.js.org/docs/next/${DOCUMENTATION_LINK}#manual-setup-advanced
+          `
+        );
+        errors.push(
+          new AddonVitestPostinstallWorkspaceUpdateError({ filePath: vitestWorkspaceFile })
+        );
+      }
+    }
🤖 Fix all issues with AI agents
In `@code/addons/vitest/src/postinstall.ts`:
- Around line 427-473: The current isConfigAlreadySetup only checks for a plugin
reference and returns true even if setupFiles is missing; update
isConfigAlreadySetup to require both the plugin reference and that the config
object defines a non-empty setupFiles entry. Keep the existing AST traversal
that collects pluginIdentifiers (ImportDeclaration) and detects plugin usage
(CallExpression), then add a traversal that finds the exported config object
(e.g., ExportDefaultDeclaration with an ObjectExpression and/or
AssignmentExpression to module.exports) and looks for a setupFiles property
whose value is an ArrayExpression with at least one element or a non-empty
string; only return true when pluginReferenced is true AND setupFiles is
present/valid. Ensure you update references inside isConfigAlreadySetup
(pluginIdentifiers, pluginReferenced, and the AST traversals) rather than
changing callers.
🧹 Nitpick comments (1)
code/addons/vitest/src/postinstall.test.ts (1)

31-49: Add a negative case for missing setupFiles.
Once isConfigAlreadySetup enforces setupFiles presence, a test where the plugin exists but setupFiles is missing will lock in the smart-skip behavior.

✅ Proposed test
   it('returns false when storybookTest plugin is not used', () => {
@@
     expect(isConfigAlreadySetup('/project/vitest.config.ts', config, setupPath)).toBe(false);
   });
+
+  it('returns false when setupFiles is missing even if the plugin is present', () => {
+    const config = `
+      import { defineConfig } from 'vitest/config';
+      import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
+
+      export default defineConfig({
+        test: {
+          projects: [
+            {
+              extends: true,
+              plugins: [storybookTest({ configDir: '.storybook' })],
+            },
+          ],
+        },
+      });
+    `;
+
+    expect(isConfigAlreadySetup('/project/vitest.config.ts', config, setupPath)).toBe(false);
+  });
 });
As per coding guidelines: Cover all branches, conditions, edge cases, error paths, and different input variations in unit tests.

Comment on lines +427 to +473
function isStorybookTestPluginSource(value: string) {
return value === STORYBOOK_TEST_PLUGIN_SOURCE;
}

export function isConfigAlreadySetup(_configPath: string, configContent: string) {
let ast: ReturnType<typeof babelParse>;
try {
ast = babelParse(configContent);
} catch (e) {
return false;
}

const pluginIdentifiers = new Set<string>();

traverse(ast, {
ImportDeclaration(path) {
const source = path.node.source.value;
if (typeof source === 'string' && isStorybookTestPluginSource(source)) {
path.node.specifiers.forEach((specifier) => {
if ('local' in specifier && specifier.local?.name) {
pluginIdentifiers.add(specifier.local.name);
}
});
}
},
});

let pluginReferenced = false;

traverse(ast, {
CallExpression(path) {
if (pluginReferenced) {
path.stop();
return;
}
const callee = path.node.callee;
if (
callee.type === 'Identifier' &&
(pluginIdentifiers.has(callee.name) || callee.name === 'storybookTest')
) {
pluginReferenced = true;
path.stop();
}
},
});

return pluginReferenced;
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

isConfigAlreadySetup should also verify setupFiles.
Right now any config that references the plugin is treated as fully configured, so setup can be skipped even when setupFiles is missing. That contradicts the “smart skip” requirement and leaves users partially configured.

🛠️ Suggested fix (plugin + setupFiles required)
-export function isConfigAlreadySetup(_configPath: string, configContent: string) {
+export function isConfigAlreadySetup(
+  configPath: string,
+  configContent: string,
+  setupPath?: string
+) {
   let ast: ReturnType<typeof babelParse>;
   try {
     ast = babelParse(configContent);
   } catch (e) {
     return false;
   }
 
   const pluginIdentifiers = new Set<string>();
@@
-  let pluginReferenced = false;
+  let pluginReferenced = false;
+  let setupFileReferenced = !setupPath;
+  const expectedSetupFiles = setupPath ? new Set<string>([setupPath]) : null;
+  if (setupPath) {
+    const relativeSetup = relative(dirname(configPath), setupPath);
+    expectedSetupFiles?.add(relativeSetup);
+    expectedSetupFiles?.add(`./${relativeSetup}`);
+  }
 
   traverse(ast, {
+    ObjectProperty(path) {
+      if (!expectedSetupFiles) return;
+      const key = path.node.key;
+      const keyName =
+        key.type === 'Identifier' ? key.name : key.type === 'StringLiteral' ? key.value : null;
+      if (keyName !== 'setupFiles') return;
+      const value = path.node.value;
+      if (value.type !== 'ArrayExpression') return;
+      for (const element of value.elements) {
+        if (element?.type === 'StringLiteral' && expectedSetupFiles.has(element.value)) {
+          setupFileReferenced = true;
+          if (pluginReferenced) path.stop();
+          return;
+        }
+      }
+    },
     CallExpression(path) {
-      if (pluginReferenced) {
+      if (pluginReferenced && setupFileReferenced) {
         path.stop();
         return;
       }
@@
-        pluginReferenced = true;
-        path.stop();
+        pluginReferenced = true;
+        if (setupFileReferenced) path.stop();
       }
     },
   });
 
-  return pluginReferenced;
+  return pluginReferenced && setupFileReferenced;
 }
🔧 Call-site updates (outside this range)
-    const alreadyConfigured = isConfigAlreadySetup(vitestWorkspaceFile, workspaceFileContent);
+    const alreadyConfigured = isConfigAlreadySetup(
+      vitestWorkspaceFile,
+      workspaceFileContent,
+      existingSetupFile ?? vitestSetupFile
+    );
-    const alreadyConfigured = isConfigAlreadySetup(rootConfig, configFile);
+    const alreadyConfigured = isConfigAlreadySetup(
+      rootConfig,
+      configFile,
+      existingSetupFile ?? vitestSetupFile
+    );
🤖 Prompt for AI Agents
In `@code/addons/vitest/src/postinstall.ts` around lines 427 - 473, The current
isConfigAlreadySetup only checks for a plugin reference and returns true even if
setupFiles is missing; update isConfigAlreadySetup to require both the plugin
reference and that the config object defines a non-empty setupFiles entry. Keep
the existing AST traversal that collects pluginIdentifiers (ImportDeclaration)
and detects plugin usage (CallExpression), then add a traversal that finds the
exported config object (e.g., ExportDefaultDeclaration with an ObjectExpression
and/or AssignmentExpression to module.exports) and looks for a setupFiles
property whose value is an ArrayExpression with at least one element or a
non-empty string; only return true when pluginReferenced is true AND setupFiles
is present/valid. Ensure you update references inside isConfigAlreadySetup
(pluginIdentifiers, pluginReferenced, and the AST traversals) rather than
changing callers.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Addon-Vitest: Add support for existing .storybook/vitest.setup.ts files during postinstall step

2 participants