Skip to content

Conversation

@kasperpeulen
Copy link
Contributor

@kasperpeulen kasperpeulen commented Sep 4, 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 pull request has been released as version 0.0.0-pr-32412-sha-b8362176. Try it out in a new sandbox by running npx [email protected] sandbox or in an existing project with npx [email protected] upgrade.

More information
Published version 0.0.0-pr-32412-sha-b8362176
Triggered by @kasperpeulen
Repository storybookjs/storybook
Branch kasper/nextjs-vite-rsc
Commit b8362176
Datetime Tue Oct 7 15:25:58 UTC 2025 (1759850758)
Workflow run 18317715830

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=32412

Greptile Summary

Updated On: 2025-09-04 12:19:39 UTC

This PR introduces a new Next.js framework variant called nextjs-vite-rsc that combines Next.js with Vite bundling and React Server Components (RSC) support. The new framework provides a comprehensive implementation including:

Core Framework Structure:

  • Complete framework setup with TypeScript configuration, build configs, and package definitions
  • Preview configuration with decorators for styling, images, routing, and head management
  • Portable stories functionality for testing outside Storybook UI
  • Error handling for async client component errors specific to RSC

Mock System Implementation:

  • Comprehensive mocking for Next.js APIs including headers, cookies, cache utilities, and navigation
  • Router mocking with support for both App Router and Pages Router patterns
  • Server actions mocking for RSC functionality
  • Singleton pattern implementation for consistent mock state

Template Files and Examples:

  • CLI templates in both JavaScript and TypeScript variants (Button, Header, Page components)
  • Comprehensive story examples covering Next.js features: Image optimization, Font handling, Dynamic imports, RSC components, Server actions, Navigation, Styling (styled-jsx), and Redirect functionality
  • RSC-specific components demonstrating async server components and server-side API usage

Integration Features:

  • Vite plugin integration for Next.js compatibility
  • Support for both Next.js App Directory and Pages Directory routing
  • Image optimization with Next.js Image component support
  • Font optimization with Google Fonts and local font examples

The framework follows the same architectural patterns as existing Next.js frameworks (nextjs and nextjs-vite) but is specifically designed for React Server Components, which require different handling for server-side rendering, component compilation, and runtime behavior. This separation allows for RSC-specific optimizations without impacting existing framework implementations.

PR Description Notes:

  • Missing description of what was implemented
  • All testing checkboxes are unchecked
  • No manual testing instructions provided
  • Missing documentation updates

Confidence score: 2/5

  • This PR has critical naming conflicts and configuration errors that will cause immediate build and publishing issues
  • Score reflects multiple package.json naming conflicts, incorrect import paths, and missing project configuration updates
  • Pay close attention to package.json files, project.json, and all import statements throughout the framework

Context used:

Context - PR titles must be in the format of 'Area: Summary', with both Area and Summary starting with a capital letter. (link)

Summary by CodeRabbit

  • New Features

    • Adds a Next.js + Vite RSC framework for Storybook with RSC runtime: navigation, headers/cookies, new preview/render surface, client loader, and server-driven testing APIs (renderServer, cleanup).
  • Templates

    • Example stories and type imports updated to target the new RSC framework.
  • Chores

    • Publishes @storybook/nextjs-vite-rsc with expanded exports and RSC dependencies; updates framework/version/renderer mappings and runtime externals.
  • Tooling

    • Vite build flow now uses a builder-based invocation; CLI NX project detection lookup updated.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

69 files reviewed, 32 comments

Edit Code Review Bot Settings | Greptile


export const RSC = async ({ label }: { label: string }) => <>RSC {label}</>;

export const Nested = async ({ children }: any) => <>Nested {children}</>;
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Use proper typing for children prop instead of any. Consider { children: React.ReactNode }

Suggested change
export const Nested = async ({ children }: any) => <>Nested {children}</>;
export const Nested = async ({ children }: { children: React.ReactNode }) => <>Nested {children}</>;

@@ -0,0 +1,93 @@
Copyright 2020 The Rubik Filtered Project Authors (https://https://github.com/NaN-xyz/Rubik-Filtered)
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: URL has double https:// protocol - should be https://github.com/NaN-xyz/Rubik-Filtered

Suggested change
Copyright 2020 The Rubik Filtered Project Authors (https://https://github.com/NaN-xyz/Rubik-Filtered)
Copyright 2020 The Rubik Filtered Project Authors (https://github.com/NaN-xyz/Rubik-Filtered)

@@ -0,0 +1,118 @@
import React, { useRef, useState } from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Remove unused useRef import

Suggested change
import React, { useRef, useState } from 'react';
import React, { useState } from 'react';

Comment on lines 32 to 36
<style jsx>{`
button {
background-color: ${backgroundColor};
}
`}</style>
Copy link
Contributor

Choose a reason for hiding this comment

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

style: styled-jsx may not work properly with React Server Components. Consider using CSS modules or external stylesheets for RSC compatibility.


if (overrides?.events) {
Object.keys(routerEvents).forEach((key) => {
if (key in routerEvents) {
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Logic error: should check key in overrides.events instead of key in routerEvents to properly override event handlers

Suggested change
if (key in routerEvents) {
if (key in overrides.events) {

@@ -0,0 +1,22 @@
import { fn } from 'storybook/test';

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
Copy link
Contributor

Choose a reason for hiding this comment

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

style: biome-ignore comment needs specific explanation - consider 'accepts any callback type for Next.js cache compatibility'

Suggested change
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
// biome-ignore lint/suspicious/noExplicitAny: accepts any callback type for Next.js cache compatibility

@@ -0,0 +1,8 @@
{
"name": "nextjs-vite",
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Project name should be 'nextjs-vite-rsc' to match directory name and avoid conflicts with existing nextjs-vite framework

Suggested change
"name": "nextjs-vite",
"name": "nextjs-vite-rsc",

test: {
// This is needed until Next will update to the React 19 beta: https://github.com/vercel/next.js/pull/65058
// In the React 19 beta ErrorBoundary errors (such as redirect) are only logged, and not thrown.
// We will also suspress console.error logs for re the console.error logs for redirect in the next framework.
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: Typo in comment: 'suspress' should be 'suppress'

Suggested change
// We will also suspress console.error logs for re the console.error logs for redirect in the next framework.
// We will also suppress console.error logs for re the console.error logs for redirect in the next framework.

} as Meta;

export const SingletonStateGetsInvalidatedAfterRedirecting: StoryObj = {
play: async ({ canvasElement, step }) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

style: The step parameter is defined but never used in the play function

Comment on lines 73 to 76
{Object.entries(params).map(([key, value]) => (
<li key={key}>
{key}: {value}
</li>
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Potential runtime error if value is an array - Object.entries(params) can return [string, string | string[]] but template expects string

@kasperpeulen kasperpeulen changed the base branch from next to kasper/nextjs-vite-rsc-base September 4, 2025 12:29
@nx-cloud
Copy link

nx-cloud bot commented Sep 4, 2025

View your CI Pipeline Execution ↗ for commit b836217

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

☁️ Nx Cloud last updated this comment at 2025-10-07 14:42:14 UTC

@storybook-app-bot
Copy link

storybook-app-bot bot commented Sep 4, 2025

Package Benchmarks

Commit: 826e97d, ran on 3 October 2025 at 13:14:35 UTC

The following packages have significant changes to their size or dependencies:

@storybook/addon-a11y

Before After Difference
Dependency count 0 2 🚨 +2 🚨
Self size 0 B 508 KB 🚨 +508 KB 🚨
Dependency size 0 B 2.80 MB 🚨 +2.80 MB 🚨
Bundle Size Analyzer Link Link

@storybook/addon-docs

Before After Difference
Dependency count 0 18 🚨 +18 🚨
Self size 0 B 2.02 MB 🚨 +2.02 MB 🚨
Dependency size 0 B 10.21 MB 🚨 +10.21 MB 🚨
Bundle Size Analyzer Link Link

@storybook/addon-jest

Before After Difference
Dependency count 0 2 🚨 +2 🚨
Self size 0 B 47 KB 🚨 +47 KB 🚨
Dependency size 0 B 53 KB 🚨 +53 KB 🚨
Bundle Size Analyzer Link Link

@storybook/addon-links

Before After Difference
Dependency count 0 1 🚨 +1 🚨
Self size 0 B 15 KB 🚨 +15 KB 🚨
Dependency size 0 B 5 KB 🚨 +5 KB 🚨
Bundle Size Analyzer Link Link

@storybook/addon-onboarding

Before After Difference
Dependency count 0 0 0
Self size 0 B 333 KB 🚨 +333 KB 🚨
Dependency size 0 B 667 B 🚨 +667 B 🚨
Bundle Size Analyzer Link Link

storybook-addon-pseudo-states

Before After Difference
Dependency count 0 0 0
Self size 0 B 23 KB 🚨 +23 KB 🚨
Dependency size 0 B 686 B 🚨 +686 B 🚨
Bundle Size Analyzer Link Link

@storybook/addon-themes

Before After Difference
Dependency count 0 1 🚨 +1 🚨
Self size 0 B 20 KB 🚨 +20 KB 🚨
Dependency size 0 B 28 KB 🚨 +28 KB 🚨
Bundle Size Analyzer Link Link

@storybook/addon-vitest

Before After Difference
Dependency count 0 6 🚨 +6 🚨
Self size 0 B 501 KB 🚨 +501 KB 🚨
Dependency size 0 B 1.53 MB 🚨 +1.53 MB 🚨
Bundle Size Analyzer Link Link

@storybook/builder-vite

Before After Difference
Dependency count 0 11 🚨 +11 🚨
Self size 0 B 330 KB 🚨 +330 KB 🚨
Dependency size 0 B 1.30 MB 🚨 +1.30 MB 🚨
Bundle Size Analyzer Link Link

@storybook/builder-webpack5

Before After Difference
Dependency count 0 187 🚨 +187 🚨
Self size 0 B 68 KB 🚨 +68 KB 🚨
Dependency size 0 B 31.78 MB 🚨 +31.78 MB 🚨
Bundle Size Analyzer Link Link

storybook

Before After Difference
Dependency count 0 43 🚨 +43 🚨
Self size 0 B 30.76 MB 🚨 +30.76 MB 🚨
Dependency size 0 B 17.31 MB 🚨 +17.31 MB 🚨
Bundle Size Analyzer Link Link

@storybook/angular

Before After Difference
Dependency count 0 187 🚨 +187 🚨
Self size 0 B 126 KB 🚨 +126 KB 🚨
Dependency size 0 B 29.94 MB 🚨 +29.94 MB 🚨
Bundle Size Analyzer Link Link

@storybook/ember

Before After Difference
Dependency count 0 191 🚨 +191 🚨
Self size 0 B 17 KB 🚨 +17 KB 🚨
Dependency size 0 B 28.49 MB 🚨 +28.49 MB 🚨
Bundle Size Analyzer Link Link

@storybook/html-vite

Before After Difference
Dependency count 0 14 🚨 +14 🚨
Self size 0 B 23 KB 🚨 +23 KB 🚨
Dependency size 0 B 1.67 MB 🚨 +1.67 MB 🚨
Bundle Size Analyzer Link Link

@storybook/nextjs

Before After Difference
Dependency count 0 532 🚨 +532 🚨
Self size 0 B 939 KB 🚨 +939 KB 🚨
Dependency size 0 B 58.47 MB 🚨 +58.47 MB 🚨
Bundle Size Analyzer Link Link

@storybook/nextjs-vite

Before After Difference
Dependency count 0 124 🚨 +124 🚨
Self size 0 B 4.08 MB 🚨 +4.08 MB 🚨
Dependency size 0 B 21.61 MB 🚨 +21.61 MB 🚨
Bundle Size Analyzer Link Link

@storybook/nextjs-vite-rsc

Before After Difference
Dependency count 0 133 🚨 +133 🚨
Self size 0 B 3.23 MB 🚨 +3.23 MB 🚨
Dependency size 0 B 23.78 MB 🚨 +23.78 MB 🚨
Bundle Size Analyzer Link Link

@storybook/preact-vite

Before After Difference
Dependency count 0 14 🚨 +14 🚨
Self size 0 B 14 KB 🚨 +14 KB 🚨
Dependency size 0 B 1.65 MB 🚨 +1.65 MB 🚨
Bundle Size Analyzer Link Link

@storybook/react-native-web-vite

Before After Difference
Dependency count 0 157 🚨 +157 🚨
Self size 0 B 31 KB 🚨 +31 KB 🚨
Dependency size 0 B 23.00 MB 🚨 +23.00 MB 🚨
Bundle Size Analyzer Link Link

@storybook/react-vite

Before After Difference
Dependency count 0 114 🚨 +114 🚨
Self size 0 B 37 KB 🚨 +37 KB 🚨
Dependency size 0 B 19.55 MB 🚨 +19.55 MB 🚨
Bundle Size Analyzer Link Link

@storybook/react-webpack5

Before After Difference
Dependency count 0 272 🚨 +272 🚨
Self size 0 B 25 KB 🚨 +25 KB 🚨
Dependency size 0 B 43.43 MB 🚨 +43.43 MB 🚨
Bundle Size Analyzer Link Link

@storybook/server-webpack5

Before After Difference
Dependency count 0 199 🚨 +199 🚨
Self size 0 B 17 KB 🚨 +17 KB 🚨
Dependency size 0 B 33.03 MB 🚨 +33.03 MB 🚨
Bundle Size Analyzer Link Link

@storybook/svelte-vite

Before After Difference
Dependency count 0 19 🚨 +19 🚨
Self size 0 B 59 KB 🚨 +59 KB 🚨
Dependency size 0 B 26.79 MB 🚨 +26.79 MB 🚨
Bundle Size Analyzer Link Link

@storybook/sveltekit

Before After Difference
Dependency count 0 20 🚨 +20 🚨
Self size 0 B 58 KB 🚨 +58 KB 🚨
Dependency size 0 B 26.85 MB 🚨 +26.85 MB 🚨
Bundle Size Analyzer Link Link

@storybook/vue3-vite

Before After Difference
Dependency count 0 109 🚨 +109 🚨
Self size 0 B 38 KB 🚨 +38 KB 🚨
Dependency size 0 B 43.78 MB 🚨 +43.78 MB 🚨
Bundle Size Analyzer Link Link

@storybook/web-components-vite

Before After Difference
Dependency count 0 15 🚨 +15 🚨
Self size 0 B 20 KB 🚨 +20 KB 🚨
Dependency size 0 B 1.70 MB 🚨 +1.70 MB 🚨
Bundle Size Analyzer Link Link

@storybook/cli

Before After Difference
Dependency count 0 187 🚨 +187 🚨
Self size 0 B 917 KB 🚨 +917 KB 🚨
Dependency size 0 B 80.39 MB 🚨 +80.39 MB 🚨
Bundle Size Analyzer Link Link

@storybook/codemod

Before After Difference
Dependency count 0 169 🚨 +169 🚨
Self size 0 B 35 KB 🚨 +35 KB 🚨
Dependency size 0 B 76.82 MB 🚨 +76.82 MB 🚨
Bundle Size Analyzer Link Link

@storybook/core-webpack

Before After Difference
Dependency count 0 1 🚨 +1 🚨
Self size 0 B 12 KB 🚨 +12 KB 🚨
Dependency size 0 B 28 KB 🚨 +28 KB 🚨
Bundle Size Analyzer Link Link

create-storybook

Before After Difference
Dependency count 0 44 🚨 +44 🚨
Self size 0 B 1.54 MB 🚨 +1.54 MB 🚨
Dependency size 0 B 48.07 MB 🚨 +48.07 MB 🚨
Bundle Size Analyzer node node

@storybook/csf-plugin

Before After Difference
Dependency count 0 9 🚨 +9 🚨
Self size 0 B 9 KB 🚨 +9 KB 🚨
Dependency size 0 B 1.27 MB 🚨 +1.27 MB 🚨
Bundle Size Analyzer Link Link

eslint-plugin-storybook

Before After Difference
Dependency count 0 35 🚨 +35 🚨
Self size 0 B 139 KB 🚨 +139 KB 🚨
Dependency size 0 B 3.15 MB 🚨 +3.15 MB 🚨
Bundle Size Analyzer Link Link

@storybook/react-dom-shim

Before After Difference
Dependency count 0 0 0
Self size 0 B 22 KB 🚨 +22 KB 🚨
Dependency size 0 B 785 B 🚨 +785 B 🚨
Bundle Size Analyzer Link Link

@storybook/preset-create-react-app

Before After Difference
Dependency count 0 68 🚨 +68 🚨
Self size 0 B 36 KB 🚨 +36 KB 🚨
Dependency size 0 B 5.98 MB 🚨 +5.98 MB 🚨
Bundle Size Analyzer Link Link

@storybook/preset-react-webpack

Before After Difference
Dependency count 0 170 🚨 +170 🚨
Self size 0 B 21 KB 🚨 +21 KB 🚨
Dependency size 0 B 30.93 MB 🚨 +30.93 MB 🚨
Bundle Size Analyzer Link Link

@storybook/preset-server-webpack

Before After Difference
Dependency count 0 10 🚨 +10 🚨
Self size 0 B 8 KB 🚨 +8 KB 🚨
Dependency size 0 B 1.20 MB 🚨 +1.20 MB 🚨
Bundle Size Analyzer Link Link

@storybook/html

Before After Difference
Dependency count 0 2 🚨 +2 🚨
Self size 0 B 30 KB 🚨 +30 KB 🚨
Dependency size 0 B 32 KB 🚨 +32 KB 🚨
Bundle Size Analyzer Link Link

@storybook/preact

Before After Difference
Dependency count 0 2 🚨 +2 🚨
Self size 0 B 17 KB 🚨 +17 KB 🚨
Dependency size 0 B 32 KB 🚨 +32 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react

Before After Difference
Dependency count 0 2 🚨 +2 🚨
Self size 0 B 883 KB 🚨 +883 KB 🚨
Dependency size 0 B 28 KB 🚨 +28 KB 🚨
Bundle Size Analyzer Link Link

@storybook/server

Before After Difference
Dependency count 0 3 🚨 +3 🚨
Self size 0 B 9 KB 🚨 +9 KB 🚨
Dependency size 0 B 716 KB 🚨 +716 KB 🚨
Bundle Size Analyzer Link Link

@storybook/svelte

Before After Difference
Dependency count 0 2 🚨 +2 🚨
Self size 0 B 48 KB 🚨 +48 KB 🚨
Dependency size 0 B 230 KB 🚨 +230 KB 🚨
Bundle Size Analyzer Link Link

@storybook/vue3

Before After Difference
Dependency count 0 3 🚨 +3 🚨
Self size 0 B 61 KB 🚨 +61 KB 🚨
Dependency size 0 B 211 KB 🚨 +211 KB 🚨
Bundle Size Analyzer Link Link

@storybook/web-components

Before After Difference
Dependency count 0 3 🚨 +3 🚨
Self size 0 B 43 KB 🚨 +43 KB 🚨
Dependency size 0 B 47 KB 🚨 +47 KB 🚨
Bundle Size Analyzer Link Link

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 28, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a new RSC framework package @storybook/nextjs-vite-rsc with RSC-focused Vite plugins, virtual modules, runtime externals, testing harnesses, router/header utilities, template updates, and switches the Vite build flow to use createBuilder(...).buildApp().

Changes

Cohort / File(s) Summary
Vite builder flow
code/builders/builder-vite/src/build.ts
Replace direct viteBuild(...) call with createBuilder(options, null) and builder.buildApp() invocation.
Core mappings & versions
code/core/src/cli/helpers.ts, code/core/src/common/utils/framework-to-renderer.ts, code/core/src/common/utils/get-storybook-info.ts, code/core/src/types/modules/frameworks.ts, code/core/src/common/versions.ts
Add nextjs-vite-rsc framework entries (builder, renderer, frameworkPackages, SupportedFrameworks type, versions); change NX detection import/lookup to use empathic/find (find.up(...)).
Framework package metadata
code/frameworks/nextjs-vite-rsc/package.json, code/frameworks/nextjs-vite-rsc/project.json
Rename package to @storybook/nextjs-vite-rsc, bump version, extend exports with RSC entry points, add @vitejs/plugin-rsc, update project name.
Build config & runtime externals
code/frameworks/nextjs-vite-rsc/build-config.ts, scripts/build/utils/entry-utils.ts
Add RSC browser entry points and extraOutputs for navigation artifacts; mark RSC client/preview and virtual RSC modules as runtime externals.
Framework preset & public surface
code/frameworks/nextjs-vite-rsc/src/index.ts, .../src/preset.ts, .../src/types.ts
Switch augmentation/exports to -rsc, remove some re-exports, set preset renderer to undefined, update preview entry, replace Next.js vite plugin usage with RSC-specific plugins.
Vite plugins & virtual modules
code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts, code/frameworks/nextjs-vite-rsc/src/virtual.d.ts
Add rscBrowserModePlugin() and vitestPluginNext() (virtual modules: load-client, build-client/server-references) and corresponding virtual module type declarations plus ImportMeta env augmentation.
Dev bootstrap & client runner
code/frameworks/nextjs-vite-rsc/src/load-client-dev.tsx, code/frameworks/nextjs-vite-rsc/src/react-client.tsx
Add load-client-dev that proxies invocations to /@vite/invoke-react-client and a react-client testing root supporting streaming payloads, server callbacks, rerender/unmount helpers, and initialization.
Routing, headers & navigation exports
code/frameworks/nextjs-vite-rsc/src/rsc/client.tsx, .../rsc/headers.ts, .../rsc/navigation.ts, .../rsc/navigation.react-server.ts
Implement Next/App router components and flight-data helpers; add headers/cookies adapters and re-export Next navigation utilities (client & react-server).
Preview & decorators API
code/frameworks/nextjs-vite-rsc/src/preview.tsx
Replace previous DecoratorFunction wiring with Decorator[]; expose a Preview surface (render, applyDecorators, renderToCanvas) using an RSC server render flow and lifecycle handling.
Testing harness & utilities
code/frameworks/nextjs-vite-rsc/src/testing-library.tsx, code/frameworks/nextjs-vite-rsc/src/repro.tsx
Add server-driven testing APIs (renderServer, cleanup, initialize, RenderConfiguration) and a ServerComponent wrapper for server-side routing.
Flight router utility & tests
code/frameworks/nextjs-vite-rsc/src/rsc/flight-router-state.ts, .../flight-router-state.test.ts
Add buildFlightRouterState(routePattern, pathname, search) utility and unit test validating nested flight router state parsing.
Routing providers & mock path updates
code/frameworks/nextjs-vite-rsc/src/routing/*, code/frameworks/nextjs-vite-rsc/src/export-mocks/headers/cookies.ts, code/frameworks/nextjs-vite-rsc/src/portable-stories.ts
Update import paths to -rsc variants and adjust mock import locations; primarily path-only edits.
Templates & stories type imports
code/frameworks/nextjs-vite-rsc/template/**
Update example/template story type imports from @storybook/nextjs-vite to @storybook/nextjs-vite-rsc.
Misc type/API tweaks
code/core/src/csf/csf-factories.ts, code/core/src/preview-api/modules/preview-web/render/mount-utils.ts
Add mount param to generated play function signature; relax mountDestructured typing to accept (...args:any[]).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor CLI as Storybook CLI
  participant BV as builder-vite/build.ts
  participant V as vite (createBuilder)

  CLI->>BV: build(options)
  BV->>V: createBuilder(options, null)
  V-->>BV: builder
  BV->>V: builder.buildApp()
  V-->>CLI: build artifacts
  note right of V: Replaces direct viteBuild(...) with builder.buildApp()
Loading
sequenceDiagram
  autonumber
  participant Preview as Preview runtime
  participant Loader as load-client-dev
  participant Vite as Vite Dev Server
  participant Server as RSC server handler
  participant Client as react-client

  Preview->>Loader: call loadClient()
  Loader->>Vite: fetch /@vite/invoke-react-client {payload}
  Vite->>Server: forward invocation
  Server-->>Loader: stream/module payload
  Loader->>Client: runner.import('.../react-client')
  Client-->>Preview: mount / rerender / unmount via streamed updates
  note over Loader,Server: browser-mode RSC virtual module & invoke endpoint flow
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly indicates the addition of a new Vite-based RSC framework variant for Next.js, which matches the pull request’s primary change of introducing the nextjs-vite-rsc framework.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4488c15 and b836217.

📒 Files selected for processing (1)
  • code/frameworks/nextjs-vite-rsc/src/repro.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/frameworks/nextjs-vite-rsc/src/repro.tsx
⏰ 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

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

Caution

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

⚠️ Outside diff range comments (4)
code/frameworks/nextjs-vite-rsc/template/stories/DynamicImport.stories.tsx (1)

19-21: Fix meta type to match exported component

meta.component is Component, but the satisfies clause targets Meta<typeof DynamicComponent>, which forces a different component type and fails under strict typing. Adjust the meta type to Component so the Storybook types compile cleanly.

-} satisfies Meta<typeof DynamicComponent>;
+} satisfies Meta<typeof Component>;
code/frameworks/nextjs-vite-rsc/src/preview.tsx (1)

21-26: Make global patches HMR-safe; avoid duplicate meta and multiple error handlers.

Hot reload will re-run this module, causing multiple <meta> tags and stacked listeners.

 function addNextHeadCount() {
-  const meta = document.createElement('meta');
-  meta.name = 'next-head-count';
-  meta.content = '0';
-  document.head.appendChild(meta);
+  if (!document.head.querySelector('meta[name="next-head-count"]')) {
+    const meta = document.createElement('meta');
+    meta.name = 'next-head-count';
+    meta.content = '0';
+    document.head.appendChild(meta);
+  }
 }
@@
-const origConsoleError = globalThis.console.error;
-globalThis.console.error = (...args: unknown[]) => {
-  const error = args[0];
-  if (isNextRouterError(error) || isAsyncClientComponentError(error)) {
-    return;
-  }
-  origConsoleError.apply(globalThis.console, args);
-};
+if (!(globalThis as any).__sb_nextjs_rsc_console_error_patched__) {
+  const origConsoleError = globalThis.console.error;
+  globalThis.console.error = (...args: unknown[]) => {
+    const error = args[0];
+    if (isNextRouterError(error) || isAsyncClientComponentError(error)) {
+      return;
+    }
+    origConsoleError.apply(globalThis.console, args);
+  };
+  (globalThis as any).__sb_nextjs_rsc_console_error_patched__ = true;
+}
@@
-globalThis.addEventListener('error', (ev: WindowEventMap['error']): void => {
-  if (isNextRouterError(ev.error) || isAsyncClientComponentError(ev.error)) {
-    ev.preventDefault();
-    return;
-  }
-});
+if (!(globalThis as any).__sb_nextjs_rsc_error_handler__) {
+  globalThis.addEventListener('error', (ev: WindowEventMap['error']): void => {
+    if (isNextRouterError(ev.error) || isAsyncClientComponentError(ev.error)) {
+      ev.preventDefault();
+      return;
+    }
+  });
+  (globalThis as any).__sb_nextjs_rsc_error_handler__ = true;
+}

Also applies to: 38-47, 49-54

code/frameworks/nextjs-vite-rsc/template/stories/Redirect.stories.tsx (1)

30-30: Typo: “suspress” → “suppress”.

-      // We will also suspress console.error logs for re the console.error logs for redirect in the next framework.
+      // We will also suppress console.error logs for redirect in the next framework.
code/frameworks/nextjs-vite-rsc/package.json (1)

107-111: Peer ranges allow React 16/17, which are incompatible with RSC.

Tighten react/react-dom to 18+ to prevent unsupported installs.

Apply:

-    "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
-    "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+    "react": "^18.2.0 || ^19.0.0",
+    "react-dom": "^18.2.0 || ^19.0.0",
🧹 Nitpick comments (22)
scripts/build/utils/entry-utils.ts (1)

74-78: Don’t hard‑code framework‑specific virtual modules in a global build util; make it data‑driven.

These Vite virtual IDs are specific to the RSC browser mode. Baking them into getExternal couples all packages to one framework and leaves a lingering TODO. Read extra externals from the package’s package.json instead.

Apply this diff:

-    // TODO where to place this
-    'virtual:vite-rsc-browser-mode/load-client',
-    'virtual:vite-rsc-browser-mode/build-server-references',
-    'virtual:vite-rsc-browser-mode/build-client-references'
+    // Allow packages to declare additional runtime externals (e.g., Vite virtual modules)
+    ...(packageJson.storybook?.runtimeExternals ?? [])

Follow‑up: add "storybook": { "runtimeExternals": ["virtual:vite-rsc-browser-mode/load-client","virtual:vite-rsc-browser-mode/build-server-references","virtual:vite-rsc-browser-mode/build-client-references"] } to the nextjs‑vite‑rsc package.json.

code/frameworks/nextjs-vite-rsc/src/rsc/client.tsx (1)

127-129: Consider rendering cache.rsc inside RedirectBoundary behind a flag.

Commenting it out may hide RSC regressions. Gate it with a prop to toggle between children and cache.rsc during testing.

code/frameworks/nextjs-vite-rsc/src/routing/page-router-provider.tsx (1)

13-15: Consider marking this provider as a client component.

Adding "use client" at the top avoids accidental server evaluation under RSC setups and clarifies intent. Optional but safer for router context providers.

code/frameworks/nextjs-vite-rsc/src/load-client-dev.tsx (1)

8-16: Ensure fetch transport surfaces server errors

Right now any non-2xx response will bubble up as a generic JSON parse failure, which hides the actual server-side error message. Please surface the HTTP status and body so devs can see what went wrong.

           const response = await fetch(
             '/@vite/invoke-react-client?' +
               new URLSearchParams({
                 data: JSON.stringify(payload),
               })
           );
-          return response.json();
+          if (!response.ok) {
+            const errorText = await response.text().catch(() => response.statusText);
+            throw new Error(
+              `Failed to invoke react client (${response.status} ${response.statusText}): ${errorText}`
+            );
+          }
+          return response.json();
code/frameworks/nextjs-vite-rsc/src/rsc/flight-router-state.test.ts (2)

5-6: Tighten the test name.

Rename to clearly state behavior, e.g., “builds flight router state for dynamic segments with query”.

-test('parse route and url to route true', () => {
+test('builds flight router state for /note/[id]/[slug] with ?a=1', () => {

5-46: Add root and mismatch tests & fix root fullPath handling

  • Add tests in flight-router-state.test.ts:
    test('root route builds __PAGE__ with query', () => {
      expect(buildFlightRouterState('/', '/', '?q=1')).toMatchInlineSnapshot();
    });
    test('throws on pattern/path mismatch', () => {
      expect(() => buildFlightRouterState('/a/[b]', '/a', '')).toThrowError();
    });
  • In flight-router-state.ts, adjust the root‐wrapper to drop the query from fullPath:
    -  const childState =
    -    patternSegs.length === 0
    -      ? makePageState('/' + (search.startsWith('?') ? search.slice(1) : search))
    -      : descend(0, '');
    +  const childState =
    +    patternSegs.length === 0
    +      ? makePageState('/' + (search.startsWith('?') ? '' : search))
    +      : descend(0, '');
code/lib/cli-storybook/src/sandbox-templates.ts (1)

252-274: Disambiguate template name and double‑check renderer mapping.

  • Name duplicates the non‑RSC template; add “, RSC” for clarity in dashboards.
  • Verify expected.renderer. Core usually maps Next/Vite frameworks to @storybook/react; ensure this matches how the new framework is registered, or adjust here. Mismatch will fail the template assertion.

Proposed tweak:

-  'nextjs-vite-rsc/default-ts': {
-    name: 'Next.js Latest (Vite | TypeScript)',
+  'nextjs-vite-rsc/default-ts': {
+    name: 'Next.js Latest (Vite | TypeScript, RSC)',
@@
-    expected: {
-      framework: '@storybook/nextjs-vite-rsc',
-      renderer: '@storybook/nextjs-vite-rsc',
-      builder: '@storybook/builder-vite',
-    },
+    expected: {
+      framework: '@storybook/nextjs-vite-rsc',
+      renderer: '@storybook/react', // if core maps RSC to react renderer
+      builder: '@storybook/builder-vite',
+    },

If the RSC package truly acts as a renderer, align core mapping accordingly instead. Please confirm with the mapping in framework-to-renderer.ts.

Optional: add a TODO next to inDevelopment: true to track removal before GA.

code/builders/builder-vite/src/build.ts (1)

18-18: Guard for older Vite versions and avoid passing null to createBuilder.

Vite 7 exports createBuilder() and a ViteBuilder.buildApp() entrypoint; add a fallback to viteBuild() for older Vite to keep backward compatibility, and pass undefined instead of null for options. (jsdocs.io)

-  const { build: viteBuild, mergeConfig, createBuilder } = await import('vite');
+  const { build: viteBuild, mergeConfig, createBuilder } = await import('vite');
@@
-  await (await createBuilder(await sanitizeEnvVars(options, finalConfig), null)).buildApp();
+  const sanitized = await sanitizeEnvVars(options, finalConfig);
+  if (typeof createBuilder === 'function') {
+    const builder = await createBuilder(sanitized, undefined);
+    await builder.buildApp();
+  } else {
+    // Fallback for older Vite without the builder API
+    await viteBuild(sanitized as any);
+  }
code/lib/create-storybook/src/bin/modernInputs.ts (1)

3-3: De-duplicate framework sources of truth.

This TODO keeps biting us—recommend importing this list from core (or generating it once) to prevent drift between CLI and core helpers.

code/frameworks/nextjs-vite-rsc/src/rsc/flight-router-state.ts (1)

15-20: Strict pattern matching rejects catch‑all/optional segments.

If you plan to support [...slug] / [[...slug]], the strict length equality will misbehave. Consider extending parsing or documenting the limitation.

code/frameworks/nextjs-vite-rsc/package.json (1)

118-120: Declare supported Node engines (Vite 7/Next 15 expect modern Node).

Recommend Node >= 18.17 (or >=20 if that’s your project policy) to match Vite/Next requirements.

Apply:

   "publishConfig": {
     "access": "public"
-  },
+  },
+  "engines": {
+    "node": ">=18.17.0"
+  },
code/frameworks/nextjs-vite-rsc/src/react-client.tsx (2)

42-51: Guard server callback updates until setter is ready

Even with the above fix, defensive code helps: if a callback races very early, setPayload can still be undefined. Add a guard or queue.

-    setPayload(payload);
+    if (setPayload) {
+      setPayload(payload);
+    }

69-76: Reset global server callback on unmount to avoid test leakage

setServerCallback is global; without cleanup, later tests may target a disposed root.

   function unmount() {
+    // prevent late server callbacks from targeting an unmounted root
+    ReactClient.setServerCallback(async () => {});
     reactRoot.unmount();
   }
code/frameworks/nextjs-vite-rsc/src/virtual.d.ts (2)

1-14: Tighten types: prefer unknown over any for dynamic imports

Avoid any in ambient declarations; unknown preserves type safety. As per coding guidelines.

 declare module 'virtual:vite-rsc-browser-mode/build-client-references' {
-  const default_: Record<string, () => Promise<any>>;
+  const default_: Record<string, () => Promise<unknown>>;
   export default default_;
 }
 
 declare module 'virtual:vite-rsc-browser-mode/build-server-references' {
-  const default_: Record<string, () => Promise<any>>;
+  const default_: Record<string, () => Promise<unknown>>;
   export default default_;
 }

18-24: Optional: declare global augmentation explicitly

To reduce chances of duplicate ambient merges in complex monorepos, consider wrapping ImportMetaEnv/ImportMeta in declare global { ... }. Current approach works with Vite’s client types; this is a nit.

code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts (3)

140-141: Double client build call — intentional?

await builder.build(reactServer); is invoked twice back-to-back before switching modes. If this is required for scanning/reference collection, add a comment; otherwise, remove the duplicate to cut build time.


46-50: noExternal: true in react_client may increase bundle size

Bundling all deps can slow builds and hinder caching. If not strictly needed, consider narrowing noExternal instead of blanket true.


112-121: Dev middleware endpoint is wide open

/@vite/invoke-react-client accepts arbitrary data and forwards to handleInvoke. Since this is dev-only, it's acceptable, but consider guarding with method check and basic validation to reduce footguns.

code/frameworks/nextjs-vite-rsc/src/preset.ts (2)

62-75: Flatten plugin arrays for clarity.

rscBrowserModePlugin() and vitestPluginNext() return arrays. Vite accepts nested PluginOptions, but flattening improves readability and avoids edge cases with custom merging.

-    plugins: [...(reactConfig?.plugins ?? []), rscBrowserModePlugin(), vitestPluginNext()],
+    plugins: [
+      ...(reactConfig?.plugins ?? []),
+      ...rscBrowserModePlugin(),
+      ...vitestPluginNext(),
+    ],

49-53: Use the final root for PostCSS search path.

You call postCssLoadConfig using config.root. If reactViteFinal adjusts root, prefer reactConfig.root to avoid false negatives.

-    const searchPath = typeof inlineOptions === 'string' ? inlineOptions : config.root;
+    const searchPath =
+      typeof inlineOptions === 'string' ? inlineOptions : (reactConfig as any)?.root ?? config.root;
code/frameworks/nextjs-vite-rsc/src/testing-library.tsx (2)

72-74: Guard against stale registry reads.

Avoid ! on the lookup; throw with context if missing.

-  } else {
-    root = mountedRootEntries.find((it) => it.container === container)!.root;
-  }
+  } else {
+    const entry = mountedRootEntries.find((it) => it.container === container);
+    if (!entry) throw new Error('Invariant: mounted root not found for container');
+    root = entry.root;
+  }

27-28: Unused option rerenderOnServerAction.

Either implement the behavior (auto‑rerender after server actions) or remove the option until needed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4483a66 and f670336.

⛔ Files ignored due to path filters (1)
  • code/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (47)
  • code/builders/builder-vite/src/build.ts (2 hunks)
  • code/core/src/cli/helpers.ts (1 hunks)
  • code/core/src/common/utils/framework-to-renderer.ts (1 hunks)
  • code/core/src/common/utils/get-storybook-info.ts (1 hunks)
  • code/core/src/types/modules/frameworks.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/build-config.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/package.json (4 hunks)
  • code/frameworks/nextjs-vite-rsc/project.json (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/export-mocks/headers/cookies.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/index.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/load-client-dev.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/portable-stories.ts (3 hunks)
  • code/frameworks/nextjs-vite-rsc/src/preset.ts (3 hunks)
  • code/frameworks/nextjs-vite-rsc/src/preview.tsx (3 hunks)
  • code/frameworks/nextjs-vite-rsc/src/react-client.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/routing/app-router-provider.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/routing/page-router-provider.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/rsc/client.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/rsc/flight-router-state.test.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/rsc/flight-router-state.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/testing-library.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/types.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/virtual.d.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/cli/ts/Button.stories.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/cli/ts/Header.stories.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/cli/ts/Page.stories.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/stories/DynamicImport.stories.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/stories/Font.stories.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/stories/GetImageProps.stories.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/stories/Head.stories.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/stories/Image.stories.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/stories/ImageLegacy.stories.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/stories/Link.stories.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/stories/Navigation.stories.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/stories/NextHeader.stories.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/stories/RSC.stories.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/stories/Redirect.stories.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/stories/Router.stories.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/stories/ServerActions.stories.tsx (1 hunks)
  • code/frameworks/nextjs-vite-rsc/template/stories/StyledJsx.stories.tsx (1 hunks)
  • code/lib/cli-storybook/src/sandbox-templates.ts (1 hunks)
  • code/lib/create-storybook/src/bin/modernInputs.ts (3 hunks)
  • code/lib/create-storybook/src/generators/baseGenerator.ts (1 hunks)
  • code/lib/create-storybook/src/ink/steps/checks/frameworkTest.tsx (1 hunks)
  • scripts/build/entry-configs.ts (2 hunks)
  • scripts/build/utils/entry-utils.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx}

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

Adhere to ESLint and Prettier rules across all JS/TS source files

Files:

  • code/frameworks/nextjs-vite-rsc/src/index.ts
  • code/frameworks/nextjs-vite-rsc/template/cli/ts/Header.stories.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc/flight-router-state.ts
  • code/lib/create-storybook/src/bin/modernInputs.ts
  • code/lib/create-storybook/src/generators/baseGenerator.ts
  • code/frameworks/nextjs-vite-rsc/src/virtual.d.ts
  • code/frameworks/nextjs-vite-rsc/template/cli/ts/Page.stories.ts
  • code/lib/cli-storybook/src/sandbox-templates.ts
  • code/frameworks/nextjs-vite-rsc/template/stories/Redirect.stories.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/Head.stories.tsx
  • code/core/src/types/modules/frameworks.ts
  • code/core/src/cli/helpers.ts
  • code/frameworks/nextjs-vite-rsc/template/stories/ImageLegacy.stories.tsx
  • code/core/src/common/utils/get-storybook-info.ts
  • code/frameworks/nextjs-vite-rsc/template/stories/StyledJsx.stories.tsx
  • code/frameworks/nextjs-vite-rsc/src/types.ts
  • code/lib/create-storybook/src/ink/steps/checks/frameworkTest.tsx
  • code/frameworks/nextjs-vite-rsc/build-config.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc/client.tsx
  • code/frameworks/nextjs-vite-rsc/src/react-client.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/RSC.stories.tsx
  • code/core/src/common/utils/framework-to-renderer.ts
  • code/frameworks/nextjs-vite-rsc/src/load-client-dev.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/Font.stories.tsx
  • code/frameworks/nextjs-vite-rsc/src/routing/app-router-provider.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/Image.stories.tsx
  • scripts/build/entry-configs.ts
  • code/frameworks/nextjs-vite-rsc/template/stories/Router.stories.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/ServerActions.stories.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/Link.stories.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/Navigation.stories.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/DynamicImport.stories.tsx
  • code/builders/builder-vite/src/build.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc/flight-router-state.test.ts
  • code/frameworks/nextjs-vite-rsc/template/cli/ts/Button.stories.ts
  • code/frameworks/nextjs-vite-rsc/src/export-mocks/headers/cookies.ts
  • code/frameworks/nextjs-vite-rsc/template/stories/NextHeader.stories.tsx
  • code/frameworks/nextjs-vite-rsc/src/portable-stories.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts
  • code/frameworks/nextjs-vite-rsc/src/preview.tsx
  • code/frameworks/nextjs-vite-rsc/src/testing-library.tsx
  • scripts/build/utils/entry-utils.ts
  • code/frameworks/nextjs-vite-rsc/src/routing/page-router-provider.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/GetImageProps.stories.tsx
  • code/frameworks/nextjs-vite-rsc/src/preset.ts
**/*.{ts,tsx}

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

Fix type errors and prefer precise typings instead of using any or suppressions, consistent with strict mode

Files:

  • code/frameworks/nextjs-vite-rsc/src/index.ts
  • code/frameworks/nextjs-vite-rsc/template/cli/ts/Header.stories.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc/flight-router-state.ts
  • code/lib/create-storybook/src/bin/modernInputs.ts
  • code/lib/create-storybook/src/generators/baseGenerator.ts
  • code/frameworks/nextjs-vite-rsc/src/virtual.d.ts
  • code/frameworks/nextjs-vite-rsc/template/cli/ts/Page.stories.ts
  • code/lib/cli-storybook/src/sandbox-templates.ts
  • code/frameworks/nextjs-vite-rsc/template/stories/Redirect.stories.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/Head.stories.tsx
  • code/core/src/types/modules/frameworks.ts
  • code/core/src/cli/helpers.ts
  • code/frameworks/nextjs-vite-rsc/template/stories/ImageLegacy.stories.tsx
  • code/core/src/common/utils/get-storybook-info.ts
  • code/frameworks/nextjs-vite-rsc/template/stories/StyledJsx.stories.tsx
  • code/frameworks/nextjs-vite-rsc/src/types.ts
  • code/lib/create-storybook/src/ink/steps/checks/frameworkTest.tsx
  • code/frameworks/nextjs-vite-rsc/build-config.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc/client.tsx
  • code/frameworks/nextjs-vite-rsc/src/react-client.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/RSC.stories.tsx
  • code/core/src/common/utils/framework-to-renderer.ts
  • code/frameworks/nextjs-vite-rsc/src/load-client-dev.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/Font.stories.tsx
  • code/frameworks/nextjs-vite-rsc/src/routing/app-router-provider.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/Image.stories.tsx
  • scripts/build/entry-configs.ts
  • code/frameworks/nextjs-vite-rsc/template/stories/Router.stories.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/ServerActions.stories.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/Link.stories.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/Navigation.stories.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/DynamicImport.stories.tsx
  • code/builders/builder-vite/src/build.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc/flight-router-state.test.ts
  • code/frameworks/nextjs-vite-rsc/template/cli/ts/Button.stories.ts
  • code/frameworks/nextjs-vite-rsc/src/export-mocks/headers/cookies.ts
  • code/frameworks/nextjs-vite-rsc/template/stories/NextHeader.stories.tsx
  • code/frameworks/nextjs-vite-rsc/src/portable-stories.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts
  • code/frameworks/nextjs-vite-rsc/src/preview.tsx
  • code/frameworks/nextjs-vite-rsc/src/testing-library.tsx
  • scripts/build/utils/entry-utils.ts
  • code/frameworks/nextjs-vite-rsc/src/routing/page-router-provider.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/GetImageProps.stories.tsx
  • code/frameworks/nextjs-vite-rsc/src/preset.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/frameworks/nextjs-vite-rsc/src/rsc/flight-router-state.test.ts
**/*.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/frameworks/nextjs-vite-rsc/src/rsc/flight-router-state.test.ts
🧠 Learnings (13)
📚 Learning: 2025-09-25T09:21:27.274Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-25T09:21:27.274Z
Learning: Applies to test-storybooks/** : Maintain test configurations and assets under test-storybooks/ for Storybook testing

Applied to files:

  • code/frameworks/nextjs-vite-rsc/template/cli/ts/Header.stories.ts
  • code/frameworks/nextjs-vite-rsc/template/cli/ts/Page.stories.ts
  • code/core/src/common/utils/get-storybook-info.ts
  • code/frameworks/nextjs-vite-rsc/template/stories/RSC.stories.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/Font.stories.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/Image.stories.tsx
  • code/frameworks/nextjs-vite-rsc/template/stories/ServerActions.stories.tsx
  • code/frameworks/nextjs-vite-rsc/template/cli/ts/Button.stories.ts
  • code/frameworks/nextjs-vite-rsc/src/preset.ts
📚 Learning: 2025-09-17T08:11:47.197Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.197Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Group related mocks together

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
  • code/frameworks/nextjs-vite-rsc/package.json
📚 Learning: 2025-09-17T08:11:47.196Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.196Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid mocking only a subset of required dependencies

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
  • code/frameworks/nextjs-vite-rsc/package.json
📚 Learning: 2025-09-17T08:11:47.196Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.196Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Mock all required dependencies that the test subject uses

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.197Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.197Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Document complex mock behaviors

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.197Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.197Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Keep mock implementations simple and focused

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.196Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.196Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Mock all required properties and methods that the test subject uses

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.196Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.196Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid inline mock implementations within test cases

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.196Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.196Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Mock at the highest level of abstraction needed

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.196Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.196Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Implement mock behaviors in beforeEach blocks

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.196Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.196Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use vi.mocked() to access and implement mock behaviors

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-24T09:39:39.209Z
Learnt from: ndelangen
PR: storybookjs/storybook#32507
File: code/core/src/manager/globals/globals-module-info.ts:25-33
Timestamp: 2025-09-24T09:39:39.209Z
Learning: In Storybook, storybook/actions/decorator is a preview-only entrypoint and should not be included in manager globals configuration. The duplicatedKeys array in code/core/src/manager/globals/globals-module-info.ts is specifically for manager-side externalization, not preview entrypoints.

Applied to files:

  • code/frameworks/nextjs-vite-rsc/src/preview.tsx
  • code/frameworks/nextjs-vite-rsc/src/preset.ts
📚 Learning: 2025-09-17T07:31:04.432Z
Learnt from: ndelangen
PR: storybookjs/storybook#32484
File: code/core/package.json:326-326
Timestamp: 2025-09-17T07:31:04.432Z
Learning: In Storybook's core package, dependencies like `open` are bundled into the final distribution during the build process, so they should remain in devDependencies rather than being moved to dependencies. End users don't need these packages as separate runtime dependencies since they're included in the bundled code.

Applied to files:

  • code/frameworks/nextjs-vite-rsc/src/preset.ts
🧬 Code graph analysis (7)
code/frameworks/nextjs-vite-rsc/src/types.ts (1)
code/core/src/types/modules/core-common.ts (1)
  • CompatibleString (658-658)
code/frameworks/nextjs-vite-rsc/src/rsc/client.tsx (1)
code/frameworks/nextjs-vite-rsc/src/rsc/flight-router-state.ts (1)
  • buildFlightRouterState (6-64)
code/frameworks/nextjs-vite-rsc/src/react-client.tsx (1)
code/frameworks/nextjs-vite-rsc/src/testing-library.tsx (2)
  • RenderConfiguration (101-104)
  • initialize (115-133)
code/frameworks/nextjs-vite-rsc/src/rsc/flight-router-state.test.ts (1)
code/frameworks/nextjs-vite-rsc/src/rsc/flight-router-state.ts (1)
  • buildFlightRouterState (6-64)
code/frameworks/nextjs-vite-rsc/src/preview.tsx (4)
code/frameworks/nextjs-vite-rsc/src/rsc/client.tsx (1)
  • NextRouter (38-79)
code/renderers/react/src/types.ts (2)
  • ReactRenderer (8-12)
  • RenderContext (6-6)
code/frameworks/nextjs-vite-rsc/src/react-client.tsx (1)
  • initialize (95-109)
code/frameworks/nextjs-vite-rsc/src/testing-library.tsx (2)
  • initialize (115-133)
  • renderServer (17-88)
code/frameworks/nextjs-vite-rsc/src/testing-library.tsx (2)
code/frameworks/nextjs-vite-rsc/src/react-client.tsx (4)
  • TestingLibraryClientRoot (14-14)
  • FetchRsc (16-19)
  • RscPayload (9-12)
  • initialize (95-109)
code/frameworks/nextjs-vite-rsc/src/load-client-dev.tsx (1)
  • loadClient (3-25)
code/frameworks/nextjs-vite-rsc/src/preset.ts (1)
code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts (2)
  • rscBrowserModePlugin (4-209)
  • vitestPluginNext (211-260)
⏰ 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 (36)
code/frameworks/nextjs-vite-rsc/template/cli/ts/Header.stories.ts (1)

1-1: Import path matches the new framework surface.

Good catch switching the Storybook generics to @storybook/nextjs-vite-rsc; this keeps the template aligned with the RSC framework package and preserves the existing TS typing flow.

code/frameworks/nextjs-vite-rsc/template/cli/ts/Button.stories.ts (1)

1-1: Updated import is consistent with the RSC variant.

The Meta/StoryObj types now come from the correct @storybook/nextjs-vite-rsc package, ensuring the CLI template stays in sync with the new framework.

code/frameworks/nextjs-vite-rsc/template/stories/Link.stories.tsx (1)

3-3: RSC package import looks correct.

Bringing the Storybook types in via @storybook/nextjs-vite-rsc matches the framework rename and keeps the story typing intact.

code/frameworks/nextjs-vite-rsc/template/stories/Font.stories.tsx (1)

1-1: Framework-specific import verified.

Meta/StoryObj now resolve from @storybook/nextjs-vite-rsc, so this template will type-check against the new package without any extra tweaks.

code/frameworks/nextjs-vite-rsc/template/stories/Image.stories.tsx (1)

3-3: Import realignment to RSC package is spot on.

Switching the type import to @storybook/nextjs-vite-rsc keeps the image stories aligned with the new Next.js/Vite RSC framework entry.

code/frameworks/nextjs-vite-rsc/template/stories/ServerActions.stories.tsx (3)

45-49: Fix typo and noisy comment.

Use “suppress” and simplify the sentence.

Apply this diff:

-      // We will also suspress console.error logs for re the console.error logs for redirect in the next framework.
+      // We will also suppress console.error logs for redirects in the Next framework.

3-6: Verify root exports for Meta/StoryObj — package.json confirms that ./cache.mock, ./headers.mock and ./navigation.mock are exported; please ensure the package’s main entrypoint also exports the Meta and StoryObj types to prevent template breakage.


76-80: Use play or loaders instead of unsupported beforeEach on StoryObj
The Story (and StoryObj) type does not include a beforeEach hook—only play, decorators, loaders, and params are supported. Move your setup into a loader or inline in play to keep types strict.

code/frameworks/nextjs-vite-rsc/project.json (1)

2-2: LGTM: name aligns with directory and avoids collisions.

code/frameworks/nextjs-vite-rsc/src/portable-stories.ts (1)

37-41: Docs updated to @storybook/nextjs-vite-rsc — good; ensure top‑level exports exist.

The examples now point to the new package. Double‑check the package’s export map includes setProjectAnnotations/composeStory/composeStories at the root to prevent surprises for users.

Also applies to: 71-75, 115-118

code/frameworks/nextjs-vite-rsc/template/stories/Redirect.stories.tsx (1)

53-56: Remove unused step param in play.

-export const SingletonStateGetsInvalidatedAfterRedirecting: StoryObj = {
-  play: async ({ canvasElement, step }) => {
+export const SingletonStateGetsInvalidatedAfterRedirecting: StoryObj = {
+  play: async ({ canvasElement }) => {
     const canvas = within(canvasElement);
     await userEvent.click(canvas.getByRole('button'));
   },
 };
code/lib/create-storybook/src/generators/baseGenerator.ts (1)

202-209: LGTM: framework templates updated to include nextjs-vite-rsc.

code/frameworks/nextjs-vite-rsc/template/stories/GetImageProps.stories.tsx (1)

3-3: Import path aligned with the new framework variant

Switching the Meta/StoryObj types to @storybook/nextjs-vite-rsc keeps this template consistent with the RSC-aware framework package. Nice cleanup.

code/frameworks/nextjs-vite-rsc/template/stories/NextHeader.stories.tsx (1)

1-3: Correct mocks/types source for RSC stories

Pointing both the story types and the header/cookie mocks at the RSC package ensures the story exercises the right singleton state for the new framework.

code/core/src/types/modules/frameworks.ts (1)

8-8: Supported frameworks list updated

Adding 'nextjs-vite-rsc' here keeps the public typings in sync with the new framework entry.

code/frameworks/nextjs-vite-rsc/src/routing/app-router-provider.tsx (1)

8-8: Router mock import matches the RSC bundle

Updating getRouter to pull from the RSC mock package maintains the singleton behavior when consumers use the new framework entrypoint.

code/core/src/common/utils/framework-to-renderer.ts (1)

14-14: Renderer mapping covers the new framework

Mapping 'nextjs-vite-rsc' to the React renderer keeps framework detection consistent across tooling.

code/frameworks/nextjs-vite-rsc/template/stories/Router.stories.tsx (2)

3-4: Imports correctly switched to RSC variant.

Type and mock imports point to @storybook/nextjs-vite-rsc; looks consistent with the new framework.


85-96: Fix duplicate/incorrect step assertions; first step should validate useRouter (DOM), not the Router singleton.

Both steps assert Router.*. Make the first step assert via the rendered DOM (which is driven by useRouter() in Component) to match its title and avoid redundancy.

-    await step('Router property overrides should be available in useRouter fn', async () => {
-      await expect(Router.pathname).toBe('/hello');
-      await expect(Router.query).toEqual({ foo: 'bar' });
-    });
+    await step('Router property overrides should be available in useRouter fn', async () => {
+      await expect(await canvas.findByText('pathname: /hello')).toBeInTheDocument();
+      await expect(await canvas.findByText('foo: bar')).toBeInTheDocument();
+    });

If toBeInTheDocument isn’t available in your test environment, replace with a truthiness check on the returned elements.

code/frameworks/nextjs-vite-rsc/template/stories/RSC.stories.tsx (2)

4-4: RSC typings source aligned.

Switch to @storybook/nextjs-vite-rsc for Meta/StoryObj is correct and consistent with the new framework.


22-28: Confirm authoritative RSC parameter and override behavior.

Found react.rsc: true in code/renderers/react/src/entry-preview-rsc.tsx and in story templates, while this story sets nextjs.rsc: false; the repository search did not reveal the parameter-merge/reader logic that determines precedence — confirm which key is authoritative and that story-level nextjs.rsc correctly overrides react.rsc.

Locations:

  • code/renderers/react/src/entry-preview-rsc.tsx — parameters.react.rsc: true
  • code/frameworks/nextjs-vite-rsc/template/stories/RSC.stories.tsx — story parameters.nextjs.rsc: false
code/frameworks/nextjs-vite-rsc/src/routing/page-router-provider.tsx (1)

9-9: Import path updated to RSC mock — looks good.

Keeps the mock singleton semantics by importing from the package entry.

code/core/src/cli/helpers.ts (1)

151-152: Mapping the new framework to the Vite builder makes sense.

Using CoreBuilder.Vite for nextjs-vite-rsc matches the expectations set by the rest of the framework wiring.

code/core/src/common/utils/get-storybook-info.ts (1)

43-44: Good to see the framework package registered.

Adding '@storybook/nextjs-vite-rsc' ensures detection flows resolve the new framework correctly.

scripts/build/entry-configs.ts (1)

32-113: Build entry wiring looks correct.

Importing the new build config and exposing it through buildEntries keeps the package buildable alongside the existing Next.js variants.

code/frameworks/nextjs-vite-rsc/build-config.ts (1)

31-41: New browser entry points are aligned with the exported surface.

Registering load-client-dev, react-client, and rsc/client here will let the builder emit the corresponding bundles.

code/frameworks/nextjs-vite-rsc/template/stories/Navigation.stories.tsx (1)

3-4: Template imports now target the correct package.

Switching to @storybook/nextjs-vite-rsc keeps the scaffolded stories in sync with the new framework.

code/lib/create-storybook/src/ink/steps/checks/frameworkTest.tsx (1)

17-19: nextjs-vite-rsc support fully wired. Verified inclusion in Framework type and supportedFrameworksNames mapping.

code/frameworks/nextjs-vite-rsc/template/stories/Head.stories.tsx (1)

3-4: Approve import change: Meta and StoryObj are re-exported from @storybook/react in @storybook/nextjs-vite-rsc via export * from '@storybook/react', so these imports are valid.

code/frameworks/nextjs-vite-rsc/package.json (4)

2-2: Package name fix looks good.


14-14: Homepage path points to the wrong directory.

Apply:

-  "homepage": "https://github.com/storybookjs/storybook/tree/next/code/frameworks/nextjs-vite",
+  "homepage": "https://github.com/storybookjs/storybook/tree/next/code/frameworks/nextjs-vite-rsc",

21-22: Repository.directory is incorrect.

Apply:

-    "directory": "code/frameworks/nextjs"
+    "directory": "code/frameworks/nextjs-vite-rsc"

96-96: Verified compatibility for @vitejs/plugin-rsc@^0.4.31: supports Vite 7+ (requires Node ^20.19.0 or ≥22.12.0); Next.js 15 (React 19) isn’t officially supported out-of-the-box—custom integration per project is required.

code/lib/create-storybook/src/bin/modernInputs.ts (1)

10-10: Approve nextjs-vite-rsc integration
Wiring is complete and type-safe—nextjs-vite-rsc appears in all ID lists, package mappings, display names, core types, and build configs.

code/frameworks/nextjs-vite-rsc/src/preset.ts (2)

30-35: Correct preview entrypoint for RSC.

Appending @storybook/nextjs-vite-rsc/preview looks right and addresses earlier mis-reference to the non‑RSC preview.


26-27: Verify framework-to-renderer fallback is correct
nextjs-vite-rsc is mapped to "react" in framework-to-renderer (core/src/common/utils/framework-to-renderer.ts:14); leaving renderer undefined in the preset will correctly fall back to React at runtime.

Comment on lines 38 to 44
export const optimizeViteDeps = [
'@storybook/nextjs-vite/navigation.mock',
'@storybook/nextjs-vite/router.mock',
'@storybook/nextjs-vite > styled-jsx',
'@storybook/nextjs-vite > styled-jsx/style',
'next/dist/client/components/is-next-router-error',
'@storybook/nextjs-vite-rsc/navigation.mock',
'@storybook/nextjs-vite-rsc/router.mock',
'@storybook/nextjs-vite-rsc > styled-jsx',
'@storybook/nextjs-vite-rsc > styled-jsx/style',
];
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 | 🔴 Critical

🧩 Analysis chain

Double‑check optimizeDeps include syntax for transitive deps.

Entries like @storybook/nextjs-vite-rsc > styled-jsx/... > styled-jsx/style are unconventional for Vite’s optimizeDeps.include. Ensure the builder/preset pipeline consumes this list and expands the dep > subdep form as intended (or switch to plain bare imports if not needed). Note that vitestPluginNext also includes is-next-router-error, which may duplicate this include.

Run:


🏁 Script executed:

#!/bin/bash
# See how other frameworks structure this and whether '>' is used project-wide
rg -n -C2 'optimizeViteDeps|optimizeDeps' code/ | sed -n '1,200p'
rg -nP '>\s*styled-jsx' code/

Length of output: 7058


Replace unconventional '>’ entries with bare imports
Vite’s optimizeDeps.include won’t expand "@storybook/nextjs-vite-rsc > styled-jsx". In code/frameworks/nextjs-vite-rsc/src/preset.ts update:

  • '@storybook/nextjs-vite-rsc > styled-jsx''styled-jsx'
  • '@storybook/nextjs-vite-rsc > styled-jsx/style''styled-jsx/style'
    Also remove the duplicate 'next/dist/client/components/is-next-router-error' since it’s already included by vitestPluginNext.
🤖 Prompt for AI Agents
In code/frameworks/nextjs-vite-rsc/src/preset.ts around lines 38 to 44, the
optimizeViteDeps array contains unconventional entries using the " > " scoping
and a duplicate entry; replace '@storybook/nextjs-vite-rsc > styled-jsx' with
'styled-jsx' and '@storybook/nextjs-vite-rsc > styled-jsx/style' with
'styled-jsx/style', and remove the duplicate
'next/dist/client/components/is-next-router-error' entry so optimizeViteDeps
uses the bare imports and has no duplicates.

import { createRouter } from '@storybook/nextjs-vite/router.mock';
import { NextRouter } from '@storybook/nextjs-vite-rsc/rsc/client';

import { isNextRouterError } from 'next/dist/client/components/is-next-router-error';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Deep import from Next internals is brittle; confirm stability or add fallback.

next/dist/client/components/is-next-router-error is not a public API and may change across Next 15.x.


🌐 Web query:

Is `next/dist/client/components/is-next-router-error` considered stable across Next.js 14/15, and what is the recommended way to detect Next router redirect/not-found errors?

💡 Result:

Short answer: No — import paths under next/dist (e.g. next/dist/client/components/is-next-router-error or other next/dist/* helpers) are internal implementation details and not a stable public API across major versions. Relying on them is fragile across Next.js 14 → 15 upgrades. [1][2][3]

Recommended approaches

  • For redirects and not-found behaviour, use the public next/navigation functions: redirect(), permanentRedirect(), and notFound() and the documented app routing/error conventions. These functions intentionally throw special Next errors that the framework handles. Use them instead of importing internal helpers. [4][5]
  • If you must catch and rethrow Next's internal redirect/not-found errors inside a try/catch, either:
    • Re-throw when you detect the sentinel (examples in the docs/StackOverflow show checking the error message like "NEXT_REDIRECT"/"NEXT_NOT_FOUND"), or
    • Use the community/work-in-progress helpers (isRedirectError / isNotFoundError) only knowing they come from next/dist and are not stable — prefer a pattern that rethrows Next errors rather than swallowing them. See the GitHub discussion requesting exported helpers. [6][7][1]

Concrete guidance

  • Prefer the public APIs in next/navigation and the app router error conventions (error.js / not-found.js). Catching Next's special errors is an advanced workaround; if done, ensure you detect & rethrow Next's redirect/not-found errors so Next can perform the correct routing behaviour. [4][5][6]

References

  • Next.js docs: redirect / notFound (public APIs). [4][5]
  • Next.js docs: error handling / app router conventions. [3]
  • Discussion / examples showing isRedirectError / isNotFoundError live under next/dist and community requests to export them. [6][7]

If you want, I can paste example code patterns for safely catching/rethrowing redirect/not-found errors.


Avoid deep import of Next internals: isNextRouterError from next/dist/* isn’t a public API and may break in Next 15. Use the public redirect(), permanentRedirect(), and notFound() functions from next/navigation (which throw Next’s special errors) or, if you must catch those errors, detect and rethrow based on the sentinel messages (NEXT_REDIRECT/NEXT_NOT_FOUND).

🤖 Prompt for AI Agents
In code/frameworks/nextjs-vite-rsc/src/preview.tsx around line 10, the file
imports isNextRouterError from next/dist/... which is a deep internal import and
must be removed; replace usage by relying on Next's public navigation helpers
(redirect, permanentRedirect, notFound) from next/navigation which throw the
framework sentinel errors, or if you need to catch those thrown errors
explicitly, detect them by matching the sentinel identifiers (e.g. check
error.message or a sentinel string for "NEXT_REDIRECT" / "NEXT_NOT_FOUND") and
rethrow non-sentinel errors—remove the internal import and update error handling
to use the public APIs or sentinel-string checks.

Comment on lines 56 to 61
export const decorators: Decorator[] = [
(Story, context) => (
<NextRouter>
<Story />
</NextRouter>
),
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

Decorator ignores story nextjs.navigation params; pass route/url to NextRouter.

Without wiring context.parameters.nextjs.navigation, stories can’t control the initial URL/path.

-export const decorators: Decorator[] = [
-  (Story, context) => (
-    <NextRouter>
-      <Story />
-    </NextRouter>
-  ),
+export const decorators: Decorator[] = [
+  (Story, context) => {
+    const nav = context.parameters?.nextjs?.navigation ?? {};
+    const url = nav.url ?? (nav.pathname ? `${nav.pathname}${nav.search ?? ''}` : undefined);
+    return (
+      <NextRouter url={url} route={nav.pathname}>
+        <Story />
+      </NextRouter>
+    );
+  },

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In code/frameworks/nextjs-vite-rsc/src/preview.tsx around lines 56 to 61, the
Storybook decorator always renders <NextRouter> without passing the story's
nextjs.navigation parameters, so stories cannot control the initial route;
update the decorator to read context.parameters.nextjs?.navigation (or the
appropriate property name) and pass its route/url value into NextRouter (with a
sensible default when undefined) so each story can specify the initial path for
NextRouter.

Comment on lines 68 to 70
export const loaders: LoaderFunction<ReactRenderer> = async ({ globals, parameters }) => {
const { router, appDirectory } = parameters.nextjs ?? {};
if (appDirectory) {
createNavigation(router);
} else {
createRouter({
locale: globals.locale,
...(router as Record<string, unknown>),
});
}
initialize({ rootOptions: parameters.react?.rootOptions });
};
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 | 🔴 Critical

loaders must be an array; exporting a single function under loaders will not run.

Preview loaders is LoaderFunction[]. Exporting a bare function will likely be ignored at runtime.

-export const loaders: LoaderFunction<ReactRenderer> = async ({ globals, parameters }) => {
-  initialize({ rootOptions: parameters.react?.rootOptions });
-};
+export const loaders: LoaderFunction<ReactRenderer>[] = [
+  async ({ parameters }) => {
+    initialize({ rootOptions: parameters.react?.rootOptions });
+  },
+];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const loaders: LoaderFunction<ReactRenderer> = async ({ globals, parameters }) => {
const { router, appDirectory } = parameters.nextjs ?? {};
if (appDirectory) {
createNavigation(router);
} else {
createRouter({
locale: globals.locale,
...(router as Record<string, unknown>),
});
}
initialize({ rootOptions: parameters.react?.rootOptions });
};
export const loaders: LoaderFunction<ReactRenderer>[] = [
async ({ parameters }) => {
initialize({ rootOptions: parameters.react?.rootOptions });
},
];
🤖 Prompt for AI Agents
In code/frameworks/nextjs-vite-rsc/src/preview.tsx around lines 68 to 70, the
export currently exposes a single LoaderFunction but Preview expects loaders to
be an array (LoaderFunction[]); change the export to export an array containing
the async function (i.e., wrap the existing function in an array) so the runtime
will execute it; keep the function body as-is (initialize with rootOptions) and
ensure the exported name is still loaders.

Comment on lines 70 to 72
mountedRootEntries.push({ container, root });
mountedContainers.add(container);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Persist ownership flag.

-    mountedRootEntries.push({ container, root });
+    mountedRootEntries.push({ container, root, createdByUs });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mountedRootEntries.push({ container, root });
mountedContainers.add(container);
} else {
mountedRootEntries.push({ container, root, createdByUs });
mountedContainers.add(container);
} else {

Comment on lines +76 to +87
return {
container,
baseElement,
unmount: root.unmount,
rerender: async (newUi) => {
ui = newUi;
await root.rerender();
},
asFragment: () => {
return document.createRange().createContextualFragment(container.innerHTML);
},
};
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

Unmount should also deregister the root and remove auto‑created containers to avoid leaks.

Currently unmount delegates to the client root only; the registry stays populated and containers remain in DOM if they weren’t attached to document.body.

-    unmount: root.unmount,
+    unmount: async () => {
+      await root.unmount();
+      mountedContainers.delete(container);
+      const idx = mountedRootEntries.findIndex((it) => it.container === container);
+      if (idx !== -1) {
+        const [entry] = mountedRootEntries.splice(idx, 1);
+        if (entry.createdByUs && container.parentNode) {
+          container.parentNode.removeChild(container);
+        }
+      }
+    },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return {
container,
baseElement,
unmount: root.unmount,
rerender: async (newUi) => {
ui = newUi;
await root.rerender();
},
asFragment: () => {
return document.createRange().createContextualFragment(container.innerHTML);
},
};
return {
container,
baseElement,
unmount: async () => {
await root.unmount();
mountedContainers.delete(container);
const idx = mountedRootEntries.findIndex((it) => it.container === container);
if (idx !== -1) {
const [entry] = mountedRootEntries.splice(idx, 1);
if (entry.createdByUs && container.parentNode) {
container.parentNode.removeChild(container);
}
}
},
rerender: async (newUi) => {
ui = newUi;
await root.rerender();
},
asFragment: () => {
return document.createRange().createContextualFragment(container.innerHTML);
},
};
🤖 Prompt for AI Agents
In code/frameworks/nextjs-vite-rsc/src/testing-library.tsx around lines 76–87,
update the returned unmount so it not only calls root.unmount but also
deregisters the root from the internal registry and removes any auto-created
containers to prevent DOM leaks: after calling root.unmount(), remove the entry
for this root/container from the registry (e.g. delete from whatever Map or
registry holding roots), and if the container was created by the helper
(trackable via a flag/property on the element or by checking it is not attached
to document.body) remove it from the DOM; ensure the registry cleanup happens
even if root.unmount throws by performing deregistration/removal in a finally
block or equivalent.

Comment on lines +90 to +99
export async function cleanup() {
for (const { root, container } of mountedRootEntries) {
await root.unmount();
if (container.parentNode === document.body) {
document.body.removeChild(container);
}
}
mountedRootEntries.length = 0;
mountedContainers.clear();
}
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

cleanup doesn’t remove containers when baseElement ≠ document.body. Track ownership and always remove owned containers.

This can leave stray nodes when users pass a custom baseElement.

-export async function cleanup() {
-  for (const { root, container } of mountedRootEntries) {
-    await root.unmount();
-    if (container.parentNode === document.body) {
-      document.body.removeChild(container);
-    }
-  }
-  mountedRootEntries.length = 0;
-  mountedContainers.clear();
-}
+export async function cleanup() {
+  for (const { root, container, createdByUs } of mountedRootEntries) {
+    await root.unmount();
+    if (createdByUs && container.parentNode) {
+      container.parentNode.removeChild(container);
+    }
+  }
+  mountedRootEntries.length = 0;
+  mountedContainers.clear();
+}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In code/frameworks/nextjs-vite-rsc/src/testing-library.tsx around lines 90 to
99, cleanup only removes containers when their parent is document.body, which
leaves stray nodes if tests used a custom baseElement; update cleanup to remove
containers that this utility created regardless of which baseElement they were
appended to by checking ownership (e.g., consult mountedContainers or an owned
flag stored on each mountedRootEntry) and call parent.removeChild(container)
when the container is owned by this library, then clear mountedRootEntries and
mountedContainers as before.

Comment on lines +113 to +114
const client = await loadClient();

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

Top‑level await on loadClient can break scan/build phases and triggers network access at import time.

During RSC scan builds (manager.isScanBuild) the virtual module returns a no‑op function; await loadClient() could yield undefined, breaking downstream calls. Also, importing this file will immediately make a dev‑server request. Lazily load and cache the client instead.

-const client = await loadClient();
+let clientPromise: Promise<typeof import('./react-client')> | null = null;
+async function getClient() {
+  return (clientPromise ??= loadClient());
+}

Then replace calls:

-    root = await client.createTestingLibraryClientRoot({
+    const client = await getClient();
+    root = await client.createTestingLibraryClientRoot({
       container,
       config,
       fetchRsc,
     });

And:

-  client.initialize();
+  (await getClient()).initialize();
🤖 Prompt for AI Agents
In code/frameworks/nextjs-vite-rsc/src/testing-library.tsx around lines 113-114,
remove the top-level await of loadClient() because it triggers network access at
import time and can return undefined during RSC scan builds; instead implement a
lazy-loading, cached getter function (e.g., getClientAsync) that checks a cached
promise/value and only calls loadClient() on first demand, handles
manager.isScanBuild by returning the no-op fallback when loadClient() yields
undefined, and update all call sites to await this getter rather than using a
top-level client variable.

Comment on lines +118 to +130
ReactServer.setRequireModule({
load: (id) => {
if (import.meta.env.__vite_rsc_build__) {
const import_ = buildServerReferences[id];
if (!import_) {
throw new Error(`invalid server reference: ${id}`);
}
return import_();
} else {
return __vite_rsc_raw_import__(/* @vite-ignore */ id);
}
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Confirm __vite_rsc_raw_import__ availability in dev.

If the Vite RSC plugin doesn’t define this helper in your dev environment, calls will fail. Consider falling back to dynamic import(/* @vite-ignore */ id) if unavailable.

-      } else {
-        return __vite_rsc_raw_import__(/* @vite-ignore */ id);
-      }
+      } else {
+        const raw = (globalThis as any).__vite_rsc_raw_import__;
+        return raw ? raw(/* @vite-ignore */ id) : import(/* @vite-ignore */ id);
+      }

To confirm:


🌐 Web query:

Does @vitejs/plugin-rsc expose a global function named __vite_rsc_raw_import__ at runtime for server-side module loading in dev?

💡 Result:

Short answer: No. @vitejs/plugin-rsc does not expose a runtime global named vite_rsc_raw_import; it provides the environment API (e.g. import.meta.viteRsc.loadModule) and the RSC runtime modules instead. See the plugin docs/README. [1][2]

Sources:

  • @vitejs/plugin-rsc (npm) — environment API / import.meta.viteRsc.loadModule. [1]
  • Vite plugins listing / plugin README (plugin-rsc description). [2]

Replace __vite_rsc_raw_import__ with import.meta.viteRsc.loadModule
@vitejs/plugin-rsc doesn’t expose a global __vite_rsc_raw_import__ at runtime. In dev, call import.meta.viteRsc.loadModule(id) and fall back to import(/* @vite-ignore */ id) if undefined.

-      } else {
-        return __vite_rsc_raw_import__(/* @vite-ignore */ id);
-      }
+      } else {
+        const { viteRsc } = import.meta;
+        return viteRsc?.loadModule
+          ? viteRsc.loadModule(id)
+          : import(/* @vite-ignore */ id);
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ReactServer.setRequireModule({
load: (id) => {
if (import.meta.env.__vite_rsc_build__) {
const import_ = buildServerReferences[id];
if (!import_) {
throw new Error(`invalid server reference: ${id}`);
}
return import_();
} else {
return __vite_rsc_raw_import__(/* @vite-ignore */ id);
}
},
});
ReactServer.setRequireModule({
load: (id) => {
if (import.meta.env.__vite_rsc_build__) {
const import_ = buildServerReferences[id];
if (!import_) {
throw new Error(`invalid server reference: ${id}`);
}
return import_();
} else {
const { viteRsc } = import.meta;
return viteRsc?.loadModule
? viteRsc.loadModule(id)
: import(/* @vite-ignore */ id);
}
},
});
🤖 Prompt for AI Agents
In code/frameworks/nextjs-vite-rsc/src/testing-library.tsx around lines 118 to
130, the loader currently calls a non-existent global __vite_rsc_raw_import__;
update the else branch to first attempt import.meta.viteRsc.loadModule(id)
(guarding for undefined with optional chaining or a conditional) and, if that
returns undefined or the API is not present, fall back to dynamic import(/*
@vite-ignore */ id). Ensure the code uses the plugin-provided
import.meta.viteRsc.loadModule in dev and only uses the dynamic import as the
fallback.

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

🧹 Nitpick comments (1)
code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts (1)

218-220: Consider preserving some process.env values

Setting process.env to an empty object might break code that expects environment variables like NODE_ENV. Consider preserving essential variables.

           define: {
-            'process.env': JSON.stringify({}),
+            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
+            'process.env': JSON.stringify({ NODE_ENV: process.env.NODE_ENV || 'development' }),
             __dirname: JSON.stringify(null),
           },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f670336 and 4e0feb2.

📒 Files selected for processing (8)
  • code/frameworks/nextjs-vite-rsc/build-config.ts (2 hunks)
  • code/frameworks/nextjs-vite-rsc/package.json (4 hunks)
  • code/frameworks/nextjs-vite-rsc/src/index.ts (2 hunks)
  • code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/rsc/headers.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/rsc/navigation.react-server.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/rsc/navigation.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/testing-library.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/frameworks/nextjs-vite-rsc/src/index.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}

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

Adhere to ESLint and Prettier rules across all JS/TS source files

Files:

  • code/frameworks/nextjs-vite-rsc/src/rsc/navigation.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc/navigation.react-server.ts
  • code/frameworks/nextjs-vite-rsc/src/testing-library.tsx
  • code/frameworks/nextjs-vite-rsc/src/rsc/headers.ts
  • code/frameworks/nextjs-vite-rsc/build-config.ts
**/*.{ts,tsx}

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

Fix type errors and prefer precise typings instead of using any or suppressions, consistent with strict mode

Files:

  • code/frameworks/nextjs-vite-rsc/src/rsc/navigation.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc/navigation.react-server.ts
  • code/frameworks/nextjs-vite-rsc/src/testing-library.tsx
  • code/frameworks/nextjs-vite-rsc/src/rsc/headers.ts
  • code/frameworks/nextjs-vite-rsc/build-config.ts
🧠 Learnings (10)
📚 Learning: 2025-09-17T08:11:47.197Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.197Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Group related mocks together

Applied to files:

  • code/frameworks/nextjs-vite-rsc/package.json
  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.196Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.196Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid mocking only a subset of required dependencies

Applied to files:

  • code/frameworks/nextjs-vite-rsc/package.json
  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.196Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.196Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Mock all required dependencies that the test subject uses

Applied to files:

  • code/frameworks/nextjs-vite-rsc/package.json
  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.197Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.197Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Document complex mock behaviors

Applied to files:

  • code/frameworks/nextjs-vite-rsc/package.json
  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.197Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.197Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Keep mock implementations simple and focused

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.196Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.196Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Mock all required properties and methods that the test subject uses

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.196Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.196Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid inline mock implementations within test cases

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.196Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.196Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Mock at the highest level of abstraction needed

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.196Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.196Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Implement mock behaviors in beforeEach blocks

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
📚 Learning: 2025-09-17T08:11:47.196Z
Learnt from: CR
PR: storybookjs/storybook#0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-09-17T08:11:47.196Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use vi.mocked() to access and implement mock behaviors

Applied to files:

  • code/frameworks/nextjs-vite-rsc/build-config.ts
🧬 Code graph analysis (1)
code/frameworks/nextjs-vite-rsc/src/testing-library.tsx (2)
code/frameworks/nextjs-vite-rsc/src/react-client.tsx (4)
  • TestingLibraryClientRoot (14-14)
  • FetchRsc (16-19)
  • RscPayload (9-12)
  • initialize (95-109)
code/frameworks/nextjs-vite-rsc/src/load-client-dev.tsx (1)
  • loadClient (3-25)
⏰ 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 (10)
code/frameworks/nextjs-vite-rsc/package.json (2)

14-14: Update homepage to the new package path.

The homepage still links to the old nextjs-vite package. Please point it at the nextjs-vite-rsc directory to keep the published metadata accurate.

-  "homepage": "https://github.com/storybookjs/storybook/tree/next/code/frameworks/nextjs-vite",
+  "homepage": "https://github.com/storybookjs/storybook/tree/next/code/frameworks/nextjs-vite-rsc",

21-21: Fix the repository directory entry.

repository.directory still references code/frameworks/nextjs. Please update it to the new code/frameworks/nextjs-vite-rsc location so tooling can resolve the correct folder.

-    "directory": "code/frameworks/nextjs"
+    "directory": "code/frameworks/nextjs-vite-rsc"
code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts (2)

136-147: Ensure manager.isScanBuild and write flags are restored on error

If a build throws, flags remain mutated, affecting subsequent builds. Wrap in try/finally.

       async buildApp(builder) {
         const reactServer = builder.environments.client!;
         const reactClient = builder.environments['react_client']!;
-        manager.isScanBuild = true;
-        reactServer.config.build.write = false;
-        await builder.build(reactServer);
-        await builder.build(reactServer);
-        manager.isScanBuild = false;
-        reactServer.config.build.write = true;
-        await builder.build(reactClient);
-        await builder.build(reactServer);
+        const prevScan = manager.isScanBuild;
+        const prevWrite = reactServer.config.build.write;
+        manager.isScanBuild = true;
+        reactServer.config.build.write = false;
+        try {
+          await builder.build(reactServer);
+          await builder.build(reactServer);
+          manager.isScanBuild = false;
+          reactServer.config.build.write = true;
+          await builder.build(reactClient);
+          await builder.build(reactServer);
+        } finally {
+          manager.isScanBuild = prevScan;
+          reactServer.config.build.write = prevWrite;
+        }
       },

159-166: Hard-coded absolute public path breaks non-root base URLs

import("/storybook-static/react_client/index.mjs") fails when base is not / (e.g., GitHub Pages, custom CDN path). Use import.meta.env.BASE_URL.

-            return `export default async () => import("/storybook-static/react_client/index.mjs")`;
+            return `export default async () => import(/* @vite-ignore */ \`\${import.meta.env.BASE_URL}storybook-static/react_client/index.mjs\`)`;
code/frameworks/nextjs-vite-rsc/src/testing-library.tsx (6)

12-16: Track container ownership to support correct unmount/cleanup behavior

Augment the registry to track whether we created the container.

-const mountedRootEntries: {
-  container: Container;
-  root: TestingLibraryClientRoot;
-}[] = [];
+const mountedRootEntries: {
+  container: Container;
+  root: TestingLibraryClientRoot;
+  createdByUs: boolean;
+}[] = [];

36-37: Record whether the container was created by this helper

This flag drives proper cleanup.

-  container ??= baseElement.appendChild(document.createElement('div'));
+  const createdByUs = !container;
+  container ??= baseElement.appendChild(document.createElement('div'));

68-79: Unmount should also deregister the root and remove auto-created containers to avoid leaks

Currently unmount delegates to the client root only; the registry stays populated and containers remain in DOM if they weren't attached to document.body.

-    unmount: root.unmount,
+    unmount: async () => {
+      await root.unmount();
+      mountedContainers.delete(container);
+      const idx = mountedRootEntries.findIndex((it) => it.container === container);
+      if (idx !== -1) {
+        const [entry] = mountedRootEntries.splice(idx, 1);
+        if (entry.createdByUs && container.parentNode) {
+          container.parentNode.removeChild(container);
+        }
+      }
+    },

82-91: cleanup doesn't remove containers when baseElement ≠ document.body. Track ownership and always remove owned containers

This can leave stray nodes when users pass a custom baseElement.

-export async function cleanup() {
-  for (const { root, container } of mountedRootEntries) {
-    await root.unmount();
-    if (container.parentNode === document.body) {
-      document.body.removeChild(container);
-    }
-  }
-  mountedRootEntries.length = 0;
-  mountedContainers.clear();
-}
+export async function cleanup() {
+  for (const { root, container, createdByUs } of mountedRootEntries) {
+    await root.unmount();
+    if (createdByUs && container.parentNode) {
+      container.parentNode.removeChild(container);
+    }
+  }
+  mountedRootEntries.length = 0;
+  mountedContainers.clear();
+}

105-106: Top-level await on loadClient can break scan/build phases and triggers network access at import time

During RSC scan builds (manager.isScanBuild) the virtual module returns a no-op function; await loadClient() could yield undefined, breaking downstream calls. Also, importing this file will immediately make a dev-server request. Lazily load and cache the client instead.

-const client = await loadClient();
+let clientPromise: Promise<typeof import('./react-client')> | null = null;
+async function getClient() {
+  return (clientPromise ??= loadClient());
+}

Then replace calls:

-    root = await client.createTestingLibraryClientRoot({
+    const client = await getClient();
+    root = await client.createTestingLibraryClientRoot({
       container,
       config,
       fetchRsc,
     });

And:

-  client.initialize();
+  (await getClient()).initialize();

110-122: Replace __vite_rsc_raw_import__ with import.meta.viteRsc.loadModule

@vitejs/plugin-rsc doesn't expose a global __vite_rsc_raw_import__ at runtime. In dev, call import.meta.viteRsc.loadModule(id) and fall back to import(/* @vite-ignore */ id) if undefined.

       } else {
-        return __vite_rsc_raw_import__(/* @vite-ignore */ id);
+        const { viteRsc } = import.meta;
+        return viteRsc?.loadModule
+          ? viteRsc.loadModule(id)
+          : import(/* @vite-ignore */ id);
       }

Comment on lines +114 to +124
server.middlewares.use(async (req, res, next) => {
const url = new URL(req.url ?? '/', 'https://any.local');
if (url.pathname === '/@vite/invoke-react-client') {
const payload = JSON.parse(url.searchParams.get('data')!);
const result = await server.environments['react_client']!.hot.handleInvoke(payload);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(result));
return;
}
next();
});
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 | 🟡 Minor

Add error handling for JSON parsing in middleware

The middleware directly parses JSON from query parameters without validation. This could crash the server if data is missing or malformed.

       configureServer(server) {
         server.middlewares.use(async (req, res, next) => {
           const url = new URL(req.url ?? '/', 'https://any.local');
           if (url.pathname === '/@vite/invoke-react-client') {
-            const payload = JSON.parse(url.searchParams.get('data')!);
-            const result = await server.environments['react_client']!.hot.handleInvoke(payload);
-            res.setHeader('Content-Type', 'application/json');
-            res.end(JSON.stringify(result));
-            return;
+            try {
+              const data = url.searchParams.get('data');
+              if (!data) {
+                res.statusCode = 400;
+                res.end(JSON.stringify({ error: 'Missing data parameter' }));
+                return;
+              }
+              const payload = JSON.parse(data);
+              const result = await server.environments['react_client']!.hot.handleInvoke(payload);
+              res.setHeader('Content-Type', 'application/json');
+              res.end(JSON.stringify(result));
+            } catch (error) {
+              res.statusCode = 500;
+              res.setHeader('Content-Type', 'application/json');
+              res.end(JSON.stringify({ error: error.message }));
+            }
+            return;
           }
           next();
         });
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
server.middlewares.use(async (req, res, next) => {
const url = new URL(req.url ?? '/', 'https://any.local');
if (url.pathname === '/@vite/invoke-react-client') {
const payload = JSON.parse(url.searchParams.get('data')!);
const result = await server.environments['react_client']!.hot.handleInvoke(payload);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(result));
return;
}
next();
});
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
const url = new URL(req.url ?? '/', 'https://any.local');
if (url.pathname === '/@vite/invoke-react-client') {
try {
const data = url.searchParams.get('data');
if (!data) {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Missing data parameter' }));
return;
}
const payload = JSON.parse(data);
const result = await server.environments['react_client']!.hot.handleInvoke(payload);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(result));
} catch (error) {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: (error as Error).message }));
}
return;
}
next();
});
},
🤖 Prompt for AI Agents
In code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts around lines 114 to 124,
the middleware assumes the query param "data" exists and is valid JSON which can
crash the server; wrap retrieval and JSON.parse in a try/catch, first check that
url.searchParams.get('data') is non-null/non-empty, attempt to parse and if
parsing fails send a 400 response with Content-Type: application/json and a safe
error payload (e.g. { error: 'invalid_json' }) then return, and only call
server.environments['react_client']!.hot.handleInvoke when parsing succeeds;
ensure any unexpected exceptions are caught and result in a 500 response (or
next() as appropriate) to avoid crashing the process.

Comment on lines +62 to +66
const root = await client.createTestingLibraryClientRoot({
container,
config,
fetchRsc,
});
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 | 🔴 Critical

Add the root to mountedRootEntries for proper cleanup

The created root is never added to mountedRootEntries, so it won't be cleaned up by the cleanup() function.

   const root = await client.createTestingLibraryClientRoot({
     container,
     config,
     fetchRsc,
   });
+  mountedRootEntries.push({ container, root, createdByUs });
+  mountedContainers.add(container);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const root = await client.createTestingLibraryClientRoot({
container,
config,
fetchRsc,
});
const root = await client.createTestingLibraryClientRoot({
container,
config,
fetchRsc,
});
mountedRootEntries.push({ container, root, createdByUs });
mountedContainers.add(container);

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e0feb2 and f72091c.

📒 Files selected for processing (1)
  • code/frameworks/nextjs-vite-rsc/src/rsc/headers.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}

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

Adhere to ESLint and Prettier rules across all JS/TS source files

Files:

  • code/frameworks/nextjs-vite-rsc/src/rsc/headers.ts
**/*.{ts,tsx}

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

Fix type errors and prefer precise typings instead of using any or suppressions, consistent with strict mode

Files:

  • code/frameworks/nextjs-vite-rsc/src/rsc/headers.ts
⏰ 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

Comment on lines 5 to 7
const headersAdapter = new HeadersAdapter({});
export const headers = () => headersAdapter;
export const cookies = () => RequestCookiesAdapter.seal(new RequestCookies(headersAdapter));
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 30, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Match Next 15’s promise-like headers/cookies contract.

In Next 15 the headers() and cookies() helpers are async Promise-like utilities—you can await them or pass them to React.use, while still calling methods synchronously thanks to the Promise being prototyped with the adapter. Returning the raw adapters here breaks the React.use(headers())/React.use(cookies()) pathway that the codemods now emit and that the docs recommend. (nextjs.org)

Please wrap the adapters in a thenable so we preserve both behaviors, e.g.:

-const headersAdapter = new HeadersAdapter({});
-export const headers = () => headersAdapter;
-export const cookies = () => RequestCookiesAdapter.seal(new RequestCookies(headersAdapter));
+const headersAdapter = new HeadersAdapter({});
+
+const wrapThenable = <T extends object>(value: T) =>
+  Object.assign(
+    value,
+    { then: (onFulfilled: (val: T) => unknown) => Promise.resolve(value).then(onFulfilled) }
+  );
+
+export const headers = () => wrapThenable(headersAdapter);
+
+export const cookies = () =>
+  wrapThenable(RequestCookiesAdapter.seal(new RequestCookies(headersAdapter)));

This mirrors how Next implements the async upgrade: await headers() keeps working, React.use(headers()) succeeds, and existing synchronous call sites still see the adapter methods. (nextjs.org)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const headersAdapter = new HeadersAdapter({});
export const headers = () => headersAdapter;
export const cookies = () => RequestCookiesAdapter.seal(new RequestCookies(headersAdapter));
// code/frameworks/nextjs-vite-rsc/src/rsc/headers.ts
const headersAdapter = new HeadersAdapter({});
const wrapThenable = <T extends object>(value: T) =>
Object.assign(
value,
{
then: (onFulfilled: (val: T) => unknown) =>
Promise.resolve(value).then(onFulfilled),
}
);
export const headers = () => wrapThenable(headersAdapter);
export const cookies = () =>
wrapThenable(RequestCookiesAdapter.seal(new RequestCookies(headersAdapter)));
🤖 Prompt for AI Agents
In code/frameworks/nextjs-vite-rsc/src/rsc/headers.ts around lines 5 to 7, the
functions currently return raw adapter instances which breaks Next 15’s
promise-like contract; wrap each adapter in a thenable (a Promise that resolves
to the adapter and also exposes the adapter methods on the Promise object) so
callers can either await them or pass them into React.use while still calling
adapter methods synchronously. Concretely, create a Promise for each helper that
resolves to the adapter and assign the adapter’s methods/properties onto that
Promise (or prototype) so it is thenable and also immediately callable; ensure
cookies() uses the same headersAdapter instance when constructing the
RequestCookies so behavior stays consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

why not

const wrapThenable = <T extends object>(value: T) => Object.assign(value, Promise.resolve(value));

Copy link
Contributor

Choose a reason for hiding this comment

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

@kasperpeulen Good question! Your approach is more concise, but there's a subtle difference:

Your approach:

Object.assign(value, Promise.resolve(value))

Copies all enumerable own properties from the Promise instance onto value. However, Promise methods like then, catch, finally are on Promise.prototype, not as own properties. So Object.assign won't actually copy them—you'd end up with just value plus whatever own properties the Promise instance has (which is typically none that are enumerable).

The original approach:

Object.assign(value, { then: (onFulfilled) => Promise.resolve(value).then(onFulfilled) })

Explicitly adds a then method, which is the minimum requirement for a thenable. This is sufficient because:

  • React.use() only needs .then() to recognize it as a thenable
  • await also only requires .then()
  • The adapter methods remain directly callable on the object

Better alternative if you want all Promise methods:

const wrapThenable = <T extends object>(value: T) => 
  Object.setPrototypeOf(value, Promise.resolve(value));

This would make value inherit from a resolved Promise, giving you access to all Promise methods. However, this changes the prototype chain which might have unexpected side effects with the adapter's own prototype methods.

Recommendation: Stick with the explicit then approach—it's clear, minimal, and does exactly what's needed without potential prototype conflicts or confusion about what properties are being copied.

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

♻️ Duplicate comments (8)
code/frameworks/nextjs-vite-rsc/src/preview.tsx (4)

9-9: Deep import from Next.js internals is fragile across versions.

isNextRouterError is imported from next/dist/client/components/is-next-router-error, which is an internal API that may break in Next.js updates. The public alternative is to use redirect(), permanentRedirect(), and notFound() from next/navigation, which throw special errors, or detect these errors by checking for sentinel strings like "NEXT_REDIRECT" or "NEXT_NOT_FOUND" in the error message.

Consider replacing with:

-import { isNextRouterError } from 'next/dist/client/components/is-next-router-error';
+
+// Detect Next.js router errors by sentinel strings
+function isNextRouterError(error: unknown): boolean {
+  if (typeof error === 'object' && error !== null) {
+    const message = String((error as any).message || '');
+    return message.includes('NEXT_REDIRECT') || message.includes('NEXT_NOT_FOUND');
+  }
+  return false;
+}

Based on learnings


67-69: Loader must be wrapped in an array to execute.

The loaders export is typed as a single LoaderFunction<ReactRenderer>, but Storybook's preview API expects LoaderFunction<ReactRenderer>[]. The current code will not execute the loader at runtime.

Apply this diff:

-export const loaders: LoaderFunction<ReactRenderer> = async ({ globals, parameters }) => {
+export const loaders: LoaderFunction<ReactRenderer>[] = [async ({ globals, parameters }) => {
   initialize({ rootOptions: parameters.react?.rootOptions });
-};
+}];

100-105: Story function is incorrectly invoked as a React component.

React.createElement(storyFn, context) treats the story function as a component constructor and passes context as props, rather than calling it as a function with the context argument. This breaks the story's access to args, parameters, and other context properties.

Apply this diff:

   applyDecorators: (
     storyFn: LegacyStoryFn<ReactRenderer>,
     decorators: DecoratorFunction<ReactRenderer>[]
   ): LegacyStoryFn<ReactRenderer> => {
-    return defaultDecorateStory((context) => React.createElement(storyFn, context), decorators);
+    return defaultDecorateStory((context) => storyFn(context), decorators);
   },

107-122: Story function is incorrectly mounted as a React component.

unboundStoryFn is a story function that should be invoked with storyContext to produce a React element. The current code treats it as a component (<Story {...storyContext} />), which spreads the context as props instead of passing it as a function argument, breaking the story's rendering.

Apply this diff:

   renderToCanvas: async function (
     { storyContext, unboundStoryFn, showMain }: RenderContext<ReactRenderer>,
     canvasElement: ReactRenderer['canvasElement']
   ) {
-    const Story = unboundStoryFn;
-
-    const { unmount } = await renderServer(<Story {...storyContext} />, {
+    const element = unboundStoryFn(storyContext);
+    const { unmount } = await renderServer(element, {
       container: canvasElement,
     });
 
     showMain();
 
     return async () => {
       await unmount();
     };
   },
code/frameworks/nextjs-vite-rsc/src/rsc/headers.ts (1)

1-17: Match Next 15’s thenable headers/cookies contract.

Next 15 treats headers()/cookies() as thenables; returning the raw adapters breaks codemodded React.use(headers())/await cookies(). Wrap the adapters in a thenable (and seal cookies) so both sync and async call sites behave like real Next.

-import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies';
+import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies';
+import { RequestCookiesAdapter } from 'next/dist/server/web/spec-extension/adapters/request-cookies';
 import { HeadersAdapter } from 'next/dist/server/web/spec-extension/adapters/headers';
 import { fn } from 'storybook/test';
 
 let headersAdapter = new HeadersAdapter({});
-let requestCookies = new RequestCookies(headersAdapter);
+let requestCookies = RequestCookiesAdapter.seal(new RequestCookies(headersAdapter));
+
+const wrapThenable = <T extends object>(value: T) =>
+  Object.assign(value, {
+    then: (onFulfilled: (val: T) => unknown) => Promise.resolve(value).then(onFulfilled),
+  });
 
-export const cookies = fn(() => requestCookies);
-export const headers = fn(() => headersAdapter);
+export const cookies = fn(() => wrapThenable(requestCookies));
+export const headers = fn(() => wrapThenable(headersAdapter));
 
 const originalRestore = cookies.mockRestore.bind(null);
 
 cookies.mockRestore = () => {
   originalRestore();
   headersAdapter = new HeadersAdapter({});
-  requestCookies = new RequestCookies(headersAdapter);
+  requestCookies = RequestCookiesAdapter.seal(new RequestCookies(headersAdapter));
 };
code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts (3)

121-128: Guard /@vite/invoke-react-client JSON parsing.

A bad/missing data query currently crashes the dev server. Validate the param and handle parse errors so we return 4xx/5xx responses instead of throwing.

         server.middlewares.use(async (req, res, next) => {
           const url = new URL(req.url ?? '/', 'https://any.local');
           if (url.pathname === '/@vite/invoke-react-client') {
-            const payload = JSON.parse(url.searchParams.get('data')!);
-            const result = await server.environments['react_client']!.hot.handleInvoke(payload);
-            res.setHeader('Content-Type', 'application/json');
-            res.end(JSON.stringify(result));
-            return;
+            try {
+              const data = url.searchParams.get('data');
+              if (!data) {
+                res.statusCode = 400;
+                res.setHeader('Content-Type', 'application/json');
+                res.end(JSON.stringify({ error: 'Missing data parameter' }));
+                return;
+              }
+              const payload = JSON.parse(data);
+              const result = await server.environments['react_client']!.hot.handleInvoke(payload);
+              res.setHeader('Content-Type', 'application/json');
+              res.end(JSON.stringify(result));
+            } catch (error) {
+              res.statusCode = 500;
+              res.setHeader('Content-Type', 'application/json');
+              res.end(
+                JSON.stringify({
+                  error: error instanceof Error ? error.message : 'Unknown error',
+                })
+              );
+            }
+            return;
           }
           next();
         });

146-154: Restore manager flags even when builds throw.

If any build step rejects, manager.isScanBuild and reactServer.config.build.write stay flipped, breaking the next build. Wrap the sequence in try/finally and reset the flags afterward.

         const reactServer = builder.environments.client!;
         const reactClient = builder.environments['react_client']!;
-        manager.isScanBuild = true;
-        reactServer.config.build.write = false;
-        await builder.build(reactServer);
-        await builder.build(reactServer);
-        manager.isScanBuild = false;
-        reactServer.config.build.write = true;
-        await builder.build(reactClient);
-        await builder.build(reactServer);
+        const prevScan = manager.isScanBuild;
+        const prevWrite = reactServer.config.build.write;
+        manager.isScanBuild = true;
+        reactServer.config.build.write = false;
+        try {
+          await builder.build(reactServer);
+          await builder.build(reactServer);
+          manager.isScanBuild = false;
+          reactServer.config.build.write = true;
+          await builder.build(reactClient);
+          await builder.build(reactServer);
+        } finally {
+          manager.isScanBuild = prevScan;
+          reactServer.config.build.write = prevWrite;
+        }

167-172: Make the load-client import base-aware.

Hard-coding /storybook-static/... breaks when Vite serves under a non-root base. Use import.meta.env.BASE_URL so the generated import respects the configured base path.

         if (id === '\0virtual:vite-rsc-browser-mode/load-client') {
           if (manager.isScanBuild) {
             return `export default async () => {}`;
           } else {
-            return `export default async () => import("/storybook-static/react_client/index.mjs")`;
+            return `export default async () => import(/* @vite-ignore */ \`\${import.meta.env.BASE_URL}storybook-static/react_client/index.mjs\`)`;
           }
         }
🧹 Nitpick comments (1)
code/frameworks/nextjs-vite-rsc/src/index.ts (1)

13-13: Remove commented code.

Commented-out exports create ambiguity. Since this is an intentional API change for the new nextjs-vite-rsc framework, remove the comment entirely.

Apply this diff:

-// export * from '@storybook/react';
 export * from './types';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f72091c and 45e55fb.

📒 Files selected for processing (5)
  • code/frameworks/nextjs-vite-rsc/src/index.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/preview.tsx (3 hunks)
  • code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/rsc/headers.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/src/rsc/navigation.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}

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

Adhere to ESLint and Prettier rules across all JS/TS source files

Files:

  • code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts
  • code/frameworks/nextjs-vite-rsc/src/index.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc/navigation.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc/headers.ts
  • code/frameworks/nextjs-vite-rsc/src/preview.tsx
**/*.{ts,tsx}

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

Fix type errors and prefer precise typings instead of using any or suppressions, consistent with strict mode

Files:

  • code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts
  • code/frameworks/nextjs-vite-rsc/src/index.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc/navigation.ts
  • code/frameworks/nextjs-vite-rsc/src/rsc/headers.ts
  • code/frameworks/nextjs-vite-rsc/src/preview.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-24T09:39:39.233Z
Learnt from: ndelangen
PR: storybookjs/storybook#32507
File: code/core/src/manager/globals/globals-module-info.ts:25-33
Timestamp: 2025-09-24T09:39:39.233Z
Learning: In Storybook, storybook/actions/decorator is a preview-only entrypoint and should not be included in manager globals configuration. The duplicatedKeys array in code/core/src/manager/globals/globals-module-info.ts is specifically for manager-side externalization, not preview entrypoints.

Applied to files:

  • code/frameworks/nextjs-vite-rsc/src/preview.tsx
🧬 Code graph analysis (2)
code/frameworks/nextjs-vite-rsc/src/index.ts (4)
code/frameworks/nextjs-vite-rsc/src/vite-plugin/index.ts (1)
  • storybookNextJsPlugin (7-7)
code/core/src/csf/csf-factories.ts (2)
  • PreviewAddon (62-63)
  • InferTypes (32-34)
code/frameworks/nextjs-vite-rsc/src/preview.tsx (1)
  • preview (125-125)
code/frameworks/nextjs-vite-rsc/src/types.ts (1)
  • NextJsTypes (62-64)
code/frameworks/nextjs-vite-rsc/src/preview.tsx (2)
code/renderers/react/src/types.ts (1)
  • ReactRenderer (8-12)
code/frameworks/nextjs-vite-rsc/src/testing-library.tsx (2)
  • initialize (107-125)
  • renderServer (17-80)
⏰ 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 (4)
code/frameworks/nextjs-vite-rsc/src/index.ts (3)

2-2: LGTM! Alias resolves naming conflict.

The import alias definePreviewBase correctly avoids shadowing the exported definePreview function while maintaining clear intent.


18-20: Module path correction looks good.

The module declaration now correctly references @storybook/nextjs-vite-rsc/vite-plugin, fixing the previous mismatch flagged in earlier review.


26-26: LGTM! Consistent with import alias.

The call to definePreviewBase correctly matches the aliased import and maintains the intended composition pattern.

code/frameworks/nextjs-vite-rsc/src/preview.tsx (1)

90-98: LGTM: Standard Preview render implementation.

The render function correctly extracts the component from context, validates its presence, and renders it with args.

Comment on lines +11 to +17
const originalRestore = cookies.mockRestore.bind(null);

cookies.mockRestore = () => {
originalRestore();
headersAdapter = new HeadersAdapter({});
requestCookies = new RequestCookies(headersAdapter);
};
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 | 🔴 Critical

Preserve mockRestore’s this binding.

Binding the original mockRestore to null makes it throw when invoked (Vitest/Jest expect the mock as this). Call it with the mock instance instead so restores keep working.

-const originalRestore = cookies.mockRestore.bind(null);
+const originalRestore = cookies.mockRestore;
 
 cookies.mockRestore = () => {
-  originalRestore();
+  originalRestore.call(cookies);
   headersAdapter = new HeadersAdapter({});
   requestCookies = new RequestCookies(headersAdapter);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const originalRestore = cookies.mockRestore.bind(null);
cookies.mockRestore = () => {
originalRestore();
headersAdapter = new HeadersAdapter({});
requestCookies = new RequestCookies(headersAdapter);
};
const originalRestore = cookies.mockRestore;
cookies.mockRestore = () => {
originalRestore.call(cookies);
headersAdapter = new HeadersAdapter({});
requestCookies = new RequestCookies(headersAdapter);
};
🤖 Prompt for AI Agents
In code/frameworks/nextjs-vite-rsc/src/rsc/headers.ts around lines 11 to 17, the
code binds cookies.mockRestore to null which loses the mock’s this context and
breaks restore; change the call to preserve the mock instance as this (either
bind to cookies instead of null or invoke originalRestore with .call(cookies))
so mockRestore executes with the correct this and then continue recreating
headersAdapter and requestCookies.

@storybook-bot
Copy link
Contributor

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

@storybook-bot
Copy link
Contributor

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

1 similar comment
@storybook-bot
Copy link
Contributor

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

@storybook-bot
Copy link
Contributor

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

1 similar comment
@storybook-bot
Copy link
Contributor

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

@storybook-bot
Copy link
Contributor

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

@storybook-bot
Copy link
Contributor

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

@storybook-bot
Copy link
Contributor

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

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

Caution

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

⚠️ Outside diff range comments (6)
code/core/src/manager/components/sidebar/HighlightStyles.tsx (1)

9-10: Remove the @ts-expect-error and fix the component’s prop type.

The imported Highlight is defined as ItemRef | null, so destructuring refId/itemId triggers a type error. Narrow the props to ItemRef (e.g. FC<ItemRef>) or guard against null before destructuring.

code/core/src/common/utils/sync-main-preview-addons.test.ts (1)

12-12: Missing spy: true in vi.mock() call.

Per coding guidelines for Vitest tests, use vi.mock() with the spy: true option for all mocks.

As per coding guidelines.

Apply this diff:

-vi.mock('./get-addon-annotations');
+vi.mock('./get-addon-annotations', { spy: true });
code/core/src/components/components/tooltip/WithTooltip.tsx (1)

178-224: Specify dependencies for the useEffect hook
The effect currently has no dependency array—causing it to re-run on every render and risking stale closures or performance issues. Add onVisibilityChange to its dependencies:

-  });
+  }, [onVisibilityChange]);
code/addons/docs/src/blocks/components/Preview.tsx (1)

157-165: Add type guards and return type annotation.

The function has type safety concerns:

  1. The cast to ReactElement at line 159 is unsafe without validating that children is actually a valid React element
  2. Missing explicit return type annotation
  3. The props check at line 160 doesn't prevent accessing invalid elements

Apply this diff to improve type safety:

-function getChildProps(children: ReactNode) {
+function getChildProps(children: ReactNode): Record<string, any> | null {
   if (Children.count(children) === 1) {
-    const elt = children as ReactElement;
-    if (elt.props) {
+    if (React.isValidElement(children)) {
+      const elt = children;
+      if (elt.props) {
-      return elt.props;
+        return elt.props;
+      }
     }
   }
   return null;
 }

This adds proper element validation using React.isValidElement() and an explicit return type.

MIGRATION.md (1)

570-579: Convert the import.meta.resolve() result before assigning it to core.builder.

import.meta.resolve() returns a file URL (e.g. file:///.../index.js), not a plain filesystem path. Passing that URL straight into core.builder will violate the “fully resolved path” requirement and break any filesystem operations Storybook performs on it. Please update the snippet to convert the URL to a path first (for example with fileURLToPath(new URL('@storybook/builder-vite', import.meta.url))) before assigning it to core.builder.

code/core/src/preview-api/modules/store/StoryStore.ts (1)

288-341: Restore correct extract output and drop debug logs

Two problems here:

  1. The new console.log calls will spam consumer builds; please remove them.
  2. Setting storyId to entry.parent rewrites every story’s exported ID to its component ID (since normal stories always have a parent component). That breaks consumers relying on the actual story ID. Keep storyId as the original storyId, only overriding it when you truly need special handling (e.g., for entry.subtype === 'test').
♻️ Duplicate comments (1)
code/core/src/common/js-package-manager/BUNProxy.ts (1)

8-8: Verify the 'empathic/find' package exists.

This import has the same issue as noted in JsPackageManager.ts line 6. The empathic/find package could not be found in public npm registries. Please ensure the package is available before this code can run.

🧹 Nitpick comments (33)
code/addons/docs/src/blocks/blocks/external/ExternalPreview.ts (1)

78-78: Address the TODO: clarify story test handling requirements.

The TODO comment indicates uncertain requirements for story test handling. This gap could result in:

  • Story tests not being indexed or registered properly
  • Missing change notifications when story tests are added/modified
  • Incomplete integration with the broader story test feature

Please clarify:

  1. Should story tests be added to storyIndex.entries with a different subtype (e.g., 'test')?
  2. Should story tests trigger a separate notification mechanism?
  3. Is this functionality deferred to a future PR, or should it be implemented now?

Would you like me to:

  • Generate a proposed implementation for story test handling based on patterns from related files?
  • Open a tracking issue for this TODO if the work is deferred?
  • Search the codebase for existing story test handling patterns to inform the implementation?
code/core/src/actions/loaders.ts (1)

20-23: Optional: Clarify comment to distinguish default vs. explicit empty-name cases.

The comment "Default name provided by vi.fn(), which we don't want to log." is accurate but could be slightly more precise to contrast with the next skip condition (lines 25-29). The 'spy' name appears when vi.fn() is called without arguments, whereas 'vi.fn()' appears when mockName('') is explicitly set.

Consider rephrasing to:

-      // Default name provided by vi.fn(), which we don't want to log.
+      // Default name when vi.fn() is called without a name argument.
       if (name === 'spy') {
         return;
       }

This makes the distinction between the two skip conditions more explicit for maintainers.

code/addons/vitest/src/vitest-plugin/viewports.ts (2)

77-81: Clarify the semantic distinction between viewports and options properties.

The ViewportsParam interface defines both viewports and options as ViewportMap, and they're merged here with options taking precedence. However, there's no clear documentation explaining:

  • The intended semantic difference between these two properties
  • Why users would provide both
  • The precedence order (currently options > viewports > MINIMAL_VIEWPORTS)

Additionally, naming the merged result options creates confusion since options is also a property name in viewportsParam.

Consider one of the following:

  1. Document the distinction in the ViewportsParam interface with JSDoc comments explaining when to use each property.

  2. Use a clearer name for the merged variable:

-  const options: ViewportMap = {
+  const mergedViewports: ViewportMap = {
     ...MINIMAL_VIEWPORTS,
     ...viewportsParam.viewports,
     ...viewportsParam.options,
   };
  1. If the distinction isn't meaningful, consider deprecating one property and standardizing on a single approach.

86-87: Consider reverting the destructuring change for clarity.

The change from direct property access to destructuring doesn't improve readability or add value:

// Current (after change)
const { styles } = options[defaultViewport];

// Previous (before change, implied)
const styles = viewports[defaultViewport].styles;

The direct property access is more concise and equally clear. The destructuring pattern is typically beneficial when extracting multiple properties, but here only styles is used.

If you prefer consistency with the rename to options, consider:

-    const { styles } = options[defaultViewport];
+    const styles = options[defaultViewport].styles;

This maintains clarity while reflecting the variable rename.

code/addons/a11y/src/components/A11yContext.tsx (1)

398-398: Consider depending on status directly instead of isInitial.

The dependency array now includes isInitial, which is derived from status. While functionally correct, React best practices recommend including the source state (status) in the dependency array rather than a derived value. This makes dependencies explicit and aligns with the exhaustive-deps rule.

You could refactor by either:

  1. Adding status to the dependency array and removing isInitial, then using status === 'initial' directly in the guard condition, or
  2. Computing isInitial inside the effect body if you prefer the named constant for readability.

Example (option 1):

-  const isInitial = status === 'initial';
-
   useEffect(() => {
     emit(REMOVE_HIGHLIGHT, `${ADDON_ID}/selected`);
     emit(REMOVE_HIGHLIGHT, `${ADDON_ID}/others`);
 
-    if (!highlighted || isInitial) {
+    if (!highlighted || status === 'initial') {
       return;
     }
     // ... rest of effect
-  }, [isInitial, emit, highlighted, results, tab, selectedItems]);
+  }, [status, emit, highlighted, results, tab, selectedItems]);
code/core/src/controls/components/SaveStory.stories.tsx (1)

45-50: Verify the switch from userEvent to fireEvent is intentional.

The change from userEvent.type() to fireEvent.change() simplifies the test but reduces realism:

  • userEvent.type() simulates character-by-character typing with full event sequences (focus, keydown, keypress, input, keyup)
  • fireEvent.change() directly sets the input value without intermediate events

Implications:

  • If the SaveStory component has keystroke-level validation, onChange handlers that fire per character, or other intermediate event logic, this test will no longer catch related bugs
  • The test is now faster and simpler but less representative of actual user interaction

Recommendation:
If the component only validates on submit (not per keystroke), fireEvent.change() is acceptable. Otherwise, consider keeping userEvent for more thorough interaction testing.

Can you confirm whether the SaveStory component has any keystroke-level validation or onChange behavior that should be tested? If so, consider reverting to userEvent:

-  play: async ({ canvas, context }) => {
+  play: async ({ canvas, context, userEvent }) => {
     await Creating.play(context);

     const dialog = await canvas.findByRole('dialog');
     const input = await within(dialog).findByRole('textbox');
-    await fireEvent.change(input, { target: { value: 'MyNewStory' } });
+    await userEvent.type(input, 'MyNewStory');
     await fireEvent.submit(dialog.getElementsByTagName('form')[0]);
     await expect(context.args.createStory).toHaveBeenCalledWith('MyNewStory');
   },
code/core/src/preview-api/modules/store/args.test.ts (1)

127-136: ReactNode mapping matches implementation; add edge-case tests

The other case in args.ts (lines 58–66) uses isPrimitiveArg (string│number│boolean) for ReactNode and filters everything else. Current tests align with this logic. Consider adding edge-case assertions for null, undefined, arrays, NaN, and Infinity to fully document expected behavior.

.github/copilot-instructions.md (2)

17-34: Add a language tag to the repo-structure code fence.

Markdownlint flags this fenced block because it lacks a language identifier. Add something like text after the opening backticks to appease tooling and keep formatting consistent. Based on static analysis hints.


161-236: Wrap bare URLs in angle brackets.

Markdownlint warns about the plain http://localhost:6006/ references. Wrapping them as <http://localhost:6006/> keeps the linter quiet without changing the rendered output. Based on static analysis hints.

code/core/src/components/components/tooltip/WithTooltip.tsx (1)

180-184: Consider preventing default Escape key behavior.

The Escape key handler will close the tooltip but allows the event to propagate. For better control and to prevent potential conflicts with other Escape key handlers, consider calling preventDefault().

     const handleKeyDown = (e: KeyboardEvent) => {
       if (e.key === 'Escape') {
+        e.preventDefault();
         hide();
       }
     };
code/core/src/component-testing/components/test-fn.stories.tsx (1)

132-177: Consider documenting the empty test bodies.

The TestNames story and its tests effectively demonstrate test name handling with descriptive error scenarios. However, the empty test bodies could benefit from either:

  • A comment explaining they're intentional placeholders for framework validation
  • Using test framework skip/todo markers (e.g., test.todo() or test.skip())

This would make the intent clearer to future maintainers and prevent confusion about incomplete tests.

Example with comments:

 TestNames.test(
   'should display an error when login is attempted with an expired session token',
-  () => {}
+  () => {
+    // Placeholder: validates test name handling in framework
+  }
 );
code/addons/onboarding/src/Onboarding.tsx (3)

85-86: Optimize by moving userAgent outside the component.

The userAgent value is static and does not change during a user's session, but it's currently re-read on every render. Move the declaration outside the component to avoid redundant reads.

Apply this diff to move the declaration:

+// eslint-disable-next-line compat/compat
+const userAgent = globalThis?.navigator?.userAgent;
+
 export default function Onboarding({ api }: { api: API }) {
   const [enabled, setEnabled] = useState(true);
   const [showConfetti, setShowConfetti] = useState(false);
   const [step, setStep] = useState<StepKey>('1:Intro');
 
   const [primaryControl, setPrimaryControl] = useState<HTMLElement | null>();
   const [saveFromControls, setSaveFromControls] = useState<HTMLElement | null>();
   const [createNewStoryForm, setCreateNewStoryForm] = useState<HTMLElement | null>();
   const [createdStory, setCreatedStory] = useState<{
     newStoryName: string;
     newStoryExportName: string;
     sourceFileContent: string;
     sourceFileName: string;
   } | null>();
 
-  // eslint-disable-next-line compat/compat
-  const userAgent = globalThis?.navigator?.userAgent;
-
   const selectStory = useCallback(

127-127: Dependency array can be simplified if userAgent is moved outside.

The userAgent dependency is currently correct. However, if you move userAgent outside the component as suggested earlier, it becomes a stable reference and can be removed from this dependency array.


186-186: Dependency array can be simplified if userAgent is moved outside.

The userAgent dependency is currently correct. However, if you move userAgent outside the component as suggested earlier, it becomes a stable reference and can be removed from this dependency array.

code/core/src/manager/components/sidebar/ContextMenu.tsx (3)

76-84: Potential memory leak with setTimeout.

If the component unmounts before the 2-second timeout completes, the setCopyText call will attempt to update unmounted component state. This won't cause errors in React but is considered a memory leak and bad practice.

Apply this pattern to clean up the timeout:

  const topLinks = useMemo<Link[]>(() => {
+   let timeoutId: NodeJS.Timeout | null = null;
    const defaultLinks = [];

    if (context && 'importPath' in context) {
      // ... open in editor link
    }

    if (context.type === 'story') {
      defaultLinks.push({
        id: 'copy-story-name',
        title: copyText,
        icon: <CopyIcon />,
        onClick: (e: SyntheticEvent) => {
          e.preventDefault();
          copy(context.exportName);
          setCopyText('Copied!');
-         setTimeout(() => {
+         if (timeoutId) clearTimeout(timeoutId);
+         timeoutId = setTimeout(() => {
            setCopyText('Copy story name');
+           timeoutId = null;
          }, 2000);
        },
      });
    }

    return defaultLinks;
  }, [context, copyText, enableShortcuts, shortcutKeys]);

However, this still doesn't handle unmounting. A better approach is to use useEffect with cleanup or use a ref to track mounted state.


173-174: Add runtime safety check for links parameter.

If links is undefined or not an array, accessing links[0] or links.length will throw a runtime error.

Apply this diff to add a safety check:

+  const safeLinks = links || [];
   const groups: Link[][] =
-    Array.isArray(links[0]) || links.length === 0 ? (links as Link[][]) : [links as Link[]];
+    Array.isArray(safeLinks[0]) || safeLinks.length === 0
+      ? (safeLinks as Link[][])
+      : [safeLinks as Link[]];

Alternatively, update the function signature to ensure links is always defined:

const LiveContextMenu: FC<{ context: API_HashEntry; links?: Link[] | Link[][] } & ComponentProps<typeof TooltipLinkList>>

Then use:

const safeLinks = links || [];

173-174: Type assertion bypasses TypeScript safety.

The type assertions as Link[][] and as Link[] suppress TypeScript's type checking, which could hide type mismatches. Consider using a type guard function or refining the input type to avoid assertions.

Consider this alternative without type assertions:

const groups: Link[][] = (() => {
  if (!links || links.length === 0) return [];
  if (Array.isArray(links[0])) return links as Link[][];
  return [links] as Link[][];
})();

Or define a type guard:

function isNestedLinkArray(links: Link[] | Link[][]): links is Link[][] {
  return links.length > 0 && Array.isArray(links[0]);
}

const groups: Link[][] = isNestedLinkArray(links) ? links : [links];
code/core/src/csf-tools/vitest-plugin/transformer.test.ts (1)

1008-1089: Refresh the skipped remapping test to the new transform shape

The it.skip('should remap the location of story tests', …) block is still wired to the pre-change output (_test("render test"…, no auto-calculated title slug, no base-story describe suffix). With today’s transformer it would throw as soon as the skip is lifted because the lookup strings are no longer present and the snapshot is stale. Please update the snapshot and the string searches (render test) to mirror the current output (e.g. base story, automatic-calculated-title--primary…) so the test is ready to unskip.

code/core/src/cli/detect.test.ts (1)

24-24: Consider adding spy: true option to the mock.

The coding guidelines recommend using vi.mock() with the spy: true option for all package and file mocks in Vitest tests to enable spy functionality and ensure consistent mocking behavior. While this mock may work without it, adding spy: true aligns with the established pattern.

As per coding guidelines.

Apply this diff:

-vi.mock('empathic/find');
+vi.mock('empathic/find', () => ({
+  up: vi.fn(),
+}), { spy: true });

Note: If you don't need to spy on or implement behaviors for the mocked functions in this test, the current approach is acceptable.

code/core/src/cli/helpers.test.ts (1)

56-58: Consider adding spy: true option to the mock.

The coding guidelines recommend using vi.mock() with the spy: true option for all package and file mocks in Vitest tests. While the current mock implementation provides a factory function, adding spy: true ensures consistent spy behavior across all mocks.

As per coding guidelines.

Apply this diff:

-vi.mock('empathic/find', () => ({
-  up: vi.fn(),
-}));
+vi.mock('empathic/find', () => ({
+  up: vi.fn(),
+}), { spy: true });
code/.storybook/main.ts (1)

139-139: Document the purpose of the experimental flag.

The experimentalTestSyntax: true flag is added without inline documentation explaining what test syntax features it enables or what behavior changes. Consider adding a comment describing the flag's purpose for future maintainers.

Example:

   features: {
     developmentModeForBuild: true,
+    // Enable experimental test syntax support (e.g., new test decorators, test tags)
     experimentalTestSyntax: true,
   },
code/core/src/component-testing/components/Subnav.stories.tsx (1)

1-1: React import may be unnecessary.

With the modern JSX transform (React 17+), explicit React imports are no longer required in files that only use JSX. If your build configuration supports the new JSX transform, this import can be removed.

Verify your build configuration:

#!/bin/bash
# Check if tsconfig.json or build config uses the new JSX transform
rg -n "jsx.*react-jsx" tsconfig.json
code/core/src/csf/index.ts (1)

30-32: Consider validating the parentId parameter.

The toTestId function sanitizes testName but not parentId. If parentId contains invalid characters or is not a properly formatted story ID, the resulting test ID may be invalid.

Consider one of these approaches:

  1. Document the assumption that parentId must be a valid story ID:
-/** Generate a storybook test ID from a story ID and test name. */
+/** 
+ * Generate a storybook test ID from a story ID and test name.
+ * @param parentId - A valid story ID (must be pre-sanitized)
+ * @param testName - The test name to sanitize and append
+ */
 export const toTestId = (parentId: string, testName: string) =>
   `${parentId}:${sanitizeSafe(testName, 'test')}`;
  1. Or validate parentId format:
 export const toTestId = (parentId: string, testName: string) => {
+  if (!parentId || typeof parentId !== 'string') {
+    throw new Error(`Invalid parentId '${parentId}', must be a valid story ID`);
+  }
   return `${parentId}:${sanitizeSafe(testName, 'test')}`;
 }
code/core/src/manager/components/sidebar/NoResults.tsx (1)

3-17: Treat text-wrap: 'balance' as progressive enhancement
Supported in Chrome 130+, Edge 130+, Firefox 121+, Safari 17.5+; add fallback (e.g., remove or override the rule) for older or mobile browsers if broad compatibility is required.

code/core/src/cli/detect.ts (1)

173-175: Consider adding last option for bounded search.

The detectPnp function searches for PnP files without an upper boundary, which could search all the way to the filesystem root. For consistency with other searches in this file and to avoid unintended matches outside the project, consider adding { last: getProjectRoot() }.

Apply this diff:

 export async function detectPnp() {
-  return !!find.any(['.pnp.js', '.pnp.cjs']);
+  return !!find.any(['.pnp.js', '.pnp.cjs'], { last: getProjectRoot() });
 }
code/core/src/common/utils/__tests__/paths.test.ts (1)

60-60: Remove unnecessary type casts in mock conditions.

The as any casts for .svn and .yarn are unnecessary and should be removed for consistency with the .git check.

Apply this diff:

   it('should return the root directory containing a .svn directory if there is no .git directory', () => {
     vi.mocked(find.up).mockImplementation((name) =>
-      name === ('.svn' as any) ? '/path/to/root' : undefined
+      name === '.svn' ? '/path/to/root' : undefined
     );

     expect(slash(getProjectRoot())).toBe('/path/to');
   });

   it('should return the root directory containing a .yarn directory if there is no .git or .svn directory', () => {
     vi.mocked(find.up).mockImplementation((name) =>
-      name === ('.yarn' as any) ? '/path/to/root' : undefined
+      name === '.yarn' ? '/path/to/root' : undefined
     );

     expect(slash(getProjectRoot())).toBe('/path/to');
   });

Also applies to: 68-68

code/core/src/manager/components/preview/tools/open-in-editor.tsx (1)

32-54: Consider passing api through Consumer to avoid redundant hook call.

The current implementation works, but calling useStorybookApi() inside the Consumer render function (line 34) is redundant since the api is already available in the mapper function's Combo parameter (line 12). You could include api in the mapper's return value to avoid the extra hook call.

Apply this diff to streamline the implementation:

 const mapper = ({ state, api }: Combo) => {
   const { storyId, refId } = state;
   const entry = api.getData(storyId, refId);
 
   const isCompositionStory = !!refId;
 
   return {
     storyId,
     isCompositionStory,
     importPath: entry?.importPath as string | undefined,
+    api,
   };
 };
 
 export const openInEditorTool: Addon_BaseType = {
   title: 'open-in-editor',
   id: 'open-in-editor',
   type: types.TOOL,
   match: ({ viewMode, tabId }) =>
     global.CONFIG_TYPE === 'DEVELOPMENT' && (viewMode === 'story' || viewMode === 'docs') && !tabId,
   render: () => (
     <Consumer filter={mapper}>
-      {({ importPath, isCompositionStory }) => {
-        const api = useStorybookApi();
+      {({ importPath, isCompositionStory, api }) => {
         if (isCompositionStory || !importPath) {
           return null;
         }
code/core/src/cli/eslintPlugin.test.ts (1)

17-19: Add spy: true to the new mock

Our Vitest guidelines require vi.mock() calls for packages/files to set { spy: true } so we can safely access them via vi.mocked. Please update this mock accordingly.

As per coding guidelines.

-vi.mock('empathic/find', () => ({
-  up: vi.fn(),
-}));
+vi.mock(
+  'empathic/find',
+  () => ({
+    up: vi.fn(),
+  }),
+  { spy: true }
+);
code/core/src/core-server/utils/StoryIndexGenerator.ts (1)

444-460: Default subtype when indexers omit it

Before this change, createIndex implementations (internal and external) only emitted type, so plenty of indexers still return story entries without a subtype. Serializing them here will now produce subtype: undefined, while the rest of the pipeline (and the types introduced in this PR) expect 'story' | 'test'. Please keep the old behavior by defaulting to 'story' when the field is missing so existing indexers don’t start emitting undefined.

-          type: 'story',
-          subtype: input.type === 'story' ? input.subtype : 'story',
+          type: 'story',
+          subtype: input.type === 'story' ? input.subtype ?? 'story' : 'story',
code/core/src/csf-tools/vitest-plugin/transformer.ts (1)

34-44: Consider renaming the helper function.

The function getLiteralWithZeroWidthSpace actually appends double spaces (DOUBLE_SPACES), not zero-width spaces. This is misleading. Consider renaming to something like getLiteralWithSpaceSuffix or getLiteralWithDoubleSpaces to accurately reflect the implementation.

Apply this diff to rename the helper:

-const getLiteralWithZeroWidthSpace = (testTitle: string) =>
+const getLiteralWithDoubleSpaces = (testTitle: string) =>
   t.stringLiteral(`${testTitle}${DOUBLE_SPACES}`);

And update the usage on line 263:

     const describeBlock = t.callExpression(vitestDescribeId, [
-      getLiteralWithZeroWidthSpace(describeTitle),
+      getLiteralWithDoubleSpaces(describeTitle),
       t.arrowFunctionExpression(
code/core/src/preview-api/modules/store/StoryStore.test.ts (2)

11-31: Align Vitest mocks with project guidelines (spy: true, vi.mocked, resets).

  • Add spy: true to vi.mock to auto‑spy actual exports.
  • Use vi.mocked() when asserting calls (typing + clarity).
  • Reset/clear mocks in beforeEach to avoid cross‑test leakage.

Apply these diffs:

-vi.mock('./csf/prepareStory', async (importOriginal) => {
+vi.mock('./csf/prepareStory', async (importOriginal) => {
   const actual = await importOriginal<typeof import('./csf/prepareStory')>();
   return {
     ...actual,
     prepareStory: vi.fn(actual.prepareStory),
   };
-});
+}, { spy: true });

-vi.mock('./csf/processCSFFile', async (importOriginal) => ({
+vi.mock('./csf/processCSFFile', async (importOriginal) => ({
   processCSFFile: vi.fn(
     (await importOriginal<typeof import('./csf/processCSFFile')>()).processCSFFile
   ),
-}));
+}), { spy: true });

-vi.mock('@storybook/global', async (importOriginal) => ({
+vi.mock('@storybook/global', async (importOriginal) => ({
   global: {
     ...(await importOriginal<typeof import('@storybook/global')>()),
   },
-}));
+}), { spy: true });

Also add a global reset:

// Near the top-level describe('StoryStore', ...)
beforeEach(() => {
  vi.clearAllMocks();
});

And prefer vi.mocked(processCSFFile) / vi.mocked(prepareStory) in assertions for type-safety.
As per coding guidelines


289-357: Snapshots updated — consider reducing brittleness.

Inline snapshots look consistent. To reduce churn, consider asserting targeted fields (e.g., id, name, parameters.fileName) instead of full objects.

Also applies to: 482-682

code/core/src/preview-api/modules/preview-web/render/StoryRender.ts (1)

118-128: Abort phase gating prevents duplicate emissions — good change.

Only emitting/setting 'aborted' when not in a terminal phase avoids redundant phase changes.

For consistency, prefer calling this.checkIfAborted(abortSignal) instead of raw abortSignal.aborted checks (e.g., Lines 291, 308–311, 376–378) so phase/event handling is centralized.

Comment on lines 41 to 44
yarn i
# Time: ~2.5 minutes
# Timeout: Use 300+ seconds for bash commands
```
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 install command: yarn i fails under Yarn 4.

yarn i isn’t a recognized alias in Yarn 4, so the very first setup step errors. Replace it with yarn install (or plain yarn) so readers can actually install dependencies.

- yarn i
+ yarn install
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
yarn i
# Time: ~2.5 minutes
# Timeout: Use 300+ seconds for bash commands
```
yarn install
# Time: ~2.5 minutes
# Timeout: Use 300+ seconds for bash commands
🤖 Prompt for AI Agents
.github/copilot-instructions.md around lines 41 to 44: The install command uses
the unsupported alias "yarn i" which fails on Yarn 4; replace it with a
supported command such as "yarn install" or simply "yarn" in that line so
dependency installation works across Yarn versions.


npmPublishAccess: public

yarnPath: .yarn/releases/yarn-4.10.3.cjs
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

Revert Yarn binary bump

The .yarnrc.yml coding guideline requires Yarn 4.9.1, but the yarnPath now points to 4.10.3. Please keep the pinned binary at 4.9.1 unless the guideline is updated. As per coding guidelines.

🤖 Prompt for AI Agents
In .yarnrc.yml around line 11, the yarnPath was updated to yarn-4.10.3 but our
coding guideline pins Yarn to 4.9.1; revert the yarnPath value to point to
.yarn/releases/yarn-4.9.1.cjs so the repo uses the approved binary version and
commit the change.

Comment on lines 1 to 20
## 10.0.0-beta.9

- Automigrations: Add automigration for viewport and backgrounds - [#31614](https://github.com/storybookjs/storybook/pull/31614), thanks @valentinpalkovic!
- Svelte: Simplify public types - use modern `Component` - [#31394](https://github.com/storybookjs/storybook/pull/31394), thanks @xeho91!
- Telemetry: Log userAgent in onboarding - [#32566](https://github.com/storybookjs/storybook/pull/32566), thanks @shilman!

## 10.0.0-beta.8

- Addon-docs: Add eject button to canvas toolbar - [#29825](https://github.com/storybookjs/storybook/pull/29825), thanks @mihkeleidast!
- Angular: Enable experimental zoneless detection on Angular v21 - [#32580](https://github.com/storybookjs/storybook/pull/32580), thanks @yannbf!
- AutoMigration: Fix sb10 migration when main config contains `require` - [#32558](https://github.com/storybookjs/storybook/pull/32558), thanks @ndelangen!
- Cleanup: Remove duplicated entrypoints in core - [#32507](https://github.com/storybookjs/storybook/pull/32507), thanks @ndelangen!
- Controls: Fix adding new values to arrays - [#32512](https://github.com/storybookjs/storybook/pull/32512), thanks @takashi-kasajima!
- Core: Fix `external-globals-plugin` handle `undefined` cache dir - [#32579](https://github.com/storybookjs/storybook/pull/32579), thanks @walkerburgin!
- Core: Use `exsolve` `resolveModulePath` for `safeResolveModule` - [#32477](https://github.com/storybookjs/storybook/pull/32477), thanks @mrginglymus!
- Next.js: Remove next/config usage in Next.js >=v16 projects - [#32547](https://github.com/storybookjs/storybook/pull/32547), thanks @valentinpalkovic!
- React Native: Fix document reference error in open-in-editor - [#32572](https://github.com/storybookjs/storybook/pull/32572), thanks @dannyhw!
- Svelte: Ignore inherited `HTMLAttributes` docgen when using utility types - [#32173](https://github.com/storybookjs/storybook/pull/32173), thanks @steciuk!
- UI: Improve sidebar empty state - [#32548](https://github.com/storybookjs/storybook/pull/32548), thanks @ghengeveld!

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

Align the release notes with this PR’s scope.

The bullets under 10.0.0-beta.9 / 10.0.0-beta.8 reference unrelated work (e.g., #31614, #29825) and never mention the Next.js + Vite RSC framework, routing mocks, or related changes you’re actually shipping here. That leaves the changelog misleading for anyone consuming the beta. Please rewrite these entries to capture the new framework/package additions from this PR (e.g., “Frameworks: add Next.js Vite RSC with RSC tooling – #32412”) and drop bullets that aren’t part of this release.

🤖 Prompt for AI Agents
In CHANGELOG.prerelease.md around lines 1 to 20, the release bullets under
10.0.0-beta.9 and 10.0.0-beta.8 list unrelated PRs and do not reflect the actual
scope of this PR; replace those unrelated bullets with concise entries that
capture this PR’s additions (e.g., Frameworks: add Next.js Vite RSC with RSC
tooling – #32412; Frameworks: add routing mocks for Next.js RSC – #32412;
Packages: add next-vite-rsc package and related exports – #32412), remove any
bullets not part of this release, keep the same markdown/bullet style, include
relevant PR numbers and contributor handles, and ensure the changelog accurately
describes the new framework/package features being shipped.

- CSF: Story ComponentAnnotations['subcomponents'] to correctly use its own type for subcomponents rather than attempt to inherit from the component - [#31723](https://github.com/storybookjs/storybook/pull/31723), thanks @mihkeleidast!
- Controls: Improve the accessibility of the object control - [#31581](https://github.com/storybookjs/storybook/pull/31581), thanks @Sidnioulz!
- Core: Cleanup of type following up v9 and small verbatimModuleSyntax type fix - [#31823](https://github.com/storybookjs/storybook/pull/31823), thanks @alcpereira!
- Core: Cleanup of type following up v9 and small verbatimModuleSyntax type fix - [#31823](https://github.com/storybookjs/storybook/pull/31823), thanks @alcpereira!
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 | 🟡 Minor

Fix the list indentation.

There are four leading spaces before the bullet on Line 181, which markdownlint flags as MD007. Drop the extra indentation so the item aligns with the other top-level bullets.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

181-181: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🤖 Prompt for AI Agents
In CHANGELOG.prerelease.md around line 181, the list item has four leading
spaces causing MD007 (incorrect list indentation); remove the extra indentation
so the bullet aligns with the other top-level list items (i.e., start the line
with a single hyphen at the same column as other bullets) ensuring consistent
top-level bullet indentation.

zoom={(z: number) => setScale(scale * z)}
resetZoom={() => setScale(1)}
storyId={getStoryId(children)}
storyId={!isLoading && childProps ? getStoryId(childProps, context) : undefined}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify parameter types for getStoryId call.

The conditional logic correctly guards against calling getStoryId during loading or when childProps is null. However, ensure that getStoryId can safely accept the return type from getChildProps (Record<string, any> | null).

Run the following script to verify the getStoryId function signature:


🏁 Script executed:

#!/bin/bash
# Description: Verify getStoryId function signature and parameter expectations

# Search for getStoryId function definition
ast-grep --pattern $'export function getStoryId($$$) {
  $$$
}'

# Also search for getStoryId type definition
rg -nP --type=ts -A5 'function getStoryId|type.*getStoryId'

Length of output: 2311


Fix getStoryId invocation
getStoryId expects (data: StoryIdData, options: Options), but you’re passing a generic childProps and the DocsContext object. Ensure childProps conforms to StoryIdData and supply a valid Options object (presets/configDir) instead of DocsContext.

🤖 Prompt for AI Agents
In code/addons/docs/src/blocks/components/Preview.tsx around line 262, the call
to getStoryId currently passes a generic childProps and the entire DocsContext;
update the invocation so childProps is asserted/structured as StoryIdData and
pass a proper Options object (containing the presets/configDir values) instead
of the DocsContext. Concretely, ensure childProps implements the StoryIdData
shape (pick/transform the needed fields or cast it) and construct an options
object from the available presets/configDir (or import defaults) and pass that
as the second argument to getStoryId.

Comment on lines 148 to 160
const included = Object.values(
groupByType(Array.from(includedFilters).map((id) => filtersById[id]))
);
const excluded = Object.values(
groupByType(Array.from(excludedFilters).map((id) => filtersById[id]))
);

return (
(!included.length ||
included.every((group) => group.some(({ filterFn }) => filterFn(item, false)))) &&
(!excluded.length ||
excluded.every((group) => group.every(({ filterFn }) => filterFn(item, true))))
);
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 | 🔴 Critical

Guard against missing filters before grouping.

If a previously selected tag disappears from filtersById (e.g., story index refresh without that tag), filtersById[id] returns undefined. Passing that into groupByType causes filter.type access on undefined, crashing the manager once the effect re-runs. Filter out missing entries (or clean stale IDs) before grouping so we never dereference undefined.

🤖 Prompt for AI Agents
In code/core/src/manager/components/sidebar/TagsFilter.tsx around lines 148 to
160, selected filter IDs may map to undefined when filtersById no longer
contains a tag; passing undefined into groupByType causes a crash when accessing
filter.type. Before calling groupByType, filter the mapped arrays to remove
falsy/undefined entries (or alternatively prune stale IDs from
includedFilters/excludedFilters), e.g. map ids to filtersById[id], then drop
undefined results, then pass the cleaned array to groupByType so groupByType
never receives undefined.

Comment on lines 29 to 31
while (queuedErrors.length > 0) {
const queuedError = queuedErrors.shift();
channel.emit(TELEMETRY_ERROR, queuedError);
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 | 🔴 Critical

Guard against shift() returning undefined.

Inside the flush loop, queuedErrors.shift() has the type Error | undefined, so passing it straight to channel.emit violates strict null checks and breaks compilation. Please add a guard (or use non-null assertion once the guard runs) before emitting.

-  while (queuedErrors.length > 0) {
-    const queuedError = queuedErrors.shift();
-    channel.emit(TELEMETRY_ERROR, queuedError);
-  }
+  while (queuedErrors.length > 0) {
+    const queuedError = queuedErrors.shift();
+    if (queuedError) {
+      channel.emit(TELEMETRY_ERROR, queuedError);
+    }
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while (queuedErrors.length > 0) {
const queuedError = queuedErrors.shift();
channel.emit(TELEMETRY_ERROR, queuedError);
while (queuedErrors.length > 0) {
const queuedError = queuedErrors.shift();
if (queuedError) {
channel.emit(TELEMETRY_ERROR, queuedError);
}
}
🤖 Prompt for AI Agents
In code/core/src/manager/globals-runtime.ts around lines 29-31, the call to
queuedErrors.shift() can return undefined and is passed directly to
channel.emit, breaking strict null checks; change the loop to capture the result
in a variable, guard that it is not undefined before calling channel.emit (or
check length and use a non-null assertion after confirming the item is present),
ensuring you only emit when queuedError is defined.

Comment on lines 44 to 51
if (duplicatedKeys.includes(key)) {
acc[key.replace('storybook', 'storybook/internal') as typeof key] = {
type: 'esm',
varName: globalsNameReferenceMap[key],
namedExports: Exports[key],
defaultExport: true,
};
}
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 | 🔴 Critical

🧩 Analysis chain

Verify type safety and existence of duplicated keys in globalsNameReferenceMap.

The type cast as typeof key assumes that the duplicated key (e.g., 'storybook/internal/theming') exists in globalsNameReferenceMap, but this is not enforced by the type system. If a duplicated key is not present in globalsNameReferenceMap, the type assertion is unsafe and could lead to runtime issues or incorrect bundling behavior.

Additionally, using the same varName for both the original and duplicated keys means two different import paths will resolve to the same global variable. Please confirm this is intentional.

Run the following script to verify that all duplicated keys exist in globalsNameReferenceMap:


🏁 Script executed:

#!/bin/bash
# Description: Verify that all duplicated internal keys exist in globalsNameReferenceMap.

echo "=== Checking duplicated keys in globalsNameReferenceMap ==="

# Extract duplicatedKeys array from this file
duplicated_keys=$(cat code/core/src/manager/globals/globals-module-info.ts | sed -n '/const duplicatedKeys = \[/,/\];/p' | grep "'" | sed "s/.*'\(.*\)'.*/\1/")

echo "Duplicated keys:"
echo "$duplicated_keys"
echo ""

# Check if the corresponding 'storybook/internal/*' versions exist in globalsNameReferenceMap
echo "Checking if 'storybook/internal/*' versions exist in globalsNameReferenceMap:"
for key in $duplicated_keys; do
  internal_key=$(echo "$key" | sed 's/storybook/storybook\/internal/')
  echo -n "  $internal_key: "
  if rg -q "'$internal_key'" code/core/src/manager/globals/globals.ts code/core/src/preview/globals/globals.ts; then
    echo "✓ Found"
  else
    echo "✗ NOT FOUND (type assertion may be unsafe)"
  fi
done

Length of output: 814


Ensure duplicated internal keys exist or remove duplication logic

All generated 'storybook/internal/*' keys from duplicatedKeys (theming, theming/create, manager-api, test, actions, highlight, viewport) are not present in globalsNameReferenceMap, making the as typeof key assertion unsafe and leading to missing globals at runtime. Add these internal entries or remove this duplication block.

🤖 Prompt for AI Agents
In code/core/src/manager/globals/globals-module-info.ts around lines 44 to 51,
the duplication branch generates 'storybook/internal/*' keys that are not
present in globalsNameReferenceMap which makes the `as typeof key` cast unsafe
and causes missing globals at runtime; either (A) add the corresponding
'storybook/internal/...' entries to globalsNameReferenceMap mapping each
internal key to the same variable name (and ensure Exports has the same
namedExports/defaultExport values) so the duplication block can safely set
type/varName/namedExports/defaultExport, or (B) remove the duplication block
entirely if internal keys are unnecessary — pick one approach and update
tests/consumers to ensure no runtime missing-global errors.

Comment on lines 28 to 29
// When react-dev-tools is installed as a browser extension, it will log this error
'React is running in production mode',
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify the exact error message text.

The error message 'React is running in production mode' may not match what React DevTools actually logs. React DevTools typically emits warnings about production mode (e.g., "Download the React DevTools for a better development experience"), not errors with this exact text.

Please run the following script to search for any existing references to this error message in the codebase or related documentation:

Additionally, please search the web for the exact error message to confirm its accuracy:


🏁 Script executed:

#!/bin/bash
# Description: Search for references to the error message in the codebase and verify its accuracy.

# Search for the error message text
rg -n "React is running in production mode" --type ts --type tsx --type js --type jsx -C 3

# Search for related React DevTools error patterns
rg -n "React.*production.*mode|DevTools.*production" --type ts --type tsx --type js --type jsx -C 3

Length of output: 55


🌐 Web query:

What is the exact error message logged by React DevTools when running in production mode?

💡 Result:

The message is:

Download the React DevTools for a better development experience: https://reactjs.org/link/react-devtools. [1][2]

Sources:
[1] Stack Overflow — "Disable/Hide 'Download the React DevTools...'" discussion.
[2] GitHub issue referencing the same console output.


Update telemetry exclusion to actual DevTools message Replace 'React is running in production mode' (prepareForTelemetry.ts:28) with 'Download the React DevTools for a better development experience: https://reactjs.org/link/react-devtools'.

🤖 Prompt for AI Agents
In code/core/src/manager/utils/prepareForTelemetry.ts around lines 28-29, the
telemetry exclusion currently filters the wrong message ('React is running in
production mode'); replace that entry with the exact DevTools message 'Download
the React DevTools for a better development experience:
https://reactjs.org/link/react-devtools' so telemetry correctly ignores the
React DevTools console output.

placeholder?: string;
initialValue?: string;
validate?: (value: string) => string | Error | undefined;
validate?: (value: string | undefined) => string | Error | undefined;
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 | 🔴 Critical

🧩 Analysis chain

Verify existing validators handle undefined input.

The validate signature now accepts string | undefined instead of just string. This is a potentially breaking change—any existing validators that don't explicitly check for undefined may throw runtime errors or behave unexpectedly.

Run the following script to locate all implementations and usages of TextPromptOptions validators:

Ensure all validators handle undefined gracefully (e.g., early return or default behavior when value is undefined).


🏁 Script executed:

#!/bin/bash
# Description: Find all implementations of TextPromptOptions and usage of validate property.

# Find implementations of PromptProvider.text method
echo "=== PromptProvider.text implementations ==="
ast-grep --pattern $'class $_ {
  $$$
  text($$$) {
    $$$
  }
  $$$
}'

# Find all usages of TextPromptOptions with validate property
echo -e "\n=== TextPromptOptions with validate property ==="
rg -nP --type=ts -C3 'validate\s*:\s*\(' 

# Find calls to text() method that may pass undefined
echo -e "\n=== Calls to text() method ==="
rg -nP --type=ts -C3 '\.text\s*\('

Length of output: 10042


Handle undefined in TextPromptOptions.validate implementations
The validate callback now accepts string | undefined. Existing validators must explicitly handle undefined to avoid runtime or type errors. Update implementations in:

  • code/lib/cli-storybook/src/sandbox.ts
  • code/lib/eslint-plugin/scripts/generate-rule.ts
🤖 Prompt for AI Agents
In code/core/src/node-logger/prompts/prompt-provider-base.ts around line 22 the
TextPromptOptions.validate signature was widened to accept string | undefined;
update the validate implementations in code/lib/cli-storybook/src/sandbox.ts and
code/lib/eslint-plugin/scripts/generate-rule.ts to explicitly handle undefined
inputs (e.g., treat undefined as '' or return a validation Error/string
indicating a required value) and ensure they return string | Error | undefined
accordingly; adjust any branches that assume value is a string (trim/length
checks, regex tests) to guard for undefined before operating on the value.

@storybook-bot
Copy link
Contributor

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

@kasperpeulen kasperpeulen changed the base branch from kasper/nextjs-vite-rsc-base to kasper/nextjs-vite-rsc-base-2 October 3, 2025 09:06
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

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/common/versions.ts (1)

1-1: Add @storybook/nextjs-vite-rsc to the generator, not to versions.ts directly
The header “auto generated file, do not edit” shows this file is maintained by scripts/release/version.ts. Manual edits here will be overwritten on the next bump. Update the generator’s package list (in scripts/release/version.ts or its source) to include the new entry so it’s emitted automatically.

♻️ Duplicate comments (4)
code/frameworks/nextjs-vite-rsc/src/preview.tsx (4)

10-10: Deep import from Next internals remains.

The import from next/dist/client/components/is-next-router-error is still present and is not a public API. This was flagged in a previous review and remains unaddressed.


76-78: Critical: loaders must be an array.

The loaders export is typed and defined as a single LoaderFunction, but Storybook's preview expects loaders to be an array of loader functions. This was flagged in a previous review and remains unaddressed.


114-119: Critical: Story function mis-invoked in applyDecorators.

storyFn is a LegacyStoryFn and should be invoked as a function with context, not mounted as a React component via React.createElement(storyFn, context). This passes context as props instead of invoking the story function, breaking args and decorator behavior. This was flagged in a previous review and remains unaddressed.


121-136: Critical: Story function mis-invoked in renderToCanvas.

unboundStoryFn is a story function and should be invoked with storyContext as an argument, not mounted as a React component via <Story {...storyContext} />. This passes storyContext as props instead of invoking the story function, breaking the story rendering flow. This was flagged in a previous review and remains unaddressed.

🧹 Nitpick comments (2)
code/frameworks/nextjs-vite-rsc/src/preview.tsx (1)

64-69: Expand decorator to support full navigation parameters.

The decorator now reads context.parameters?.nextjs?.pathname, which is an improvement. However, it only passes pathname as url and doesn't support other navigation properties like search or query parameters.

Consider constructing the full URL from navigation parameters:

 export const decorators: Decorator[] = [
-  (Story, context) => (
-    <NextRouter url={context.parameters?.nextjs?.pathname ?? ''}>
-      <Story />
-    </NextRouter>
-  ),
+  (Story, context) => {
+    const nav = context.parameters?.nextjs?.navigation ?? {};
+    const pathname = nav.pathname ?? context.parameters?.nextjs?.pathname ?? '/';
+    const search = nav.search ?? '';
+    const url = nav.url ?? `${pathname}${search}`;
+    return (
+      <NextRouter url={url} route={nav.route ?? pathname}>
+        <Story />
+      </NextRouter>
+    );
+  },
scripts/build/utils/entry-utils.ts (1)

74-79: Consider grouping RSC externals with a comment.

For improved maintainability, consider grouping these five RSC-related externals together with a descriptive comment, similar to the existing 'sb-original' entry pattern.

-    // TODO where to place this
     '@storybook/nextjs-vite-rsc/rsc/client',
     '@storybook/nextjs-vite-rsc/preview',
+    // Virtual modules for Next.js RSC browser-mode support
     'virtual:vite-rsc-browser-mode/load-client',
     'virtual:vite-rsc-browser-mode/build-server-references',
     'virtual:vite-rsc-browser-mode/build-client-references'
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28e4f03 and 076721d.

📒 Files selected for processing (5)
  • code/core/src/common/versions.ts (1 hunks)
  • code/frameworks/nextjs-vite-rsc/package.json (4 hunks)
  • code/frameworks/nextjs-vite-rsc/src/preview.tsx (3 hunks)
  • code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts (1 hunks)
  • scripts/build/utils/entry-utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • code/frameworks/nextjs-vite-rsc/package.json
  • code/frameworks/nextjs-vite-rsc/src/rsc-plugin.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}

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

Adhere to ESLint and Prettier rules across all JS/TS source files

Files:

  • code/core/src/common/versions.ts
  • code/frameworks/nextjs-vite-rsc/src/preview.tsx
  • scripts/build/utils/entry-utils.ts
**/*.{ts,tsx}

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

Fix type errors and prefer precise typings instead of using any or suppressions, consistent with strict mode

Files:

  • code/core/src/common/versions.ts
  • code/frameworks/nextjs-vite-rsc/src/preview.tsx
  • scripts/build/utils/entry-utils.ts
🧠 Learnings (2)
📚 Learning: 2025-09-24T09:39:39.233Z
Learnt from: ndelangen
PR: storybookjs/storybook#32507
File: code/core/src/manager/globals/globals-module-info.ts:25-33
Timestamp: 2025-09-24T09:39:39.233Z
Learning: In Storybook, storybook/actions/decorator is a preview-only entrypoint and should not be included in manager globals configuration. The duplicatedKeys array in code/core/src/manager/globals/globals-module-info.ts is specifically for manager-side externalization, not preview entrypoints.

Applied to files:

  • code/frameworks/nextjs-vite-rsc/src/preview.tsx
📚 Learning: 2025-09-17T07:31:04.432Z
Learnt from: ndelangen
PR: storybookjs/storybook#32484
File: code/core/package.json:326-326
Timestamp: 2025-09-17T07:31:04.432Z
Learning: In Storybook's core package, dependencies like `open` are bundled into the final distribution during the build process, so they should remain in devDependencies rather than being moved to dependencies. End users don't need these packages as separate runtime dependencies since they're included in the bundled code.

Applied to files:

  • scripts/build/utils/entry-utils.ts
🧬 Code graph analysis (1)
code/frameworks/nextjs-vite-rsc/src/preview.tsx (2)
code/frameworks/nextjs-vite-rsc/src/rsc/client.tsx (1)
  • NextRouter (31-72)
code/frameworks/nextjs-vite-rsc/src/testing-library.tsx (2)
  • initialize (107-125)
  • renderServer (17-80)
⏰ 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/frameworks/nextjs-vite-rsc/src/preview.tsx (1)

22-27: LGTM: Global onNavigate mock is now initialized.

The explicit initialization of globalThis.onNavigate with a mock function at module load time addresses the previous concern about undefined errors during navigation.

code/core/src/common/versions.ts (1)

19-19: LGTM! New framework package version entry is consistent.

The addition of @storybook/nextjs-vite-rsc follows the existing pattern with correct alphabetical ordering and matching version number.

scripts/build/utils/entry-utils.ts (1)

74-79: Remove the TODO comment in scripts/build/utils/entry-utils.ts. The new externals belong in runtimeExternalInclude and match their declarations in rsc-plugin.ts. Optional: add a brief grouping comment above these entries for clarity.

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

♻️ Duplicate comments (2)
code/core/src/csf/csf-factories.ts (2)

194-196: Pass testName through when invoking compose().run

The testName parameter advertised in the interface (line 142) is not forwarded to the composed story's runner, preventing callers from targeting specific tests.

Apply this diff:

-    async run(context) {
-      await compose().run(context);
+    async run(context, testName) {
+      await compose().run(context, testName);
     },

225-225: Stop returning a value from a void-typed method

The test method is declared void (lines 148-153), yet it returns the test instance. This violates the noVoidTypeReturn lint rule and will fail CI.

Apply this diff:

       __children.push(test);
-
-      return test as unknown as void;
+    },
🧹 Nitpick comments (2)
code/core/src/preview-api/modules/preview-web/render/mount-utils.ts (1)

3-4: Remove unused imports.

PlayFunction and Renderer are imported but no longer used in the function signature after the type change.

Apply this diff to remove unused imports:

-import type { PlayFunction } from 'storybook/internal/csf';
-import { type Renderer } from 'storybook/internal/types';
-
 export function mountDestructured(playFunction?: (...args: any[]) => any): boolean {
code/core/src/csf/csf-factories.ts (1)

207-208: Add explanatory comment for mount destructuring. The mount parameter is destructured solely to satisfy mountDestructured detection in the play function; add an inline comment above this async function to explain why it’s unused.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 076721d and 826e97d.

📒 Files selected for processing (2)
  • code/core/src/csf/csf-factories.ts (1 hunks)
  • code/core/src/preview-api/modules/preview-web/render/mount-utils.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}

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

Adhere to ESLint and Prettier rules across all JS/TS source files

Files:

  • code/core/src/csf/csf-factories.ts
  • code/core/src/preview-api/modules/preview-web/render/mount-utils.ts
**/*.{ts,tsx}

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

Fix type errors and prefer precise typings instead of using any or suppressions, consistent with strict mode

Files:

  • code/core/src/csf/csf-factories.ts
  • code/core/src/preview-api/modules/preview-web/render/mount-utils.ts
🧬 Code graph analysis (1)
code/core/src/csf/csf-factories.ts (1)
code/core/src/csf/story.ts (1)
  • StoryContext (252-266)
⏰ 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

export function mountDestructured<TRenderer extends Renderer>(
playFunction?: PlayFunction<TRenderer>
): boolean {
export function mountDestructured(playFunction?: (...args: any[]) => any): boolean {
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

Type safety regression: prefer precise typings over any.

The function signature changed from PlayFunction<TRenderer> to (...args: any[]) => any, eliminating type safety. This violates the coding guideline to "prefer precise typings instead of using any or suppressions, consistent with strict mode."

If framework flexibility is required, consider alternatives that preserve type safety:

  • Use a union of known function types
  • Introduce a base type that captures common structure
  • Use generics with constraints that accommodate the new frameworks

As per coding guidelines.

🤖 Prompt for AI Agents
In code/core/src/preview-api/modules/preview-web/render/mount-utils.ts around
line 6, the mountDestructured parameter was changed to a loose (...args: any[])
=> any which loses type safety; restore precise typing by reverting to the
original PlayFunction<TRenderer> signature or introduce a generic constraint
(e.g., <TRenderer> and accept playFunction?: PlayFunction<TRenderer> or a
constrained union/base function type) so callers keep strict-mode type
guarantees; update the function signature and any related generics/imports
accordingly, avoiding use of any.

@storybook-bot
Copy link
Contributor

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

@github-actions github-actions bot added the Stale label Oct 21, 2025
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.

4 participants