Skip to content

Conversation

@yannbf
Copy link
Member

@yannbf yannbf commented Oct 17, 2025

Closes #

What I did

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

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 PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

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

Summary by CodeRabbit

  • New Features
    • Stories now support an extend() method to create variants with custom merged arguments
    • Added public Component property on stories for direct component access

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 17, 2025

📝 Walkthrough

Walkthrough

This PR extends CSF stories with an extend() method that enables creating new story variants with merged arguments. The implementation adds a runtime side-effect in the defineStory extend method to assign the Component property when extending, and introduces corresponding tests across React implementations and type definitions.

Changes

Cohort / File(s) Summary
Core CSF Factory
code/core/src/csf/csf-factories.ts
Modified extend method to conditionally assign extendedStory.Component = extendedStory.__compose() after creating the extended story via defineStory(...), ensuring the Component is properly set for React renderers without altering the composed story data structure.
React Type Definitions
code/renderers/react/src/preview.tsx
Added extend<TNewInput extends StoryAnnotations<T, T['args']>>(input: TNewInput): ReactStory<T, TNewInput> method signature to the ReactStory interface to enable type-safe story extension.
React Tests
code/renderers/react/src/csf-factories.test.tsx
Added runtime checks verifying that MyStory.Component is defined and MyStory.extend({ args: { ... } }) correctly merges arguments and returns a story with a defined Component property.
Portable Stories Tests
code/renderers/react/src/__test__/portable-stories-factory.test.tsx
Added new "Story extend" test suite verifying that CSF3Primary.extend creates an ExtendedStory with merged args and renders updated children correctly.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • storybookjs/storybook#32526: Directly modifies the same defineStory.extend behavior to create extended stories and assign their Component via __compose().
  • storybookjs/storybook#32455: Updates CSF factory logic in the same file including story definition and extend behavior patterns.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch yann/support-component-property-csf-extend

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: 0

🧹 Nitpick comments (1)
code/core/src/csf/csf-factories.ts (1)

256-260: Consider moving renderer-specific logic to the renderer layer.

This code adds React-specific Component assignment logic to the core CSF layer, creating coupling between the core and a specific renderer. While the approach is pragmatic and consistent with the existing pattern in code/renderers/react/src/preview.tsx (line 47), it violates separation of concerns.

Additionally, there's a TODO comment in preview.tsx (line 44) questioning whether this Component construct is needed: "Are we sure we want this? the Component construct was for compatibility with raw portable stories. We don't actually use this in vitest."

Consider:

  1. Having the React renderer override the extend method to handle Component assignment
  2. Implementing a hook/callback system where renderers can customize extend behavior
  3. Keeping core CSF logic renderer-agnostic

That said, if this coupling is intentional for pragmatic reasons (e.g., avoiding duplication across multiple renderers), the current implementation is correct and the conditional check prevents errors for non-React renderers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28ab482 and bd00e44.

📒 Files selected for processing (4)
  • code/core/src/csf/csf-factories.ts (2 hunks)
  • code/renderers/react/src/__test__/portable-stories-factory.test.tsx (1 hunks)
  • code/renderers/react/src/csf-factories.test.tsx (1 hunks)
  • code/renderers/react/src/preview.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier formatting on changed files before committing
Run ESLint on changed files and fix all errors/warnings before committing (use yarn lint:js:cmd <file>)

Files:

  • code/renderers/react/src/preview.tsx
  • code/renderers/react/src/csf-factories.test.tsx
  • code/renderers/react/src/__test__/portable-stories-factory.test.tsx
  • code/core/src/csf/csf-factories.ts
**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Export functions from modules when they need to be unit-tested

Files:

  • code/renderers/react/src/preview.tsx
  • code/renderers/react/src/csf-factories.test.tsx
  • code/renderers/react/src/__test__/portable-stories-factory.test.tsx
  • code/core/src/csf/csf-factories.ts
code/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

In application code, use Storybook loggers instead of console.* (client code: storybook/internal/client-logger; server code: storybook/internal/node-logger)

Files:

  • code/renderers/react/src/preview.tsx
  • code/renderers/react/src/csf-factories.test.tsx
  • code/renderers/react/src/__test__/portable-stories-factory.test.tsx
  • code/core/src/csf/csf-factories.ts
{code/**,scripts/**}/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/renderers/react/src/preview.tsx
  • code/renderers/react/src/csf-factories.test.tsx
  • code/renderers/react/src/__test__/portable-stories-factory.test.tsx
  • code/core/src/csf/csf-factories.ts
code/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

code/**/*.{test,spec}.{ts,tsx}: Place all test files under the code/ directory
Name test files as *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx

Files:

  • code/renderers/react/src/csf-factories.test.tsx
  • code/renderers/react/src/__test__/portable-stories-factory.test.tsx
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/spy-mocking.mdc)

**/*.test.{ts,tsx,js,jsx}: Use vi.mock() with the spy: true option for all package and file mocks in Vitest tests
Place all mocks at the top of the test file before any test cases
Use vi.mocked() to type and access mocked functions
Implement mock behaviors in beforeEach blocks
Mock all required dependencies that the test subject uses
Mock implementations should be placed in beforeEach blocks
Each mock implementation should return a Promise for async functions
Mock implementations should match the expected return type of the original function
Use vi.mocked() to access and implement mock behaviors
Mock all required properties and methods that the test subject uses
Avoid direct function mocking without vi.mocked()
Avoid mock implementations outside of beforeEach blocks
Avoid mocking without the spy: true option
Avoid inline mock implementations within test cases
Avoid mocking only a subset of required dependencies
Mock at the highest level of abstraction needed
Keep mock implementations simple and focused
Use type-safe mocking with vi.mocked()
Document complex mock behaviors
Group related mocks together

Files:

  • code/renderers/react/src/csf-factories.test.tsx
  • code/renderers/react/src/__test__/portable-stories-factory.test.tsx
**/*.@(test|spec).{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.@(test|spec).{ts,tsx,js,jsx}: Unit tests should import and execute the functions under test rather than only asserting on syntax patterns
Mock external dependencies in tests using vi.mock() (e.g., filesystem, loggers)

Files:

  • code/renderers/react/src/csf-factories.test.tsx
  • code/renderers/react/src/__test__/portable-stories-factory.test.tsx
🧬 Code graph analysis (1)
code/renderers/react/src/__test__/portable-stories-factory.test.tsx (1)
code/renderers/svelte/src/__test__/composeStories/Button.stories.ts (1)
  • CSF3Primary (95-101)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: normal
  • GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (3)
code/renderers/react/src/__test__/portable-stories-factory.test.tsx (1)

105-112: LGTM! Test verifies extend functionality with merged args.

The test correctly verifies that:

  1. CSF3Primary.extend() creates a new story with merged args
  2. The extended story's Component property is defined and renders correctly
  3. The updated children arg is reflected in the rendered output
code/renderers/react/src/preview.tsx (1)

120-122: LGTM! Type signature correctly extends Story.extend for React.

The method signature properly:

  • Constrains the input to StoryAnnotations<T, T['args']>
  • Returns a narrowed ReactStory<T, TNewInput> type
  • Maintains type safety for the extend functionality
code/renderers/react/src/csf-factories.test.tsx (1)

44-49: LGTM! Runtime checks verify Component property on extended stories.

The tests properly validate:

  1. MyStory.Component is defined after story creation
  2. MyStory.extend() returns a new story with merged args
  3. ExtendedMyStory.Component is also defined

This provides good runtime coverage of the new extend API.

@nx-cloud
Copy link

nx-cloud bot commented Oct 17, 2025

View your CI Pipeline Execution ↗ for commit bd00e44

Command Status Duration Result
nx run-many -t build --parallel=3 ✅ Succeeded 1m View ↗

☁️ Nx Cloud last updated this comment at 2025-10-17 15:09:57 UTC

@IanVS
Copy link
Member

IanVS commented Oct 23, 2025

@storybookjs/core could I get a canary of this? I believe it's necessary for me to migrate to CSF factories.

Edit: never mind, I can use the private .__compose() method for now, and it works. :)

@yannbf
Copy link
Member Author

yannbf commented Oct 28, 2025

Closing this as not implemented, we can revisit this later

@yannbf yannbf closed this Oct 28, 2025
@github-project-automation github-project-automation bot moved this from Needs Discussion to Done in Core Team Projects Oct 28, 2025
@IanVS
Copy link
Member

IanVS commented Oct 28, 2025

Yann, what is the reason not to implement this? I'm a bit nervous now, because I migrated my codebase using the undocumented private method expecting this PR would merge and I could change to .Component.

@yannbf
Copy link
Member Author

yannbf commented Oct 28, 2025

Yann, what is the reason not to implement this? I'm a bit nervous now, because I migrated my codebase using the undocumented private method expecting this PR would merge and I could change to .Component.

Hey there! This PR was about an experimental feature we thought of providing. There's a lot to unpack here:

  1. Nowadays there's the concept of "raw portable stories" and "portable stories". The raw one is when using composeStory directly. The other one is when using the vitest plugin.
  2. With the vitest plugin, we can compose stories but also do preset handling and all the other necessary things that require a "middleware", like auto title, mocking, etc. We have added many features to stories and testing and they are slowly making it hard/impossible to make raw portable stories support them.
  3. The .Component property (or the ability to render components outside of Storybook and outside of Vitest BM) is tough to maintain, as it requires each renderer to provide its own implementation for that. However it would still mean that some stories or features will just not work (RSC, mocking, etc), and the more people start to use it, the harder it would be for Storybook to remove that API.

In the meantime, we have decided to park this. You can still use portable stories though, here are examples you can follow:

import { composeStory } from '@storybook/react-vite'
const meta = preview.meta({})
const Primary = meta.story({})
const Secondary = meta.story({})

const composed = [Primary, Secondary].map((story) => {
  return composeStory(story.input, meta.input, meta.preview.composed)
});

// or using this internal API (for now)
const composed = [Primary, Secondary].map((story) => {
  return story.__compose()
});

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

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants