Skip to content

fix: resolve #34468 storybook eslint defineConfig issue#34487

Open
justismailmemon wants to merge 2 commits intostorybookjs:nextfrom
justismailmemon:fix/34468-storybook-eslint
Open

fix: resolve #34468 storybook eslint defineConfig issue#34487
justismailmemon wants to merge 2 commits intostorybookjs:nextfrom
justismailmemon:fix/34468-storybook-eslint

Conversation

@justismailmemon
Copy link
Copy Markdown

@justismailmemon justismailmemon commented Apr 7, 2026

Summary

Fixes #34468

This resolves the defineConfig type issue when adding storybook.configs["flat/recommended"].

What changed

  • added a safe cast for TS ESLint config files
  • kept normal spread for JS ESLint config files
  • passed eslintConfigFile into flat config transformation so TS and JS can be handled correctly

Why

The original fix solves the TypeScript defineConfig error, but using as any[] everywhere would break JS config files. This change fixes the TS error without introducing that JS regression.

Result

  • fixes the defineConfig type error in TS configs
  • avoids breaking .js, .mjs, and .cjs ESLint config files

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of ESLint flat configuration for TypeScript projects, ensuring proper type-casting and support for various configuration formats.
  • Refactor

    • Enhanced internal configuration logic to better detect and process different ESLint setup patterns while maintaining compatibility with existing configurations.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

Updated configureFlatConfig function to accept an optional eslintConfigFile parameter for determining how to type-cast the Storybook flat config spread element in TypeScript configs. Introduced helper functions and refactored control-flow logic for inserting the spread across various AST patterns (array exports, typescript-eslint config calls, eslint/config defineConfig, and resolved identifiers).

Changes

Cohort / File(s) Summary
ESLint Plugin Configuration Handler
code/core/src/cli/eslintPlugin.ts
Updated configureFlatConfig signature to accept optional eslintConfigFile parameter. Added helper functions isTypeScriptEslintConfig and createStorybookConfigSpread for conditional type-casting logic. Refactored control-flow for flat-config insertion across multiple AST shape targets while maintaining insertion points (array exports, typescript-eslint config(...), eslint/config defineConfig([...]), and identifier-resolved arrays/calls). Minor structural updates include quote normalization and conditional branch reorganization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Caution

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

⚠️ Outside diff range comments (1)
code/core/src/cli/eslintPlugin.ts (1)

224-250: ⚠️ Potential issue | 🟠 Major

Handle export default config when config comes from tseslint.config(...).

Case 3 only mutates raw arrays and defineConfig([...]) calls. The pattern const config = tseslint.config(...); export default config; is skipped, creating an inconsistency: export default tseslint.config(...) (Case 2) is rewritten, but the variable-assigned form is not.

Fix
       if (t.isIdentifier(eslintConfigExpression)) {
         const binding = path.scope.getBinding(eslintConfigExpression.name);
         if (binding && t.isVariableDeclarator(binding.path.node)) {
           const init = unwrapTSExpression(binding.path.node.init);

           if (t.isArrayExpression(init)) {
             init.elements.push(createStorybookConfigSpread());
+          } else if (
+            t.isCallExpression(init) &&
+            t.isMemberExpression(init.callee) &&
+            tsEslintLocalName &&
+            t.isIdentifier(init.callee.object, { name: tsEslintLocalName }) &&
+            t.isIdentifier(init.callee.property, { name: "config" })
+          ) {
+            init.arguments.push(storybookConfig);
           } else if (
             t.isCallExpression(init) &&
             init.arguments.length > 0 &&
             t.isIdentifier(init.callee) &&
             eslintDefineConfigLocalName &&
             init.callee.name === eslintDefineConfigLocalName
           ) {

No test coverage exists for this pattern; add one as part of the fix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/cli/eslintPlugin.ts` around lines 224 - 250, The code only
handles variable-initialized arrays and defineConfig(...) calls but misses when
the variable is initialized by a CallExpression like tseslint.config(...);
update the block in eslintPlugin.ts that inspects binding.path.node.init inside
the "if (t.isIdentifier(eslintConfigExpression))" branch to also detect when
init is a CallExpression whose callee is a MemberExpression with object
identifier "tseslint" and property name "config" (or the equivalent import
identifier if you have one), then locate the array argument (unwrapTSExpression
on the first argument) and push createStorybookConfigSpread({ castToAnyArray:
shouldCastStorybookConfig }) into that array just like the existing defineConfig
handling; add a unit test that covers "const config = tseslint.config([...]);
export default config;" to ensure the transformation runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/core/src/cli/eslintPlugin.ts`:
- Around line 255-275: The Program visitor currently always injects an ESM
import (constructed as importDecl) even for CommonJS flat config files because
isFlatConfig's pattern matches .cjs/.mjs and the rewrite only happens in the
ExportDefaultDeclaration path; fix this by introducing and using a boolean flag
(e.g., fileRewritten or rewrittenFlatConfig) that you set to true when the
ExportDefaultDeclaration rewrite succeeds, then change the Program(path) logic
to only insert the importDecl when alreadyImported is false AND that
rewrittenFlatConfig flag is true so CommonJS files that weren't rewritten won't
receive an ESM import.

---

Outside diff comments:
In `@code/core/src/cli/eslintPlugin.ts`:
- Around line 224-250: The code only handles variable-initialized arrays and
defineConfig(...) calls but misses when the variable is initialized by a
CallExpression like tseslint.config(...); update the block in eslintPlugin.ts
that inspects binding.path.node.init inside the "if
(t.isIdentifier(eslintConfigExpression))" branch to also detect when init is a
CallExpression whose callee is a MemberExpression with object identifier
"tseslint" and property name "config" (or the equivalent import identifier if
you have one), then locate the array argument (unwrapTSExpression on the first
argument) and push createStorybookConfigSpread({ castToAnyArray:
shouldCastStorybookConfig }) into that array just like the existing defineConfig
handling; add a unit test that covers "const config = tseslint.config([...]);
export default config;" to ensure the transformation runs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b868c6c9-f3c1-4a1c-a596-bd25baafacd4

📥 Commits

Reviewing files that changed from the base of the PR and between ac76d42 and db43e55.

📒 Files selected for processing (1)
  • code/core/src/cli/eslintPlugin.ts

Comment on lines 255 to 275
Program(path) {
const alreadyImported = path.node.body.some(
(node) => t.isImportDeclaration(node) && node.source.value === 'eslint-plugin-storybook'
(node) =>
t.isImportDeclaration(node) &&
node.source.value === "eslint-plugin-storybook",
);

if (!alreadyImported) {
// Add import: import storybook from 'eslint-plugin-storybook'
// Add import: import storybook from "eslint-plugin-storybook"
const importDecl = t.importDeclaration(
[t.importDefaultSpecifier(t.identifier('storybook'))],
t.stringLiteral('eslint-plugin-storybook')
[t.importDefaultSpecifier(t.identifier("storybook"))],
t.stringLiteral("eslint-plugin-storybook"),
);
(importDecl as any).comments = [
{
type: 'CommentLine',
type: "CommentLine",
value:
' For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format',
" For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format",
},
];
path.node.body.unshift(importDecl);
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n-- supported flat-config extensions --\n'
sed -n '18,27p' code/core/src/cli/eslintPlugin.ts

printf '\n-- export/default handling vs CommonJS handling --\n'
rg -n -C2 'ExportDefaultDeclaration|AssignmentExpression|module\.exports' code/core/src/cli/eslintPlugin.ts

printf '\n-- unconditional import insertion --\n'
sed -n '255,276p' code/core/src/cli/eslintPlugin.ts

printf '\n-- CommonJS flat-config coverage in tests --\n'
rg -n -C2 'module\.exports|eslint\.config\.cjs|eslint\.config\.js' code/core/src/cli/eslintPlugin.test.ts

Repository: storybookjs/storybook

Length of output: 2988


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the full configureFlatConfig function
printf '\n-- Full configureFlatConfig function --\n'
sed -n '126,295p' code/core/src/cli/eslintPlugin.ts

# Check if there's any early return or condition that prevents import in CommonJS
printf '\n-- Looking for early returns or conditions around Program visitor --\n'
sed -n '240,280p' code/core/src/cli/eslintPlugin.ts

Repository: storybookjs/storybook

Length of output: 7062


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check what "isFlatConfig" means in tests - does it include CommonJS flat configs?
printf '\n-- Test context for eslint.config.js (flat config) --\n'
sed -n '295,320p' code/core/src/cli/eslintPlugin.test.ts

# Check the CommonJS module.exports test case more carefully
printf '\n-- CommonJS module.exports test --\n'
sed -n '238,285p' code/core/src/cli/eslintPlugin.test.ts

# Look for any test that tests CommonJS with flat config
printf '\n-- Searching for flat config tests with CommonJS --\n'
rg -n -B5 -A10 'isFlatConfig.*true' code/core/src/cli/eslintPlugin.test.ts | head -80

Repository: storybookjs/storybook

Length of output: 6207


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify the exact scenario: check if eslint.config.cjs could be treated as flat config
printf '\n-- How isFlatConfig is determined --\n'
rg -n -B5 -A5 'isFlatConfig' code/core/src/cli/eslintPlugin.ts | grep -A10 -B10 'extractEslintInfo\|eslint\.config'

# Check if a cjs file could trigger isFlatConfig = true
printf '\n-- File detection logic --\n'
sed -n '276,340p' code/core/src/cli/eslintPlugin.ts

Repository: storybookjs/storybook

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the extractEslintInfo function to see how isFlatConfig is determined
printf '\n-- extractEslintInfo function (lines 276+) --\n'
sed -n '276,380p' code/core/src/cli/eslintPlugin.ts

# Look for any logic that checks file extensions to determine flat config
printf '\n-- File extension checks for flat config detection --\n'
rg -n 'eslint\.config|isFlatConfig' code/core/src/cli/eslintPlugin.ts | head -30

Repository: storybookjs/storybook

Length of output: 3333


Don't add ESM import to CommonJS flat config files that weren't rewritten.

The isFlatConfig detection uses the pattern /eslint\.config\.[^/]+/, which matches .cjs and .mjs extensions in addition to .js and .ts. If a file like eslint.config.cjs uses CommonJS module.exports = [...], the Program visitor still unconditionally adds an ESM import statement even though the file was never rewritten (since only ExportDefaultDeclaration is handled). This creates invalid syntax and breaks ESLint.

Guard the import insertion to only run after a successful rewrite:

Fix
export const configureFlatConfig = async (
  code: string,
  eslintConfigFile?: string,
) => {
  const ast = babelParse(code);
  const shouldCastStorybookConfig = isTypeScriptEslintConfig(eslintConfigFile);
+  let didMutateFlatConfig = false;
   
   traverse(ast, {
     ExportDefaultDeclaration(path) {
       // ... existing code ...
       
       if (t.isArrayExpression(eslintConfigExpression)) {
         eslintConfigExpression.elements.push(createStorybookConfigSpread());
+        didMutateFlatConfig = true;
       }
       
       if (t.isCallExpression(eslintConfigExpression) && /* tseslint.config */) {
         eslintConfigExpression.arguments.push(storybookConfig);
+        didMutateFlatConfig = true;
       }
       
       if (t.isCallExpression(eslintConfigExpression) && /* defineConfig */) {
         // ... existing code ...
         if (unwrappedArg && t.isArrayExpression(unwrappedArg)) {
           unwrappedArg.elements.push(...);
+          didMutateFlatConfig = true;
         }
       }
       
       if (t.isIdentifier(eslintConfigExpression)) {
         // ... existing code ...
         if (t.isArrayExpression(init)) {
           init.elements.push(createStorybookConfigSpread());
+          didMutateFlatConfig = true;
         } else if (/* defineConfig case */) {
           // ... existing code ...
           if (unwrappedArg && t.isArrayExpression(unwrappedArg)) {
             unwrappedArg.elements.push(...);
+            didMutateFlatConfig = true;
           }
         }
       }
     },
     
     Program(path) {
       const alreadyImported = path.node.body.some(
         (node) =>
           t.isImportDeclaration(node) &&
           node.source.value === "eslint-plugin-storybook",
       );
       
-      if (!alreadyImported) {
+      if (didMutateFlatConfig && !alreadyImported) {
         // Add import
         const importDecl = t.importDeclaration(...);
         // ... rest of code ...
       }
     },
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/cli/eslintPlugin.ts` around lines 255 - 275, The Program
visitor currently always injects an ESM import (constructed as importDecl) even
for CommonJS flat config files because isFlatConfig's pattern matches .cjs/.mjs
and the rewrite only happens in the ExportDefaultDeclaration path; fix this by
introducing and using a boolean flag (e.g., fileRewritten or
rewrittenFlatConfig) that you set to true when the ExportDefaultDeclaration
rewrite succeeds, then change the Program(path) logic to only insert the
importDecl when alreadyImported is false AND that rewrittenFlatConfig flag is
true so CommonJS files that weren't rewritten won't receive an ESM import.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Config is not assignable to type when using defineConfig

1 participant