Skip to content

fix: resolve #34468 storybook eslint defineConfig issue#34487

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

fix: resolve #34468 storybook eslint defineConfig issue#34487
justismailmemon wants to merge 5 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

    • Handle ESLint flat configs in TypeScript projects by adding Storybook entries with an "as any[]" cast only when needed; avoid adding imports when no rewrite occurred and leave CommonJS flat configs untouched.
  • Refactor

    • Improved detection and rewrite logic across multiple flat-config patterns to prevent incorrect edits and preserve existing config formats.
  • Tests

    • Standardized formatting, reset mocks between tests, and added stricter flat-config tests asserting precise write/no-write behavior and casting.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 74516e5e-3bbe-4533-8297-1624b4f42efd

📥 Commits

Reviewing files that changed from the base of the PR and between 7608e9d and 78e0bbe.

📒 Files selected for processing (1)
  • code/core/src/cli/eslintPlugin.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/core/src/cli/eslintPlugin.test.ts

📝 Walkthrough

Walkthrough

configureFlatConfig now accepts an optional eslintConfigFile, detects TypeScript ESLint config filenames, and emits the Storybook flat-config spread conditionally cast to any[] for TS configs. All flat-config AST rewrite paths use this helper and the plugin import is inserted only when a flat-config rewrite occurred.

Changes

Cohort / File(s) Summary
ESLint Plugin Core
code/core/src/cli/eslintPlugin.ts
Changed configureFlatConfig(code, eslintConfigFile?) signature; added createStorybookConfigSpread({ castToAnyArray }) to produce ...storybook.configs["flat/recommended"] optionally cast as any[] for TS ESLint configs; updated all flat-config AST rewrite paths to use this helper; refined import-detection and only add the eslint-plugin-storybook import when a flat-config rewrite occurred; minor quote/formatting tweaks.
Tests (ESLint Plugin)
code/core/src/cli/eslintPlugin.test.ts
Normalized string quotes to double quotes; reset readFile/writeFile mocks in beforeEach; updated flat-config tests to expect no writes for non-ESLint/config or unknown formats; added TS defineConfig test asserting as any[] cast; added test ensuring CommonJS flat config (eslint.config.cjs) is not modified; adjusted assertion formatting.

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 thread code/core/src/cli/eslintPlugin.ts Outdated
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.

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.test.ts (1)

439-486: ⚠️ Potential issue | 🟡 Minor

Missing test coverage for TypeScript + defineConfig scenario.

The PR's primary fix addresses a TypeScript type incompatibility when using defineConfig from eslint/config. However, the existing defineConfig tests use .mjs extensions (JavaScript), not TypeScript. There should be a test that verifies the as any[] cast is applied when:

  1. The config file is TypeScript (e.g., eslint.config.ts)
  2. Using defineConfig from "eslint/config"

Consider adding a test like:

Suggested test case
it("should apply 'as any[]' cast for TypeScript defineConfig", async () => {
  const mockPackageManager = {
    getAllDependencies: vi.fn(),
  } satisfies Partial<JsPackageManager>;

  const mockConfigFile = dedent`import { defineConfig } from "eslint/config";
    export default defineConfig([
      { rules: { "no-console": "error" } },
    ]);`;

  vi.mocked(readFile).mockResolvedValue(mockConfigFile);

  await configureEslintPlugin({
    eslintConfigFile: "eslint.config.ts", // TypeScript config
    packageManager: mockPackageManager as any,
    isFlatConfig: true,
  });
  
  const [, content] = vi.mocked(writeFile).mock.calls[0];
  // Verify the cast is applied
  expect(content).toContain("...storybook.configs[\"flat/recommended\"] as any[]");
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/cli/eslintPlugin.test.ts` around lines 439 - 486, The test
suite is missing coverage for the TypeScript + defineConfig case; add a new test
in eslintPlugin.test.ts that calls configureEslintPlugin with eslintConfigFile
set to "eslint.config.ts", mocks readFile to return a defineConfig TypeScript
file (importing from "eslint/config"), stubs packageManager.getAllDependencies,
sets isFlatConfig: true, and then asserts the written content includes the cast
"...storybook.configs[\"flat/recommended\"] as any[]" (or equivalent) to verify
the TypeScript-only fix is applied; reference configureEslintPlugin and the
writeFile/readFile mocks when implementing the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@code/core/src/cli/eslintPlugin.test.ts`:
- Around line 439-486: The test suite is missing coverage for the TypeScript +
defineConfig case; add a new test in eslintPlugin.test.ts that calls
configureEslintPlugin with eslintConfigFile set to "eslint.config.ts", mocks
readFile to return a defineConfig TypeScript file (importing from
"eslint/config"), stubs packageManager.getAllDependencies, sets isFlatConfig:
true, and then asserts the written content includes the cast
"...storybook.configs[\"flat/recommended\"] as any[]" (or equivalent) to verify
the TypeScript-only fix is applied; reference configureEslintPlugin and the
writeFile/readFile mocks when implementing the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3d61d272-a2fb-45b2-8dee-383e7648d455

📥 Commits

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

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

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.test.ts (1)

17-28: ⚠️ Potential issue | 🟠 Major

Add spy: true option to both module mocks to comply with repository test guidelines.

Lines 17-28 contain two vi.mock() calls that lack the required spy: true option. Per the spy mocking rules defined in .cursor/rules/spy-mocking.mdc, all package and file mocks in Vitest tests must use { spy: true }.

Update both mocks:

  • Line 17: Add { spy: true } to the empathic/find mock
  • Line 21: Add { spy: true } to the node:fs/promises mock
Suggested fix
-vi.mock("empathic/find", () => ({
-  up: vi.fn(),
-}));
+vi.mock("empathic/find", { spy: true });

-vi.mock(import("node:fs/promises"), async (importOriginal) => {
-  const actual = await importOriginal();
-  return {
-    ...actual,
-    readFile: vi.fn(),
-    writeFile: vi.fn(),
-  };
-});
+vi.mock("node:fs/promises", { spy: true });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/cli/eslintPlugin.test.ts` around lines 17 - 28, Both vi.mock
calls need the repository-required spy option; update the mocks for
"empathic/find" (the vi.mock("empathic/find", () => ({ up: vi.fn() })) call) and
for the dynamic import mock (vi.mock(import("node:fs/promises"), async
(importOriginal) => { ... })) to pass { spy: true } as the third argument to
vi.mock (i.e., vi.mock(module, factory, { spy: true })) so the mocks comply with
the spy-mocking rule.
🧹 Nitpick comments (2)
code/core/src/cli/eslintPlugin.test.ts (2)

201-237: Remove duplicated inline snapshot assertion in the same test.

The comment-json test asserts the same inline snapshot twice. Keeping one snapshot + targeted toContain checks is enough and reduces churn.

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

In `@code/core/src/cli/eslintPlugin.test.ts` around lines 201 - 237, The test
contains a duplicated inline snapshot assertion for the same `content` value;
remove the redundant `expect(content).toMatchInlineSnapshot(...)` (the
second/duplicate occurrence) and keep the single snapshot plus the targeted
`expect(content).toContain(...)` checks so the test asserts the snapshot once
while still verifying specific comment and entry presence; look for the two
identical `expect(content).toMatchInlineSnapshot` calls and delete the later
one.

508-509: Strengthen the TS cast assertion to validate placement, not just presence.

toContain("as any[]") can pass even if the cast appears in an unrelated part of the output. Prefer asserting the spread expression is specifically casted.

Example assertion
-      expect(content).toContain('storybook.configs["flat/recommended"]');
-      expect(content).toContain("as any[]");
+      expect(content).toMatch(
+        /\.\.\.storybook\.configs\["flat\/recommended"\]\s+as any\[\]/,
+      );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/cli/eslintPlugin.test.ts` around lines 508 - 509, Replace the
loose existence check for "as any[]" with a targeted assertion that the spread
expression for storybook.configs["flat/recommended"] is the thing being cast;
update the test (where content is asserted) to use a regex match such as
matching storybook.configs["flat/recommended"] followed by optional whitespace
and then "as any[]" (e.g.
expect(content).toMatch(/storybook\.configs\["flat\/recommended"\]\s+as
any\[\]/)) so the test validates placement, not just presence.
🤖 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.test.ts`:
- Around line 118-123: The tests in describe("configureEslintPlugin") rely on
precise readFile/writeFile call counts but currently define mock implementations
outside test setup, causing cross-test bleed; move any mock implementations for
readFile, writeFile and the mockPackageManager.getAllDependencies into a
beforeEach inside this describe block, and call vi.resetAllMocks() or
vi.clearAllMocks() at the start of that beforeEach so each test (including
"should not configure ESLint plugin if it is already configured") starts with
fresh mocks and predictable mock.calls[0] behavior.

---

Outside diff comments:
In `@code/core/src/cli/eslintPlugin.test.ts`:
- Around line 17-28: Both vi.mock calls need the repository-required spy option;
update the mocks for "empathic/find" (the vi.mock("empathic/find", () => ({ up:
vi.fn() })) call) and for the dynamic import mock
(vi.mock(import("node:fs/promises"), async (importOriginal) => { ... })) to pass
{ spy: true } as the third argument to vi.mock (i.e., vi.mock(module, factory, {
spy: true })) so the mocks comply with the spy-mocking rule.

---

Nitpick comments:
In `@code/core/src/cli/eslintPlugin.test.ts`:
- Around line 201-237: The test contains a duplicated inline snapshot assertion
for the same `content` value; remove the redundant
`expect(content).toMatchInlineSnapshot(...)` (the second/duplicate occurrence)
and keep the single snapshot plus the targeted `expect(content).toContain(...)`
checks so the test asserts the snapshot once while still verifying specific
comment and entry presence; look for the two identical
`expect(content).toMatchInlineSnapshot` calls and delete the later one.
- Around line 508-509: Replace the loose existence check for "as any[]" with a
targeted assertion that the spread expression for
storybook.configs["flat/recommended"] is the thing being cast; update the test
(where content is asserted) to use a regex match such as matching
storybook.configs["flat/recommended"] followed by optional whitespace and then
"as any[]" (e.g.
expect(content).toMatch(/storybook\.configs\["flat\/recommended"\]\s+as
any\[\]/)) so the test validates placement, not just presence.
🪄 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: a4fe01d7-a588-4911-ba6d-00b031ecc212

📥 Commits

Reviewing files that changed from the base of the PR and between 1d65168 and 7608e9d.

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

Comment thread code/core/src/cli/eslintPlugin.test.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Empathy Backlog

Development

Successfully merging this pull request may close these issues.

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

2 participants